974690132e
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.
73 lines
2.5 KiB
Python
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
|