import os from typing import NamedTuple from PIL import Image pillow_extensions = { ext for ext, f in Image.registered_extensions().items() if f in Image.OPEN } class ThumbnailParameters(NamedTuple): bounds: tuple[int, int] options: dict def params_from(config): return { "full": ThumbnailParameters( bounds=( config.getint("import.scale.full", "width", fallback=4200), config.getint("import.scale.full", "height", fallback=2000), ), options={"quality": 82, "method": 5}, ), "thumb": ThumbnailParameters( bounds=( config.getint("import.scale.thumb", "width", fallback=1680), config.getint("import.scale.thumb", "height", fallback=800), ), options={"quality": 75, "method": 5}, ), } def object_path(directory, hash, suffix): return os.path.join(directory, hash[:2], f"{hash[2:]}_{suffix}.webp") class Thumbnailer: def __init__(self, directory, params): self.directory = directory self.params = params @classmethod def can_process(cls, extension): return extension in pillow_extensions def object(self, hash, suffix): return object_path(self.directory, hash, suffix) def process(self, handle, hash, reprocess=False): size = None for suffix, parameters in self.params.items(): source = Image.open(handle, mode="r") if not size: size = source.size output = self.object(hash, suffix) if os.path.exists(output) and not reprocess: continue else: os.makedirs(os.path.dirname(output), exist_ok=True) if source.mode != "RGB": target = source.convert() else: target = source target.thumbnail(parameters.bounds, resample=Image.Resampling.LANCZOS) target.save(output, **parameters.options) return size