8bitlenser/lenser/atari/convert/gr9.py
2026-07-03 19:35:35 -07:00

48 lines
2 KiB
Python

"""Atari GR.9 (GTIA): 80x192, 16 luminance shades of one hue.
Excellent greyscale (hue 0) or tinted monochrome (any of 16 hues) -- 16 real
shades, not just dithered. ``base_color`` selects the hue (0..15); None = grey.
"""
from __future__ import annotations
import numpy as np
from ...convert.base import Conversion, perceptual_error
from .. import palette as apal
from . import _common
WIDTH, HEIGHT = 80, 192
PIXEL_ASPECT = 4.0
def convert(img_rgb, palette_name="ntsc", dither_mode="floyd",
intensive=False, base_color=None):
hue = 0 if base_color is None else (int(base_color) & 0x0F)
plab = apal.palette_lab(palette_name)
prgb = apal.get_palette(palette_name).astype(np.uint8)
img_mono, plab_mono = _common.luminance_lab(img_rgb, plab)
ramp = apal.hue_ramp(hue) # 16 register values of this hue
idx = _common.quantize_global(img_mono, plab_mono, ramp, dither_mode)
val = (idx & 0x0F).astype(np.uint8) # GR.9 pixel = 4-bit luminance
# GTIA mode 9 takes the hue from COLBK and the luminance from each pixel. A
# COLBK of exactly $00, though, blanks the whole playfield to black -- the
# register must be non-zero to enable the display. For a tinted hue that is
# automatic ((hue<<4) != 0); for greyscale (hue 0) force a non-zero luminance
# nibble, which the mode ignores for output (luminance still comes from the
# pixels) but which switches the 16-shade display on.
colbk = (hue & 0x0F) << 4
if colbk == 0:
colbk = 0x0E
data = _common.split_screen(_common.pack_4bpp(val)) + bytes([colbk])
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
return Conversion(
mode="gr9", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR,
viewer="gr9", preview_rgb=preview,
error=perceptual_error(idx, img_mono, plab_mono),
meta={"palette": palette_name, "dither": dither_mode, "hue": hue},
)