8bitlenser/lenser/spectrum/convert/mono.py
2026-07-03 19:35:35 -07:00

47 lines
1.7 KiB
Python

"""ZX Spectrum monochrome / tinted-mono mode.
256x192 matched by luminance. The Spectrum has no grey, so greyscale is a 2-level
black/white halftone (bright black + bright white) -- which at 256x192 with
dithering is a crisp, high-detail image free of attribute clash. A base colour
gives a 3-level tinted ramp (black -> colour -> white), all within one brightness
group so the per-cell BRIGHT constraint is satisfied. Reuses the hires packing.
"""
from __future__ import annotations
import numpy as np
from ...convert import base
from .. import palette as spal
from . import hires
def _ramp(base_color):
"""Luminance ramp kept inside ONE brightness group (shared BRIGHT bit)."""
if base_color is None:
return [8, 15] # bright black + bright white
c = base_color & 7 # base hue 0-7
if c in (0, 7):
return [8, 15]
return [8, 8 | c, 15] # bright black, bright hue, bright white
def convert(img_rgb, palette_name="spectrum", dither_mode="atkinson",
intensive=False, base_color=None):
plab = spal.palette_lab()
prgb = spal.get_palette().astype(np.uint8)
ramp = _ramp(base_color)
idx, sets, rows, cols, err = base.mono_render(
img_rgb, plab, ramp, hires.WIDTH, hires.HEIGHT,
hires.CELL_W, hires.CELL_H, dither_mode, n_free=2)
scr = hires._encode(idx, sets, rows, cols)
return base.Conversion(
mode="mono", width=hires.WIDTH, height=hires.HEIGHT,
pixel_aspect=hires.PIXEL_ASPECT, index_image=idx.astype(np.uint16),
data=bytes(scr), data_addr=0x4000, viewer="spectrum", preview_rgb=prgb[idx],
error=err,
meta={"palette": "spectrum", "dither": dither_mode, "base_color": base_color},
)