Implement NUFLI delta encoding with scanlines
- Add tools/apply_scanlines.py for CRT scanline effect - Add tools/nufli_delta.py for delta encoding (base + bitmask deltas) - Delta format: 2880 byte bitmask + N byte values per screen - 40.1% size reduction vs raw NUFLI (69KB vs 115KB for 5 screens) - Base (23KB) at 000-FFF, title delta (7KB) at - - Other deltas use title as fallback (TODO: disk loading) - Update tasks.md with scanline and delta encoding documentation
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply scanline effect: darken every odd row for CRT aesthetic."""
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
src, dst = sys.argv[1], sys.argv[2]
|
||||
img = Image.open(src).resize((320, 200), Image.LANCZOS).convert('RGB')
|
||||
px = img.load()
|
||||
for y in range(200):
|
||||
if y % 2 == 1:
|
||||
for x in range(320):
|
||||
r, g, b = px[x, y]
|
||||
px[x, y] = (r // 4, g // 4, b // 4)
|
||||
img.save(dst)
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NUFLI delta encoder — extract shared base + per-screen deltas.
|
||||
|
||||
Given multiple .nuf files, finds the consensus base (most common byte
|
||||
at each position) and generates per-screen delta files.
|
||||
|
||||
Output format:
|
||||
base.bin - consensus data (23040 bytes)
|
||||
*.delta - per-screen patches using bitmask:
|
||||
2880 bytes bitmask (1 bit per byte position)
|
||||
N bytes of differing values (in order)
|
||||
|
||||
Usage:
|
||||
python3 nufli_delta.py output_base screen1.nuf screen2.nuf ...
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
|
||||
NUFLI_SIZE = 23040
|
||||
BITMASK_SIZE = (NUFLI_SIZE + 7) // 8 # 2880 bytes
|
||||
|
||||
|
||||
def load_nuf(path):
|
||||
"""Load .nuf file, skip 2-byte load address."""
|
||||
data = Path(path).read_bytes()
|
||||
if len(data) == NUFLI_SIZE + 2:
|
||||
return data[2:]
|
||||
elif len(data) == NUFLI_SIZE:
|
||||
return data
|
||||
else:
|
||||
print(f"Warning: {path} is {len(data)} bytes, expected {NUFLI_SIZE} or {NUFLI_SIZE + 2}")
|
||||
return data[:NUFLI_SIZE]
|
||||
|
||||
|
||||
def find_base(screens):
|
||||
"""Find consensus base: most common byte at each position."""
|
||||
base = bytearray(NUFLI_SIZE)
|
||||
for i in range(NUFLI_SIZE):
|
||||
counts = Counter()
|
||||
for s in screens:
|
||||
counts[s[i]] += 1
|
||||
base[i] = counts.most_common(1)[0][0]
|
||||
return bytes(base)
|
||||
|
||||
|
||||
def compute_delta_bitmask(base, screen):
|
||||
"""Compute delta between base and screen using bitmask format.
|
||||
Returns (bitmask, values) where bitmask indicates which bytes differ."""
|
||||
bitmask = bytearray(BITMASK_SIZE)
|
||||
values = bytearray()
|
||||
for i in range(NUFLI_SIZE):
|
||||
if base[i] != screen[i]:
|
||||
byte_idx = i // 8
|
||||
bit_idx = i % 8
|
||||
bitmask[byte_idx] |= (1 << bit_idx)
|
||||
values.append(screen[i])
|
||||
return bytes(bitmask), bytes(values)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="NUFLI delta encoder")
|
||||
ap.add_argument("output_base", help="Output base path (no extension)")
|
||||
ap.add_argument("inputs", nargs="+", help="Input .nuf files")
|
||||
args = ap.parse_args()
|
||||
|
||||
if len(args.inputs) < 2:
|
||||
print("Error: need at least 2 input files for delta encoding")
|
||||
sys.exit(1)
|
||||
|
||||
# Load all screens
|
||||
screens = []
|
||||
names = []
|
||||
for path in args.inputs:
|
||||
screens.append(load_nuf(path))
|
||||
names.append(Path(path).stem)
|
||||
|
||||
# Find consensus base
|
||||
base = find_base(screens)
|
||||
out_path = Path(args.output_base)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write base
|
||||
base_file = out_path.with_suffix(".base")
|
||||
base_file.write_bytes(base)
|
||||
print(f"Base: {base_file} ({len(base)} bytes)")
|
||||
|
||||
# Compute and write deltas
|
||||
total_delta_bytes = 0
|
||||
for name, screen in zip(names, screens):
|
||||
bitmask, values = compute_delta_bitmask(base, screen)
|
||||
delta_data = bitmask + values
|
||||
delta_file = out_path.parent / f"{name}.delta"
|
||||
delta_file.write_bytes(delta_data)
|
||||
total_delta_bytes += len(delta_data)
|
||||
print(f" {name}.delta: {len(values)} patches, {len(delta_data)} bytes "
|
||||
f"(bitmask={len(bitmask)} + values={len(values)})")
|
||||
|
||||
# Summary
|
||||
total_raw = len(screens) * NUFLI_SIZE
|
||||
total_encoded = len(base) + total_delta_bytes
|
||||
print(f"\nTotal raw: {total_raw} bytes")
|
||||
print(f"Total encoded: {total_encoded} bytes (base={len(base)} + deltas={total_delta_bytes})")
|
||||
print(f"Savings: {total_raw - total_encoded} bytes ({100 * (1 - total_encoded / total_raw):.1f}%)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user