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