Skip to content

Commit 48845c7

Browse files
authored
Merge pull request #147 from WordPress/feature/142-multiple-mimes-for-image-sizes
Add the `sources` property to each image size
2 parents bd87192 + acd12f1 commit 48845c7

6 files changed

Lines changed: 614 additions & 113 deletions

File tree

‎modules/images/webp-uploads/load.php‎

Lines changed: 264 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,284 @@
44
* Description: Uses WebP as the default format for new JPEG image uploads if the server supports it.
55
* Experimental: No
66
*
7+
* @since 1.0.0
78
* @package performance-lab
8-
* @since 1.0.0
99
*/
1010

1111
/**
12-
* Filter the image editor default output format mapping.
12+
* Hook called by `wp_generate_attachment_metadata` to create the `sources` property for every image
13+
* size, the sources' property would create a new image size with all the mime types specified in
14+
* `webp_uploads_valid_image_mime_types`. If the original image is one of the mimes from
15+
* `webp_uploads_valid_image_mime_types` the image is just added to the `sources` property and not
16+
* created again. If the uploaded attachment is not a valid image this function does not alter the
17+
* metadata of the attachment, on the other hand a `sources` property is added.
1318
*
14-
* For uploaded JPEG images, map the default output format to WebP.
19+
* @since n.e.x.t
1520
*
16-
* @since 1.0.0
21+
* @see wp_generate_attachment_metadata()
22+
* @see webp_uploads_valid_image_mime_types()
1723
*
18-
* @param string $output_format The image editor default output format mapping.
19-
* @param string $filename Path to the image.
20-
* @param string $mime_type The source image mime type.
21-
* @return string The new output format mapping.
24+
* @param array $metadata An array with the metadata from this attachment.
25+
* @param int $attachment_id The ID of the attachment where the hook was dispatched.
26+
* @return array An array with the updated structure for the metadata before is stored in the database.
2227
*/
23-
function webp_uploads_filter_image_editor_output_format( $output_format, $filename, $mime_type ) {
24-
// Only enable if the server supports WebP.
25-
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
26-
return $output_format;
28+
function webp_uploads_create_sources_property( array $metadata, $attachment_id ) {
29+
// This should take place only on the JPEG image.
30+
$valid_mime_transforms = webp_uploads_get_supported_image_mime_transforms();
31+
32+
// Not a supported mime type to create the sources property.
33+
$mime_type = get_post_mime_type( $attachment_id );
34+
if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
35+
return $metadata;
36+
}
37+
38+
$file = get_attached_file( $attachment_id, true );
39+
40+
// File does not exist.
41+
if ( ! file_exists( $file ) ) {
42+
return $metadata;
43+
}
44+
45+
$dirname = pathinfo( $file, PATHINFO_DIRNAME );
46+
$image_sizes = array();
47+
if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
48+
$image_sizes = $metadata['sizes'];
49+
}
50+
51+
foreach ( wp_get_registered_image_subsizes() as $size_name => $properties ) {
52+
// This image size does not exist on the defined sizes.
53+
if ( ! isset( $image_sizes[ $size_name ] ) || ! is_array( $image_sizes[ $size_name ] ) ) {
54+
continue;
55+
}
56+
57+
$current_size = $image_sizes[ $size_name ];
58+
$sources = array();
59+
if ( isset( $current_size['sources'] ) && is_array( $current_size['sources'] ) ) {
60+
$sources = $current_size['sources'];
61+
}
62+
63+
// Try to find the mime type of the image size.
64+
$current_mime = '';
65+
if ( isset( $current_size['mime-type'] ) ) {
66+
$current_mime = $current_size['mime-type'];
67+
} elseif ( isset( $current_size['file'] ) ) {
68+
$current_mime = wp_check_filetype( $current_size['file'] )['type'];
69+
}
70+
71+
if ( empty( $current_mime ) ) {
72+
continue;
73+
}
74+
75+
$sources[ $current_mime ] = array(
76+
'file' => isset( $current_size['file'] ) ? $current_size['file'] : '',
77+
'filesize' => 0,
78+
);
79+
80+
// Set the filesize from the current mime image.
81+
$file_location = path_join( $dirname, $sources[ $current_mime ]['file'] );
82+
if ( file_exists( $file_location ) ) {
83+
$sources[ $current_mime ]['filesize'] = filesize( $file_location );
84+
}
85+
86+
$formats = isset( $valid_mime_transforms[ $current_mime ] ) ? $valid_mime_transforms[ $current_mime ] : array();
87+
88+
foreach ( $formats as $mime ) {
89+
if ( empty( $sources[ $mime ] ) ) {
90+
$source = webp_uploads_generate_image_size( $attachment_id, $size_name, $mime );
91+
if ( is_array( $source ) ) {
92+
$sources[ $mime ] = $source;
93+
}
94+
}
95+
}
96+
97+
$current_size['sources'] = $sources;
98+
$metadata['sizes'][ $size_name ] = $current_size;
99+
}
100+
101+
return $metadata;
102+
}
103+
104+
add_filter( 'wp_generate_attachment_metadata', 'webp_uploads_create_sources_property', 10, 2 );
105+
106+
/**
107+
* Creates a new image based of the specified attachment with a defined mime type
108+
* this image would be stored in the same place as the provided size name inside the
109+
* metadata of the attachment.
110+
*
111+
* @since n.e.x.t
112+
*
113+
* @param int $attachment_id The ID of the attachment we are going to use as a reference to create the image.
114+
* @param string $size The size name that would be used to create this image, out of the registered subsizes.
115+
* @param string $mime A mime type we are looking to use to create this image.
116+
*
117+
* @return array|WP_Error
118+
*/
119+
function webp_uploads_generate_image_size( $attachment_id, $size, $mime ) {
120+
$sizes = wp_get_registered_image_subsizes();
121+
$metadata = wp_get_attachment_metadata( $attachment_id );
122+
123+
if (
124+
! isset( $metadata['sizes'][ $size ], $sizes[ $size ] )
125+
|| ! is_array( $metadata['sizes'][ $size ] )
126+
|| ! is_array( $sizes[ $size ] )
127+
) {
128+
return new WP_Error( 'image_mime_type_invalid_metadata', __( 'The image does not have a valid metadata.', 'performance-lab' ) );
129+
}
130+
131+
// All subsizes are created out of the attached file.
132+
$file = get_attached_file( $attachment_id );
133+
134+
// File does not exist.
135+
if ( ! file_exists( $file ) ) {
136+
return new WP_Error( 'image_file_size_not_found', __( 'The provided size does not have a valid image file.', 'performance-lab' ) );
137+
}
138+
139+
// Create the subsizes out of the attached file.
140+
$editor = wp_get_image_editor( $file );
141+
142+
if ( is_wp_error( $editor ) ) {
143+
return $editor;
144+
}
145+
146+
$allowed_mimes = array_flip( wp_get_mime_types() );
147+
if ( ! isset( $allowed_mimes[ $mime ] ) || ! is_string( $allowed_mimes[ $mime ] ) ) {
148+
return new WP_Error( 'image_mime_type_invalid', __( 'The provided mime type is not allowed.', 'performance-lab' ) );
27149
}
28150

29-
// WebP lossless support is still limited on servers, so only apply to JPEGs.
30-
if ( 'image/jpeg' !== $mime_type ) {
31-
return $output_format;
151+
if ( ! wp_image_editor_supports( array( 'mime_type' => $mime ) ) ) {
152+
return new WP_Error( 'image_mime_type_not_supported', __( 'The provided mime type is not supported.', 'performance-lab' ) );
32153
}
33154

34-
// Skip conversion when creating the `-scaled` image (for large image uploads).
35-
if ( preg_match( '/-scaled\..{3}.?$/', $filename ) ) {
36-
return $output_format;
155+
$extension = explode( '|', $allowed_mimes[ $mime ] );
156+
$extension = reset( $extension );
157+
158+
$width = null;
159+
$height = null;
160+
$crop = false;
161+
162+
if ( isset( $metadata['sizes'][ $size ]['width'] ) ) {
163+
$width = $metadata['sizes'][ $size ]['width'];
164+
} elseif ( isset( $sizes[ $size ]['widht'] ) ) {
165+
$width = $sizes[ $size ];
166+
}
167+
168+
if ( isset( $metadata['sizes'][ $size ]['height'] ) ) {
169+
$height = $metadata['sizes'][ $size ]['height'];
170+
} elseif ( isset( $sizes[ $size ]['width'] ) ) {
171+
$height = $sizes[ $size ];
172+
}
173+
174+
if ( isset( $sizes[ $size ]['crop'] ) ) {
175+
$crop = (bool) $sizes[ $size ]['crop'];
176+
}
177+
178+
$editor->resize( $width, $height, $crop );
179+
$filename = $editor->generate_filename( null, null, $extension );
180+
$filename = preg_replace( '/-(scaled|rotated|imagifyresized)/', '', $filename );
181+
$image = $editor->save( $filename, $mime );
182+
183+
if ( is_wp_error( $image ) ) {
184+
return $image;
37185
}
38186

39-
$output_format['image/jpeg'] = 'image/webp';
187+
if ( empty( $image['file'] ) ) {
188+
return new WP_Error( 'image_file_not_present', __( 'The file key is not present on the image data', 'performance-lab' ) );
189+
}
190+
191+
return array(
192+
'file' => $image['file'],
193+
'filesize' => isset( $image['path'] ) ? filesize( $image['path'] ) : 0,
194+
);
195+
}
40196

41-
return $output_format;
197+
/**
198+
* Returns an array with the list of valid mime types that a specific mime type can be converted into it,
199+
* for example an image/jpeg can be converted into an image/webp.
200+
*
201+
* @since n.e.x.t
202+
*
203+
* @return array<string, array<string>> An array of valid mime types, where the key is the mime type and the value is the extension type.
204+
*/
205+
function webp_uploads_get_supported_image_mime_transforms() {
206+
$image_mime_transforms = array(
207+
'image/jpeg' => array( 'image/webp' ),
208+
'image/webp' => array( 'image/jpeg' ),
209+
);
210+
211+
/**
212+
* Filter to allow the definition of a custom mime types, in which a defined mime type
213+
* can be transformed and provide a wide range of mime types.
214+
*
215+
* @since n.e.x.t
216+
*
217+
* @param array $image_mime_transforms A map with the valid mime transforms.
218+
*/
219+
return (array) apply_filters( 'webp_uploads_supported_image_mime_transforms', $image_mime_transforms );
220+
}
221+
222+
/**
223+
* Hook fired when an attachment is deleted, this hook is in charge of removing any
224+
* additional mime types created by this plugin besides the original image. Any source
225+
* with the same as the main image would not be removed by this hook due this file would
226+
* be removed by WordPress when the attachment is deleted, usually this happens after this
227+
* hook is executed.
228+
*
229+
* @since n.e.x.t
230+
*
231+
* @see wp_delete_attachment()
232+
*
233+
* @param int $attachment_id The ID of the attachment the sources are going to be deleted.
234+
*/
235+
function webp_uploads_remove_sources_files( $attachment_id ) {
236+
$metadata = wp_get_attachment_metadata( $attachment_id );
237+
$file = get_attached_file( $attachment_id );
238+
239+
if (
240+
! isset( $metadata['sizes'] )
241+
|| empty( $file )
242+
|| ! is_array( $metadata['sizes'] )
243+
) {
244+
return;
245+
}
246+
247+
$upload_path = wp_get_upload_dir();
248+
if ( empty( $upload_path['basedir'] ) ) {
249+
return;
250+
}
251+
252+
$intermediate_dir = path_join( $upload_path['basedir'], dirname( $file ) );
253+
$basename = wp_basename( $file );
254+
255+
foreach ( $metadata['sizes'] as $size ) {
256+
if ( ! isset( $size['sources'] ) || ! is_array( $size['sources'] ) ) {
257+
continue;
258+
}
259+
260+
$original_size_mime = empty( $size['mime-type'] ) ? '' : $size['mime-type'];
261+
262+
foreach ( $size['sources'] as $mime => $properties ) {
263+
/**
264+
* When we face the same mime type as the original image, we ignore this file as this file
265+
* would be removed when the size is removed by WordPress itself. The meta information as well
266+
* would be deleted as soon as the image is removed.
267+
*
268+
* @see wp_delete_attachment
269+
*/
270+
if ( $original_size_mime === $mime ) {
271+
continue;
272+
}
273+
274+
if ( ! is_array( $properties ) || empty( $properties['file'] ) ) {
275+
continue;
276+
}
277+
278+
$intermediate_file = str_replace( $basename, $properties['file'], $file );
279+
if ( ! empty( $intermediate_file ) ) {
280+
$intermediate_file = path_join( $upload_path['basedir'], $intermediate_file );
281+
wp_delete_file_from_directory( $intermediate_file, $intermediate_dir );
282+
}
283+
}
284+
}
42285
}
43-
add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
44286

287+
add_action( 'delete_attachment', 'webp_uploads_remove_sources_files', 10, 1 );

0 commit comments

Comments
 (0)