Skip to content

Commit 3d88f7b

Browse files
westonruterb1ink0adamsilversteinfelixarntzswissspidy
authored
Merge pull request #2024 from b1ink0/fix/palette-pngs-to-avif-webp-conversion
Fixes palette-based PNG uploads failing original full-size AVIF/WebP conversion under GD Unlinked contributors: IlyaZha, mytory, chimok. Co-authored-by: b1ink0 <[email protected]> Co-authored-by: adamsilverstein <[email protected]> Co-authored-by: westonruter <[email protected]> Co-authored-by: felixarntz <[email protected]> Co-authored-by: swissspidy <[email protected]> Co-authored-by: mukeshpanchal27 <[email protected]> Co-authored-by: benniledl <[email protected]>
2 parents 3c95d4f + 15fd0ea commit 3d88f7b

File tree

3 files changed

+217
-3
lines changed

3 files changed

+217
-3
lines changed

plugins/webp-uploads/hooks.php

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ function webp_uploads_remove_sources_files( int $attachment_id ): void {
537537
}
538538
}
539539
}
540-
add_action( 'delete_attachment', 'webp_uploads_remove_sources_files', 10, 1 );
540+
add_action( 'delete_attachment', 'webp_uploads_remove_sources_files' );
541541

542542
/**
543543
* Filters `wp_content_img_tag` to update images so that they use the preferred MIME type where possible.
@@ -662,7 +662,7 @@ function webp_uploads_img_tag_update_mime_type( string $original_image, string $
662662
}
663663

664664
/**
665-
* Updates the references of the featured image to the a new image format if available, in the same way it
665+
* Updates the references of the featured image to the new image format if available, in the same way it
666666
* occurs in the_content of a post.
667667
*
668668
* @since 1.0.0
@@ -901,3 +901,65 @@ function webp_uploads_enable_additional_mime_type_support_for_all_sizes( array $
901901
return $allowed_sizes;
902902
}
903903
add_filter( 'webp_uploads_image_sizes_with_additional_mime_type_support', 'webp_uploads_enable_additional_mime_type_support_for_all_sizes' );
904+
905+
/**
906+
* Converts palette PNG images to truecolor PNG images.
907+
*
908+
* GD cannot convert palette-based PNG to WebP/AVIF formats, causing conversion failures.
909+
* This function detects and converts palette PNG to truecolor during upload.
910+
*
911+
* @since n.e.x.t
912+
*
913+
* @param array<string, mixed>|mixed $file The uploaded file data.
914+
* @return array<string, mixed> The modified file data.
915+
*/
916+
function webp_uploads_convert_palette_png_to_truecolor( $file ): array {
917+
// Because plugins do bad things.
918+
if ( ! is_array( $file ) ) {
919+
$file = array();
920+
}
921+
if ( ! isset( $file['tmp_name'], $file['name'] ) ) {
922+
return $file;
923+
}
924+
if ( isset( $file['type'] ) && is_string( $file['type'] ) ) {
925+
if ( 'image/png' !== strtolower( $file['type'] ) ) {
926+
return $file;
927+
}
928+
} elseif ( 'image/png' !== wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] )['type'] ) {
929+
return $file;
930+
}
931+
932+
$editor = wp_get_image_editor( $file['tmp_name'] );
933+
934+
if ( is_wp_error( $editor ) || ! $editor instanceof WP_Image_Editor_GD ) {
935+
return $file;
936+
}
937+
938+
$image = imagecreatefrompng( $file['tmp_name'] );
939+
940+
// Check if the image was created successfully.
941+
if ( false === $image ) {
942+
return $file;
943+
}
944+
945+
// Check if the image is already truecolor.
946+
if ( imageistruecolor( $image ) ) {
947+
imagedestroy( $image );
948+
return $file;
949+
}
950+
951+
// Preserve transparency.
952+
imagealphablending( $image, false );
953+
imagesavealpha( $image, true );
954+
955+
// Convert the palette to truecolor.
956+
if ( imagepalettetotruecolor( $image ) ) {
957+
// Overwrite the upload with the new truecolor PNG.
958+
imagepng( $image, $file['tmp_name'] );
959+
}
960+
imagedestroy( $image );
961+
962+
return $file;
963+
}
964+
add_filter( 'wp_handle_upload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );
965+
add_filter( 'wp_handle_sideload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' );
40.4 KB
Loading

plugins/webp-uploads/tests/test-load.php

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ public function test_it_should_allow_the_upload_of_a_webp_image_if_at_least_one_
720720
static function ( $editors ) {
721721
// WP core does not choose the WP_Image_Editor instance based on MIME type support,
722722
// therefore the one that does support modern images needs to be first in this list.
723-
array_unshift( $editors, 'WP_Image_Doesnt_Support_Modern_Images' );
723+
array_unshift( $editors, WP_Image_Doesnt_Support_Modern_Images::class );
724724
return $editors;
725725
}
726726
);
@@ -1116,4 +1116,156 @@ public function test_that_it_should_convert_webp_to_avif_on_upload(): void {
11161116
}
11171117
wp_delete_attachment( $attachment_id );
11181118
}
1119+
1120+
/**
1121+
* Tests that the `webp_uploads_convert_palette_png_to_truecolor` function is hooked to the upload filters.
1122+
*/
1123+
public function test_webp_uploads_convert_palette_png_to_truecolor_hooks(): void {
1124+
$this->assertSame( 10, has_filter( 'wp_handle_upload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' ) );
1125+
$this->assertSame( 10, has_filter( 'wp_handle_sideload_prefilter', 'webp_uploads_convert_palette_png_to_truecolor' ) );
1126+
}
1127+
1128+
/**
1129+
* Tests converting a palette PNG to a truecolor PNG.
1130+
*
1131+
* @dataProvider data_to_test_webp_uploads_convert_palette_png_to_truecolor
1132+
*
1133+
* @covers ::webp_uploads_convert_palette_png_to_truecolor
1134+
*
1135+
* @param string|null $image_path The path to the image file to test.
1136+
* @param bool $expect_changed Whether the png should be converted to truecolor.
1137+
*/
1138+
public function test_webp_uploads_convert_palette_png_to_truecolor( ?string $image_path, bool $expect_changed ): void {
1139+
if ( ! extension_loaded( 'gd' ) ) {
1140+
$this->markTestSkipped( 'GD extension is not loaded' );
1141+
}
1142+
1143+
add_filter(
1144+
'wp_image_editors',
1145+
static function () {
1146+
return array( WP_Image_Editor_GD::class );
1147+
}
1148+
);
1149+
1150+
// Temp file will be copied and unlinked by WordPress core during sideload processing.
1151+
$tmp_file = wp_tempnam();
1152+
copy( $image_path, $tmp_file );
1153+
$file = array(
1154+
'name' => basename( $image_path ),
1155+
'tmp_name' => $tmp_file,
1156+
'type' => wp_check_filetype( $image_path )['type'],
1157+
'size' => filesize( $tmp_file ),
1158+
'error' => UPLOAD_ERR_OK,
1159+
);
1160+
1161+
// Store the original file hash and the original file size for later comparison.
1162+
$original_file_hash = isset( $file['tmp_name'] ) ? md5_file( $file['tmp_name'] ) : '';
1163+
$original_file_size = (int) filesize( $file['tmp_name'] );
1164+
1165+
// This will trigger the `wp_handle_sideload_prefilter` filter.
1166+
$attachment_id = media_handle_sideload( $file );
1167+
1168+
try {
1169+
$this->assertIsNumeric( $attachment_id );
1170+
1171+
// For getting an original image path for computation of the file hash.
1172+
$meta = wp_get_attachment_metadata( $attachment_id );
1173+
$upload_dir = wp_get_upload_dir();
1174+
$path = null;
1175+
if ( isset( $meta['original_image'], $meta['file'] ) ) {
1176+
$path = path_join(
1177+
$upload_dir['basedir'],
1178+
dirname( $meta['file'] ) . '/' . $meta['original_image']
1179+
);
1180+
}
1181+
$this->assertNotNull( $path );
1182+
$this->assertFileExists( $path );
1183+
1184+
// Hash will be modified if the image was converted to truecolor.
1185+
$modified_file_hash = md5_file( $path );
1186+
1187+
if ( ! $expect_changed ) {
1188+
$this->assertSame( $original_file_hash, $modified_file_hash );
1189+
} else {
1190+
$this->assertNotSame( $original_file_hash, $modified_file_hash );
1191+
$img = imagecreatefrompng( $path );
1192+
$this->assertTrue( imageistruecolor( $img ) );
1193+
imagedestroy( $img );
1194+
1195+
// Make sure the image converted to modern image format is not 0 bytes.
1196+
$modern_image_format_path = get_attached_file( $attachment_id );
1197+
$this->assertNotFalse( $modern_image_format_path );
1198+
$this->assertFileExists( $modern_image_format_path );
1199+
$modern_image_format_filesize = (int) filesize( $modern_image_format_path );
1200+
$this->assertGreaterThan( 0, $modern_image_format_filesize );
1201+
1202+
// Ensure the file size of the converted image is less than or equal to the original indexed PNG file size.
1203+
$this->assertLessThanOrEqual( $original_file_size, $modern_image_format_filesize );
1204+
}
1205+
} finally {
1206+
wp_delete_attachment( $attachment_id );
1207+
}
1208+
}
1209+
1210+
/**
1211+
* Data provider for `test_webp_uploads_convert_palette_png_to_truecolor`.
1212+
*
1213+
* @return array<string, mixed> Returns an array of test cases.
1214+
*/
1215+
public function data_to_test_webp_uploads_convert_palette_png_to_truecolor(): array {
1216+
$non_palette_png = TESTS_PLUGIN_DIR . '/tests/data/images/dice.png';
1217+
$palette_png = TESTS_PLUGIN_DIR . '/tests/data/images/dice-palette.png';
1218+
$test_jpg = TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg';
1219+
1220+
return array(
1221+
'wrong_extension' => array(
1222+
'image_path' => $test_jpg,
1223+
'expected_changed' => false,
1224+
),
1225+
'non_palette_png' => array(
1226+
'image_path' => $non_palette_png,
1227+
'expected_changed' => false,
1228+
),
1229+
'palette_png' => array(
1230+
'image_path' => $palette_png,
1231+
'expected_changed' => true,
1232+
),
1233+
);
1234+
}
1235+
1236+
/**
1237+
* Tests the webp_uploads_convert_palette_png_to_truecolor function with various conditions.
1238+
*
1239+
* @covers ::webp_uploads_convert_palette_png_to_truecolor
1240+
*/
1241+
public function test_webp_uploads_convert_palette_png_to_truecolor_conditions(): void {
1242+
$this->assertSame( array(), webp_uploads_convert_palette_png_to_truecolor( 'test' ) );
1243+
$this->assertSameSets( array(), webp_uploads_convert_palette_png_to_truecolor( array() ) );
1244+
1245+
$file = array(
1246+
'tmp_name' => TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg',
1247+
'name' => 'leaves.jpg',
1248+
'type' => 'image/jpeg',
1249+
);
1250+
$this->assertSameSets( $file, webp_uploads_convert_palette_png_to_truecolor( $file ) );
1251+
1252+
$file = array(
1253+
'tmp_name' => TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg',
1254+
'name' => 'leaves.jpg',
1255+
);
1256+
$this->assertSameSets( $file, webp_uploads_convert_palette_png_to_truecolor( $file ) );
1257+
1258+
add_filter(
1259+
'wp_image_editors',
1260+
static function () {
1261+
return array();
1262+
}
1263+
);
1264+
$file = array(
1265+
'tmp_name' => TESTS_PLUGIN_DIR . '/tests/data/images/dice-palette.png',
1266+
'name' => 'dice-palette.png',
1267+
'type' => 'image/png',
1268+
);
1269+
$this->assertSameSets( $file, webp_uploads_convert_palette_png_to_truecolor( $file ) );
1270+
}
11191271
}

0 commit comments

Comments
 (0)