Coverage for imcluster/thumbnails.py: 100.00%
23 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-12 01:58 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-12 01:58 +0000
1"""Thumbnail generation for cluster reports."""
3import base64
4from io import BytesIO
5from pathlib import Path
7from PIL import Image, ImageOps, UnidentifiedImageError
8from rich.console import Console
10from .io import ImclusterIO
12console = Console()
15def generate_thumbnail(path: str | Path, width: int, height: int) -> str:
16 """Create a base64-encoded JPEG thumbnail bounded by given dimensions.
18 Args:
19 path: Source image path.
20 width: Maximum thumbnail width in pixels.
21 height: Maximum thumbnail height in pixels.
23 Returns:
24 ASCII base64 data for the generated JPEG.
25 """
26 try:
27 with Image.open(path) as source:
28 image = ImageOps.exif_transpose(source).convert("RGB")
29 image.thumbnail((width, height), Image.Resampling.LANCZOS)
30 buffered = BytesIO()
31 image.save(buffered, format="JPEG")
32 except (OSError, UnidentifiedImageError) as error:
33 raise ValueError(f"Cannot create thumbnail for '{path}': {error}") from error
34 return base64.b64encode(buffered.getvalue()).decode("ascii")
37def generate_thumbnails(
38 imcluster_io: ImclusterIO,
39 thumbnail_width: int = 256,
40 thumbnail_height: int = 256,
41 force: bool = False,
42 force_thumbnails: bool = False,
43) -> None:
44 """Generate and cache thumbnails used by the HTML cluster report.
46 Args:
47 imcluster_io: Image collection and its persisted result table.
48 thumbnail_width: Maximum thumbnail width in pixels.
49 thumbnail_height: Maximum thumbnail height in pixels.
50 force: Regenerate thumbnails regardless of cached data.
51 force_thumbnails: Regenerate only the thumbnail cache.
52 """
54 if not imcluster_io.has_column("thumbnail") or force or force_thumbnails:
55 console.print(
56 "[cyan]Generating thumbnails:[/cyan] "
57 f"{len(imcluster_io.images)} images, maximum size "
58 f"{thumbnail_width}x{thumbnail_height}; caching results in "
59 f"'{imcluster_io.output}'."
60 )
61 with console.status("[cyan]Creating and caching thumbnails...[/cyan]"):
62 imcluster_io.save_column(
63 "thumbnail",
64 imcluster_io.df.apply(
65 lambda row: generate_thumbnail(
66 row["path"],
67 thumbnail_width,
68 thumbnail_height,
69 ),
70 axis=1,
71 ),
72 )
73 else:
74 console.print(
75 "[green]Using cached thumbnails:[/green] "
76 f"loaded {len(imcluster_io.images)} thumbnails from "
77 f"'{imcluster_io.output}'."
78 )