"""Headless slideshow builder: read a JSON manifest, write one drive image that steps through several converted pictures. Manifest shape (all fields except ``items`` and each item's ``image`` optional):: { "platform": "c64", "format": "d64", "advance": "both", # key | seconds | both "seconds": 10, "loop": true, "video": "pal", # pal | ntsc (seconds timing) "disk_name": "holiday", "items": [ {"image": "beach.jpg", "mode": "multicolor", "dither": "atkinson", "palette": "colodore", "brightness": 1.1, "contrast": 1.2}, {"image": "sunset.png", "mode": "hires", "tint": 20, "saturation": 1.3} ] } Per-item image adjustments are the same knobs as the single-image CLI: aspect, brightness, contrast, saturation, gamma, tint, red, green, blue, plus mode / palette / dither / mono_base. """ from __future__ import annotations import argparse import json import os import sys from . import imageprep, slideshow from .diskimage import DiskError _PREP_FIELDS = ("aspect", "brightness", "contrast", "saturation", "gamma", "tint", "red", "green", "blue", "border_index") _ITEM_FIELDS = ("mode", "palette", "dither", "mono_base") def _item_from(obj: dict, base_dir: str) -> slideshow.SlideItem: if "image" not in obj: raise ValueError("each slideshow item needs an \"image\" path") image = obj["image"] if not os.path.isabs(image): image = os.path.join(base_dir, image) prep = imageprep.PrepOptions(**{k: obj[k] for k in _PREP_FIELDS if k in obj}) kw = {k: obj[k] for k in _ITEM_FIELDS if k in obj} return slideshow.SlideItem(source_path=image, prep=prep, **kw) def _show_from(manifest: dict, base_dir: str) -> slideshow.Slideshow: items = [_item_from(it, base_dir) for it in manifest.get("items", [])] if not items: raise ValueError("manifest has no items") return slideshow.Slideshow( platform=manifest.get("platform", "c64"), disk_format=manifest.get("format", "d64"), advance=manifest.get("advance", "both"), seconds=int(manifest.get("seconds", 10)), loop=bool(manifest.get("loop", True)), items=items, ) def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="8bitlenser-slideshow", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("manifest", help="JSON slideshow manifest") p.add_argument("-o", "--output", required=True, help="output drive image (e.g. show.d64/.d71/.d81)") p.add_argument("--disk-name", default=None, help="disk + viewer name") p.add_argument("--video", default=None, choices=["pal", "ntsc"], help="seconds-timer rate (default from manifest, else pal)") p.add_argument("--intensive", action="store_true", help="slower, higher-quality conversion of each slide") return p def main(argv=None) -> int: args = build_parser().parse_args(argv) with open(args.manifest) as f: manifest = json.load(f) base_dir = os.path.dirname(os.path.abspath(args.manifest)) show = _show_from(manifest, base_dir) if not slideshow.supports_slideshow(show.platform): print(f"slideshow is not supported for platform '{show.platform}' " f"(supported: {', '.join(slideshow.SLIDESHOW_PLATFORMS)})", file=sys.stderr) return 2 video = args.video or manifest.get("video", "pal") convs = [slideshow.convert_item(show, it, args.intensive) for it in show.items] for i, (it, c) in enumerate(zip(show.items, convs)): print(f"slide {i:02d}: {os.path.basename(it.source_path)} " f"mode={c.mode} data={slideshow.image_nbytes(c)}B dE={c.error:.2f}") b = slideshow.budget(show.platform, show.disk_format, [slideshow.image_nbytes(c) for c in convs], slideshow.viewer_length(show, convs, video)) print(f"storage: {b.used_blocks}/{b.total_blocks} blocks, " f"{b.files}/{b.file_cap} files -> {'fits' if b.fits else 'OVER: ' + b.reason}") try: out = slideshow.build_disk(show, args.output, intensive=args.intensive, disk_name=args.disk_name, video=video, convs=convs) except (DiskError, NotImplementedError, ValueError) as e: print(f"error: {e}", file=sys.stderr) return 1 print(f"wrote slideshow {out} ({len(convs)} images, advance={show.advance}" f"{'' if show.advance == 'key' else f'/{show.seconds}s'}, " f"loop={'on' if show.loop else 'off'})") return 0 if __name__ == "__main__": raise SystemExit(main())