Skip to content

perf(preview): shrink with Imaginary before rotating - #65483

Open
martin-rueegg wants to merge 1 commit into
nextcloud:masterfrom
metaworx:perf/imaginary-shrink-before-rotate
Open

martin-rueegg wants to merge 1 commit into
nextcloud:masterfrom
metaworx:perf/imaginary-shrink-before-rotate

Conversation

@martin-rueegg

@martin-rueegg martin-rueegg commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

imaginary-powered preview generation is applying multiple operations on the full-sized image, where it can be handled more efficiently if - in a first step - the resize to the target scale is performed and other operations like rotation and cropping followed from there. Saves memory pressure and increases speed.

What changes

  • Images: fit into a square box of the larger side, without rotating and keeping EXIF →
    autorotate → fit to the requested box, stripping metadata.
  • perf(preview): only autorotate with Imaginary when needed #65148's EXIF check in PHP goes again, as it is no longer needed.
  • The HEIC case goes.
  • PDF, SVG and Illustrator: unchanged.
  • Crop: unchanged, autorotate → smartcrop.

Technical details

  • Why autorotate first is expensive: bimg's autorotate-only path decodes and re-encodes the
    whole image whatever the orientation (resizer.go:38-45). The 2022 premise, "almost instant
    without rotation", does not hold.
  • Why not a single fit:
    • Imaginary's Fit swaps the box for orientation > 4 (image.go:134-147, since 2018), and bimg
      rotates as well.
    • For orientations 5–8 the result is an upright image in a box of the wrong shape.
    • This is very likely what Fix imaginary with rotated exif images #31829 worked around in 2022.
  • Why the square box first: with a box that is not square, fitting before rotating fits the
    stored image, not the shown one. Measured: 2048 × 1536 where 2731 × 2048 fits.

Measurements

AIO Imaginary sha256:0e32abf8…, 2500 MB memory limit, box 4096², quality 80.

  • Peak anonymous memory:

    Input today this PR
    JPEG 200 MP, orientation 1 715 MB 193 MB
    JPEG 200 MP, orientations 2–8 ~710 MB ~200 MB
    LZW TIFF 200 MP ~2.0 GB, at the limit ~1.0 GB
    uncompressed TIFF 761 MB killed, out of memory passes
    • Times: JPEG about 2.8 s instead of 5–8 s.
    • PNG 100 MP: 24 s instead of 52 s.
  • Correctness: every result upright, sized as expected, and without EXIF:

    • JPEG at all eight orientations, at boxes 4096², 4096 × 2048, 2048 × 4096 and 256²;
    • TIFF at orientations 1 and 6, and PNG;
    • 15 iPhone HEICs at orientations 1, 3, 6, 8 and none, checked against heif-convert.

Tests

  • ImaginaryTest covers six image types, a non-square box, WebP output and the three convert types.
  • manual test:
    • checked HEIC, JPEG, and TIFF
    • EXIF orientation 1,2,3,4,5,6,7,8
    • generated preview all with correct orientation, no metadata

Checklist

AI (if applicable)

  • The content of this PR was mostly generated using AI: the measurements, code and test cases. Manual test verified on a local ddev instance with Nextcloud 35.0.1 - as required.

@martin-rueegg
martin-rueegg requested a review from a team as a code owner October 13, 2026 19:59
@martin-rueegg
martin-rueegg requested review from Altahrim, come-nc, icewind1991 and sorbaugh and removed request for a team October 13, 2026 19:59
@martin-rueegg martin-rueegg changed the title Perf/imaginary shrink before rotate perf(preview): shrink with Imaginary before rotating Oct 13, 2026
@martin-rueegg
martin-rueegg force-pushed the perf/imaginary-shrink-before-rotate branch from d9548fe to 9a0876b Compare October 14, 2026 08:32
@martin-rueegg

Copy link
Copy Markdown
Contributor Author

Thanks, @CarlSchwan, for the review!

PR has been rebased and force-pushed.

Tests: ImaginaryTest passes on the rebased branch (11 tests, 47 assertions).

Comment thread lib/private/Preview/Imaginary.php Outdated
Every operation of an Imaginary pipeline decodes and encodes its input, so
autorotate ahead of fit did that at full resolution for every image, whatever
its orientation. Fit first, into a square box of the larger side and keeping
the EXIF orientation, then autorotate the small result, then fit to the
requested box and strip the metadata.

This covers what the EXIF check before it did only for JPEGs at orientation
1: every other format, and rotated images. HEIC no longer needs a case of its
own, as the HEIF loader applies the image's rotation already.

Measured against a 2500 MB container: a 200 MP JPEG peaks at about 200 MB
instead of 715 MB, a 200 MP TIFF at about 1 GB instead of 2 GB, and a 761 MB
TIFF no longer runs out of memory. A single fit without norotation is no
alternative: fit swaps the box for EXIF orientations 5 to 8, and bimg rotates
on top of that.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Martin Rüegg <martin.rueegg@metaworx.ch>
@martin-rueegg
martin-rueegg force-pushed the perf/imaginary-shrink-before-rotate branch from 9a0876b to 7af437b Compare October 15, 2026 12:29
@martin-rueegg
martin-rueegg requested a review from come-nc October 15, 2026 12:32

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants