[wp-trac] [WordPress Trac] #45407: Add block attributes to `wp_calculate_image_sizes` to allow for proper handling of `sizes` attribute
WordPress Trac
noreply at wordpress.org
Mon Mar 9 08:23:17 UTC 2026
#45407: Add block attributes to `wp_calculate_image_sizes` to allow for proper
handling of `sizes` attribute
--------------------------------------+-----------------------------
Reporter: mor10 | Owner: joemcgill
Type: defect (bug) | Status: assigned
Priority: high | Milestone: Future Release
Component: Media | Version:
Severity: major | Resolution:
Keywords: dev-feedback needs-patch | Focuses:
--------------------------------------+-----------------------------
Comment (by sanket.parmar):
== Proposed fix: inject correct `sizes` in `render_block_core_image()`
=== Where should this fix land?
This fix should follow the standard Gutenberg → Core merge flow:
1. '''Primary: Gutenberg plugin''' (`packages/block-
library/src/image/index.php`)[[BR]]
Gutenberg ships on its own release cycle (every two weeks) and reaches
sites running the plugin immediately. This is where the change should be
authored and reviewed first, as a Gutenberg PR.
2. '''Secondary: WordPress Core''' (`src/wp-
includes/blocks/image.php`)[[BR]]
`src/wp-includes/blocks/image.php` in `wordpress-develop` is a
'''build artifact''' — it is generated from the Gutenberg package during
the build and is listed in `.gitignore` under `/src/wp-includes/blocks/*`.
It should '''not''' be patched directly. Once the Gutenberg PR is merged,
the file gets synced into Core automatically as part of the regular
Gutenberg → Core merge that happens each WordPress release cycle.
In short: '''open the PR against the Gutenberg repository'''. The Core
ticket (#45407) should reference that PR and be resolved when the sync
lands in a Core release.
----
=== Why the original approach stalled
The `45407.diff` patch required the image block to be server-rendered so
block attributes could be passed into `wp_calculate_image_sizes()`. At the
time (WP 5.0), `core/image` was client-rendered. '''That blocker no longer
exists.''' Since WP 5.9, `core/image` has `render_block_core_image()` as a
registered `render_callback`, giving us full access to
`$attributes['align']` at render time.
=== Proposed fix
Two additions to `packages/block-library/src/image/index.php`.
The later `wp_filter_content_tags()` pass (priority 12 on `the_content`)
does not override a pre-existing `sizes` attribute —
`wp_image_add_srcset_and_sizes()` already contains this guard:
{{{
#!php
// Check if there is already a 'sizes' attribute.
$sizes = strpos( $image, ' sizes=' );
if ( ! $sizes ) {
$sizes = wp_calculate_image_sizes( ... );
}
}}}
So setting `sizes` earlier in the render callback is safe and will not be
double-applied.
----
=== 1. New helper function — add before `render_block_core_image()`
{{{
#!php
/**
* Calculates a correct `sizes` attribute for a core/image block based on
its alignment.
*
* The generic wp_filter_content_tags() pipeline sets sizes based on the
registered
* image size width, which is correct for default/center/left/right
aligned images but
* is too narrow for alignwide and alignfull images that visually break
out of the
* content column. This function produces a sizes value that matches the
actual display
* width of the image in those contexts.
*
* Themes and plugins can override the result via the {@see
'block_core_image_sizes'}
* filter hook.
*
* @see https://core.trac.wordpress.org/ticket/45407
*
* @global int $content_width The theme's maximum content width in pixels.
*
* @param string $align Block alignment value: 'wide', 'full',
'center',
* 'left', 'right', or '' for no alignment.
* @param int $image_width The intrinsic width of the image in pixels
as stored
* in the block's markup.
* @return string The `sizes` attribute value to inject, or an empty
string when the
* default core behaviour should be preserved.
*/
function block_core_image_calculate_sizes( $align, $image_width ) {
global $content_width;
switch ( $align ) {
case 'full':
/*
* Full-width images span the entire viewport width regardless
of the
* theme's layout, so 100vw is always the correct hint.
*/
$sizes = '100vw';
break;
case 'wide':
/*
* Wide-aligned images should be displayed at the theme's wide
layout
* width. Prefer the pixel value declared in theme.json's
wideSize;
* fall back to a 130% multiple of the classic-theme
$content_width
* global as a reasonable heuristic.
*/
$layout = wp_get_global_settings( array( 'layout' ) );
$wide_size_raw = isset( $layout['wideSize'] ) ?
$layout['wideSize'] : '';
$wide_px = 0;
if ( $wide_size_raw ) {
// Extract the largest px value (handles 'clamp(…)'
expressions too).
preg_match_all( '/(\d+)px/', $wide_size_raw, $px_matches
);
if ( ! empty( $px_matches[1] ) ) {
$wide_px = (int) max( $px_matches[1] );
}
}
if ( ! $wide_px && ! empty( $content_width ) ) {
$wide_px = (int) round( $content_width * 1.3 );
}
$sizes = $wide_px
? sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $wide_px )
: '100vw';
break;
default:
/*
* For center, left, right, or no alignment the default core
behaviour
* (sizing based on the rendered image width) is already
correct.
* Return an empty string to signal that no override is
needed.
*/
return '';
}
/**
* Filters the `sizes` attribute computed for a core/image block.
*
* Themes and plugins can return a custom value that matches their own
* layout — for example when using a non-standard wide breakout width
or
* complex CSS breakpoints.
*
* @param string $sizes The computed `sizes` attribute value.
* @param string $align The block alignment ('wide', 'full',
etc.).
* @param int $image_width The intrinsic width of the image in
pixels.
*/
return (string) apply_filters( 'block_core_image_sizes', $sizes,
$align, $image_width );
}
}}}
----
=== 2. Sizes injection — insert inside `render_block_core_image()`, after
the `data-id` block and before the `FIGCAPTION` block
Find this existing comment in `render_block_core_image()`:
{{{
#!php
/*
* If the `caption` attribute is empty and we encounter a
`<figcaption>` element,
* we take note of its span so we can remove it later.
*/
if ( $processor->next_tag( 'FIGCAPTION' ) && empty(
$attributes['caption'] ) ) {
}}}
Insert the following block '''immediately before''' it:
{{{
#!php
/*
* Inject a corrected `sizes` attribute for alignwide/alignfull images
before
* the processor moves away from the <img> tag.
*
* The later wp_filter_content_tags() pass (priority 12 on
the_content) only
* computes sizes from the image's registered file size and has no
knowledge
* of block alignment, so wide/full aligned images end up with a sizes
value
* that is too narrow and causes blurry stretched output. Setting
sizes here,
* while we still have the full block attribute context, prevents the
generic
* pipeline from overriding it (wp_image_add_srcset_and_sizes()
preserves any
* pre-existing sizes attribute).
*
* @see https://core.trac.wordpress.org/ticket/45407
*/
if ( null === $processor->get_attribute( 'sizes' ) ) {
$image_align = isset( $attributes['align'] ) ?
$attributes['align'] : '';
$img_width = (int) $processor->get_attribute( 'width' );
$computed_sizes = block_core_image_calculate_sizes( $image_align,
$img_width );
if ( $computed_sizes ) {
$processor->set_attribute( 'sizes', $computed_sizes );
}
}
}}}
----
=== How to reproduce the bug
1. Upload an image '''≥ 2000px wide''' so WordPress generates `1536w` and
`2048w` srcset candidates.
2. Insert an Image block, set display size to '''Large (1024px)''' in the
block sidebar, set alignment to '''Full Width'''.
3. Publish and inspect the rendered `<img>` tag.
'''Without fix:'''
{{{
sizes="(max-width: 1024px) 100vw, 1024px"
}}}
The `1536w`/`2048w` srcset candidates are never selected. The 1024px file
is stretched at full viewport width — most visible on Retina/HiDPI
(simulate via DevTools → DPR: 2).
'''With fix:'''
{{{
sizes="100vw"
}}}
On a 1440px Retina viewport the browser correctly downloads the `2048w`
candidate.
=== Backwards compatibility
* `wp_calculate_image_sizes()` — signature and filter unchanged
* `aligncenter` / `alignleft` / `alignright` / no alignment — unaffected,
existing behaviour preserved
* Any image where `sizes` is already present in markup — injection is
skipped entirely
* Works with block themes (reads `wideSize` from `theme.json`) and
classic themes (falls back to `$content_width`)
* New `block_core_image_sizes` filter gives themes/plugins a scoped
override hook
Happy to write a formal `.diff` attachment and unit tests once the
approach is agreed on. Thanks for any feedback!
--
Ticket URL: <https://core.trac.wordpress.org/ticket/45407#comment:51>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list