Skip to content

Accept images and image folders in the CLI - #26

Merged
FreeApophis merged 2 commits into
mainfrom
feat/cli-image-sheets
Sep 24, 2026
Merged

FreeApophis merged 2 commits into
mainfrom
feat/cli-image-sheets

Conversation

@FreeApophis

Copy link
Copy Markdown
Owner

vcs only spoke video while the library had grown an image pipeline. It now takes folders and
image files too, detected from the input itself rather than through a new verb or a mode flag.

vcs holiday/                          # one sheet from the folder -> holiday.png
vcs holiday/ --recursive --all        # every image, subfolders included
vcs holiday/ --fit cover -c 5 -r 4    # crop to fill the cells instead of letterboxing
vcs holiday/*.jpg -o summer.jpg -f jpg
vcs clip.mp4 holiday/                 # two sheets, one per input

Detection order is Directory.Exists, then a known image extension, then video — so anything
that resolved to video before still does
, unusual containers included.

The awkward part

The positional argument means "one input → one sheet" for video, but N images make one sheet.
InputResolver owns that asymmetry: a video and a directory each get their own sheet, while loose
image files coalesce into a single group that takes the position of the first one.

vcs a.mkv one.jpg b.mkv two.jpg
  → [ a.mkv ] [ one.jpg + two.jpg ] [ b.mkv ]      3 sheets

It also expands wildcards itself, since cmd and PowerShell hand them through verbatim unlike a
POSIX shell — so vcs holiday/*.jpg behaves identically on every platform.

New options

--recursive, --fit <contain|cover|stretch> and --all, mirroring the library knobs, plus a
matching [image] section in the TOML config. Everything else already applied to both media.
--timestamp is now described as the caption overlay, since for images it carries the file name.

No short alias for --recursive: -r is already --rows, and -r/-R differing only in case
is a footgun on a tool where both are plausible.

Misapplied options are errors

$ vcs holiday/ --interval 3m30
--interval applies to video inputs only.          # exit 1

$ vcs clip.mp4 holiday/ --interval 3m30           # fine — applies to the video

The difference between a typo and a sheet that quietly ignored what you asked for. Mixed runs are
exempt, since there each option legitimately applies to part of the input.

Structure

InputResolver (detection + grouping + globbing), OptionScopeValidator (scope rules),
ImageOptionsFactory (config→CLI layering + collection construction), and SheetRunner switching
on the kind. Video and ImageCollection already expose the same SaveContactSheetAsync shape,
so the runner needed a switch rather than an abstraction over two cases.

Verification

36 new tests (112 total, all passing), clean under -warnaserror. Beyond the unit tests, run
against the real binary with a 16-photo folder:

Case Result
vcs <folder> 16-image sheet
vcs '03*.jpg' from cmd-style invocation glob expanded by the CLI, 9 images
vcs sample.mp4 photos -i 5 two sheets, sample.png + photos.png, interval applied to the video only
vcs <folder> --interval 3m30 error, exit 1
vcs sample.mp4 --fit cover error, exit 1
vcs sample.mp4 unchanged, exit 0
--all vs sampling at -c 4 -r 2 4 rows (483px) vs 2 rows (305px)
--fit cover portraits cropped to fill, no letterbox bars
[image] config via --config round-trips through Tomlyn; invalid value names the config as the source

Supersedes the "CLI covers video only for now" line from #25.

🤖 Generated with Claude Code

FreeApophis and others added 2 commits September 24, 2026 09:22
vcs only spoke video, while the library had grown an image pipeline. It
now takes folders and image files too, detected from the input itself
rather than through a new verb or a mode flag:

  vcs holiday/                          -> holiday.png
  vcs holiday/ --fit cover -c 5 -r 4
  vcs holiday/*.jpg -o summer.jpg -f jpg
  vcs clip.mp4 holiday/                 -> two sheets

Detection is Directory.Exists, then a known image extension, then video,
so anything that resolved to video before still does - unusual
containers included.

The awkward part is that the positional argument means "one input, one
sheet" for video, but N images make one sheet. InputResolver owns that
asymmetry: a video and a directory each get their own sheet, while loose
image files coalesce into a single group that takes the position of the
first one. It also expands wildcards itself, since cmd and PowerShell
hand them through verbatim, so vcs holiday/*.jpg behaves the same on
every platform.

--recursive, --fit and --all mirror the library knobs, with a matching
[image] section in the TOML config. Everything else already applied to
both media; --timestamp is described as the caption overlay now, since
for images it carries the file name.

Options that cannot apply to the given inputs are rejected rather than
silently doing nothing, which is the difference between a typo and a
sheet that quietly ignored what you asked for. A run that mixes both
kinds is exempt: there, each option applies to the inputs it belongs to,
so vcs clip.mp4 holiday/ -i 2m is fine.

Verified against the real binary: folder, glob, mixed input with default
output names, both misapplied-option errors with their exit codes, video
unchanged, --all vs sampling, --fit cover, and the [image] config
section round-tripping through Tomlyn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BuildContactSheet_ReportsProgressOncePerImage failed on a loaded Ubuntu
runner with 3 of 4 reports. The test used Progress<T>, which posts to the
synchronization context rather than invoking inline, so the reports could
still be in flight when the build returned; it then polled for them with
a five second deadline, which is a race rather than a wait.

That asynchrony belongs to Progress<T>, not to the library, so the test
now passes a synchronous IProgress recorder. Every report has landed by
the time the awaited call returns, and the assertion can be the exact
sequence - 0.25, 0.5, 0.75, 1.0 - instead of a count and the last value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FreeApophis
FreeApophis merged commit a69a935 into main Sep 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant