Make WordPress Core


Ignore:
Timestamp:
07/21/2022 06:01:01 PM (2 years ago)
Author:
adamsilverstein
Message:

Media: enable generating multiple mime types for image uploads; specifically WebP versions for JPEG images by default.

This changeset adds the capability for core media uploads to generate sub sized images in more than a single mime type. The output formats for each mime type can be controlled through a filter. WebP is used as an additional output format for JPEG images by default to improve front end performance.

When generating additional mime types, only images which are smaller than the respective original are retained. By default, additional mime type images are only generated for the built-in core image sizes and any custom sizes that have opted in.

Image meta is updated with a new 'sources' array containing file details for each mime type. Each image size in the 'sizes' array also gets a new 'sources' array that contains the image file details for each mime type.

This change also increases image upload retries to accommodate additional image sizes. It also adds a $mime_type parameter to the wp_get_missing_image_subsizes function and filter.

This change adds three new filters to enable full control of secondary mime image generation and output:

  • A new filter wp_image_sizes_with_additional_mime_type_support that filters the sizes that support secondary mime type output. Developers can use this to control the output of additional mime type sub-sized images on a per size basis.
  • A new filter wp_upload_image_mime_transforms that filters the output mime types for a given input mime type. Developers can use this to control generation of additional mime types for a given input mime type or even override the original mime type.
  • A new filter wp_content_image_mimes which controls image mime type output selection and order for frontend content. Developers can use this to control the mime type output preference order for content images. Content images inserted from the media library will use the available image versions based on the order from this filter.

Thanks to the many contributors who helped develop, test and give feedback on this feature.

A haiku to summarize:

Upload a JPEG
Images of all sizes
Output as WebPs

Props flixos90, MatthiasReinholz, studiolxv, markhowellsmead, eatingrules, pbiron, mukesh27, joegrainger, mehulkaklotar, tweetythierry, akshitsethi, peterwilsoncc, eugenemanuilov, mitogh, shetheliving, clarkeemily, codekraft, mikeschroder, clorith, kasparsd, spacedmonkey, trevorpfromsandee, jb510, scofennellgmailcom, seedsca, cagsmith, karinclimber, dainemawer, baxbridge, grapplerulrich, sobatkras, chynnabenton, tonylocalword, barneydavey, kwillmorth, garymatthews919, olliejones, imarkinteractive, jeffpaul, feastdesignco, webbeetle, masteradhoc.

See #55443.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/image/editor.php

    r52248 r53751  
    132132
    133133        // Removing PNG to WEBP conversion on save. Quality setting should reset to the default.
     134        $editor->reset_output_mime_type();
    134135        remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_formats' ) );
    135136        $editor->save();
     
    155156
    156157        // After removing the conversion the quality setting should reset to the filtered value for the original image type, JPEG.
     158        $editor->reset_output_mime_type();
    157159        remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_formats' ) );
    158160        $editor->save();
     
    227229
    228230        // Test with a suffix only.
    229         $this->assertSame( 'canola-100x50.png', wp_basename( $editor->generate_filename( null, null, 'png' ) ) );
     231        $this->assertSame( 'canola-100x50-jpg.png', wp_basename( $editor->generate_filename( null, null, 'png' ) ) );
    230232
    231233        // Combo!
    232         $this->assertSame( trailingslashit( realpath( get_temp_dir() ) ) . 'canola-new.png', $editor->generate_filename( 'new', realpath( get_temp_dir() ), 'png' ) );
     234        $this->assertSame( trailingslashit( realpath( get_temp_dir() ) ) . 'canola-new-jpg.png', $editor->generate_filename( 'new', realpath( get_temp_dir() ), 'png' ) );
    233235
    234236        // Test with a stream destination.
     
    363365    }
    364366
     367    /**
     368     * Test creating  the original image mime type when the image is uploaded.
     369     *
     370     * @ticket 55443
     371     *
     372     * @dataProvider provider_image_with_default_behaviors_during_upload
     373     */
     374    public function it_should_create_the_original_image_mime_type_when_the_image_is_uploaded( $file_location, $expected_mime, $targeted_mime ) {
     375        $attachment_id = $this->factory->attachment->create_upload_object( $file_location );
     376
     377        $metadata = wp_get_attachment_metadata( $attachment_id );
     378
     379        $this->assertIsArray( $metadata );
     380        foreach ( $metadata['sizes'] as $size_name => $properties ) {
     381            $this->assertArrayHasKey( 'sources', $properties );
     382            $this->assertIsArray( $properties['sources'] );
     383            $this->assertArrayHasKey( $expected_mime, $properties['sources'] );
     384            $this->assertArrayHasKey( 'filesize', $properties['sources'][ $expected_mime ] );
     385            $this->assertArrayHasKey( 'file', $properties['sources'][ $expected_mime ] );
     386            $this->assertArrayHasKey( $targeted_mime, $properties['sources'] );
     387            $this->assertArrayHasKey( 'filesize', $properties['sources'][ $targeted_mime ] );
     388            $this->assertArrayHasKey( 'file', $properties['sources'][ $targeted_mime ] );
     389        }
     390    }
     391
     392    /**
     393     * Data provider for it_should_create_the_original_image_mime_type_when_the_image_is_uploaded.
     394     */
     395    public function provider_image_with_default_behaviors_during_upload() {
     396        yield 'JPEG image' => array(
     397            DIR_TESTDATA . '/images/test-image.jpg',
     398            'image/jpeg',
     399            'image/webp',
     400        );
     401
     402        yield 'WebP image' => array(
     403            DIR_TESTDATA . '/images/webp-lossy.webp',
     404            'image/webp',
     405            'image/jpeg',
     406        );
     407    }
     408
     409    /**
     410     * Test Do not create the sources property if no transform is provided.
     411     *
     412     * @ticket 55443
     413     */
     414    public function it_should_not_create_the_sources_property_if_no_transform_is_provided() {
     415        add_filter( 'wp_upload_image_mime_transforms', '__return_empty_array' );
     416
     417        $attachment_id = $this->factory->attachment->create_upload_object(
     418            DIR_TESTDATA . '/images/test-image.jpg'
     419        );
     420
     421        $metadata = wp_get_attachment_metadata( $attachment_id );
     422
     423        $this->assertIsArray( $metadata );
     424        foreach ( $metadata['sizes'] as $size_name => $properties ) {
     425            $this->assertArrayNotHasKey( 'sources', $properties );
     426        }
     427    }
     428
     429    /**
     430     * Test creating the sources property when no transform is available.
     431     *
     432     * @ticket 55443
     433     */
     434    public function it_should_create_the_sources_property_when_no_transform_is_available() {
     435        add_filter(
     436            'wp_upload_image_mime_transforms',
     437            function () {
     438                return array( 'image/jpeg' => array() );
     439            }
     440        );
     441
     442        $attachment_id = $this->factory->attachment->create_upload_object(
     443            DIR_TESTDATA . '/images/test-image.jpg'
     444        );
     445
     446        $metadata = wp_get_attachment_metadata( $attachment_id );
     447
     448        $this->assertIsArray( $metadata );
     449        foreach ( $metadata['sizes'] as $size_name => $properties ) {
     450            $this->assertArrayHasKey( 'sources', $properties );
     451            $this->assertIsArray( $properties['sources'] );
     452            $this->assertArrayHasKey( 'image/jpeg', $properties['sources'] );
     453            $this->assertArrayHasKey( 'filesize', $properties['sources']['image/jpeg'] );
     454            $this->assertArrayHasKey( 'file', $properties['sources']['image/jpeg'] );
     455            $this->assertArrayNotHasKey( 'image/webp', $properties['sources'] );
     456        }
     457    }
     458
     459    /**
     460     * Test not creating the sources property if the mime is not specified on the transforms images.
     461     *
     462     * @ticket 55443
     463     */
     464    public function it_should_not_create_the_sources_property_if_the_mime_is_not_specified_on_the_transforms_images() {
     465        add_filter(
     466            'wp_upload_image_mime_transforms',
     467            function () {
     468                return array( 'image/jpeg' => array() );
     469            }
     470        );
     471
     472        $attachment_id = $this->factory->attachment->create_upload_object(
     473            DIR_TESTDATA . '/images/webp-lossy.webp'
     474        );
     475
     476        $metadata = wp_get_attachment_metadata( $attachment_id );
     477
     478        $this->assertIsArray( $metadata );
     479        foreach ( $metadata['sizes'] as $size_name => $properties ) {
     480            $this->assertArrayNotHasKey( 'sources', $properties );
     481        }
     482    }
     483
     484
     485    /**
     486     * Test creating a WebP version with all the required properties.
     487     *
     488     * @ticket 55443
     489     */
     490    public function it_should_create_a_webp_version_with_all_the_required_properties() {
     491        $attachment_id = $this->factory->attachment->create_upload_object(
     492            DIR_TESTDATA . '/images/test-image.jpg'
     493        );
     494
     495        $metadata = wp_get_attachment_metadata( $attachment_id );
     496        $this->assertArrayHasKey( 'sources', $metadata['sizes']['thumbnail'] );
     497        $this->assertArrayHasKey( 'image/jpeg', $metadata['sizes']['thumbnail']['sources'] );
     498        $this->assertArrayHasKey( 'filesize', $metadata['sizes']['thumbnail']['sources']['image/jpeg'] );
     499        $this->assertArrayHasKey( 'file', $metadata['sizes']['thumbnail']['sources']['image/jpeg'] );
     500        $this->assertArrayHasKey( 'image/webp', $metadata['sizes']['thumbnail']['sources'] );
     501        $this->assertArrayHasKey( 'filesize', $metadata['sizes']['thumbnail']['sources']['image/webp'] );
     502        $this->assertArrayHasKey( 'file', $metadata['sizes']['thumbnail']['sources']['image/webp'] );
     503        $this->assertStringEndsNotWith( '.jpeg', $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] );
     504        $this->assertStringEndsWith( '.webp', $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] );
     505    }
     506
     507    /**
     508     * Test removing `scaled` suffix from the generated filename.
     509     *
     510     * @ticket 55443
     511     */
     512    public function it_should_remove_scaled_suffix_from_the_generated_filename() {
     513        // The leafs image is 1080 pixels wide with this filter we ensure a -scaled version is created.
     514        add_filter(
     515            'big_image_size_threshold',
     516            function () {
     517                return 850;
     518            }
     519        );
     520
     521        $attachment_id = $this->factory->attachment->create_upload_object(
     522            DIR_TESTDATA . '/images/test-image.jpg'
     523        );
     524        $metadata      = wp_get_attachment_metadata( $attachment_id );
     525        $this->assertStringEndsWith( '-scaled.jpg', get_attached_file( $attachment_id ) );
     526        $this->assertArrayHasKey( 'image/webp', $metadata['sizes']['medium']['sources'] );
     527        $this->assertStringEndsNotWith( '-scaled.webp', $metadata['sizes']['medium']['sources']['image/webp']['file'] );
     528        $this->assertStringEndsWith( '-300x200.webp', $metadata['sizes']['medium']['sources']['image/webp']['file'] );
     529    }
     530
     531    /**
     532     * Test removing the generated webp images when the attachment is deleted.
     533     *
     534     * @ticket 55443
     535     */
     536    public function it_should_remove_the_generated_webp_images_when_the_attachment_is_deleted() {
     537        // Make sure no editor is available.
     538        $attachment_id = $this->factory->attachment->create_upload_object(
     539            DIR_TESTDATA . '/images/test-image.jpg'
     540        );
     541
     542        $file    = get_attached_file( $attachment_id, true );
     543        $dirname = pathinfo( $file, PATHINFO_DIRNAME );
     544
     545        $this->assertIsString( $file );
     546        $this->assertFileExists( $file );
     547
     548        $metadata = wp_get_attachment_metadata( $attachment_id );
     549        $sizes    = array( 'thumbnail', 'medium' );
     550
     551        foreach ( $sizes as $size_name ) {
     552            $this->assertArrayHasKey( 'image/webp', $metadata['sizes'][ $size_name ]['sources'] );
     553            $this->assertArrayHasKey( 'file', $metadata['sizes'][ $size_name ]['sources']['image/webp'] );
     554            $this->assertFileExists(
     555                path_join( $dirname, $metadata['sizes'][ $size_name ]['sources']['image/webp']['file'] )
     556            );
     557        }
     558
     559        wp_delete_attachment( $attachment_id );
     560
     561        foreach ( $sizes as $size_name ) {
     562            $this->assertFileDoesNotExist(
     563                path_join( $dirname, $metadata['sizes'][ $size_name ]['sources']['image/webp']['file'] )
     564            );
     565        }
     566    }
     567
     568    /**
     569     * Test removing the attached WebP version if the attachment is force deleted but empty trash day is not defined.
     570     *
     571     * @ticket 55443
     572     */
     573    public function it_should_remove_the_attached_webp_version_if_the_attachment_is_force_deleted_but_empty_trash_day_is_not_defined() {
     574        // Make sure no editor is available.
     575        $attachment_id = $this->factory->attachment->create_upload_object(
     576            DIR_TESTDATA . '/images/test-image.jpg'
     577        );
     578
     579        $file    = get_attached_file( $attachment_id, true );
     580        $dirname = pathinfo( $file, PATHINFO_DIRNAME );
     581
     582        $this->assertIsString( $file );
     583        $this->assertFileExists( $file );
     584
     585        $metadata = wp_get_attachment_metadata( $attachment_id );
     586
     587        $this->assertFileExists(
     588            path_join( $dirname, $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] )
     589        );
     590
     591        wp_delete_attachment( $attachment_id, true );
     592
     593        $this->assertFileDoesNotExist(
     594            path_join( $dirname, $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] )
     595        );
     596    }
     597
     598    /**
     599     * Test removing the WebP version of the image if the image is force deleted and empty trash days is set to zero.
     600     *
     601     * @ticket 55443
     602     */
     603    public function it_should_remove_the_webp_version_of_the_image_if_the_image_is_force_deleted_and_empty_trash_days_is_set_to_zero() {
     604        // Make sure no editor is available.
     605        $attachment_id = $this->factory->attachment->create_upload_object(
     606            DIR_TESTDATA . '/images/test-image.jpg'
     607        );
     608
     609        $file    = get_attached_file( $attachment_id, true );
     610        $dirname = pathinfo( $file, PATHINFO_DIRNAME );
     611
     612        $this->assertIsString( $file );
     613        $this->assertFileExists( $file );
     614
     615        $metadata = wp_get_attachment_metadata( $attachment_id );
     616
     617        $this->assertFileExists(
     618            path_join( $dirname, $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] )
     619        );
     620
     621        define( 'EMPTY_TRASH_DAYS', 0 );
     622
     623        wp_delete_attachment( $attachment_id, true );
     624
     625        $this->assertFileDoesNotExist(
     626            path_join( $dirname, $metadata['sizes']['thumbnail']['sources']['image/webp']['file'] )
     627        );
     628    }
     629
     630    /**
     631     * Test avoiding the change of URLs of images that are not part of the media library.
     632     *
     633     * @ticket 55443
     634     */
     635    public function it_should_avoid_the_change_of_urls_of_images_that_are_not_part_of_the_media_library() {
     636        $paragraph = '<p>Donec accumsan, sapien et <img src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">, id commodo nisi sapien et est. Mauris nisl odio, iaculis vitae pellentesque nec.</p>';
     637
     638        $this->assertSame( $paragraph, webp_uploads_update_image_references( $paragraph ) );
     639    }
     640
     641    /**
     642     * Test avoiding replacing not existing attachment IDs.
     643     *
     644     * @ticket 55443
     645     */
     646    public function it_should_avoid_replacing_not_existing_attachment_i_ds() {
     647        $paragraph = '<p>Donec accumsan, sapien et <img class="wp-image-0" src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">, id commodo nisi sapien et est. Mauris nisl odio, iaculis vitae pellentesque nec.</p>';
     648
     649        $this->assertSame( $paragraph, webp_uploads_update_image_references( $paragraph ) );
     650    }
     651
     652    /**
     653     * Test preventing replacing a WebP image.
     654     *
     655     * @ticket 55443
     656     */
     657    public function it_should_test_preventing_replacing_a_webp_image() {
     658        $attachment_id = $this->factory->attachment->create_upload_object(
     659            DIR_TESTDATA . '/images/webp-lossy.webp'
     660        );
     661
     662        $tag = wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => "wp-image-{$attachment_id}" ) );
     663
     664        $this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
     665    }
     666
     667    /**
     668     * Test preventing replacing a jpg image if the image does not have the target class name.
     669     *
     670     * @ticket 55443
     671     */
     672    public function it_should_test_preventing_replacing_a_jpg_image_if_the_image_does_not_have_the_target_class_name() {
     673        $attachment_id = $this->factory->attachment->create_upload_object(
     674            DIR_TESTDATA . '/images/test-image.jpg'
     675        );
     676
     677        $tag = wp_get_attachment_image( $attachment_id, 'medium' );
     678
     679        $this->assertSame( $tag, webp_uploads_update_image_references( $tag ) );
     680    }
     681
     682    /**
     683     * Test replacing the references to a JPG image to a WebP version.
     684     *
     685     * @dataProvider provider_replace_images_with_different_extensions
     686     *
     687     * @ticket 55443
     688     */
     689    public function it_should_replace_the_references_to_a_jpg_image_to_a_webp_version( $image_path ) {
     690        $attachment_id = $this->factory->attachment->create_upload_object( $image_path );
     691
     692        $tag          = wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => "wp-image-{$attachment_id}" ) );
     693        $expected_tag = $tag;
     694        $metadata     = wp_get_attachment_metadata( $attachment_id );
     695        foreach ( $metadata['sizes'] as $size => $properties ) {
     696            $expected_tag = str_replace( $properties['sources']['image/jpeg']['file'], $properties['sources']['image/webp']['file'], $expected_tag );
     697        }
     698
     699        $this->assertNotEmpty( $expected_tag );
     700        $this->assertNotSame( $tag, $expected_tag );
     701        $this->assertSame( $expected_tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
     702    }
     703
     704    public function provider_replace_images_with_different_extensions() {
     705        yield 'An image with a .jpg extension' => array( DIR_TESTDATA . '/images/test-image.jpg' );
     706        yield 'An image with a .jpeg extension' => array( DIR_TESTDATA . '/images/test-image.jpeg' );
     707    }
     708
     709    /**
     710     * Test the full image size from the original mime type.
     711     *
     712     * @ticket 55443
     713     */
     714    public function it_should_contain_the_full_image_size_from_the_original_mime() {
     715        $attachment_id = $this->factory->attachment->create_upload_object(
     716            DIR_TESTDATA . '/images/test-image.jpg'
     717        );
     718
     719        $tag = wp_get_attachment_image( $attachment_id, 'full', false, array( 'class' => "wp-image-{$attachment_id}" ) );
     720
     721        $expected = array(
     722            'ext'  => 'jpg',
     723            'type' => 'image/jpeg',
     724        );
     725        $this->assertSame( $expected, wp_check_filetype( get_attached_file( $attachment_id ) ) );
     726        $this->assertContains( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
     727    }
     728
     729    /**
     730     * Test preventing replacing an image with no available sources.
     731     *
     732     * @ticket 55443
     733     */
     734    public function it_should_prevent_replacing_an_image_with_no_available_sources() {
     735        add_filter( 'wp_upload_image_mime_transforms', '__return_empty_array' );
     736
     737        $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image.jpg' );
     738
     739        $tag = wp_get_attachment_image( $attachment_id, 'full', false, array( 'class' => "wp-image-{$attachment_id}" ) );
     740        $this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
     741    }
     742
     743    /**
     744     * Test preventing update not supported images with no available sources.
     745     *
     746     * @dataProvider provider_it_should_prevent_update_not_supported_images_with_no_available_sources
     747     *
     748     * @ticket 55443
     749     */
     750    public function it_should_prevent_update_not_supported_images_with_no_available_sources( $image_path ) {
     751        $attachment_id = $this->factory->attachment->create_upload_object( $image_path );
     752
     753        $this->assertIsNumeric( $attachment_id );
     754        $tag = wp_get_attachment_image( $attachment_id, 'full', false, array( 'class' => "wp-image-{$attachment_id}" ) );
     755
     756        $this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
     757    }
     758
     759    /**
     760     * Data provider for it_should_prevent_update_not_supported_images_with_no_available_sources.
     761     */
     762    public function provider_it_should_prevent_update_not_supported_images_with_no_available_sources() {
     763        yield 'PNG image' => array( DIR_TESTDATA . '/images/test-image.png' );
     764        yield 'GIFT image' => array( DIR_TESTDATA . '/images/test-image.gif' );
     765    }
     766
    365767}
Note: See TracChangeset for help on using the changeset viewer.