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.tsreadJpegOrientation(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.

Ingress inventory

PathHow orientation is handledStatus
Browser paste / drop / fileimage-paste.ts decodeOriented decodes with createImageBitmap(..., { imageOrientation: "from-image" }) (falling back to <img>, whose default is also from-image), then the canvas re-encode emits upright pixels with no tagNormalized
Browser cameracamera.ts draws a live getUserMedia video frame to a canvas — upright by construction, no EXIFNormalized
Screenshot capture / relayCanvas / PNG capture — no EXIFNormalized
Native camera & photo library (iOS)NativeComposerView.swift redraws the UIImage upright via UIGraphicsImageRenderer before JPEG/PNG compressionNormalized (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 visibleGuarded (detect, not fix)
Stored image files → <img> renderThe Image primitive renders through <img>, whose default image-orientation: from-image displays them correctlyHonored 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:

History: issues/closed/bugs/2026-07-17-image-orientation-exif-boundaries.md.