feat(imageset_thumbnails): split sheets into pages

This commit is contained in:
abdus committed 2026-10-08 11:27:31 +00:00
1 parent d572ca199f
commit 6eb0ab7d5a
1 file changed
+49 -18
+49 -18
View File
@@ -59,6 +59,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--output", type=Path, help="JPEG output for a single existing input; defaults to <input-name>_thumbs.jpg beside each input")
parser.add_argument("--width", type=int, default=2500, help="Sheet width in pixels")
parser.add_argument("--target-height", type=int, default=300, help="Preferred row height in pixels; rows may be taller or shorter")
parser.add_argument("--max-rows-per-page", type=int, help="Maximum rows per output image; additional rows are written to numbered pages")
parser.add_argument("--gap", type=int, default=8, help="Gap between images and at sheet edges in pixels")
parser.add_argument("--bg-color", default="white", help="Background color as a color name or hex value")
parser.add_argument("--workers", type=int, default=os.cpu_count() or 1, help="Number of resize worker processes")
@@ -69,6 +70,8 @@ def parse_args() -> argparse.Namespace:
parser.error("width must exceed twice the gap")
if arguments.workers <= 0:
parser.error("workers must be positive")
if arguments.max_rows_per_page is not None and arguments.max_rows_per_page <= 0:
parser.error("maximum rows per page must be positive")
try:
ImageColor.getcolor(color=arguments.bg_color, mode="RGB")
except ValueError as error:
@@ -141,7 +144,14 @@ def discover_sources(input_path: Path, output_path: Path, archive: ZipFile | Non
candidates = [
Source(name=path.relative_to(input_path).as_posix(), path=path)
for path in input_path.rglob("*")
if path.is_file() and path.suffix.lower() in extensions and path.resolve() != output_path.resolve()
if path.is_file()
and path.suffix.lower() in extensions
and path.resolve() != output_path.resolve()
and not (
path.parent.resolve() == output_path.parent.resolve()
and path.suffix.lower() == output_path.suffix.lower()
and re.fullmatch(pattern=rf"{re.escape(output_path.stem)}_page_[0-9]+", string=path.stem)
)
]
sources = []
logging.info(f"Reading metadata for {len(candidates)} images")
@@ -331,6 +341,7 @@ def create_sheet(
gap: int,
bg_color: str = "white",
workers: int | None = None,
max_rows_per_page: int | None = None,
processor: Callable[..., bytes] = resize_with_pillow,
) -> None:
if width <= 0 or target_height <= 0 or gap < 0 or width <= 2 * gap:
@@ -343,30 +354,49 @@ def create_sheet(
raise ValueError("Output must differ from input")
if workers is not None and workers <= 0:
raise ValueError("Workers must be positive")
if max_rows_per_page is not None and max_rows_per_page <= 0:
raise ValueError("Maximum rows per page must be positive")
background = ImageColor.getcolor(color=bg_color, mode="RGB")
with ExitStack() as stack:
archive = None if input_path.is_dir() else stack.enter_context(ZipFile(file=input_path))
sources = discover_sources(input_path=input_path, output_path=output_path, archive=archive)
rows = layout_rows(sources=sources, width=width, gap=gap, target_height=target_height)
height = sum(row.height for row in rows) + gap * (len(rows) + 1)
worker_count = min(workers if workers is not None else os.cpu_count() or 1, len(sources))
logging.info(f"Rendering {len(sources)} images in {len(rows)} rows on a {width}x{height} sheet using {worker_count} workers")
sheet = stack.enter_context(Image.new(mode="RGB", size=(width, height), color=background))
started = perf_counter()
rendered_count = render_rows(
sheet=sheet,
rows=rows,
input_path=input_path,
workers=worker_count,
gap=gap,
processor=processor,
)
if not rendered_count:
page_size = max_rows_per_page or len(rows)
total_rendered = 0
page_count = (len(rows) + page_size - 1) // page_size
for page_index, start in enumerate(range(0, len(rows), page_size), start=1):
page_rows = rows[start : start + page_size]
page_sources = sum(len(row.sources) for row in page_rows)
height = sum(row.height for row in page_rows) + gap * (len(page_rows) + 1)
page_output = output_path if page_index == 1 else output_path.with_name(
f"{output_path.stem}_page_{page_index}{output_path.suffix}"
)
logging.info(
f"Rendering page {page_index}/{page_count}: {page_sources} images in {len(page_rows)} rows "
f"on a {width}x{height} sheet using {worker_count} workers"
)
started = perf_counter()
with Image.new(mode="RGB", size=(width, height), color=background) as sheet:
rendered_count = render_rows(
sheet=sheet,
rows=page_rows,
input_path=input_path,
workers=worker_count,
gap=gap,
processor=processor,
)
if not rendered_count:
logging.warning(f"Skipping empty page {page_index}: no images could be decoded")
continue
total_rendered += rendered_count
logging.info(f"Rendering finished in {perf_counter() - started:.1f}s; saving {page_output}")
page_output.parent.mkdir(parents=True, exist_ok=True)
sheet.save(fp=page_output, quality=95)
logging.info(f"Wrote {page_output} ({rendered_count} images, {width}x{height})")
if not total_rendered:
raise ValueError("No usable images could be decoded")
logging.info(f"Rendering finished in {perf_counter() - started:.1f}s; saving {output_path}")
output_path.parent.mkdir(parents=True, exist_ok=True)
sheet.save(fp=output_path, quality=95)
logging.info(f"Wrote {output_path} ({rendered_count} images, {width}x{height})")
logging.info(f"Wrote {total_rendered} images across {page_count} page(s)")
def main() -> None:
@@ -400,6 +430,7 @@ def main() -> None:
gap=arguments.gap,
bg_color=arguments.bg_color,
workers=arguments.workers,
max_rows_per_page=arguments.max_rows_per_page,
processor=processor,
)
except (OSError, ValueError, RuntimeError, BadZipFile) as error: