119 lines
4.7 KiB
Python
119 lines
4.7 KiB
Python
"""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())
|