Bee Box documentation · directory: https://beebox.run/docs/dev/ · index: https://beebox.run/docs/dev/index.md · root: https://beebox.run/llms.txt # Image orientation contract Photos carry an EXIF **orientation** tag (values 1–8: rotations and mirrors) so a camera can store sensor-native pixels and let the viewer rotate them. That only works if *every* downstream decoder honors the tag. Images enter a box through several routes — some re-render pixels, others forward the source bytes — so a tag that one boundary drops makes a photo that looked upright at capture render rotated later (the bug this contract closes). ## The invariant **An image at rest inside a box is orientation-normalized:** its pixels are already upright and it carries no non-trivial EXIF orientation (value `1`, or none). Every path that *transcodes* an image must bake the orientation into the pixels and emit a value-`1` / absent tag. The oracle is [`src/shared/image-orientation.ts`](../src/shared/image-orientation.ts) — `readJpegOrientation(bytes)` returns the EXIF orientation (1–8), and `isOrientationNormalized(bytes)` is the contract predicate. It is pure and shared by the backend and frontend. Tested across all eight values (both TIFF byte orders) and hostile inputs in [`test/shared/image-orientation.doctest.md`](../test/shared/image-orientation.doctest.md). ## Ingress inventory | Path | How orientation is handled | Status | |------|----------------------------|--------| | Browser paste / drop / file | `image-paste.ts` `decodeOriented` decodes with `createImageBitmap(..., { imageOrientation: "from-image" })` (falling back to ``, whose default is also from-image), then the canvas re-encode emits upright pixels with no tag | Normalized | | Browser camera | `camera.ts` draws a live `getUserMedia` video frame to a canvas — upright by construction, no EXIF | Normalized | | Screenshot capture / relay | Canvas / PNG capture — no EXIF | Normalized | | Native camera & photo library (iOS) | `NativeComposerView.swift` redraws the `UIImage` upright via `UIGraphicsImageRenderer` before JPEG/PNG compression | Normalized (Swift) | | Chat send (server ingress) | No codec server-side; `warnOnUnnormalizedImageOrientation` (`chat-helpers.ts`) reads the EXIF tag and **logs** a contract violation so a non-conforming client is visible | Guarded (detect, not fix) | | Stored image files → `` render | The `Image` primitive renders through ``, whose default `image-orientation: from-image` displays them correctly | Honored at render | ## What is deliberately left (see the closed issue) The server has **no image codec** (no `sharp`/`jimp`), so it cannot re-render pixels — it can only detect and warn. Two gaps remain, both out of scope for a pure-TS change: - **Pre-existing stored files handed to the model.** A JPEG already in a box that carries an orientation tag, sent to the model as raw bytes, depends on the model honoring EXIF. Normalizing those needs a server-side codec (a future intake step, tracked with the PNG→WebP archival idea). - **Real-browser / real-device coverage.** The reader is unit-tested for all eight values, but end-to-end coverage of each orientation through a live browser canvas and a physical iPhone can only be confirmed by hand. History: `issues/closed/bugs/2026-07-17-image-orientation-exif-boundaries.md`.