Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions src/wp-admin/includes/image.php
Original file line number Diff line number Diff line change
Expand Up @@ -484,24 +484,73 @@ function _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id ) {
}
}

/*
* Group the sizes that would produce an identical file so each is generated
* only once and then shared by every size name that requested it.
*
* Sub-sizes are stored on disk by their dimensions, so two registered sizes
* with the same width, height, and crop resolve to the same file. Without
* grouping, that file is re-encoded and re-written once per size name, which
* slows uploads and is a common cause of timeouts when many sizes are
* registered (for example a plugin size equal to a core size).
*/
$size_groups = array();

foreach ( $new_sizes as $new_size_name => $new_size_data ) {
$signature = wp_json_encode(
array(
'width' => isset( $new_size_data['width'] ) ? $new_size_data['width'] : null,
'height' => isset( $new_size_data['height'] ) ? $new_size_data['height'] : null,
'crop' => isset( $new_size_data['crop'] ) ? $new_size_data['crop'] : false,
)
);

if ( ! isset( $size_groups[ $signature ] ) ) {
$size_groups[ $signature ] = array(
'data' => $new_size_data,
'names' => array(),
);
}

$size_groups[ $signature ]['names'][] = $new_size_name;
}

if ( method_exists( $editor, 'make_subsize' ) ) {
foreach ( $new_sizes as $new_size_name => $new_size_data ) {
$new_size_meta = $editor->make_subsize( $new_size_data );
foreach ( $size_groups as $group ) {
$new_size_meta = $editor->make_subsize( $group['data'] );

if ( is_wp_error( $new_size_meta ) ) {
// TODO: Log errors.
} else {
// Save the size meta value.
$image_meta['sizes'][ $new_size_name ] = $new_size_meta;
// Save the size meta value for every size name with these dimensions.
foreach ( $group['names'] as $new_size_name ) {
$image_meta['sizes'][ $new_size_name ] = $new_size_meta;
}

wp_update_attachment_metadata( $attachment_id, $image_meta );
}
}
} else {
// Fall back to `$editor->multi_resize()`.
$created_sizes = $editor->multi_resize( $new_sizes );
// Fall back to `$editor->multi_resize()`, generating one file per unique size.
$unique_sizes = array();

foreach ( $size_groups as $signature => $group ) {
$unique_sizes[ $signature ] = $group['data'];
}

$created_sizes = $editor->multi_resize( $unique_sizes );
Comment on lines +537 to +541

if ( ! empty( $created_sizes ) ) {
$image_meta['sizes'] = array_merge( $image_meta['sizes'], $created_sizes );
foreach ( $size_groups as $signature => $group ) {
if ( ! isset( $created_sizes[ $signature ] ) ) {
continue;
}

foreach ( $group['names'] as $new_size_name ) {
$image_meta['sizes'][ $new_size_name ] = $created_sizes[ $signature ];
}
}

wp_update_attachment_metadata( $attachment_id, $image_meta );
}
}
Expand Down
118 changes: 118 additions & 0 deletions tests/phpunit/tests/image/makeSubsizes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';

/**
* Tests for the `_wp_make_subsizes()` function.
*
* @group image
* @group media
* @group upload
*
* @covers ::_wp_make_subsizes
*/
class Tests_Image_MakeSubsizes extends WP_UnitTestCase {

public function set_up() {
parent::set_up();

require_once ABSPATH . 'wp-admin/includes/image.php';

if ( ! call_user_func( array( 'WP_Image_Editor_GD', 'test' ) ) ) {
$this->markTestSkipped( 'The GD image editor is not available.' );
}
}

public function tear_down() {
remove_all_filters( 'wp_image_editors' );
$this->remove_added_uploads();

parent::tear_down();
}

/**
* Ensures registered sizes that resolve to the same file are generated once.
*
* Sub-sizes are stored on disk by their dimensions, so two sizes registered
* with matching width, height, and crop produce an identical file. They
* should be generated a single time and shared by every size name, rather
* than re-encoded once per name.
*
* @ticket 61925
*
* @covers ::_wp_make_subsizes
*/
public function test_identical_sizes_are_generated_once_and_shared() {
Spy_Make_Subsize_Image_Editor::$make_subsize_count = 0;

add_filter(
'wp_image_editors',
static function () {
return array( 'Spy_Make_Subsize_Image_Editor' );
}
);

$attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' );

$file = get_attached_file( $attachment_id );
$image_meta = wp_get_attachment_metadata( $attachment_id );

// Count only the sub-sizes generated by the call under test.
Spy_Make_Subsize_Image_Editor::$make_subsize_count = 0;

// Two size names requesting an identical file, plus one distinct size.
$new_sizes = array(
'test_dedup_a' => array(
'width' => 40,
'height' => 40,
'crop' => true,
),
'test_dedup_b' => array(
'width' => 40,
'height' => 40,
'crop' => true,
),
'test_distinct' => array(
'width' => 80,
'height' => 80,
'crop' => true,
),
);

$result = _wp_make_subsizes( $new_sizes, $file, $image_meta, $attachment_id );

$this->assertSame(
2,
Spy_Make_Subsize_Image_Editor::$make_subsize_count,
'Two identical sizes and one distinct size should generate two files, not three.'
);

$this->assertArrayHasKey( 'test_dedup_a', $result['sizes'], 'The first identical size should be recorded.' );
$this->assertArrayHasKey( 'test_dedup_b', $result['sizes'], 'The second identical size should be recorded.' );
$this->assertSame(
$result['sizes']['test_dedup_a'],
$result['sizes']['test_dedup_b'],
'Both identical size names should reference the same generated file.'
);
}
}

/**
* A GD image editor that counts calls to make_subsize() for the dedup test.
*/
class Spy_Make_Subsize_Image_Editor extends WP_Image_Editor_GD {

/**
* Number of times make_subsize() has been called.
*
* @var int
*/
public static $make_subsize_count = 0;

public function make_subsize( $size_data ) {
++self::$make_subsize_count;

return parent::make_subsize( $size_data );
}
}
Loading