Skip to content

Add an opt-in filter to generate animated image sub-sizes - #80385

Open
adamsilverstein wants to merge 23 commits into
trunkfrom
add/80383-animated-subsizes-optin
Open

Add an opt-in filter to generate animated image sub-sizes#80385
adamsilverstein wants to merge 23 commits into
trunkfrom
add/80383-animated-subsizes-optin

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jul 16, 2026

Copy link
Copy Markdown
Member

What?

Adds a developer opt-in filter, wp_generate_animated_image_subsizes, that re-enables animated (multi-frame) sub-size generation for animated GIFs in the client-side media processing pipeline:

add_filter( 'wp_generate_animated_image_subsizes', '__return_true' );

Fixes #80383.
Also fixes the 12-year-old core request Trac #28474 - "WordPress destroys animation in animated GIF when it resizes", as proposed in comment:58.

Core backport: Trac #65656 / WordPress/wordpress-develop#12572.

Note

Stacked on #80268, which makes static first-frame sub-sizes the default. This PR adds the opt-in path back on top of it. Only the last commit is new; review the diff from fix/animated-gif-subsize-performance.

Why?

#80268 switched sub-sizes of animated GIFs to static first-frame images, matching what WordPress core has always done server-side, because full animated re-encodes were extremely expensive (~88 s combined for a 769-frame GIF, with sub-sizes larger than the original - see #80266).

But there is long-standing, sustained demand for resized GIFs that keep their animation: Trac #28474 has been open since 2014 and stalled server-side because GD cannot do it and Imagick is only available on a subset of hosts. Client-side processing sidesteps both constraints - the cost is paid once in the uploading user's browser, and wasm-vips is available regardless of host configuration. That makes animated sub-sizes reasonable as an explicit developer opt-in while keeping the fast, core-consistent static behavior as the default.

How?

The flag follows the exact same path as image_strip_meta / image_max_bit_depth (#80218):

  1. PHP (lib/media/load.php): wp_generate_animated_image_subsizes (boolean, default false) is applied in gutenberg_media_processing_filter_rest_index() and exposed as animated_image_subsizes on the REST API root index. The field is also added to the preload/entities field lists (lib/compat/wordpress-7.1/preload.php, packages/core-data/src/entities.js), which must match exactly.
  2. Editor settings: mapped to a generateAnimatedImageSubsizes setting in use-block-editor-settings.js and forwarded by use-media-upload-settings.js.
  3. Upload pipeline: resizeCropItem reads the setting from the @wordpress/upload-media store and passes it to the vips worker as a new preserveAnimation option on resizeImage() (options object from Client Side Media: Consolidate optional positional params into options objects in vips / upload-media #80328).
  4. Vips: when preserveAnimation is set and the resize is uncropped, resizeImage() restores the pre-Client-side media: generate animated image sub-sizes from the first frame only, matching core #80268 [n=-1] load path so all frames are decoded and re-encoded. Cropped sizes (e.g. thumbnail) always flatten to the first frame, matching the pre-existing behavior - per-frame smart-cropping is out of scope.

Tuned gifsave settings

The profiling in #80266 showed ~85% of the animated resize cost is GIF re-encoding (per-frame palette quantization), and that default gifsave settings cause the output-larger-than-input bloat. When writing an animated GIF, this PR applies the settings benchmarked there:

  • effort: 2, interframe_maxerror: 8, interpalette_maxerror: 16 - measured 4-8x faster and eliminates the size bloat.

That turns the opt-in cost from ~88 s into roughly 10-20 s for a very large GIF - still too slow to be the default, but a reasonable trade for a site that has explicitly chosen animated sub-sizes.

Notes

  • The filter name says "animated image", not "GIF", deliberately: vips can also preserve frames for animated WebP, and APNG has come up on the Trac ticket. This implementation is exercised with GIFs; the door is open.
  • Core's wp_calculate_image_srcset() still never mixes the full-size GIF and its sub-sizes in one srcset. With animated sub-sizes that guard becomes overly conservative but harmless; relaxing it is a server-side follow-up.
  • Server-side uploads (Media Library taking the server path) still produce static sub-sizes, so enabling the filter reintroduces a difference between upload surfaces - this time as an explicit developer choice. Documented on the filter.
  • Guardrail interplay with Client-side media: add timeout and size guardrails to GIF to video conversion #80376 (timeout/pixel budgets, falling back to a static sub-size) is a follow-up once Media: Add timeout and size guardrails to client-side GIF to video conversion #80379 lands.

Testing Instructions

Test in WordPress Playground

  1. Enable the filter, e.g. in a mu-plugin: add_filter( 'wp_generate_animated_image_subsizes', '__return_true' );
  2. In the block editor, upload an animated GIF to an Image block and wait for the upload to finish.
  3. Inspect the attachment's medium / large sub-sizes (e.g. via /wp-json/wp/v2/media/<id>): they should be animated GIFs (multiple frames), while thumbnail (cropped) remains a static first frame.
  4. Without the filter, all sub-sizes remain static first-frame images (the Client-side media: generate animated image sub-sizes from the first frame only, matching core #80268 default), and the existing e2e test covers this.

Documentation

The client-side media docs (#75895) are updated alongside: the how-to guide gains an "Animated image sub-sizes" section for the new filter, and the architecture reference adds it to the filter table and REST index field list (plus corrects the now-stale note that sub-sizes preserve all frames).

Automated tests

  • packages/vips/src/test/resize-image.ts: new preserveAnimation suite - [n=-1] + tuned gifsave for uncropped animated resizes, first-frame flattening for crops, no effect on still formats.
  • phpunit/media/media-processing-test.php: REST index exposes generate_animated_image_subsizes (default false, honors the filter, hidden without upload_files).
  • test/e2e/specs/editor/various/gif-to-video.spec.js: new test uploads an animated GIF with the filter enabled (via a new e2e test plugin) and asserts the medium sub-size keeps all frames.
npm run test:unit packages/vips/src/test/
vendor/bin/phpunit phpunit/media/media-processing-test.php
npm run test:e2e -- test/e2e/specs/editor/various/gif-to-video.spec.js

adamsilverstein and others added 5 commits July 14, 2026 12:09
Match WordPress core's server-side behavior, where both GD and Imagick
flatten animated images when resizing and wp_calculate_image_srcset()
keeps flattened sub-sizes and the animated full-size image from mixing.

Loading all frames ([n=-1]) re-encoded a full animated GIF per uncropped
sub-size, which took 16-47 seconds per size for a 769-frame GIF and
produced sub-sizes larger than the original file (5.5MB medium from a
2.2MB source). Cropped sizes already flattened to the first frame, so
behavior was inconsistent, and Media Library uploads taking the server
path already produced static sub-sizes.

See #80266.
… output

mediabunny's default 2-second key frame cadence roughly doubles the
output size for long GIF conversions (2.2MB vs 1.14MB for a 769-frame
GIF) with no encode-time benefit. These looping, autoplaying GIF
replacements don't need fine seek granularity.

See #80266.
…size-performance

# Conflicts:
#	packages/vips/CHANGELOG.md
#	packages/vips/src/index.ts
#	test/e2e/specs/editor/various/gif-to-video.spec.js
Add the wp_generate_animated_image_subsizes filter (boolean, default
false). When a site opts in, uncropped sub-sizes of animated GIFs keep
their animation instead of flattening to the first frame, resolving the
long-standing request in https://core.trac.wordpress.org/ticket/28474
without depending on server-side Imagick availability.

The flag travels the same path as image_strip_meta: REST API root index
field -> block editor setting -> upload-media store -> vips worker,
where it restores the pre-#80268 [n=-1] load path for uncropped resizes.
When writing an animated GIF, gifsave is tuned (effort 2,
interframe_maxerror 8, interpalette_maxerror 16), measured 4-8x faster
than the defaults and avoiding sub-sizes larger than the original.

See #80383.
@adamsilverstein adamsilverstein added [Type] Enhancement A suggestion for improvement. [Feature] Client Side Media Media processing in the browser with WASM labels Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Size Change: +311 B (0%)

Total Size: 7.78 MB

📦 View Changed
Filename Size Change
build/modules/vips/worker.min.js 3.69 MB +247 B (+0.01%)
build/scripts/block-editor/index.min.js 472 kB +19 B (0%)
build/scripts/core-data/index.min.js 37.4 kB +20 B (+0.05%)
build/scripts/editor/index.min.js 549 kB +28 B (+0.01%)
build/scripts/upload-media/index.min.js 16.3 kB -3 B (-0.02%)

compressed-size-action

@adamsilverstein

Copy link
Copy Markdown
Member Author

Core backport of the server-side changes: WordPress/wordpress-develop#12572 (draft; Trac ticket to follow).

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: swissspidy <swissspidy@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: andrewserong <andrewserong@git.wordpress.org>
Co-authored-by: annezazu <annezazu@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@adamsilverstein adamsilverstein added the Backport to WP 7.1 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Jul 17, 2026
@andrewserong

Copy link
Copy Markdown
Contributor

Honestly 10-20 seconds doesn't seem like that long, but still is a significant change in behavior (storage also increases) so probably not what we want to make the default? Curious what others think the default behavior should be?

For me, these sorts of questions mean that I think we should park this feature for 7.1 and give it more time to explore for 7.2+. Speaking for myself, I'm a little spread thin across 7.1 features that I'm trying to support, and I'd be keen for us to carefully contain scope here if we can.

It also seems that in order for GIF sub-size processing to feel stable, we'll need more UI state in order to balance out the longer processing time (#80329).

Taken together, this feels like a good well-scoped feature for 7.2 ("WP now preserves animated GIFs at all image sub-sizes"), whereas in the context of client-side media processing, this seems more the nice-to-have territory than must have.

What do you all think? I'll just ping @annezazu for visibility on this one, too, as I want to make sure I'm helping out with the high priority features in the release. (If this is high priority, happy to help review of course).

@swissspidy swissspidy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer the final call to someone else, but code-wise this LGTM. It's a low-risk opt-in filter for devs who really would like to have animated thumbnails and are aware of the trade-offs.

Comment thread lib/media/load.php Outdated
* take the server-side path (e.g. some Media Library uploads) also still
* produce static sub-sizes, as core has no animated resize support.
*
* @since 23.7.0

@t-hamano t-hamano Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @since 23.7.0
* @since 23.9.0

Nit: I intend to backport this PR not only to 7.1 but also to Gutenberg 23.6.

Update: If we merge this PR now, it will be released as part of GB 23.9.

@annezazu

Copy link
Copy Markdown
Contributor

Hey folks! Thanks for tagging me in this. I read through the issue and, zooming out across the release, I'd like this to be punted to 7.2. This is a conservative decision and it comes from not looking solely at just this isolated change but looking at the weight of all of the features across the release when combined against the collective capacity of our current active contributors. Put another way, we already have a lot of features that we will need to be ready to do bug fixes for during beta and I'm hesitant to continue adding to it. Looking at this in isolation, I can see how it's lower risk since it's a dev focused change but it's not zero risk and everything we add takes up review time.

We do sometimes add enhancements to new features introduced in a cycle during beta such as this filter. That said, I am also fine leaving it for 7.2!

Relatedly, client side media wasn't noted as an area to be "blessed" and I want to honor that original commitment when it was discussed previously. Otherwise, it becomes easy to start "moving the goal posts" late in the game and I don't believe in doing that, unless project leadership overrides something.

@adamsilverstein

Copy link
Copy Markdown
Member Author

Hey folks! Thanks for tagging me in this. I read through the issue and, zooming out across the release, I'd like this to be punted to 7.2.

That makes sense, thanks for deciding! We will wait until 7.2 to land this one.

@adamsilverstein adamsilverstein removed Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) Backport to WP 7.1 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta labels Jul 20, 2026
@adamsilverstein adamsilverstein moved this from 🔎 Needs Review to 🦵 Punted to 7.2 in WordPress 7.1 Editor Tasks Jul 20, 2026
…ubsizes-optin

# Conflicts:
#	packages/upload-media/CHANGELOG.md
…ess/gutenberg into add/80383-animated-subsizes-optin
@t-hamano

Copy link
Copy Markdown
Contributor

As the 7.1 release cycle is nearing its end, I believe we can move this PR forward again, but it requires at least the following changes.

  • backport-changelog/7.1/12572.md > backport-changelog/7.2/12572.md
  • lib/compat/wordpress-7.1/preload.php > lib/compat/wordpress-7.2/preload.php

@adamsilverstein

Copy link
Copy Markdown
Member Author

As the 7.1 release cycle is nearing its end, I believe we can move this PR forward again, but it requires at least the following changes.

  • backport-changelog/7.1/12572.md > backport-changelog/7.2/12572.md
  • lib/compat/wordpress-7.1/preload.php > lib/compat/wordpress-7.2/preload.php

good point @t-hamano - will update.

The 7.1 release cycle is closing, so the backport changelog entry moves to
7.2 and the preload field is added from a new 7.2 compat file instead of
being edited into the 7.1 one.

The 7.2 filter splices the field into whatever field list is already
preloaded rather than restating it, so a field added to the 7.1 list later
is not silently dropped.
@adamsilverstein

adamsilverstein commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

I pointed Claude at both items, here is what changed:

Both moves are in 3054d13, along with a trunk merge.

backport-changelog/7.1/12572.md is now backport-changelog/7.2/12572.md, and lib/compat/wordpress-7.1/preload.php is back to its trunk state. The preload field comes from a new lib/compat/wordpress-7.2/preload.php instead.

One small departure worth flagging: rather than restating the whole field list the way the 7.1 file does, the 7.2 filter runs at priority 11 and splices generate_animated_image_subsizes in right after description. That way a field added to the 7.1 list later doesn't get silently dropped by the 7.2 one. The resulting _fields string is identical to what the 7.1 edit produced, so preloading still matches entities.js exactly.

Does that placement look right to you @t-hamano?

@adamsilverstein adamsilverstein changed the title Media: Add an opt-in filter to generate animated image sub-sizes Add an opt-in filter to generate animated image sub-sizes Aug 13, 2026
Trunk now renders a real default block in place of the default appender
(#81231), so the button that guarded the animated sub-sizes test no longer
exists. Use the same document-role locator the sibling tests in this file
already use.
@adamsilverstein

Copy link
Copy Markdown
Member Author

Claude ran the suites locally, results below:

Verified against wp-env with the plugin built:

  • The full filter chain now produces /?_fields=description,generate_animated_image_subsizes,gmt_offset,home,image_max_bit_depth,image_sizes,image_size_threshold,image_strip_meta,name,site_icon,site_icon_url,site_logo,timezone_string,url,page_for_posts,page_on_front,show_on_front, which matches entities.js exactly, so preloading still hits.
  • PHPUnit Media_Processing: 16/16.
  • Jest packages/vips + packages/upload-media: 453/453.
  • Playwright gif-to-video.spec.js: 4/4.

The e2e run needed one fix, pushed in 64cb3fb. Merging trunk pulled in #81231, which renders a real default block in place of the default appender, so the role: 'button' guard on the animated sub-sizes test stopped matching anything. It now uses the same document-role locator the three sibling tests in that file already use.

The preserveAnimation path loads every frame with [n=-1], so memory
scales with the frame count, not the single-frame dimensions. A long
animation of modest dimensions could therefore exhaust the fixed 1 GiB
wasm-vips heap and abort the upload. Check the frame count while
decoding is still lazy and fall back to a first-frame sub-size when the
animation does not fit.
The tuned settings were keyed off preserveAnimation being requested, so
a static GIF uploaded to an opted-in site was encoded at effort 2
instead of libvips' default 7. Inter-frame and inter-palette error
tolerances mean nothing for a single frame, so that traded compression
for no benefit. Key the tuning off the decoded frame count instead.
The filter's @SInCE named a version that has already shipped; trunk is
at 23.8.0-rc.1, so the next release to carry it is 23.9.0.

The 7.2 preload filter reproduces entities.js field ordering relative to
'description'. Without that anchor it was appending to the end, which
produces a list that can never match and silently disables preloading.
Leave the path alone instead.

Assert only that the medium sub-size stays animated: cgifsave runs with
inter-frame error tolerances, so pinning its exact frame count to the
source would turn an encoder change into a failing test.
@github-actions

Copy link
Copy Markdown

Flaky tests detected in f059b22.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/32655191817
📝 Reported tests:

does not disable collaboration when all meta boxes are RTC-compatible in /test/e2e/specs/editor/collaboration/collaboration-metabox-lock.spec.ts, passed after 1 failed attempt.
Error: apiRequestContext.fetch: socket hang up
Call log:
  - → GET http://localhost:8889/wp-json/wp/v2/users?per_page=100
    - user-agent: Playwright/1.62.1 (x64; ubuntu 24.04) node/20.20 CI/1
    - accept: */*
    - accept-encoding: gzip,deflate,br
    - X-WP-Nonce: bbcfb9e215
    - cookie: wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_23778236db82f19306f247e20a353a99=admin%7C1787679714%7C3VRfFIS8EpzcLwnF4GKoZL9cFeLp9yWAnuxJl1Zb1mI%7Ca8029073eaa057b5a3b0f8d8e7563b36bebbca48e874a0cfcfc2d33954b19f0a; wp-settings-time-1=1787506941

    at RequestUtils.rest (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/rest.ts:112:39)
    at RequestUtils.listUsers (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/users.ts:34:30)
    at RequestUtils.deleteAllUsers (/home/runner/work/gutenberg/gutenberg/packages/e2e-test-utils-playwright/src/request-utils/users.ts:106:44)
    at CollaborationUtils.teardown (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/fixtures/collaboration-utils.ts:610:28)
    at Object.collaborationUtils (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/fixtures/index.ts:31:5)

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

Labels

[Feature] Client Side Media Media processing in the browser with WASM [Package] Block editor /packages/block-editor [Package] Core data /packages/core-data [Package] Editor /packages/editor [Status] In Progress Tracking issues with work in progress [Type] Enhancement A suggestion for improvement.

Projects

Status: 🔎 Needs Review

Development

Successfully merging this pull request may close these issues.

Client-side media processing: add an opt-in filter to generate animated GIF sub-sizes

5 participants