feat(imageset_thumbnails): split sheets into pages
This commit is contained in:
1 parent
d572ca199f
commit
6eb0ab7d5a
1 file changed
+49
-18
+49
-18
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user