Coverage for nexusLIMS/extractors/plugins/preview_generators/image_preview.py: 100%
62 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
1"""Image file preview generator."""
3import logging
4import shutil
5from pathlib import Path
6from typing import ClassVar, Tuple
8import matplotlib.pyplot as plt
9from PIL import Image, UnidentifiedImageError
11from nexusLIMS.extractors.base import ExtractionContext
13_logger = logging.getLogger(__name__)
15_LANCZOS = Image.Resampling.LANCZOS
18def _pad_to_square(im_path: Path, new_width: int = 500):
19 """
20 Pad an image to square.
22 Helper method to pad an image saved on disk to a square with size
23 ``width x width``. This ensures consistent display on the front-end web
24 page. Increasing the size of a dimension is done by padding with empty
25 space. The original image is overwritten.
27 Method adapted from:
28 https://jdhao.github.io/2017/11/06/resize-image-to-square-with-padding/
30 Parameters
31 ----------
32 im_path
33 The path to the image that should be resized/padded
34 new_width
35 Desired output width/height of the image (in pixels)
36 """
37 image = Image.open(im_path)
38 old_size = image.size # old_size[0] is in (width, height) format
39 ratio = float(new_width) / max(old_size)
40 new_size = tuple(int(x * ratio) for x in old_size)
41 image = image.resize(new_size, _LANCZOS)
43 new_im = Image.new("RGBA", (new_width, new_width))
44 new_im.paste(
45 image,
46 ((new_width - new_size[0]) // 2, (new_width - new_size[1]) // 2),
47 )
48 new_im.save(im_path)
51def image_to_square_thumbnail(f: Path, out_path: Path, output_size: int) -> bool:
52 """
53 Generate a preview thumbnail from a non-data image file.
55 Images of common filetypes will be transformed into 500 x 500 pixel images
56 by first scaling the largest dimension to 500 pixels and then padding the
57 resulting image to square.
59 Parameters
60 ----------
61 f
62 The string of the path of an image file for which a thumbnail should be
63 generated.
64 out_path
65 A path to the desired thumbnail filename. All formats supported by
66 :py:meth:`~PIL.Image.Image.save` can be used.
67 output_size
68 The desired resulting size of the thumbnail image.
70 Returns
71 -------
72 bool
73 Whether a preview was generated
74 """
75 shutil.copy(f, out_path)
76 try:
77 _pad_to_square(out_path, output_size)
78 except UnidentifiedImageError as exc:
79 _logger.warning("no preview generated; PIL error text: %s", str(exc))
80 out_path.unlink()
81 return False
83 return True
86def down_sample_image(
87 fname: Path,
88 out_path: Path,
89 output_size: Tuple[int, int] | None = None,
90 factor: int | None = None,
91):
92 """
93 Load an image file from disk, down-sample it to the requested dpi, and save.
95 Sometimes the data doesn't need to be loaded as a HyperSpy signal,
96 and it's better just to down-sample existing image data (such as for .tif
97 files created by the Quanta SEM).
99 Parameters
100 ----------
101 fname
102 The filepath that will be resized. All formats supported by
103 :py:func:`PIL.Image.open` can be used
104 out_path
105 A path to the desired thumbnail filename. All formats supported by
106 :py:meth:`PIL.Image.Image.save` can be used.
107 output_size
108 A tuple of ints specifying the width and height of the output image.
109 Either this argument or ``factor`` should be provided (not both).
110 factor
111 The multiple of the image size to reduce by (i.e. a value of 2
112 results in an image that is 50% of each original dimension). Either
113 this argument or ``output_size`` should be provided (not both).
114 """
115 if output_size is None and factor is None:
116 msg = "One of output_size or factor must be provided"
117 raise ValueError(msg)
118 if output_size is not None and factor is not None:
119 msg = "Only one of output_size or factor should be provided"
120 raise ValueError(msg)
122 image = Image.open(fname)
123 size = image.size
125 if output_size is not None:
126 resized = output_size
127 else:
128 resized = tuple(s // factor for s in size)
130 if "I" in image.mode:
131 image = image.point(lambda i: i * (1.0 / 256)).convert("L")
133 image.thumbnail(resized, resample=_LANCZOS)
134 image.save(out_path)
135 _pad_to_square(out_path, new_width=500)
137 plt.rcParams["image.cmap"] = "gray"
138 f = plt.figure()
139 f.gca().imshow(image)
141 return f
144class ImagePreviewGenerator:
145 """
146 Preview generator for standard image files.
148 This generator creates square thumbnail previews from image files
149 (PNG, JPEG, TIFF, BMP, GIF) using PIL/Pillow.
150 """
152 name = "image_preview"
153 priority = 100
154 supported_extensions: ClassVar = {
155 "png",
156 "jpg",
157 "jpeg",
158 "tiff",
159 "tif",
160 "bmp",
161 "gif",
162 }
164 def supports(self, context: ExtractionContext) -> bool:
165 """
166 Check if this generator supports the given file.
168 Parameters
169 ----------
170 context
171 The extraction context containing file information
173 Returns
174 -------
175 bool
176 True if file extension is a supported image format
177 """
178 extension = context.file_path.suffix.lower().lstrip(".")
179 return extension in self.supported_extensions
181 def generate(self, context: ExtractionContext, output_path: Path) -> bool:
182 """
183 Generate a square thumbnail preview from an image file.
185 Parameters
186 ----------
187 context
188 The extraction context containing file information
189 output_path
190 Path where the preview image should be saved
192 Returns
193 -------
194 bool
195 True if preview was successfully generated, False otherwise
196 """
197 try:
198 _logger.debug("Generating image preview for: %s", context.file_path)
200 # Generate the thumbnail using the local function
201 return image_to_square_thumbnail(
202 context.file_path,
203 output_path,
204 output_size=500,
205 )
207 except Exception as e:
208 _logger.warning(
209 "Failed to generate image preview for %s: %s",
210 context.file_path,
211 e,
212 )
213 return False