"""Image file preview generator."""
import logging
import shutil
from pathlib import Path
from typing import ClassVar, Tuple
import matplotlib.pyplot as plt
from PIL import Image, UnidentifiedImageError
from nexusLIMS.extractors.base import ExtractionContext
_logger = logging.getLogger(__name__)
_LANCZOS = Image.Resampling.LANCZOS
def _pad_to_square(im_path: Path, new_width: int = 500):
"""
Pad an image to square.
Helper method to pad an image saved on disk to a square with size
``width x width``. This ensures consistent display on the front-end web
page. Increasing the size of a dimension is done by padding with empty
space. The original image is overwritten.
Method adapted from:
https://jdhao.github.io/2017/11/06/resize-image-to-square-with-padding/
Parameters
----------
im_path
The path to the image that should be resized/padded
new_width
Desired output width/height of the image (in pixels)
"""
image = Image.open(im_path)
old_size = image.size # old_size[0] is in (width, height) format
ratio = float(new_width) / max(old_size)
new_size = tuple(int(x * ratio) for x in old_size)
image = image.resize(new_size, _LANCZOS)
new_im = Image.new("RGBA", (new_width, new_width))
new_im.paste(
image,
((new_width - new_size[0]) // 2, (new_width - new_size[1]) // 2),
)
new_im.save(im_path)
[docs]
def image_to_square_thumbnail(f: Path, out_path: Path, output_size: int) -> bool:
"""
Generate a preview thumbnail from a non-data image file.
Images of common filetypes will be transformed into 500 x 500 pixel images
by first scaling the largest dimension to 500 pixels and then padding the
resulting image to square.
Parameters
----------
f
The string of the path of an image file for which a thumbnail should be
generated.
out_path
A path to the desired thumbnail filename. All formats supported by
:py:meth:`~PIL.Image.Image.save` can be used.
output_size
The desired resulting size of the thumbnail image.
Returns
-------
bool
Whether a preview was generated
"""
shutil.copy(f, out_path)
try:
_pad_to_square(out_path, output_size)
except UnidentifiedImageError as exc:
_logger.warning("no preview generated; PIL error text: %s", str(exc))
out_path.unlink()
return False
return True
[docs]
def down_sample_image(
fname: Path,
out_path: Path,
output_size: Tuple[int, int] | None = None,
factor: int | None = None,
):
"""
Load an image file from disk, down-sample it to the requested dpi, and save.
Sometimes the data doesn't need to be loaded as a HyperSpy signal,
and it's better just to down-sample existing image data (such as for .tif
files created by the Quanta SEM).
Parameters
----------
fname
The filepath that will be resized. All formats supported by
:py:func:`PIL.Image.open` can be used
out_path
A path to the desired thumbnail filename. All formats supported by
:py:meth:`PIL.Image.Image.save` can be used.
output_size
A tuple of ints specifying the width and height of the output image.
Either this argument or ``factor`` should be provided (not both).
factor
The multiple of the image size to reduce by (i.e. a value of 2
results in an image that is 50% of each original dimension). Either
this argument or ``output_size`` should be provided (not both).
"""
if output_size is None and factor is None:
msg = "One of output_size or factor must be provided"
raise ValueError(msg)
if output_size is not None and factor is not None:
msg = "Only one of output_size or factor should be provided"
raise ValueError(msg)
image = Image.open(fname)
size = image.size
if output_size is not None:
resized = output_size
else:
resized = tuple(s // factor for s in size)
if "I" in image.mode:
image = image.point(lambda i: i * (1.0 / 256)).convert("L")
image.thumbnail(resized, resample=_LANCZOS)
image.save(out_path)
_pad_to_square(out_path, new_width=500)
plt.rcParams["image.cmap"] = "gray"
f = plt.figure()
f.gca().imshow(image)
return f
[docs]
class ImagePreviewGenerator:
"""
Preview generator for standard image files.
This generator creates square thumbnail previews from image files
(PNG, JPEG, TIFF, BMP, GIF) using PIL/Pillow.
"""
name = "image_preview"
priority = 100
supported_extensions: ClassVar = {
"png",
"jpg",
"jpeg",
"tiff",
"tif",
"bmp",
"gif",
}
[docs]
def supports(self, context: ExtractionContext) -> bool:
"""
Check if this generator supports the given file.
Parameters
----------
context
The extraction context containing file information
Returns
-------
bool
True if file extension is a supported image format
"""
extension = context.file_path.suffix.lower().lstrip(".")
return extension in self.supported_extensions
[docs]
def generate(self, context: ExtractionContext, output_path: Path) -> bool:
"""
Generate a square thumbnail preview from an image file.
Parameters
----------
context
The extraction context containing file information
output_path
Path where the preview image should be saved
Returns
-------
bool
True if preview was successfully generated, False otherwise
"""
try:
_logger.debug("Generating image preview for: %s", context.file_path)
# Generate the thumbnail using the local function
return image_to_square_thumbnail(
context.file_path,
output_path,
output_size=500,
)
except Exception as e:
_logger.warning(
"Failed to generate image preview for %s: %s",
context.file_path,
e,
)
return False