First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
119
lenser/slideshow_cli.py
Normal file
119
lenser/slideshow_cli.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue