Skip to content
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

Create image sub-sizes in additional MIME types using sources for storage. #147

Merged
merged 37 commits into from Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0706e81
Add the `sources` property to each image size
mitogh Feb 3, 2022
9c43b89
Fix linting errors from `phpcs`
mitogh Feb 3, 2022
25479a1
Make sure the correct size is updated
mitogh Feb 3, 2022
0f560d8
Merge branch 'trunk' into feature/142-multiple-mimes-for-image-sizes
mitogh Feb 17, 2022
410c124
Update code to use existing WP Core function instead.
mitogh Feb 17, 2022
7a8b0c5
Reorder functions and hooks.
mitogh Feb 17, 2022
f71a897
Use a third person verb instead.
mitogh Feb 17, 2022
4b4cbc2
Fix `webp` typo in the extension
mitogh Feb 17, 2022
b01be99
Merge branch 'feature/142-multiple-mimes-for-image-sizes' of github.c…
mitogh Feb 17, 2022
4ba9409
Define the right type for the doc block.
mitogh Feb 17, 2022
5321483
Fix syntax error when calling `wp_get_registered_image_subsizes`
mitogh Feb 17, 2022
2bf68f6
Removal of support class no longer required
mitogh Feb 21, 2022
de38c2d
Add test images, the images would be used in tests.
mitogh Feb 21, 2022
dd13025
Add required methods for the mock Editor.
mitogh Feb 21, 2022
70eeb33
Remove non required conditional on testing method.
mitogh Feb 21, 2022
107db40
Add support for multiple mime types for existing images sizes.
mitogh Feb 21, 2022
975fddd
Include tests for the deletion of an attachment.
mitogh Feb 21, 2022
e3c2e3a
Update file to follow `PHPCS` rules.
mitogh Feb 21, 2022
6e3afee
Prevent naming collisions with existing functions.
mitogh Feb 21, 2022
76f1c2f
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
fba8fab
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
7da54dc
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
2c37d19
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
2c6307f
Fix alignment on dockblock
mitogh Feb 23, 2022
5b5ff6d
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
0e94c15
Replace `array_key_exists` with `isset`
mitogh Feb 23, 2022
a5b63df
Updates from code review.
mitogh Feb 23, 2022
e0b48c4
Fix linting errors
mitogh Feb 23, 2022
e996230
Adjust logic and variable names to follow WP Core.
mitogh Feb 23, 2022
d708dfd
Fix linting error of alignment
mitogh Feb 23, 2022
061ef31
Remove blank lines.
mitogh Feb 23, 2022
81ea713
Add parenthesis to method references.
mitogh Feb 23, 2022
411efe4
Add parenthesis to method references.
mitogh Feb 23, 2022
a2c6815
Update documentation on function return value.
mitogh Feb 23, 2022
a2c655c
Update image generation into a single process.
mitogh Feb 23, 2022
38224b0
Fix linting errors for PHP
mitogh Feb 24, 2022
acd12f1
Replace `array_key_exists` with `isset` instead.
mitogh Feb 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
285 changes: 264 additions & 21 deletions modules/images/webp-uploads/load.php
Expand Up @@ -4,41 +4,284 @@
* Description: Uses WebP as the default format for new JPEG image uploads if the server supports it.
* Experimental: No
*
* @since 1.0.0
* @package performance-lab
* @since 1.0.0
*/

/**
* Filter the image editor default output format mapping.
* Hook called by `wp_generate_attachment_metadata` to create the `sources` property for every image
* size, the sources' property would create a new image size with all the mime types specified in
* `webp_uploads_valid_image_mime_types`. If the original image is one of the mimes from
* `webp_uploads_valid_image_mime_types` the image is just added to the `sources` property and not
* created again. If the uploaded attachment is not a valid image this function does not alter the
* metadata of the attachment, on the other hand a `sources` property is added.
*
* For uploaded JPEG images, map the default output format to WebP.
* @since n.e.x.t
*
* @since 1.0.0
* @see wp_generate_attachment_metadata()
* @see webp_uploads_valid_image_mime_types()
*
* @param string $output_format The image editor default output format mapping.
* @param string $filename Path to the image.
* @param string $mime_type The source image mime type.
* @return string The new output format mapping.
* @param array $metadata An array with the metadata from this attachment.
* @param int $attachment_id The ID of the attachment where the hook was dispatched.
* @return array An array with the updated structure for the metadata before is stored in the database.
*/
function webp_uploads_filter_image_editor_output_format( $output_format, $filename, $mime_type ) {
adamsilverstein marked this conversation as resolved.
Show resolved Hide resolved
// Only enable if the server supports WebP.
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
return $output_format;
function webp_uploads_create_sources_property( array $metadata, $attachment_id ) {
mitogh marked this conversation as resolved.
Show resolved Hide resolved
// This should take place only on the JPEG image.
$valid_mime_transforms = webp_uploads_get_supported_image_mime_transforms();

// Not a supported mime type to create the sources property.
$mime_type = get_post_mime_type( $attachment_id );
if ( ! isset( $valid_mime_transforms[ $mime_type ] ) ) {
adamsilverstein marked this conversation as resolved.
Show resolved Hide resolved
return $metadata;
}

$file = get_attached_file( $attachment_id, true );

// File does not exist.
if ( ! file_exists( $file ) ) {
return $metadata;
}

$dirname = pathinfo( $file, PATHINFO_DIRNAME );
$image_sizes = array();
if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
$image_sizes = $metadata['sizes'];
}

foreach ( wp_get_registered_image_subsizes() as $size_name => $properties ) {
// This image size does not exist on the defined sizes.
if ( ! isset( $image_sizes[ $size_name ] ) || ! is_array( $image_sizes[ $size_name ] ) ) {
continue;
}

$current_size = $image_sizes[ $size_name ];
$sources = array();
if ( isset( $current_size['sources'] ) && is_array( $current_size['sources'] ) ) {
$sources = $current_size['sources'];
}

// Try to find the mime type of the image size.
$current_mime = '';
if ( isset( $current_size['mime-type'] ) ) {
$current_mime = $current_size['mime-type'];
} elseif ( isset( $current_size['file'] ) ) {
$current_mime = wp_check_filetype( $current_size['file'] )['type'];
}

if ( empty( $current_mime ) ) {
continue;
}

$sources[ $current_mime ] = array(
'file' => isset( $current_size['file'] ) ? $current_size['file'] : '',
'filesize' => 0,
);

// Set the filesize from the current mime image.
$file_location = path_join( $dirname, $sources[ $current_mime ]['file'] );
if ( file_exists( $file_location ) ) {
$sources[ $current_mime ]['filesize'] = filesize( $file_location );
}

$formats = isset( $valid_mime_transforms[ $current_mime ] ) ? $valid_mime_transforms[ $current_mime ] : array();

foreach ( $formats as $mime ) {
if ( empty( $sources[ $mime ] ) ) {
$source = webp_uploads_generate_image_size( $attachment_id, $size_name, $mime );
if ( is_array( $source ) ) {
$sources[ $mime ] = $source;
}
}
}

$current_size['sources'] = $sources;
$metadata['sizes'][ $size_name ] = $current_size;
}

return $metadata;
}

add_filter( 'wp_generate_attachment_metadata', 'webp_uploads_create_sources_property', 10, 2 );

/**
* Creates a new image based of the specified attachment with a defined mime type
* this image would be stored in the same place as the provided size name inside the
* metadata of the attachment.
*
* @since n.e.x.t
*
* @param int $attachment_id The ID of the attachment we are going to use as a reference to create the image.
* @param string $size The size name that would be used to create this image, out of the registered subsizes.
* @param string $mime A mime type we are looking to use to create this image.
*
* @return array|WP_Error
*/
function webp_uploads_generate_image_size( $attachment_id, $size, $mime ) {
$sizes = wp_get_registered_image_subsizes();
$metadata = wp_get_attachment_metadata( $attachment_id );

if (
! isset( $metadata['sizes'][ $size ], $sizes[ $size ] )
|| ! is_array( $metadata['sizes'][ $size ] )
|| ! is_array( $sizes[ $size ] )
) {
return new WP_Error( 'image_mime_type_invalid_metadata', __( 'The image does not have a valid metadata.', 'performance-lab' ) );
}

// All subsizes are created out of the attached file.
$file = get_attached_file( $attachment_id );

// File does not exist.
if ( ! file_exists( $file ) ) {
return new WP_Error( 'image_file_size_not_found', __( 'The provided size does not have a valid image file.', 'performance-lab' ) );
}
felixarntz marked this conversation as resolved.
Show resolved Hide resolved

// Create the subsizes out of the attached file.
$editor = wp_get_image_editor( $file );

if ( is_wp_error( $editor ) ) {
return $editor;
}

$allowed_mimes = array_flip( wp_get_mime_types() );
if ( ! isset( $allowed_mimes[ $mime ] ) || ! is_string( $allowed_mimes[ $mime ] ) ) {
return new WP_Error( 'image_mime_type_invalid', __( 'The provided mime type is not allowed.', 'performance-lab' ) );
}

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

// Skip conversion when creating the `-scaled` image (for large image uploads).
if ( preg_match( '/-scaled\..{3}.?$/', $filename ) ) {
return $output_format;
$extension = explode( '|', $allowed_mimes[ $mime ] );
$extension = reset( $extension );

$width = null;
$height = null;
$crop = false;

if ( isset( $metadata['sizes'][ $size ]['width'] ) ) {
$width = $metadata['sizes'][ $size ]['width'];
} elseif ( isset( $sizes[ $size ]['widht'] ) ) {
$width = $sizes[ $size ];
}

if ( isset( $metadata['sizes'][ $size ]['height'] ) ) {
$height = $metadata['sizes'][ $size ]['height'];
} elseif ( isset( $sizes[ $size ]['width'] ) ) {
$height = $sizes[ $size ];
}

if ( isset( $sizes[ $size ]['crop'] ) ) {
$crop = (bool) $sizes[ $size ]['crop'];
}

$editor->resize( $width, $height, $crop );
$filename = $editor->generate_filename( null, null, $extension );
$filename = preg_replace( '/-(scaled|rotated|imagifyresized)/', '', $filename );
adamsilverstein marked this conversation as resolved.
Show resolved Hide resolved
$image = $editor->save( $filename, $mime );

if ( is_wp_error( $image ) ) {
return $image;
}

$output_format['image/jpeg'] = 'image/webp';
if ( empty( $image['file'] ) ) {
return new WP_Error( 'image_file_not_present', __( 'The file key is not present on the image data', 'performance-lab' ) );
}

return array(
'file' => $image['file'],
'filesize' => isset( $image['path'] ) ? filesize( $image['path'] ) : 0,
);
}

return $output_format;
/**
* Returns an array with the list of valid mime types that a specific mime type can be converted into it,
* for example an image/jpeg can be converted into an image/webp.
*
* @since n.e.x.t
*
* @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.
*/
function webp_uploads_get_supported_image_mime_transforms() {
$image_mime_transforms = array(
'image/jpeg' => array( 'image/webp' ),
'image/webp' => array( 'image/jpeg' ),
);

/**
* Filter to allow the definition of a custom mime types, in which a defined mime type
* can be transformed and provide a wide range of mime types.
*
* @since n.e.x.t
*
* @param array $image_mime_transforms A map with the valid mime transforms.
*/
return (array) apply_filters( 'webp_uploads_supported_image_mime_transforms', $image_mime_transforms );
}

/**
* Hook fired when an attachment is deleted, this hook is in charge of removing any
* additional mime types created by this plugin besides the original image. Any source
* with the same as the main image would not be removed by this hook due this file would
* be removed by WordPress when the attachment is deleted, usually this happens after this
* hook is executed.
*
* @since n.e.x.t
*
* @see wp_delete_attachment()
*
* @param int $attachment_id The ID of the attachment the sources are going to be deleted.
*/
function webp_uploads_remove_sources_files( $attachment_id ) {
$metadata = wp_get_attachment_metadata( $attachment_id );
$file = get_attached_file( $attachment_id );

if (
! isset( $metadata['sizes'] )
|| empty( $file )
|| ! is_array( $metadata['sizes'] )
) {
return;
}

$upload_path = wp_get_upload_dir();
if ( empty( $upload_path['basedir'] ) ) {
return;
}

$intermediate_dir = path_join( $upload_path['basedir'], dirname( $file ) );
$basename = wp_basename( $file );

foreach ( $metadata['sizes'] as $size ) {
if ( ! isset( $size['sources'] ) || ! is_array( $size['sources'] ) ) {
continue;
}

$original_size_mime = empty( $size['mime-type'] ) ? '' : $size['mime-type'];

foreach ( $size['sources'] as $mime => $properties ) {
/**
* When we face the same mime type as the original image, we ignore this file as this file
* would be removed when the size is removed by WordPress itself. The meta information as well
* would be deleted as soon as the image is removed.
*
* @see wp_delete_attachment
*/
if ( $original_size_mime === $mime ) {
continue;
}

if ( ! is_array( $properties ) || empty( $properties['file'] ) ) {
continue;
}

$intermediate_file = str_replace( $basename, $properties['file'], $file );
if ( ! empty( $intermediate_file ) ) {
$intermediate_file = path_join( $upload_path['basedir'], $intermediate_file );
wp_delete_file_from_directory( $intermediate_file, $intermediate_dir );
}
}
}
}
add_filter( 'image_editor_output_format', 'webp_uploads_filter_image_editor_output_format', 10, 3 );
mitogh marked this conversation as resolved.
Show resolved Hide resolved
adamsilverstein marked this conversation as resolved.
Show resolved Hide resolved

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