Skip to content

feat(rotate): turn a JPEG from the viewer and keep the turn - #49

Merged
skjnldsv merged 1 commit into
mainfrom
feat/rotate-jpeg
Sep 18, 2026
Merged

skjnldsv merged 1 commit into
mainfrom
feat/rotate-jpeg

Conversation

@skjnldsv

Copy link
Copy Markdown
Contributor

A rotate button ahead of the edit button turns the picture on screen at once, and the turn is written to the file about a second later by rewriting its Exif orientation tag. Nothing is decoded and nothing is re-encoded, so the picture is the same picture however many times it is turned, and the scan is copied byte for byte.

That tag is what fixOrientation() reads during preview generation (lib/private/Preview/Image.php:42 in server), and a write clears the cached previews (lib/private/Preview/Watcher.php:37), so one PUT carries the new framing into the Files grid, Photos and the mobile clients rather than leaving it a viewer-only illusion.

Relates to nextcloud/viewer#2804, and picks up nextcloud/viewer#3190 by @vladopol, who proposed this in June and waited three and a half months on a repo we then froze. Their PR is against a branch that can no longer take it, and its rotation is visual only, but the idea and the shape of the toolbar change are theirs, so the commit carries their Co-authored-by.

JPEG only, and only where the user may write. No other format carries an orientation the stack honours: lib/private/Image.php:413 returns -1 for anything that is not IMAGETYPE_JPEG, so a PNG or WebP would need a hard rotation and a re-encode. That is written up with measurements in #47. The button is hidden rather than shown-and-failing, because a turn that silently does not stick is worse than no turn.

Written once the user settles, not once per click. apps/files_versions/lib/Storage.php:163 creates a version on every write with no interval guard, so four clicks would put four copies of a 7MB photo in the history on the way round a full circle. The composable waits 1.2s after the last turn, flushes on navigate and on close, and writes nothing at all when the net turn is zero.

The etag is deliberately not bumped after a write. It is part of the preview URL, so moving it reloads the element to a freshly turned preview while the CSS turn is still applied, and the picture appears to turn twice. The composable keeps its own etag for the next If-Match instead, and the element keeps showing the turn until the next time the file is opened, when the regenerated preview is already upright.

The fit, which is the part worth reviewing

Rotating the element alone is not enough. A landscape photo fitted to a landscape frame and then turned on its side is taller than the frame, and CSS transforms do not reflow, so it spills over the chrome above and below. The fit is now worked out against the box the picture will occupy on screen rather than its own, and zoom and pan are measured against the same box.

I reverted just that one hunk to check the test earns its place. With the fit left alone, the 3500x2335 fixture turned a quarter comes out at 947px of picture in a 633px frame:

Error: expect(received).toBeLessThanOrEqual(expected)
Expected: <= 633
Received:    947

Covered

346 unit tests, 20 of them new, over what gets offered, what gets shown, what gets written and what happens when it fails: the debounce, the net-zero skip, If-Match against the version just written rather than the stale one, the flush on navigate and on close, and the 412 path.

44 e2e in Chromium and Firefox, 16 new. The write tests intercept the PUT and parse the orientation out of the uploaded bytes with a reader written inside the spec, so the assertion does not depend on the library that produced them.

Not covered: no run against a live server, so the preview regeneration described above is read off the server source rather than observed. Worth a look when testing.

👾 This pull request was assisted by Claude Code, commits carry an Assisted-by trailer.

A rotate button ahead of the edit button turns the picture on screen at
once, and the turn is written to the file shortly after by rewriting its
Exif orientation tag. Nothing is decoded and nothing is re-encoded, so
the picture is the same picture however many times it is turned, and the
tag is what the preview generator reads, so the new framing follows the
file into the Files grid, Photos and the mobile clients.

Offered only for a JPEG the user may write. No other format carries an
orientation that both the browser and the preview generator honour, and
a turn shown on a file that cannot keep it is worse than no button.

Written once the user stops rather than once per click: every write
makes a version of the file, so a turn per click would leave four copies
of a photo in the history on the way round a full circle. A picture
turned the whole way round is not written at all.

The fit is worked out against the box the picture occupies on screen
rather than its own, or a landscape photo turned on its side would be
taller than the frame and spill over the chrome above and below. Zoom
and pan are measured against the same box.

Co-authored-by: vladopol <vladopol@gmail.com>
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
@skjnldsv skjnldsv added status: review Waiting for reviews type: enhancement 🚀 New feature or request AI assisted labels Sep 18, 2026
@codecov

codecov Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.12903% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.10%. Comparing base (4413798) to head (91ab0fd).

Files with missing lines Patch % Lines
lib/components/Images.vue 68.18% 5 Missing and 2 partials ⚠️
lib/composables/useRotation.ts 95.40% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #49      +/-   ##
==========================================
+ Coverage   88.00%   88.10%   +0.10%     
==========================================
  Files          36       37       +1     
  Lines        2510     2624     +114     
  Branches      525      558      +33     
==========================================
+ Hits         2209     2312     +103     
- Misses        297      305       +8     
- Partials        4        7       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@skjnldsv
skjnldsv merged commit ed33bbe into main Sep 18, 2026
18 checks passed
@skjnldsv
skjnldsv deleted the feat/rotate-jpeg branch September 18, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI assisted status: review Waiting for reviews type: enhancement 🚀 New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant