# Enquiry photo formats — v0.20 local checkpoint

30 October 2026. Public enquiries now support content-validated HEIC/HEIF still images alongside PNG, JPEG and WebP, up to five files at **10 MiB each / 50 MiB combined**. The interface uses the original SHS “MB” label; one file's exact allowance is **10,485,760 bytes**. This is a local implementation and automated verification checkpoint. The browser file-picker and real-phone journeys remain unverified.

## Original SHS evidence

The supplied export's `frontend/src/pages/EnquiryPage.jsx:99–135` stops at five uploaded references, uploads files individually and retries through a legacy route after any main-route failure. Its input accepts `image/*,.heic,.heif` at line 357; line 372 advertises JPEG, PNG, HEIC and WebP at 10 MB each. The handler has no byte check.

`backend/routers/files.py:52–79` enforces 10 × 1,024 × 1,024 bytes on each main-route upload after reading it. `backend/storage.py:79–119` uses extension/MIME hints, registers pillow-heif for conversion, applies rotation and usually reduces the long edge to 2,000 pixels when conversion/resizing is needed. It passes GIF through, may pass small ordinary images and non-images through, and returns the original file when conversion fails. It does not explicitly bound pixels or frames. `backend/server.py:920–972` shows a legacy local/Drive fallback without a size check or image conversion in that handler. `backend/models.py:9–20` accepts photo-reference lists without a count/total validator.

There is no explicit combined-byte allowance in those supplied handlers. Five advertised full-size files imply a useful 50 MiB journey; v0.20 accepts that amount in a single private transaction. This is a deliberate derived total, not a claim that the original server enforced it. Complete route registration, proxy limits, native runtime and deployed behaviour cannot be established from this filtered archive. The original modules and their remote tests were never imported or executed. Exact hashes, line ranges and distinctions are in [the source map](enquiry-format-source-map.json).

## Useful supported path and limits

- The server decodes actual bytes, independent of filename or claimed MIME. A renamed non-image, unreadable/truncated file, unsupported codec or failed conversion rejects the whole enquiry before any row or photo is saved.
- PNG, JPEG and WebP retain their previous path and previews. HEVC-coded HEIC/HEIF still images are converted locally; HEIF selection does not require a browser decoder. The form shows the filename and explains that the image will be checked on submission. It does not pretend that a selected HEIF file has already uploaded or passed server checks.
- Every input remains limited to **12 megapixels and 12,000 pixels per side**. Higher-resolution phone images, AVIF, sequences, animations and files containing multiple main images are rejected. No first-frame-only success is reported. HEIF auxiliary/depth images and thumbnails are not exported. No support claim is made for every phone variant, Live Photo pair or HDR colour fidelity.
- Stored output is a new RGBA PNG, with rotation applied, a maximum 1,800-pixel long edge, and no original EXIF, GPS, XMP, camera or colour-profile metadata. Ten-/twelve-bit examples become eight-bit output. Original name, detected format, byte count, SHA-256 and order remain immutable owner-only intake facts; raw originals are not retained.
- Five 10 MiB originals fit the 70 MiB encoded request cap. Normalized output allows 13 MiB per file and 65 MiB per enquiry, enough for five worst-case 1,800 × 1,800 RGBA PNGs; it does not reintroduce the old 20 MiB aggregate restriction. A fresh business can accept a full batch. The existing 1,000-photo / 128 MiB business storage quota remains explicit; exceeding it saves no partial enquiry.
- At most two anonymous enquiry bodies and two decoders are active per app process. Each file is decoded in a short-lived child, with 20 seconds wall time, 10 CPU seconds, output-file limits and a sampled 768 MiB resident-memory threshold. Linux also applies a 1 GiB address-space limit. macOS memory sampling can overshoot between checks; this is not a hard OS memory guarantee or production-load certification. Decoder libraries keep their own security limits enabled and use one decoding thread.

The authenticated owner/staff photo and website-asset upload contracts are unchanged. HEIF registration is confined to the enquiry decoder child. No external converter, anonymous media URL, public token, contact-matching relink or new permission is introduced.

## Privacy, failure and retry

Photos bind to the exact newly created enquiry/job/customer in one handled transaction. An owner must review visitor photos before assigned staff see them. Customer sharing and gallery publication still need their separate explicit decisions. The preferred date remains a preference rather than a booking.

All files pass conversion before writes begin. Handled file/transaction failures remove only the operation's exclusively created files and roll back rows. The browser keeps text and selected bytes after rejection; an uncertain response freezes editing and retries the exact original body/key. The original-intake record protects retry identity even after the general operation cache is pruned or the dataset is restored. Crash-orphan cleanup, power-loss certification, physical-device memory behaviour and production storage/load policy remain open.

## Dependency and recovery changes

The new pins are **pillow-heif 1.8.1** and **psutil 7.2.2**. Testing used a separate `/private/tmp/shs-v20-venv` and cache; Sandy's `/private/tmp/shs-v19-venv` and frozen runtimes were not changed. The tested macOS arm64 wheel reports libheif 1.23.6 and libde265 1.1.3, and includes x265 4.3 for fixture encoding. No installed dependencies or native binaries are bundled in the code ZIP.

pillow-heif's Python source has a BSD-3-Clause notice. Its binary-wheel notice identifies LGPL components and a GPLv2 x265 component; psutil carries a BSD-3-Clause notice. Those notices and the exact tested wheel hashes are retained in [dependency provenance](enquiry-format-dependencies.json). A commercial/native redistribution choice and its licence obligations remain a delivery decision; this local checkpoint does not resolve them. Primary references: [pillow-heif release](https://github.com/bigcat88/pillow_heif/releases/tag/v1.8.1), [bundled notices](https://github.com/bigcat88/pillow_heif/blob/v1.8.1/LICENSES_bundled.txt), [decoder options](https://pillow-heif.readthedocs.io/en/stable/options.html), [psutil](https://pypi.org/project/psutil/7.2.2/).

The database schema and recovery **format 12** are unchanged. v0.20 writes backup implementation metadata `0.20.0` and reads both `0.19.0` and `0.20.0` format-12 archives, plus the pinned formats 1–11. Older v0.19 code cannot validate the new HEIF/size contract; its rejection of a v0.20 backup is intentional. Restore new backups using v0.20 or a compatible newer verifier. Restored visitor-photo review/customer sharing is suspended and associated public projects are withdrawn, while original facts and normalized bytes remain preserved.

## Verification and practical trial

Final v0.20 verification: **694 backend tests, 156 client tests, 88 structural render checks and the build passed**. Ten recovery reports cover the new format and the existing customer, document, portal, feedback, enquiry, field, recurrence and workflow families. The new-format disappearance drill restored exact original facts and normalized images, revoked old access, suspended sharing/review and preserved retry identity. Browser and phone proof remain separate and unverified. See [checks](enquiry-format-milestone-checks.json) and [recovery evidence](recovery-v0.20/enquiry-formats-recovery-drill.json).

See [version-specific checks](enquiry-format-milestone-checks.json), `test-results-v0.20.xml`, and the recovery evidence beside this guide. Synthetic fixtures are actually HEVC encoded with libheif/x265. They cover a still image, a `mif1` HEIF brand, tiled content, 10-/12-bit content, container rotation plus fictitious GPS/XMP, and multiple main images. Legal PNG ancillary chunks and HEIF free-space boxes create readable exact-byte boundary files. Separately, seeded random RGB pixels exercise large encoded content, resizing and a five-image converted total above the old restriction. No fixture comes from a customer's camera, and header-only sequence/AVIF rejection tests do not prove support for those formats.

The v0.19.1 final package passed eleven additional browser workflow groups on 30 October. Selected-file retention/Clear/discard and older QA-tab cleanup remain unverified after the upload tool continued rejecting the setting Sandy had enabled. No further setting request is pending. Those controls will not be repeatedly probed or replaced with seeded browser state. The v0.20 file-picker and phone journey also remain pending; structural renders and API tests do not close them. See [the updated leave-guard guide](OWNER-LEAVE-GUARD.md).

For a separate fictional trial, extract this release to a new folder, use Python 3.12, and create a new environment:

```sh
python3 -m venv .venv-v020
.venv-v020/bin/python -m pip install -r requirements.lock
.venv-v020/bin/python run.py --port 8791 --data-dir .local-data-v020-trial
```

Check that the chosen port is free first. Use a separate browser profile; different ports do not isolate cookies. Do not use Sandy's review tabs or data, and do not stop/restart the review at `localhost:8789`. This guide does not imply that a v0.20 server is already running. The supplied frontend is built. All external services remain disabled or mocked.

All 45 capabilities remain tracked and all 14 original SHS findings remain OPEN. The gallery-permission journey, staff/PWA/device coverage, production integrations and live approval remain queued.
