Files
instameex/src/script/app/validation.py
T
Djeex 974690132e Reorganize into src/ with script/assets/templates, illustration/ at root
New layout:
  src/script/app/    Python package (was app/)
  src/assets/img/    SVGs (was static/*.svg)
  src/assets/css/    stylesheet (was static/style.css)
  src/templates/     HTML templates (was templates/)
  illustration/      README screenshot (was screenshots/), kept at
                      the repo root since it's a doc asset, not
                      something the app serves
  main.py, jobs/, VERSION, docker/  unchanged locations

main.py stays at the repo root as the entry point (matches how
Docker and local runs already invoke it) and prepends src/script to
sys.path before importing app, rather than moving into src/script/
itself and needing every entry point updated to match.

config.py's path constants are recomputed for app/config.py's new
depth (src/script/app/), with SRC_DIR and REPO_ROOT resolved
explicitly rather than a single ambiguous BASE_DIR, since "templates
and assets live under src/" and "jobs and VERSION live at the repo
root" are no longer the same directory. Flask's static_folder now
points at src/assets, which serves img/ and css/ under it at
whatever URL prefix Flask derives from the folder name (/assets, not
/static); templates already reference assets via the "static"
endpoint name through url_for, so this needed the url_for() calls'
filename argument updated with the img/ or css/ prefix, not a
hardcoded URL change.

Docker copies src/ into the image preserving this same internal
structure, so the path arithmetic in config.py resolves identically
in both a local checkout and the container, no --chdir or other
container-specific path handling needed.

Verified end to end after the move: local Flask dev server, a full
Docker rebuild, and a real HTTP conversion request through the
running container (TIFF HDR pipeline, numpy/tifffile imports
included) all succeed.
2026-08-01 15:26:18 +02:00

73 lines
2.5 KiB
Python

"""Upload validation: file extensions and Instagram's exact feed resolutions."""
from pathlib import Path
import tifffile
from PIL import Image, UnidentifiedImageError
from .config import ALLOWED_HDR_EXT, ALLOWED_SDR_EXT, IG_RESOLUTIONS, TIFF_EXT
def ext(filename: str) -> str:
return Path(filename).suffix.lower()
def is_tiff(filename: str) -> bool:
return ext(filename) in TIFF_EXT
def sdr_ext_error(filename: str) -> str | None:
if ext(filename) not in ALLOWED_SDR_EXT:
return "The SDR file must be a .jpg/.jpeg."
return None
def hdr_ext_error(filename: str) -> str | None:
if ext(filename) not in ALLOWED_HDR_EXT:
return "The HDR file must be a .jpg/.jpeg (existing gain map) or a 32-bit float .tif/.tiff."
return None
def probe_dimensions(path: Path) -> tuple[int, int]:
if is_tiff(path.name):
try:
with tifffile.TiffFile(path) as tif:
page = tif.pages[0]
return (page.imagewidth, page.imagelength)
except Exception as e:
raise ValueError(f"could not read TIFF dimensions ({e})") from e
try:
with Image.open(path) as img:
return img.size
except (UnidentifiedImageError, OSError) as e:
raise ValueError(f"could not read image dimensions ({e})") from e
def instagram_resolution_error(
sdr_path: Path, hdr_path: Path, ignore_ig_resolution: bool = False,
) -> str | None:
"""Returns an error message if the pair doesn't meet Instagram's feed
requirements, otherwise None.
The dimension-match check always applies: mismatched SDR/HDR
dimensions misalign the gain map regardless of what platform the
result is for. ignore_ig_resolution skips only the exact-match check
against Instagram's own feed resolutions, for results meant for
somewhere other than Instagram."""
try:
sdr_w, sdr_h = probe_dimensions(sdr_path)
hdr_w, hdr_h = probe_dimensions(hdr_path)
except ValueError as e:
return f"Could not validate image dimensions: {e}."
if (sdr_w, sdr_h) != (hdr_w, hdr_h):
return (
f"SDR ({sdr_w}x{sdr_h}) and HDR ({hdr_w}x{hdr_h}) must have identical "
"dimensions, otherwise the gain map will misalign."
)
if not ignore_ig_resolution and (sdr_w, sdr_h) not in IG_RESOLUTIONS:
allowed = ", ".join(f"{w}x{h} ({label})" for (w, h), label in IG_RESOLUTIONS.items())
return f"Image is {sdr_w}x{sdr_h} — Instagram requires an exact match to one of: {allowed}."
return None