"""Icon storage shared by printer icons and driver icons. Both live in `ICONS_DIR`, content-addressed by the SHA256 of the *normalized* bytes, so two entities that end up with the same 256x256 PNG share one file. """ from __future__ import annotations import hashlib from pathlib import Path import imptune.config as cfg from imptune.db.models import Driver, DriverIcon, Icon from imptune.services.image_utils import ImageError, normalize_icon MAX_ICON_BYTES = 750 * 1024 # 750 KB, on the bytes as uploaded __all__ = [ "ImageError", "IconTooLarge", "MAX_ICON_BYTES", "driver_icon_ids", "icon_path", "set_driver_icon", "set_printer_icon", "store_bytes", ] class IconTooLarge(ValueError): """Raised when the source file is over `MAX_ICON_BYTES`.""" def icon_path(sha256: str) -> Path: """On-disk location of a stored icon. Read `cfg` late — tests patch it.""" return Path(cfg.ICONS_DIR) / sha256 def store_bytes(data: bytes) -> tuple[str, int]: """Normalize `data` to a 256x256 PNG, write it, return `(sha256, size)`. Raises `IconTooLarge` or `ImageError` before anything touches the disk. """ if len(data) > MAX_ICON_BYTES: raise IconTooLarge(f"Icon exceeds {MAX_ICON_BYTES // 1024} KB limit.") png = normalize_icon(data) sha256 = hashlib.sha256(png).hexdigest() path = icon_path(sha256) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(png) return sha256, len(png) def set_printer_icon(printer_id: int, data: bytes, filename: str) -> Icon: """Replace this printer's icon. One `Icon` row per printer, always.""" sha256, size = store_bytes(data) Icon.delete().where(Icon.printer == printer_id).execute() return Icon.create( printer=printer_id, sha256=sha256, original_filename=filename or "icon.png", size_bytes=size, ) def set_driver_icon(driver_id: int, data: bytes, filename: str) -> DriverIcon: """Replace this driver's icon. Driver rows are global, so this icon is too.""" sha256, size = store_bytes(data) DriverIcon.delete().where(DriverIcon.driver == driver_id).execute() return DriverIcon.create( driver=driver_id, sha256=sha256, original_filename=filename or "icon.png", size_bytes=size, ) def driver_icon_ids(drivers=None) -> set[int]: """Ids of drivers that have an icon, for the library listing.""" query = DriverIcon.select(DriverIcon.driver) if drivers is not None: ids = [d.id for d in drivers] or [0] query = query.where(DriverIcon.driver.in_(ids)) return {row.driver_id for row in query} def driver_search_text(driver: Driver, names: list[str]) -> str: """Best guess at what this driver is, for prefilling a search box.""" if driver.display_name: return driver.display_name if names: return names[0] return Path(driver.original_filename).stem.replace("_", " ").replace("-", " ")