-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add more data about the image as block props in the image block #11973
Changes from 44 commits
4eb7ee5
fe99150
1105451
1c5b8a0
dc8133c
03444d7
fc7c8a6
1aa5b35
2a9fba9
04706dd
08473d8
32d5c8c
cf50aec
e4cfea3
4c49a8d
f9da97f
8cef6ac
84d6866
db48c26
b2bc0e7
f69ec35
009eceb
bf9d58c
51e6c96
174bee0
c26abe2
579cf12
9563082
6efdb5c
a0a6dd9
8e73feb
8535d73
4d2d959
19dad07
87130e4
e2f8007
bf67408
d8d7d6e
e3a99c7
44c7c68
652ac92
078a75b
4be9bee
4b0425f
648811f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -304,6 +304,286 @@ function gutenberg_warn_classic_about_blocks() { | |
<?php | ||
} | ||
|
||
/** | ||
* Add 'actual_size' to the prepared attachment data for the Media Library. | ||
* | ||
* Needed as `wp_prepare_attachment_for_js()` (for the media modal) constrains | ||
* the image sizes to the theme's `$content_width`. | ||
* | ||
* @param array $response Array of prepared attachment data. | ||
* @param WP_Post $attachment Attachment object. | ||
* @param array $meta Array of attachment meta data. | ||
* @return array Array of prepared attachment data. | ||
*/ | ||
function gutenberg_prepare_attachment_for_js( $response, $attachment, $meta ) { | ||
if ( ! empty( $response['type'] ) && 'image' === $response['type'] && ! empty( $response['sizes'] ) ) { | ||
foreach ( $response['sizes'] as $size_name => $size ) { | ||
if ( array_key_exists( $size_name, $meta['sizes'] ) ) { | ||
$response['sizes'][ $size_name ]['actual_size'] = array( | ||
'width' => (int) $meta['sizes'][ $size_name ]['width'], | ||
'height' => (int) $meta['sizes'][ $size_name ]['height'], | ||
); | ||
} | ||
} | ||
} | ||
|
||
return $response; | ||
} | ||
add_filter( 'wp_prepare_attachment_for_js', 'gutenberg_prepare_attachment_for_js', 10, 3 ); | ||
|
||
/** | ||
* Warm the object cache with post and meta information for all found image | ||
* blocks to avoid making individual database calls (similarly to | ||
* `wp_make_content_images_responsive()`). | ||
* | ||
* @access private | ||
* @since 4.5.0 | ||
* | ||
* @param string $content The post content. | ||
* @return string $content Unchanged post content. | ||
*/ | ||
function _gutenberg_cache_images_meta( $content ) { | ||
// Need to find all image blocks and their attachment IDs BEFORE block | ||
// filtering evaluates the rendered result so that attachements meta is | ||
// retrieved all at once from the DB. | ||
// | ||
// [TODO]: When available, the regular expression should be avoided in | ||
// favor of a filter on the parsed result of blocks, still prior to the | ||
// rendered evaluation. | ||
if ( preg_match_all( '/^<!-- wp:image {.*"id":(\d+).*} -->$/m', $content, $matches ) ) { | ||
_prime_post_caches( | ||
$matches[1], | ||
/* $update_term_cache */ false, | ||
/* $update_meta_cache */ true | ||
); | ||
} | ||
|
||
return $content; | ||
} | ||
|
||
// Run before blocks are parsed. | ||
add_filter( 'the_content', '_gutenberg_cache_images_meta', 3 ); | ||
|
||
/** | ||
* Calculates the image width and height based on $block_witdh and the | ||
* `editWidth` block attribute. | ||
* | ||
* @since 4.5.0 | ||
* | ||
* @param array $block_attributes The block attributes. | ||
* @param array $image_meta Optional. The image attachment meta data. | ||
* @return array|bool An array of the image width and height, in that order, or | ||
* false if the image data is missing from $block_attributes. | ||
*/ | ||
function gutenberg_get_image_width_height( $block_attributes, $image_meta = null ) { | ||
if ( ! empty( $block_attributes['width'] ) && ! empty( $block_attributes['height'] ) ) { | ||
// The image was resized. | ||
$image_dimensions = array( | ||
$block_attributes['width'], | ||
$block_attributes['height'], | ||
); | ||
|
||
/* | ||
* Here we can use `$block_attributes['editWidth']` to scale the image | ||
* if we know the theme's "expected width" (in pixels). | ||
* | ||
* Note that if the `$block_attributes['userSetDimensions']` is set/true, the user has entered | ||
* the width and height by hand, they shouldn't probably be changed. | ||
* | ||
* Something like: | ||
* if ( empty( $block_attributes['userSetDimensions'] ) && ! empty( $block_attributes['editWidth'] ) && $content_width <> $block_attributes['editWidth'] ) { | ||
* // Scale the image if the block width in the editor was different than the current theme width. | ||
* $scale = $content_width / $block_attributes['editWidth']; | ||
* $image_width = round( $block_attributes['width'] * $scale ); | ||
* | ||
* $image_dimensions = wp_constrain_dimensions( $image_file_width, $image_file_height, $image_width ); | ||
* } | ||
*/ | ||
} elseif ( ! empty( $block_attributes['fileWidth'] ) && ! empty( $block_attributes['fileHeight'] ) ) { | ||
$image_dimensions = array( | ||
$block_attributes['fileWidth'], | ||
$block_attributes['fileHeight'], | ||
); | ||
} else { | ||
return false; | ||
} | ||
|
||
/* | ||
* Do not constrain images with "wide" and "full" alignment to the "large" image size. | ||
* TODO: To reduce (fix) the need for upscaling or using the "full" size images | ||
* add "xlarge" image size generated by default! | ||
*/ | ||
if ( | ||
! empty( $image_meta['width'] ) && | ||
! empty( $block_attributes['fileWidth'] ) && | ||
( 'wide' === $block_attributes['align'] || 'full' === $block_attributes['align'] ) | ||
) { | ||
$size_updated = false; | ||
|
||
// Attempt to find the largest image size that may have been added by themes or plugins. | ||
if ( ! empty( $image_meta['sizes'] ) ) { | ||
foreach ( $image_meta['sizes'] as $size ) { | ||
if ( $size['width'] > $image_dimensions[0] && wp_image_matches_ratio( $block_attributes['fileWidth'], $block_attributes['fileHeight'], $size['width'], $size['height'] ) ) { | ||
$image_dimensions = array( | ||
$size['width'], | ||
$size['height'], | ||
); | ||
|
||
$size_updated = true; | ||
} | ||
} | ||
} | ||
|
||
if ( | ||
! $size_updated && | ||
$block_attributes['fileWidth'] < $image_meta['width'] && | ||
// Do not force site visitors to download HUGE images. | ||
// Max 12 MP photo (that's still pretty arbitrary, may be over 3MB, consider reducing). | ||
max( (int) $image_meta['width'], (int) $image_meta['height'] ) < 4300 && | ||
wp_image_matches_ratio( $block_attributes['fileWidth'], $block_attributes['fileHeight'], $image_meta['width'], $image_meta['height'] ) | ||
) { | ||
$image_dimensions = array( | ||
$image_meta['width'], | ||
$image_meta['height'], | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Filters the image size for the image block. | ||
* | ||
* @since 4.5.0 | ||
* | ||
* @param array $image_dimensions The calculated image size width and | ||
* height (in that order). | ||
* @param array $block_attributes The block attributes. | ||
* @param array $image_meta The image attachment meta data. | ||
*/ | ||
return apply_filters( 'block_core_image_get_width_height', $image_dimensions, $block_attributes, $image_meta ); | ||
} | ||
|
||
/** | ||
* Filters the rendered output of the Image block to include generated HTML | ||
* attributes for front-end display. | ||
* | ||
* @since 4.5.0 | ||
* | ||
* @param string $html Original HTML. | ||
* @param array $block Parsed block. | ||
* @return string Filtered Image block HTML. | ||
*/ | ||
function gutenberg_render_block_core_image( $html, $block ) { | ||
// Old post or... something's wrong. | ||
if ( empty( $html ) || empty( $block['attrs'] ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should be shortcutting here if the block is not the image block? It'll eventually be shortcutted at the below test of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, good catch. Added in 648811f. |
||
return $html; | ||
} | ||
|
||
$defaults = array( | ||
'url' => '', | ||
'alt' => '', | ||
'id' => 0, | ||
'align' => '', | ||
); | ||
|
||
$block_attributes = wp_parse_args( $block['attrs'], $defaults ); | ||
|
||
if ( empty( $block_attributes['url'] ) ) { | ||
// We don't have enough data to construct new img tag. Fall back to the existing HTML. | ||
return $html; | ||
} | ||
|
||
if ( ! empty( $block_attributes['id'] ) ) { | ||
$attachment_id = (int) $block_attributes['id']; | ||
$image_meta = wp_get_attachment_metadata( $attachment_id ); | ||
} else { | ||
$attachment_id = 0; | ||
$image_meta = null; | ||
} | ||
|
||
$image_dimensions = gutenberg_get_image_width_height( $block_attributes, $image_meta ); | ||
$image_src = ''; | ||
$srcset = ''; | ||
$sizes = ''; | ||
|
||
if ( empty( $image_dimensions ) ) { | ||
// We don't have enough data to construct new img tag. Fall back to the existing HTML. | ||
return $html; | ||
} | ||
|
||
$image_src = $block_attributes['url']; | ||
|
||
$image_attributes = array( | ||
'src' => $image_src, | ||
'alt' => empty( $block_attributes['alt'] ) ? '' : $block_attributes['alt'], | ||
'width' => $image_dimensions[0], | ||
'height' => $image_dimensions[1], | ||
); | ||
|
||
if ( $image_meta ) { | ||
// TODO: pass `$block_attributes` to the filter. | ||
$srcset = wp_calculate_image_srcset( $image_dimensions, $image_src, $image_meta, $attachment_id ); | ||
|
||
if ( ! empty( $srcset ) ) { | ||
// TODO: pass `$block_attributes` to the filter. This will let themes generate better `sizes` attribute. | ||
$sizes = wp_calculate_image_sizes( $image_dimensions, $image_src, $image_meta, $attachment_id ); | ||
} | ||
|
||
if ( $srcset && $sizes ) { | ||
$image_attributes['srcset'] = $srcset; | ||
$image_attributes['sizes'] = $sizes; | ||
} | ||
} | ||
|
||
/** | ||
* Filters the image tag attributes when rendering the core image block. | ||
* | ||
* @since 4.5.0 | ||
* | ||
* @param array $image_attributes The (recalculated) image attributes. | ||
* Note: expects valid HTML 5.0 attribute names. | ||
* @param array $block_attributes The image block attributes. | ||
* @param string $html The image block HTML coming from the | ||
* editor. The img tag will be replaced. | ||
*/ | ||
$image_attributes = apply_filters( 'render_block_core_image_tag_attributes', $image_attributes, $block_attributes, $html ); | ||
|
||
$attr = ''; | ||
foreach ( $image_attributes as $name => $value ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not super confident in going this route (i.e. rebuilding the markup) because it carries a lot of overhead in future updates to the image block client side. Also, I worry we package a lot of this functionality in the image block alone, and any other block that uses images, misses on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using block props to rebuild the Of course we can always go "the other route" like in the current processing when adding srcset and sizes: use regex to parse some of the attributes, then add or replace them with recalculated values. |
||
// Sanitize for valid HTML 5.0 attribute names. | ||
// TODO: perhaps add core function to test this. | ||
$name = strtolower( $name ); | ||
|
||
if ( strpos( $name, 'data-' ) === 0 ) { | ||
$is_invalid_attribute_name = preg_match( '/[\\\\u007F-\\\\u009F "\'>\/=\\\\uFDD0-\\\\uFDEF]/', $name ); | ||
} else { | ||
// List of valid HTML attribute names: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes. | ||
$is_invalid_attribute_name = preg_match( '/[^a-z0-9-]/', $name ); | ||
} | ||
|
||
if ( $is_invalid_attribute_name ) { | ||
continue; | ||
} | ||
|
||
if ( 'src' === $name ) { | ||
$value = esc_url( $value ); | ||
} elseif ( ( 'width' === $name || 'height' === $name ) && ! empty( $value ) ) { | ||
$value = (int) $value; | ||
} else { | ||
$value = esc_attr( $value ); | ||
} | ||
|
||
$attr .= sprintf( ' %s="%s"', $name, $value ); | ||
} | ||
|
||
$image_tag = '<img' . $attr . ' />'; | ||
|
||
// Replace the img tag. | ||
$html = preg_replace( '/<img\s[^>]+>/', $image_tag, $html ); | ||
|
||
return $html; | ||
} | ||
add_filter( 'render_block', 'gutenberg_render_block_core_image', 10, 2 ); | ||
|
||
/** | ||
* Display the privacy policy help notice. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a reason for this not to be hooking into the block filter so that we can get the actual attributes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose here is to get the IDs of all images (where we will do srcset and sizes) from the post before we parse the blocks. That's needed to get all image attachment meta in one go from the DB as that's faster.
So we need an array with all attachment IDs for the images used in the post. This is equivalent to https://core.trac.wordpress.org/browser/tags/4.9.8/src/wp-includes/media.php#L1336.
That should also include images from "Cover" and "Media and Text" blocks, and probably the Gallery block (if images there are "hardcoded"). Any ideas how to do that without a regex are very welcome :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously: #11377 (comment)
I'm not sure I understand why this would need to occur prior to the parse. If it at least occurs prior to any block evaluating its
render_callback
, wouldn't that be equally sufficient to warm the cache?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this doesn't exist as an option today anyways (requires filter on this value), we probably can't do anything of this sort right now.
Should follow-up with a Trac ticket as appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this doesn't do anything if we do it "per block". It makes sense only when it happens for all of post_content at once before we start to look at individual images and generating
srcset
andsizes
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we hook into the block-level filter (the design of which I favor in #10108) then we can track the parse through the blocks and then prime the cache on the
do_blocks
filter which occurs after parsing and rendering is complete.that is, we could imagine something like this…
and this because we know that the final
do_blocks
filter runs after block-level processing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noting that this is still based on a RegExp instead of using the per-block and per-document filters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option would be to "unify" this with the "old" cache priming. We have the (required)
wp-image-####
className on all images, including in Cover and Media and Text blocks.Will need to abstract
wp_make_content_images_responsive()
a bit, move the caching out and run it earlier onthe_content
filter.On the other hand
_prime_post_caches()
is "clever enough" to not fetch posts and meta that are already cached so running two pre-caching methods won't "hurt" anything.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning towards doing this in conjunction with #10108 because I want to be sure the design is ergonomic enough to power this use case in a way that is scalable and transitive to other cases on other blocks.