"""Headless entry point: convert an image, write a disk image and/or a preview PNG.""" from __future__ import annotations import argparse import sys from PIL import Image from . import imageprep from .convert import MODES, convert_image, render_preview from .palette import COLOR_NAMES def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="8bitlenser", description=__doc__) p.add_argument("image", help="source image (png/jpg/gif/bmp/webp)") p.add_argument("-o", "--output", help="disk image path (.d64/.d71/.d81/.atr)") p.add_argument("--platform", default="c64", choices=["c64", "atari", "apple", "ti99", "coco", "bbc", "coleco", "a2600", "intv", "vic20", "spectrum", "a5200", "a7800", "c128", "c16", "plus4", "cpc", "coco3", "nes", "iigs", "pet2001", "pet4032", "pet8032", "superpet", "sms", "amiga", "ansi"], help="target machine (coleco = ColecoVision/Adam, " "a2600 = Atari 2600/VCS, intv = Mattel Intellivision, " "vic20 = Commodore VIC-20, spectrum = Sinclair ZX Spectrum, " "a5200 = Atari 5200, a7800 = Atari 7800, " "c128 = Commodore 128 VDC 80-column, " "c16 = Commodore 16 TED hires, " "plus4 = Commodore Plus/4 TED hires, " "cpc = Amstrad CPC, " "coco3 = Tandy CoCo 3 GIME, " "nes = Nintendo NES/Famicom, " "iigs = Apple IIGS Super Hi-Res, " "pet2001/pet4032 = 40-col PET/CBM, " "pet8032/superpet = 80-col PET/CBM, " "sms = Sega Master System, " "amiga = Commodore Amiga)") p.add_argument("-m", "--mode", default="auto", choices=["auto", *MODES, "gr15", "gr9", "gr8", "gr15dli", "hgr_mono", "dhgr", "hgr_color", "gm2", "pmode4", "pmode3", "mode0", "mode1", "mode2", "mode5", "stic", "pf_il", "c160", "color", "hicolor", "gr16", "gr4", "bg", "shr", "lowres", "80x25", "80x50"], help="display mode (c64: hires/multicolor/fli/interlace/mono; " "atari: gr15/gr9/gr8/gr15dli; apple: hgr_mono/dhgr; " "ti99: gm2; coco: pmode4/pmode3; bbc: mode0/1/2/5; " "intv: stic; ansi: 80x25/80x50)") p.add_argument("-f", "--format", default=None, choices=["d64", "d71", "d81", "crt", "atr", "car", "ccc", "ssd", "col", "a26", "int", "a0", "sna", "a52", "a78", "prg", "nes", "sms", "adf", "ans"], help="output format: c64 disk d64/d71/d81 or cartridge crt; " "atari disk atr or cartridge car (default: from -o ext)") p.add_argument("-p", "--palette", default="colodore", choices=["colodore", "pepto"]) p.add_argument("-d", "--dither", default=None, choices=["bayer", "bluenoise", "yliluoma", "floyd", "atkinson", "stucki", "jarvis", "sierra", "sierra_lite", "burkes", "riemersma", "ostromoukhov", "none"], help="dithering (default: atkinson; none for intv -- " "per-platform defaults that suit each format)") p.add_argument("--mono-base", default="grayscale", choices=["grayscale", *COLOR_NAMES], help="base colour for 'mono' mode (default greyscale)") p.add_argument("-a", "--aspect", default="fit", choices=["fit", "fill", "stretch"]) p.add_argument("--video", default="pal", choices=["pal", "ntsc"], help="target video standard (affects the FLI viewer timing)") p.add_argument("--intensive", action="store_true", help="exhaustive background search + slower, higher-quality passes") p.add_argument("--brightness", type=float, default=1.0) p.add_argument("--contrast", type=float, default=1.0) p.add_argument("--saturation", type=float, default=1.0) p.add_argument("--gamma", type=float, default=1.0) p.add_argument("--tint", type=float, default=0.0, help="hue rotation in degrees (-180..180)") p.add_argument("--red", type=float, default=1.0, help="red level (gain)") p.add_argument("--green", type=float, default=1.0, help="green level (gain)") p.add_argument("--blue", type=float, default=1.0, help="blue level (gain)") p.add_argument("--preview", help="also write a PNG preview to this path") p.add_argument("--disk-name", default=None, help="disk + viewer name (PETSCII)") p.add_argument("--display", default="key", choices=["forever", "key", "seconds"], help="how long the viewer holds the picture (C64): forever, " "until a key, or --seconds N then exit to BASIC") p.add_argument("--seconds", type=int, default=10, help="seconds to display when --display seconds") p.add_argument("--viewer", default="unified", choices=["unified", "separate"], help="unified = one self-contained file; separate = viewer " "binary + a standalone 'data' image file (C64 disk)") return p def main(argv=None) -> int: args = build_parser().parse_args(argv) prep = imageprep.PrepOptions( aspect=args.aspect, brightness=args.brightness, contrast=args.contrast, saturation=args.saturation, gamma=args.gamma, tint=args.tint, red=args.red, green=args.green, blue=args.blue, ) from . import platforms # Atkinson is the default dither (only used when -d is not given): its lighter # error diffusion bleeds less across constrained colour cells and keeps clean # local contrast. The one exception is the Intellivision, whose 64-tile GRAM # dictionary bloats under any diffusion, so it defaults to no dithering. _def_dither = {"intv": "none"} dither = args.dither or _def_dither.get(args.platform, "atkinson") conv = platforms.convert(args.platform, args.image, args.mode, args.palette, dither, args.intensive, prep, args.mono_base) _dsz = len(conv.data) if isinstance(conv.data, (bytes, bytearray)) else None print(f"platform={args.platform} mode={conv.mode} mean dE={conv.error:.2f}" + (f" data={_dsz}B" if _dsz is not None else "")) if args.preview: rgb = render_preview(conv, args.palette, scale=2) Image.fromarray(rgb, "RGB").save(args.preview) print(f"wrote preview {args.preview}") if args.output: path = platforms.export(args.platform, conv, args.output, args.format, args.image, args.video, display=args.display, seconds=args.seconds, layout=args.viewer) kind = "ANSI art" if args.platform == "ansi" else "disk image" print(f"wrote {kind} {path}") if not args.output and not args.preview: print("nothing to do: pass -o DISK and/or --preview PNG", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())