nextcloud-libraries / nextcloud-libraries/nextcloud-viewer

Rotate is JPEG-only: PNG and WebP need a hard rotation to be supported

Open
#47 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

AI assisted status: triage type: enhancement
Dominant language
TypeScript
Stars
1
Forks
0
Avg merge
6h 19m
Merged PRs (30d)
43

Description

The rotate button being added for JPEG writes the Exif orientation tag: two bytes, no decode, no re-encode, nothing lost however many times it is pressed. PNG and WebP have no equivalent, so the button is hidden for them and those files cannot be rotated from the viewer at all.

Neither format carries an orientation the stack honours. lib/private/Image.php:413 in server returns -1 for anything that is not IMAGETYPE_JPEG, so fixOrientation() in lib/private/Preview/Image.php:42 never fires and previews would keep the old framing even if the file did carry a tag. PNG's eXIf chunk and WebP's EXIF chunk are both dead ends here.

The only route is a hard rotation: decode, turn the pixels, re-encode, upload. @nextcloud/image-editor already does exactly that (state.rotation, orientedSize(), ENCODABLE), so the work is not the rotation itself but deciding whether the result is worth writing.

I measured four 90° turns, decoding and re-encoding each time, on an 800x600 synthetic photo:

format PSNR after 4 turns, Chromium Firefox size after 1 turn
PNG bit exact bit exact 292KB → 273KB
WebP, default quality 40.4 dB 43.0 dB 14.4KB → 14.7KB
WebP, quality 1 bit exact bit exact 14.4KB → 141KB

PNG costs nothing in pixels. toBlob('image/webp', 1) turns out to be lossless in both engines, but a lossy source re-encoded that way grows 10x in Chromium and 3.4x in Firefox.

[!IMPORTANT]
The pixels are the easy part. What blocks PNG today is that the export drops metadata and can silently shrink the image.

jpegSource() (lib/composables/useExportImage.ts:112) returns null unless source and target are both JPEG, so carryMetadata never runs for a PNG: eXIf, ICC and text chunks are gone after a rotate. And the export is subject to the canvas cap, reporting downscaled at useExportImage.ts:235 when it had to work smaller. A rotate that quietly resizes someone's image is worse than no rotate.

So for PNG:

  • carry PNG metadata through the export the way JPEG's is carried
  • refuse the rotation, rather than writing it, when the export comes back downscaled
  • then show the button for PNG

For WebP, a re-encode has to sniff the source first: RIFF VP8L means a lossless source that can be re-encoded losslessly at no cost, VP8 means a lossy one where the choice is generation loss or a 3-10x file. Worth doing only if someone asks for it.

GIF, BMP, ICO, SVG and the preview-backed mimes (HEIC, HEIF, TIFF) are out of scope: the editor cannot write any of them back in their own format.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with lib/private/Image.php:413, lib/private/Preview/Image.php:42, and lib/composables/useExportImage.ts, especially jpegSource() at line 112 and the downscaled result at line 235. Compare the existing state.rotation, orientedSize(), and ENC0DABLE flow in @nextcloud/image-editor. Done means PNG rotation is available only when the export preserves metadata and dimensions, refuses downscaled output, and keeps PNG metadata through the export.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
54/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.