First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
20
lenser/apple/convert/__init__.py
Normal file
20
lenser/apple/convert/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Apple II conversion dispatch."""
|
||||
from __future__ import annotations
|
||||
from ... import imageprep
|
||||
from . import hgr_mono
|
||||
|
||||
_MODULES = {"hgr_mono": hgr_mono}
|
||||
for _name in ("dhgr", "hgr_color", "mono"):
|
||||
try:
|
||||
_MODULES[_name] = __import__(f"lenser.apple.convert.{_name}", fromlist=[_name])
|
||||
except Exception:
|
||||
pass
|
||||
MODES = list(_MODULES.keys())
|
||||
|
||||
def convert_image(path_or_img, mode="hgr_mono", palette_name="mono",
|
||||
dither_mode="floyd", intensive=False, prep_opt=None, base_color=None):
|
||||
prep_opt = prep_opt or imageprep.PrepOptions()
|
||||
module = _MODULES[mode]
|
||||
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
|
||||
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
|
||||
return module.convert(img_rgb, palette_name, dither_mode, intensive, base_color=base_color)
|
||||
60
lenser/apple/convert/dhgr.py
Normal file
60
lenser/apple/convert/dhgr.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""Apple //e Double Hi-Res: 140x192, 16 colours (no per-cell limit).
|
||||
|
||||
Each line is 560 bits = 140 four-bit colour groups, stored 7 bits per byte with
|
||||
the bytes interleaved between auxiliary and main memory (aux holds the even display
|
||||
bytes, main the odd). Needs a //e (auxiliary memory).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
|
||||
WIDTH, HEIGHT = 140, 192
|
||||
PIXEL_ASPECT = 2.0
|
||||
DATA_ADDR = 0x2000
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="dhgr", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.dhgr_lab()
|
||||
prgb = apal.DHGR16.astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb) # (192,140,3)
|
||||
|
||||
allowed = np.tile(np.arange(16), (HEIGHT, WIDTH, 1))
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
aux = bytearray(0x2000)
|
||||
main = bytearray(0x2000)
|
||||
for y in range(HEIGHT):
|
||||
stream = np.zeros(560, dtype=np.uint8)
|
||||
row = idx[y]
|
||||
for n in range(WIDTH):
|
||||
c = int(row[n])
|
||||
stream[4 * n + 0] = c & 1
|
||||
stream[4 * n + 1] = (c >> 1) & 1
|
||||
stream[4 * n + 2] = (c >> 2) & 1
|
||||
stream[4 * n + 3] = (c >> 3) & 1
|
||||
base = apal.hgr_row_addr(y)
|
||||
for col in range(40):
|
||||
ab = 0
|
||||
mb = 0
|
||||
for i in range(7):
|
||||
ab |= int(stream[7 * (2 * col) + i]) << i
|
||||
mb |= int(stream[7 * (2 * col + 1) + i]) << i
|
||||
aux[base + col] = ab
|
||||
main[base + col] = mb
|
||||
|
||||
data = bytes(main) + bytes(aux) # main half at $2000, aux at $4000
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
return Conversion(
|
||||
mode="dhgr", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=DATA_ADDR,
|
||||
viewer="dhgr", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_lab, plab),
|
||||
meta={"palette": "dhgr", "dither": dither_mode},
|
||||
)
|
||||
72
lenser/apple/convert/hgr_color.py
Normal file
72
lenser/apple/convert/hgr_color.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Apple II HGR artifact colour: 140x192 colour pixels (280x192 mono), ~6 colours.
|
||||
|
||||
Each 2-mono-pixel "colour pixel" can be black, white, or one of two chroma colours
|
||||
set by its byte's palette bit (violet/green for palette 0, blue/orange for palette 1).
|
||||
We pick the palette bit per byte, then dither each colour pixel to its 4 reachable
|
||||
colours. Works on the II+ and //e, and reuses the HGR boot loader.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither, palette as c64pal
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
|
||||
WIDTH, HEIGHT = 140, 192 # colour-pixel resolution
|
||||
PIXEL_ASPECT = 2.0
|
||||
DATA_ADDR = 0x2000
|
||||
N_BYTES = 40
|
||||
|
||||
# index sets reachable in a byte for each palette bit: {black, even-chroma, odd-chroma, white}
|
||||
_SET = {
|
||||
0: [apal.HGR_BLACK, apal.HGR_VIOLET, apal.HGR_GREEN, apal.HGR_WHITE],
|
||||
1: [apal.HGR_BLACK, apal.HGR_BLUE, apal.HGR_ORANGE, apal.HGR_WHITE],
|
||||
}
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="hgr", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
plab = apal.hgr6_lab()
|
||||
prgb = apal.HGR6.astype(np.uint8)
|
||||
img_lab = c64pal.srgb_to_lab(img_rgb) # (192,140,3)
|
||||
|
||||
# choose a palette bit per byte (per line), by nearest-colour error.
|
||||
pal_byte = np.zeros((HEIGHT, N_BYTES), dtype=np.uint8)
|
||||
allowed = np.zeros((HEIGHT, WIDTH, 4), dtype=np.int64)
|
||||
for y in range(HEIGHT):
|
||||
for b in range(N_BYTES):
|
||||
ks = [k for k in range(WIDTH) if (2 * k) // 7 == b]
|
||||
if not ks:
|
||||
continue
|
||||
best_p, best_e = 0, None
|
||||
for p in (0, 1):
|
||||
cols = np.array(_SET[p])
|
||||
d = np.sum((img_lab[y, ks][:, None, :] - plab[cols][None, :, :]) ** 2, axis=-1)
|
||||
e = d.min(axis=1).sum()
|
||||
if best_e is None or e < best_e:
|
||||
best_e, best_p = e, p
|
||||
pal_byte[y, b] = best_p
|
||||
for k in ks:
|
||||
allowed[y, k] = _SET[best_p]
|
||||
|
||||
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
||||
|
||||
# colour-pixel index -> two mono bits at (2k, 2k+1)
|
||||
bits = np.zeros((HEIGHT, WIDTH * 2), dtype=np.uint8)
|
||||
even_on = np.isin(idx, [apal.HGR_VIOLET, apal.HGR_BLUE, apal.HGR_WHITE])
|
||||
odd_on = np.isin(idx, [apal.HGR_GREEN, apal.HGR_ORANGE, apal.HGR_WHITE])
|
||||
bits[:, 0::2] = even_on
|
||||
bits[:, 1::2] = odd_on
|
||||
|
||||
data = apal.pack_hgr_color(bits, pal_byte)
|
||||
preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1)
|
||||
|
||||
return Conversion(
|
||||
mode="hgr_color", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=DATA_ADDR,
|
||||
viewer="hgr", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_lab, plab),
|
||||
meta={"palette": "hgr", "dither": dither_mode},
|
||||
)
|
||||
41
lenser/apple/convert/hgr_mono.py
Normal file
41
lenser/apple/convert/hgr_mono.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""Apple II HGR monochrome: 280x192, 1 bit/pixel, black & white.
|
||||
|
||||
Universal across the Apple II+ and //e. Tone is carried entirely by dithering.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ... import dither
|
||||
from ...convert.base import Conversion, perceptual_error
|
||||
from .. import palette as apal
|
||||
|
||||
WIDTH, HEIGHT = 280, 192
|
||||
PIXEL_ASPECT = 1.0
|
||||
DATA_ADDR = 0x2000 # HGR page 1
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="mono", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
from ...palette import srgb_to_lab
|
||||
plab = apal.mono_lab() # 2 entries: black, white
|
||||
L = srgb_to_lab(img_rgb)[..., 0]
|
||||
img_mono = np.zeros((HEIGHT, WIDTH, 3))
|
||||
img_mono[..., 0] = L
|
||||
plab_mono = np.zeros((2, 3))
|
||||
plab_mono[:, 0] = plab[:, 0]
|
||||
|
||||
allowed = np.tile(np.array([0, 1]), (HEIGHT, WIDTH, 1))
|
||||
idx = dither.quantize(img_mono, allowed, plab_mono, dither_mode).astype(np.uint8)
|
||||
|
||||
data = apal.pack_hgr_mono(idx) # 8192-byte HGR buffer
|
||||
preview = (apal.MONO.astype(np.uint8))[idx]
|
||||
|
||||
return Conversion(
|
||||
mode="hgr_mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
||||
index_image=idx.astype(np.uint16), data=data, data_addr=DATA_ADDR,
|
||||
viewer="hgr", preview_rgb=preview,
|
||||
error=perceptual_error(idx, img_mono, plab_mono),
|
||||
meta={"palette": "mono", "dither": dither_mode},
|
||||
)
|
||||
15
lenser/apple/convert/mono.py
Normal file
15
lenser/apple/convert/mono.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
"""Apple monochrome -- HGR's 280x192 black & white, exposed as the standard
|
||||
``mono`` mode for cross-platform parity (tone carried by dithering)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import hgr_mono
|
||||
|
||||
WIDTH, HEIGHT, PIXEL_ASPECT = hgr_mono.WIDTH, hgr_mono.HEIGHT, hgr_mono.PIXEL_ASPECT
|
||||
|
||||
|
||||
def convert(img_rgb, palette_name="mono", dither_mode="floyd",
|
||||
intensive=False, base_color=None):
|
||||
conv = hgr_mono.convert(img_rgb, palette_name, dither_mode, intensive,
|
||||
base_color=base_color)
|
||||
conv.mode = "mono"
|
||||
return conv
|
||||
Loading…
Add table
Add a link
Reference in a new issue