Skip to content

Add target_format and speed to scaleImage, scale AVIF originals to JPEG - #163

Open
MrTango wants to merge 7 commits into
masterfrom
avif-support
Open

MrTango wants to merge 7 commits into
masterfrom
avif-support

Conversation

@MrTango

@MrTango MrTango commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

scaleImage(..., target_format="AVIF") encodes a scale in another format than the original, animations included where the target supports them. pre_scale reports the target format's mimetype, so stable URLs of such scales get the right extension before the scale exists. speed passes the AVIF encoder's trade-off between encoding time and file size.

Plain scales of AVIF originals are JPEG (PNG with alpha): the fallback for browsers without AVIF support, next to the AVIF twins plone.namedfile#228 generates.

Scales of CMYK images are converted to sRGB through their embedded profile, and a scale only embeds a profile that matches its pixels. A CMYK profile on RGB pixels made AVIF scales undecodable in Chrome and added half a megabyte to every scale of a print image.

Needs:
plone/plone.outputfilters#116
plone/plone.namedfile#228
plone/plone.formwidget.namedfile#109
plone/plone.app.upgrade#387
plone/plone.base#137

and:
plone/plone.app.linkintegrity#129
or for 6.2
plone/plone.app.linkintegrity#131

the PLIP for this is here: plone/Products.CMFPlone#4384

scaleImage(..., target_format="AVIF") encodes a scale in another format
than the original, animations included where the target supports them.
pre_scale reports the target format's mimetype, so stable scale URLs get
the right extension before the scale exists. AVIF uploads keep their
format when scaled instead of becoming JPEG.
@mister-roboto

Copy link
Copy Markdown

@MrTango thanks for creating this Pull Request and helping to improve Plone!

TL;DR: Finish pushing changes, pass all other checks, then paste a comment:

@jenkins-plone-org please run jobs

To ensure that these changes do not break other parts of Plone, the Plone test suite matrix needs to pass, but it takes 30-60 min. Other CI checks are usually much faster and the Plone Jenkins resources are limited, so when done pushing changes and all other checks pass either start all Jenkins PR jobs yourself, or simply add the comment above in this PR to start all the jobs automatically.

Happy hacking!

@MrTango
MrTango requested a review from petschki September 27, 2026 06:13
Plain scales of an AVIF upload are JPEG (PNG with alpha, first frame of an
animation), so browsers without AVIF support can show them. pre_scale
reports image/jpeg for them.
@jensens

jensens commented Sep 30, 2026

Copy link
Copy Markdown
Member

Since I support AVIF in plone-pgthumbor I was curious if you run into the very same problems.
That said, pgthumbors Accept-Header awareness plus env THUMBOR_AUTO_AVIF=true handles this already natural way IMO.

It follows my AI-assistet analysis

CMYK sources produce AVIF files Chrome refuses to decode

scaleImage passes the original's icc_profile through to save() unchanged. With a CMYK source and target_format="AVIF" the result is RGB pixels with a CMYK profile attached (557 KB of profile in a 300 px thumbnail, from the cmyk.jpg test image). The JPEG path has always done the same and browsers tolerate it there. AVIF decoders do not: Chrome 149 gives naturalWidth == 0 for that file, while the same pixels without a profile or with an sRGB profile decode fine.

Repro on this branch:

import io
from PIL import Image, ImageCms
from plone.scale.scale import scaleImage

data = open("src/plone/scale/tests/data/cmyk.jpg", "rb").read()
out, _, _ = scaleImage(data, 300, 300, target_format="AVIF")
icc = Image.open(io.BytesIO(out)).info["icc_profile"]
print(ImageCms.ImageCmsProfile(io.BytesIO(icc)).profile.xcolor_space)  # 'CMYK'

Open the resulting file in Chrome: broken image. Combined with plone/plone.namedfile#228, where the AVIF <source> comes first and <picture> has no decode fallback, every CMYK upload renders as a broken image in Chrome.

Thumbor's PIL engine hit the same thing and converts to sRGB before encoding AVIF (see the comment in thumbor/engines/pil.py). I would do the same here: when target_format is set and the profile's colour space does not match the image mode, transform to sRGB with ImageCms, or at least drop the profile. testTargetFormatAvifFromPaletteAndCmyk only checks the magic bytes, so it is worth asserting the profile's colour space as well.

Smaller things

  • pre_scale reports image/jpeg for an AVIF original, but testScaledAvifWithAlphaFallsBackToPng shows the fallback becomes PNG when there is alpha. The stable URL then carries the wrong extension. It also duplicates the format policy of scaleImage into storage.py. Maybe leave the mimetype alone for the fallback case and let the generated scale report its real one.
  • The PR title and description say AVIF uploads keep their format when scaled. After the last commit the news entry says the opposite (plain scales are JPEG). One of the two needs updating.
  • AVIF encoding at Pillow's default speed is slow. On a 2000 px image I measure about 520 ms for AVIF at quality 65 against 60 ms for JPEG at quality 88 (synthetic gradient-plus-noise image, so the ratio matters more than the absolute numbers). With speed=8 it drops to about 70 ms at the same file size. A speed argument, or a generic passthrough of save kwargs, would let callers make that trade-off.

Assisted-by: Claude Fable 5.1

Scales of CMYK images were converted with a plain mode conversion and
still carried the CMYK profile, now attached to RGB pixels. Browsers
ignore that in a JPEG, but Chrome refuses to decode such an AVIF, and
every scale of a print image carried the profile's half megabyte.

The conversion now goes through the embedded profile, after scaling as
it costs per pixel, and yields an sRGB profile. At save time a profile
is only embedded when its color space matches the pixels, which also
covers scales that became greyscale.
Guessing JPEG for the plain scale of an AVIF original is wrong when the
fallback needs alpha and becomes PNG, and it duplicated the format
policy of scaleImage in the storage. As for any other format that is
re-encoded, the scale reports its real mimetype once generated.
At Pillow's default speed 6, encoding a 2000 px image as AVIF takes
about four times as long as at speed 8, for the same file size. The
parameter lets callers make that trade-off.
@MrTango MrTango changed the title Add target_format to scaleImage, keep AVIF originals as AVIF Add target_format and speed to scaleImage, scale AVIF originals to JPEG Oct 2, 2026
@MrTango

MrTango commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@jensens thx, i addressed it now.

@MrTango
MrTango requested a review from thet October 2, 2026 13:49
Scaling only changes the format of an AVIF original when target_format
says so, like for PNG and WEBP originals. The JPEG fallback for browsers
without AVIF support is requested by plone.namedfile in its
avif_with_fallback mode; in its disabled mode nothing is converted.
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