Show the photograph the way it was taken
Nothing read EXIF orientation, so every frame from a body held sideways lay on its side — in the grid, in develop, and in the read-only preview. The tag is honoured as part of *reading the file*, at the same standing as a RAW's masked-photosite crop, never as an edit. It lives as a baseline on Framing rather than as a starting value for quarter_turns, which is what keeps four things true: a sideways file opens unmodified, reset returns it to upright rather than to the sensor's scan order, its sidecar stays empty, and the rotate button still moves the image 90° whatever the file underneath it says. Framing::effective composes the baseline with the user's own turns through the group law rather than by adding turns and OR-ing flags. The naive version gets one case wrong — an odd baseline turn plus a user mirror — and gets it wrong quietly, because the result is still a plausible orientation. The composition collapses to a single permutation, so obeying the tag costs nothing per pixel. dr_decode::orientation is a header-only IFD walk, separate from metadata() for the reason the entry points are separate at all: the grid asks once per cell and must not build a rawler decoder to get one tag. CR3 and RAF fall back to the full read, being neither TIFF nor JPEG. Written down as FR-DEV-3h. Known gap: thumbnails cached before this stay sideways. The store is keyed by file and size, and its shards sync — invalidating them would have every client re-download 25 MB a shard, which is not this commit's call to make. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1332,6 +1332,13 @@ async fn fetch_one(
|
||||
collect_metadata(&header, req, found_metadata);
|
||||
}
|
||||
|
||||
// Read unconditionally, unlike the rest of the EXIF above: `needs_metadata`
|
||||
// is false once an image has been catalogued, but a thumbnail can still be
|
||||
// regenerated long after that — a cleared cache, a new size — and a
|
||||
// thumbnail that came out upright the first time must come out upright
|
||||
// every time. This is a header walk, not a decode; see `dr_decode::orientation`.
|
||||
let orientation = dr_decode::orientation(&header).unwrap_or_default();
|
||||
|
||||
// A plain JPEG is its own preview; anything else needs locating.
|
||||
let bytes = if header.starts_with(&[0xFF, 0xD8, 0xFF]) {
|
||||
match backend.get(&id, None).await {
|
||||
@@ -1368,6 +1375,14 @@ async fn fetch_one(
|
||||
Err(e) => return fail(e.to_string()),
|
||||
};
|
||||
preview.downscale_to(req.thumb_size.edge());
|
||||
// Turn it the right way up before it is measured, cached or shown. An
|
||||
// embedded preview is written in the sensor's orientation, so without this
|
||||
// every frame shot in portrait lies on its side in the grid — and, because
|
||||
// the cache is keyed by file and size alone, stays that way.
|
||||
//
|
||||
// After the downscale, so the permutation moves thumbnail-sized bytes
|
||||
// rather than the full preview's.
|
||||
preview.apply_orientation(orientation);
|
||||
|
||||
// Persist for next time, and for every other client that syncs the shard.
|
||||
// A store failure is logged and dropped: the pixels are already in hand,
|
||||
|
||||
Reference in New Issue
Block a user