Commit d89f14bde2e for php.net
commit d89f14bde2e5317b9087615dce889086dd53aec7
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Jun 2 17:01:17 2026 -0400
Add WebP support to ext/exif
WebP keeps EXIF in a RIFF "EXIF" chunk whose payload is a raw TIFF block,
parsed via the existing exif_process_TIFF_in_JPEG() helper.
Closes GH-19904
Closes GH-22213
diff --git a/NEWS b/NEWS
index 496b61ef81b..b96b12eda1b 100644
--- a/NEWS
+++ b/NEWS
@@ -41,6 +41,10 @@ PHP NEWS
values, and textContent returns NULL per the DOM specification.
(jordikroon)
+- EXIF:
+ . Added support for reading EXIF metadata from WebP images (GH-19904).
+ (iliaal)
+
- Fileinfo:
. Fixed bug GH-20679 (finfo_file() doesn't work on remote resources).
(ndossche)
diff --git a/ext/exif/exif.c b/ext/exif/exif.c
index 91be074a0be..1b8bd1f7678 100644
--- a/ext/exif/exif.c
+++ b/ext/exif/exif.c
@@ -69,7 +69,7 @@ PHP_MINFO_FUNCTION(exif)
php_info_print_table_start();
php_info_print_table_row(2, "EXIF Support", "enabled");
php_info_print_table_row(2, "Supported EXIF Version", "0220");
- php_info_print_table_row(2, "Supported filetypes", "JPEG, TIFF");
+ php_info_print_table_row(2, "Supported filetypes", "JPEG, TIFF, HEIF, WebP");
if (USE_MBSTRING) {
php_info_print_table_row(2, "Multibyte decoding support using mbstring", "enabled");
@@ -4445,6 +4445,54 @@ static bool exif_scan_HEIF_header(image_info_type *ImageInfo, unsigned char *buf
return ret;
}
+static bool exif_scan_WEBP_header(image_info_type *ImageInfo, size_t riff_size)
+{
+ /* "Exif\0\0" identifier code */
+ static const uchar ExifHeader[] = {0x45, 0x78, 0x69, 0x66, 0x00, 0x00};
+ unsigned char chunk_header[8];
+ size_t offset = 12;
+ size_t riff_end = riff_size <= ImageInfo->FileSize - 8 ? riff_size + 8 : ImageInfo->FileSize;
+
+ while (offset + 8 <= riff_end) {
+ if ((php_stream_seek(ImageInfo->infile, offset, SEEK_SET) < 0) ||
+ (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)chunk_header, 8) != 8)) {
+ return false;
+ }
+
+ size_t chunk_size = php_ifd_get32u(chunk_header + 4, 0);
+ size_t payload_offset = offset + 8;
+
+ if (chunk_size > riff_end - payload_offset) {
+ return false;
+ }
+
+ if (!memcmp(chunk_header, "EXIF", 4)) {
+ size_t skip = 0;
+ bool ret = false;
+
+ if (chunk_size < 8) {
+ return false;
+ }
+
+ char *data = emalloc(chunk_size);
+ if (exif_read_from_stream_file_looped(ImageInfo->infile, data, chunk_size) == chunk_size) {
+ if (chunk_size >= sizeof(ExifHeader) + 8 && !memcmp(data, ExifHeader, sizeof(ExifHeader))) {
+ skip = sizeof(ExifHeader);
+ }
+ exif_process_TIFF_in_JPEG(ImageInfo, data + skip, chunk_size - skip, payload_offset + skip);
+ ret = true;
+ }
+ efree(data);
+ return ret;
+ }
+
+ /* RIFF chunks are word-aligned: an odd payload is followed by a pad byte. */
+ offset = payload_offset + chunk_size + (chunk_size & 1);
+ }
+
+ return false;
+}
+
/* {{{ exif_scan_FILE_header
* Parse the marker stream until SOS or EOI is seen; */
static bool exif_scan_FILE_header(image_info_type *ImageInfo)
@@ -4521,6 +4569,17 @@ static bool exif_scan_FILE_header(image_info_type *ImageInfo)
exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid HEIF file");
return false;
}
+ } else if ((ImageInfo->FileSize >= 16) &&
+ (!memcmp(file_header, "RIFF", 4)) &&
+ (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)(file_header + 8), 4) == 4) &&
+ (!memcmp(file_header + 8, "WEBP", 4))) {
+ if (exif_scan_WEBP_header(ImageInfo, php_ifd_get32u(file_header + 4, 0))) {
+ ImageInfo->FileType = IMAGE_FILETYPE_WEBP;
+ return true;
+ } else {
+ exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid WebP file");
+ return false;
+ }
} else {
exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "File not supported");
return false;
diff --git a/ext/exif/tests/gh19904.phpt b/ext/exif/tests/gh19904.phpt
new file mode 100644
index 00000000000..cc13d88dd98
--- /dev/null
+++ b/ext/exif/tests/gh19904.phpt
@@ -0,0 +1,82 @@
+--TEST--
+GH-19904 (exif_read_data() reads EXIF metadata from WebP images)
+--EXTENSIONS--
+exif
+--INI--
+output_handler=
+zlib.output_compression=0
+--FILE--
+<?php
+var_dump(exif_read_data(__DIR__.'/gh19904.webp'));
+?>
+--EXPECTF--
+array(26) {
+ ["FileName"]=>
+ string(12) "gh19904.webp"
+ ["FileDateTime"]=>
+ int(%d)
+ ["FileSize"]=>
+ int(526)
+ ["FileType"]=>
+ int(18)
+ ["MimeType"]=>
+ string(10) "image/webp"
+ ["SectionsFound"]=>
+ string(24) "ANY_TAG, IFD0, EXIF, GPS"
+ ["COMPUTED"]=>
+ array(4) {
+ ["IsColor"]=>
+ int(0)
+ ["ByteOrderMotorola"]=>
+ int(0)
+ ["UserComment"]=>
+ string(17) "Created with GIMP"
+ ["UserCommentEncoding"]=>
+ string(9) "UNDEFINED"
+ }
+ ["ImageWidth"]=>
+ int(100)
+ ["ImageLength"]=>
+ int(100)
+ ["BitsPerSample"]=>
+ array(3) {
+ [0]=>
+ int(8)
+ [1]=>
+ int(8)
+ [2]=>
+ int(8)
+ }
+ ["ImageDescription"]=>
+ string(17) "Created with GIMP"
+ ["XResolution"]=>
+ string(5) "300/1"
+ ["YResolution"]=>
+ string(5) "300/1"
+ ["ResolutionUnit"]=>
+ int(2)
+ ["Software"]=>
+ string(10) "GIMP 3.0.4"
+ ["DateTime"]=>
+ string(19) "2025:09:21 15:30:30"
+ ["Exif_IFD_Pointer"]=>
+ int(250)
+ ["GPS_IFD_Pointer"]=>
+ int(430)
+ ["DateTimeOriginal"]=>
+ string(19) "2025:09:21 15:29:27"
+ ["DateTimeDigitized"]=>
+ string(19) "2025:09:21 15:29:27"
+ ["OffsetTime"]=>
+ string(6) "+02:00"
+ ["OffsetTimeOriginal"]=>
+ string(6) "+02:00"
+ ["OffsetTimeDigitized"]=>
+ string(6) "+02:00"
+ ["UserComment"]=>
+ string(25) "%sCreated with GIMP"
+ ["ColorSpace"]=>
+ int(1)
+ ["GPSAltitude"]=>
+ string(5) "0/100"
+}
diff --git a/ext/exif/tests/gh19904.webp b/ext/exif/tests/gh19904.webp
new file mode 100644
index 00000000000..12dbe96a845
Binary files /dev/null and b/ext/exif/tests/gh19904.webp differ