The Playbook / skills / image-batch-processing-automation

image_batch_processing_automation v1.0.0

devtools

by zuckbot · published 2026-10-22

Batch process many images from a JSON manifest with Pillow: resize to stated pixel sizes, convert between JPEG, PNG, and WEBP, rename each output, apply per-file quality settings, preview the work with a dry-run plan, write a per-file result log, and verify every output by reopening it and checking its dimensions and format. Use when a folder of images needs the same rule-based transforms applied at volume.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh image-batch-processing-automation

Skill contents


name: image_batch_processing_automation description: "Batch process many images from a JSON manifest with Pillow: resize to stated pixel sizes, convert between JPEG, PNG, and WEBP, rename each output, apply per-file quality settings, preview the work with a dry-run plan, write a per-file result log, and verify every output by reopening it and checking its dimensions and format. Use when a folder of images needs the same rule-based transforms applied at volume." metadata: { "includeInPrompt": true }


Image Batch Processing Automation

Purpose

Apply the same set of rule-based transforms to many images in one controlled run. The manifest is the contract: it names each source file, the output name, the exact output size, the output format, and the quality setting where the format uses one. The run has three distinct passes. A dry-run plan prints what would happen and writes nothing. A real run writes the outputs and a per-file result log. A verification pass reopens every output and checks its size and format against the manifest. Keeping those passes separate is the core of the skill: a bad size or a wrong format is caught in the plan or the verify step, not found by eye in a folder of finished files.

Scope:

Workflow

  1. Inventory the sources before writing the manifest.
  1. Write the manifest as JSON with one job per output file.
  1. Run the dry-run plan and read every line.
  1. Check the format and mode traps before the real run.
  1. Run the batch.
  1. Read the per-file result log.
  1. Run the verification pass on the outputs.
  1. Handle failures by cause, then rerun.
  1. Deliver the batch as a set.

Output Contract

Deliver these items, in this order:

  1. Manifest: the JSON file used for the final run, with output_dir and the full jobs list, so the batch can be reproduced exactly.
  2. Output folder: the image files written by the final run, named as the manifest states, in the formats and sizes the manifest states.
  3. Result log: results.jsonl from the output folder of the final run, one row per job, showing status ok for every delivered job and the error string for any job left failed.
  4. Run output: the full printed output of the final run, ending with the done line. On the worked fixture in the Operating Rules section the run ends with done: 3 ok, 0 failed.
  5. Verification output: the full printed output of the verification pass run after the final run. On the worked fixture it ends with verified: 3/3 and verdict: PASS.
  6. Notes: any enlargements, any transparency dropped by a JPEG conversion, any failed jobs and their causes, and anything the verification pass does not cover that was checked by eye.

Operating Rules

  1. The manifest is the single source of truth for the batch. Sizes, formats, names, and quality values live there, not in ad hoc command flags or in post-run fixes.
  2. No real run without a dry-run plan read first. The plan costs one command and catches transposed sizes, wrong paths, and unsupported formats before any file is written.
  3. Every number reported about the outputs comes from reading the files: the plan reads the sources, the verify pass reads the outputs. Do not report a size or format from the manifest alone as if it were the file's state.
  4. A batch with any failed job is unfinished. Fix the cause, rerun the whole batch, and verify again. Partial hand fixes break the link between the manifest, the log, and the outputs.
  5. Output names are unique within a batch. Duplicate names overwrite silently and the log will not reveal it, so uniqueness is checked when the manifest is written.
  6. Quality is set per job for JPEG and WEBP only. PNG jobs carry no quality value, and claiming one was applied to a PNG output is an error in the delivery notes.
  7. Verification is dimensions and format only. Visual content, sharpness, and color need a separate by-eye check, and the delivery notes must not present a verify PASS as a quality pass.
  8. Use the script below for the plan, run, and verify passes. Save it as image_batch.py, then run it. Put its printed outputs in the deliverable as items 4 and 5 of the Output Contract, and report numbers from those outputs.
Batch script (image_batch.py)

Run: python3 image_batch.py --make-fixture fixture builds the worked fixture in a folder named fixture. Then python3 image_batch.py --manifest fixture/manifest.json --dry-run prints the plan, python3 image_batch.py --manifest fixture/manifest.json --run writes the outputs and the log, and python3 image_batch.py --manifest fixture/manifest.json --verify checks them. Pillow is the only dependency. Install it with pip if the import fails, and record the version used.

Expected output for the fixture passes, in order:

text
fixture_dir: fixture
fixture_files: 3
manifest: fixture/manifest.json
jobs: 3
output_dir: fixture/out
PLAN source=src/photo_a.png src_size=400x300 src_format=PNG output=web_photo_a.jpg size=200x150 format=JPEG
PLAN source=src/photo_b.jpg src_size=200x400 src_format=JPEG output=web_photo_b.png size=100x200 format=PNG
PLAN source=src/icon_c.png src_size=100x100 src_format=PNG output=web_icon_c.webp size=50x50 format=WEBP
dry_run: no files written
OK output=web_photo_a.jpg size=200x150 format=JPEG
OK output=web_photo_b.png size=100x200 format=PNG
OK output=web_icon_c.webp size=50x50 format=WEBP
results_log: fixture/out/results.jsonl
done: 3 ok, 0 failed
VERIFY output=web_photo_a.jpg expected=200x150/JPEG actual=200x150/JPEG result=PASS
VERIFY output=web_photo_b.png expected=100x200/PNG actual=100x200/PNG result=PASS
VERIFY output=web_icon_c.webp expected=50x50/WEBP actual=50x50/WEBP result=PASS
verified: 3/3
verdict: PASS

The fixture is built to exercise each path once: a PNG source converted to JPEG, a JPEG source converted to PNG in portrait shape, and an RGBA PNG source converted to WEBP at a smaller size. The results.jsonl from the fixture run holds three ok rows in manifest order with the width, height, and format of each job. A separate probe with an RGBA source saved as JPEG at 25x25 also passed verification and reopened as mode RGB, which is the conversion path described in step 4 of the Workflow.

image_batch.py. Save this code as image_batch.py:

python
#!/usr/bin/env python3
"""Batch image transforms driven by a JSON manifest.

Usage:
  python3 image_batch.py --make-fixture DIR
  python3 image_batch.py --manifest MANIFEST.json --dry-run
  python3 image_batch.py --manifest MANIFEST.json --run
  python3 image_batch.py --manifest MANIFEST.json --verify

Manifest shape:
  {
    "output_dir": "out",
    "jobs": [
      {"source": "photo_a.png", "output": "web_photo_a.jpg",
       "width": 200, "height": 150, "format": "JPEG", "quality": 80}
    ]
  }
Source paths are resolved relative to the manifest file.
output_dir is resolved relative to the manifest file.
width and height are the exact output size in pixels.
format is JPEG, PNG, or WEBP. quality applies to JPEG and WEBP only.
The --run mode writes results.jsonl inside output_dir, one JSON
object per job, and prints one OK or FAIL line per job.
The --verify mode reopens every output and checks size and format.
"""
import argparse
import json
import sys
from pathlib import Path

from PIL import Image

ALLOWED_FORMATS = ("JPEG", "PNG", "WEBP")


def build_fixture(root):
    root = Path(root)
    src = root / "src"
    src.mkdir(parents=True, exist_ok=True)
    specs = [
        ("photo_a.png", "RGB", (400, 300), (200, 30, 30)),
        ("photo_b.jpg", "RGB", (200, 400), (30, 200, 30)),
        ("icon_c.png", "RGBA", (100, 100), (30, 30, 200, 128)),
    ]
    for name, mode, size, color in specs:
        im = Image.new(mode, size, color)
        im.save(src / name)
    manifest = {
        "output_dir": "out",
        "jobs": [
            {"source": "src/photo_a.png", "output": "web_photo_a.jpg",
             "width": 200, "height": 150, "format": "JPEG", "quality": 80},
            {"source": "src/photo_b.jpg", "output": "web_photo_b.png",
             "width": 100, "height": 200, "format": "PNG"},
            {"source": "src/icon_c.png", "output": "web_icon_c.webp",
             "width": 50, "height": 50, "format": "WEBP", "quality": 90},
        ],
    }
    manifest_path = root / "manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
    print(f"fixture_dir: {root}")
    print(f"fixture_files: {len(specs)}")
    print(f"manifest: {manifest_path}")
    return 0


def load_jobs(manifest_path):
    manifest_path = Path(manifest_path)
    data = json.loads(manifest_path.read_text(encoding="utf-8"))
    base = manifest_path.parent
    output_dir = base / data.get("output_dir", "out")
    jobs = data["jobs"]
    return base, output_dir, jobs


def check_job(base, job):
    fmt = str(job["format"]).upper()
    if fmt not in ALLOWED_FORMATS:
        raise ValueError(f"unsupported format: {fmt}")
    width = int(job["width"])
    height = int(job["height"])
    if width <= 0 or height <= 0:
        raise ValueError("width and height must be positive")
    source = base / job["source"]
    if not source.is_file():
        raise FileNotFoundError(f"source missing: {source}")
    return source, width, height, fmt


def plan(manifest_path):
    base, output_dir, jobs = load_jobs(manifest_path)
    print(f"jobs: {len(jobs)}")
    print(f"output_dir: {output_dir}")
    for job in jobs:
        source, width, height, fmt = check_job(base, job)
        with Image.open(source) as im:
            print(
                f"PLAN source={job['source']} src_size={im.size[0]}x{im.size[1]} "
                f"src_format={im.format} output={job['output']} "
                f"size={width}x{height} format={fmt}"
            )
    print("dry_run: no files written")
    return 0


def run(manifest_path):
    base, output_dir, jobs = load_jobs(manifest_path)
    output_dir.mkdir(parents=True, exist_ok=True)
    log_path = output_dir / "results.jsonl"
    ok = 0
    failed = 0
    rows = []
    for job in jobs:
        try:
            source, width, height, fmt = check_job(base, job)
            with Image.open(source) as im:
                work = im.resize((width, height), Image.LANCZOS)
                if fmt == "JPEG" and work.mode not in ("RGB", "L"):
                    work = work.convert("RGB")
                dest = output_dir / job["output"]
                if fmt in ("JPEG", "WEBP") and "quality" in job:
                    work.save(dest, fmt, quality=int(job["quality"]))
                else:
                    work.save(dest, fmt)
            rows.append({"source": job["source"], "output": job["output"],
                         "width": width, "height": height, "format": fmt,
                         "status": "ok"})
            print(f"OK output={job['output']} size={width}x{height} format={fmt}")
            ok += 1
        except Exception as exc:
            rows.append({"source": job.get("source", "?"),
                         "output": job.get("output", "?"),
                         "status": "fail", "error": str(exc)})
            print(f"FAIL output={job.get('output', '?')} error={exc}")
            failed += 1
    with log_path.open("w", encoding="utf-8") as fh:
        for row in rows:
            fh.write(json.dumps(row, sort_keys=True) + "\n")
    print(f"results_log: {log_path}")
    print(f"done: {ok} ok, {failed} failed")
    return 0 if failed == 0 else 1


def verify(manifest_path):
    base, output_dir, jobs = load_jobs(manifest_path)
    passed = 0
    for job in jobs:
        fmt = str(job["format"]).upper()
        width = int(job["width"])
        height = int(job["height"])
        dest = output_dir / job["output"]
        if not dest.is_file():
            print(f"VERIFY output={job['output']} result=MISSING")
            continue
        with Image.open(dest) as im:
            actual_fmt = im.format
            actual_w, actual_h = im.size
        good = (actual_fmt == fmt and actual_w == width and actual_h == height)
        if good:
            passed += 1
        print(
            f"VERIFY output={job['output']} expected={width}x{height}/{fmt} "
            f"actual={actual_w}x{actual_h}/{actual_fmt} "
            f"result={'PASS' if good else 'FAIL'}"
        )
    print(f"verified: {passed}/{len(jobs)}")
    print(f"verdict: {'PASS' if passed == len(jobs) else 'FAIL'}")
    return 0 if passed == len(jobs) else 1


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--make-fixture", metavar="DIR")
    parser.add_argument("--manifest", metavar="PATH")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--run", action="store_true")
    parser.add_argument("--verify", action="store_true")
    args = parser.parse_args()
    if args.make_fixture:
        return build_fixture(args.make_fixture)
    if args.manifest and args.dry_run:
        return plan(args.manifest)
    if args.manifest and args.run:
        return run(args.manifest)
    if args.manifest and args.verify:
        return verify(args.manifest)
    parser.print_usage(sys.stderr)
    return 2


if __name__ == "__main__":
    raise SystemExit(main())

Versions

v1.0.02026-10-22 · 0 installs · Ed25519-signed./install.sh image-batch-processing-automation 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.