PluginProbe ʕ •ᴥ•ʔ
Modern Image Formats / trunk
Modern Image Formats vtrunk
2.7.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 2.0.0 2.0.1 2.0.2 2.1.0 2.2.0 2.3.0 2.4.0 2.5.0 2.5.1 2.6.0 2.6.1
webp-uploads / picture-element.php
webp-uploads Last commit date
deprecated.php 20 hours ago helper.php 20 hours ago hooks.php 20 hours ago image-edit.php 20 hours ago load.php 20 hours ago picture-element.php 20 hours ago readme.txt 20 hours ago rest-api.php 20 hours ago settings.php 20 hours ago uninstall.php 20 hours ago
picture-element.php
227 lines
1 <?php
2 /**
3 * Add `picture` element support
4 *
5 * @package webp-uploads
6 *
7 * @since 2.0.0
8 */
9
10 declare( strict_types = 1 );
11
12 // @codeCoverageIgnoreStart
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16 // @codeCoverageIgnoreEnd
17
18 /**
19 * Potentially wrap an image tag in a picture element.
20 *
21 * @since 2.0.0
22 *
23 * @param string $image The image tag.
24 * @param string $context The context for the image tag.
25 * @param int $attachment_id The attachment id.
26 *
27 * @return string The new image tag.
28 */
29 function webp_uploads_wrap_image_in_picture( string $image, string $context, int $attachment_id ): string {
30 if ( ! in_array( $context, array( 'the_content', 'post_thumbnail_html', 'widget_block_content', 'wp_get_attachment_image' ), true ) ) {
31 return $image;
32 }
33
34 /*
35 * Idempotency: bail if this markup has already been processed, to avoid
36 * double-wrapping when more than one rewrite path fires on the same image.
37 *
38 * Two distinct cases are guarded:
39 *
40 * 1. The full `<picture>` string is passed in again.
41 * 2. Only the inner `<img>` is passed in again. This happens when a
42 * `<picture>` produced for a `wp_get_attachment_image()` call is embedded
43 * in post content: `wp_filter_content_tags()` then extracts that inner
44 * `<img>` and runs it back through this function via `wp_content_img_tag`.
45 * The surrounding `<picture>` is not visible at that point, so the wrapped
46 * `<img>` carries a `data-wp-picture-wrapped` marker (added below) to be
47 * recognised here.
48 *
49 * The markup is parsed with WP_HTML_Tag_Processor rather than matched as a
50 * raw substring, so a literal `<picture` or `data-wp-picture-wrapped` string
51 * appearing inside an attribute value (such as `alt` text) cannot trigger a
52 * false positive.
53 */
54 $processor = new WP_HTML_Tag_Processor( $image );
55 while ( $processor->next_tag() ) {
56 if ( 'PICTURE' === $processor->get_tag() ) {
57 return $image;
58 }
59 if ( 'IMG' === $processor->get_tag() && null !== $processor->get_attribute( 'data-wp-picture-wrapped' ) ) {
60 return $image;
61 }
62 }
63
64 $original_file_mime_type = webp_uploads_get_attachment_file_mime_type( $attachment_id );
65 if ( '' === $original_file_mime_type ) {
66 return $image;
67 }
68
69 $image_meta = wp_get_attachment_metadata( $attachment_id );
70
71 if ( ! isset( $image_meta['sizes'] ) ) {
72 return $image;
73 }
74
75 $image_sizes = $image_meta['sizes'];
76
77 // Append missing full size image in $image_sizes array for srcset.
78 if ( isset( $image_meta['sources'], $image_meta['width'], $image_meta['height'] ) ) {
79 array_unshift( $image_sizes, $image_meta );
80 }
81
82 // Extract sizes using regex to parse image tag, then use to retrieve tag.
83 $width = 0;
84 $height = 0;
85 $processor = new WP_HTML_Tag_Processor( $image );
86 if ( $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
87 $width = (int) $processor->get_attribute( 'width' );
88 $height = (int) $processor->get_attribute( 'height' );
89 }
90 $size_to_use = ( $width > 0 && $height > 0 ) ? array( $width, $height ) : 'full';
91
92 $image_src = wp_get_attachment_image_src( $attachment_id, $size_to_use );
93 if ( false === $image_src ) {
94 return $image;
95 }
96 list( $src, $width, $height ) = $image_src;
97 $size_array = array( absint( $width ), absint( $height ) );
98
99 // Collect all the sub size image mime types.
100 $mime_type_data = array();
101 foreach ( $image_sizes as $size ) {
102 if ( isset( $size['sources'] ) && isset( $size['width'] ) && isset( $size['height'] ) ) {
103 foreach ( $size['sources'] as $mime_type => $data ) {
104 if ( wp_image_matches_ratio( $size_array[0], $size_array[1], $size['width'], $size['height'] ) ) {
105 $mime_type_data[ $mime_type ] = $mime_type_data[ $mime_type ] ?? array();
106 $mime_type_data[ $mime_type ]['w'][ $size['width'] ] = $data;
107 $mime_type_data[ $mime_type ]['h'][ $size['height'] ] = $data;
108 }
109 }
110 }
111 }
112 $sub_size_mime_types = array_keys( $mime_type_data );
113
114 // If original image type fallback is not available, don't wrap in picture element.
115 if ( ! in_array( $original_file_mime_type, $sub_size_mime_types, true ) ) {
116 return $image;
117 }
118
119 /**
120 * Filter the image mime types that can be used for the <picture> element.
121 *
122 * Default is: ['image/avif', 'image/webp']. Returning an empty array will skip using the `picture` element.
123 *
124 * The mime types will output in the picture element in the order they are provided.
125 * The original image will be used as the fallback image for browsers that don't support the picture element.
126 *
127 * @since 2.0.0
128 * @since 2.1.0 The default value was updated, removing 'image/jpeg'.
129 *
130 * @param string[] $mime_types Mime types than can be used.
131 * @param int $attachment_id The id of the image being evaluated.
132 */
133 $enabled_mime_types = (array) apply_filters(
134 'webp_uploads_picture_element_mime_types',
135 array(
136 'image/avif',
137 'image/webp',
138 ),
139 $attachment_id
140 );
141
142 $mime_types = array_intersect( $enabled_mime_types, $sub_size_mime_types );
143
144 // No eligible mime types.
145 if ( count( $mime_types ) === 0 ) {
146 return $image;
147 }
148
149 // If the original mime types is the only one available, no picture element is needed.
150 if ( 1 === count( $mime_types ) && current( $mime_types ) === $original_file_mime_type ) {
151 return $image;
152 }
153
154 // Add each mime type to the picture's sources.
155 $picture_sources = '';
156
157 // Gets the srcset and sizes from the IMG tag.
158 $sizes = $processor->get_attribute( 'sizes' );
159 $srcset = $processor->get_attribute( 'srcset' );
160
161 if ( null !== $srcset && null !== $sizes ) {
162 foreach ( $mime_types as $image_mime_type ) {
163 // Filter core's wp_get_attachment_image_srcset to return the sources for the current mime type.
164 $filter = static function ( $sources ) use ( $mime_type_data, $image_mime_type ): array {
165 $filtered_sources = array();
166 foreach ( $sources as $source ) {
167 // Swap the URL for the current mime type.
168 if ( isset( $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ] ) ) {
169 $filename = $mime_type_data[ $image_mime_type ][ $source['descriptor'] ][ $source['value'] ]['file'];
170 $filtered_sources[] = array(
171 'url' => dirname( $source['url'] ) . '/' . $filename,
172 'descriptor' => $source['descriptor'],
173 'value' => $source['value'],
174 );
175 }
176 }
177 return $filtered_sources;
178 };
179 add_filter( 'wp_calculate_image_srcset', $filter );
180 $image_srcset = wp_get_attachment_image_srcset( $attachment_id, $size_array, $image_meta );
181 remove_filter( 'wp_calculate_image_srcset', $filter );
182 if ( is_string( $image_srcset ) ) {
183 $picture_sources .= sprintf(
184 '<source type="%s" srcset="%s"%s>',
185 esc_attr( $image_mime_type ),
186 esc_attr( $image_srcset ),
187 is_string( $sizes ) ? sprintf( ' sizes="%s"', esc_attr( $sizes ) ) : ''
188 );
189 }
190 }
191 } else {
192 foreach ( $mime_types as $image_mime_type ) {
193 $image_srcset = webp_uploads_get_mime_type_image( $attachment_id, $src, $image_mime_type );
194 if ( is_string( $image_srcset ) ) {
195 $picture_sources .= sprintf(
196 '<source type="%s" srcset="%s">',
197 esc_attr( $image_mime_type ),
198 esc_attr( $image_srcset )
199 );
200 }
201 }
202 }
203
204 // Never emit a `<picture>` with no `<source>` children: if every modern-format
205 // source failed to resolve (e.g. the attachment has no modern sub-sizes), return
206 // the original markup untouched instead of a pointless empty wrapper element.
207 if ( '' === $picture_sources ) {
208 return $image;
209 }
210
211 // Tag the inner `<img>` so a later rewrite pass (for example `wp_content_img_tag`
212 // once this markup is embedded in post content) recognises it as already wrapped
213 // and skips it. See the idempotency guard above.
214 $marker = new WP_HTML_Tag_Processor( $image );
215 if ( $marker->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
216 $marker->set_attribute( 'data-wp-picture-wrapped', true );
217 $image = $marker->get_updated_html();
218 }
219
220 return sprintf(
221 '<picture class="%s" style="display: contents;">%s%s</picture>',
222 esc_attr( 'wp-picture-' . $attachment_id ),
223 $picture_sources,
224 $image
225 );
226 }
227