diff --git a/ext/exif/exif.c b/ext/exif/exif.c index 9169861728869..514e3154d4568 100644 --- a/ext/exif/exif.c +++ b/ext/exif/exif.c @@ -1290,6 +1290,18 @@ typedef struct { mn_offset_mode_t offset_mode; } maker_note_type; +#define FOURCC(id) (((uint32_t)(id[0])<<24) | (id[1]<<16) | (id[2]<<8) | (id[3])) + +typedef struct { + uint64_t size; + uint32_t type; +} isobmff_box_type; + +typedef struct { + uint32_t offset; + uint32_t size; +} isobmff_item_pos_type; + /* Some maker notes (e.g. DJI info tag) require custom parsing */ #define REQUIRES_CUSTOM_PARSING NULL @@ -4279,11 +4291,128 @@ static bool exif_process_IFD_in_TIFF(image_info_type *ImageInfo, size_t dir_offs return result; } +static int exif_isobmff_parse_box(unsigned char *buf, isobmff_box_type *box) +{ + box->size = php_ifd_get32u(buf, 1); + buf += 4; + box->type = php_ifd_get32u(buf, 1); + if (box->size != 1) { + return 8; + } + buf += 4; + box->size = php_ifd_get64u(buf, 1); + return 16; +} + +static void exif_isobmff_parse_meta(unsigned char *data, unsigned char *end, isobmff_item_pos_type *pos) +{ + isobmff_box_type box, item; + unsigned char *box_offset, *p, *p2; + int header_size, exif_id = -1, version, item_count, i; + + for (box_offset = data + 4; box_offset < end; box_offset += box.size) { + header_size = exif_isobmff_parse_box(box_offset, &box); + if (box.type == FOURCC("iinf")) { + p = box_offset + header_size; + version = p[0]; + p += 4; + if (version < 2) { + item_count = php_ifd_get16u(p, 1); + p += 2; + } else { + item_count = php_ifd_get32u(p, 1); + p += 4; + } + for (i=0; i < item_count; i++) { + header_size = exif_isobmff_parse_box(p, &item); + if (!memcmp(p + header_size + 8, "Exif", 4)) { + exif_id = php_ifd_get16u(p + header_size + 4, 1); + break; + } + p += item.size; + } + if (exif_id < 0) { + break; + } + } + else if (box.type == FOURCC("iloc")) { + p = box_offset + header_size; + version = p[0]; + p += 6; + if (version < 2) { + item_count = php_ifd_get16u(p, 1); + p += 2; + } else { + item_count = php_ifd_get32u(p, 1); + p += 4; + } + for (i = 0, p2 = p; ioffset = php_ifd_get32u(p2 + 8, 1); + pos->size = php_ifd_get32u(p2 + 12, 1); + break; + } + } + break; + } + } +} + +static bool exif_scan_HEIF_header(image_info_type *ImageInfo, unsigned char *buf) +{ + isobmff_box_type box; + isobmff_item_pos_type pos; + unsigned char *data; + off_t offset; + uint64_t limit; + int box_header_size, remain; + bool ret = false; + + pos.size = 0; + for (offset = php_ifd_get32u(buf, 1); ImageInfo->FileSize > offset + 16; offset += box.size) { + if ((php_stream_seek(ImageInfo->infile, offset, SEEK_SET) < 0) || + (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)buf, 16) != 16)) { + break; + } + box_header_size = exif_isobmff_parse_box(buf, &box); + if (box.type == FOURCC("meta")) { + limit = box.size - box_header_size; + data = (unsigned char *)emalloc(limit); + remain = 16 - box_header_size; + if (remain) { + memcpy(data, buf + box_header_size, remain); + } + if (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)(data + remain), limit - remain) == limit - remain) { + exif_isobmff_parse_meta(data, data + limit, &pos); + } + if ((pos.size) && + (ImageInfo->FileSize >= pos.offset + pos.size) && + (php_stream_seek(ImageInfo->infile, pos.offset + 2, SEEK_SET) >= 0)) { + if (limit >= pos.size - 2) { + limit = pos.size - 2; + } else { + limit = pos.size - 2; + efree(data); + data = (unsigned char *)emalloc(limit); + } + if (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)data, limit) == limit) { + exif_process_APP1(ImageInfo, (char*)data, limit, pos.offset + 2); + ret = true; + } + } + efree(data); + break; + } + } + + return ret; +} + /* {{{ exif_scan_FILE_header * Parse the marker stream until SOS or EOI is seen; */ static bool exif_scan_FILE_header(image_info_type *ImageInfo) { - unsigned char file_header[8]; + unsigned char file_header[16]; ImageInfo->FileType = IMAGE_FILETYPE_UNKNOWN; @@ -4344,6 +4473,17 @@ static bool exif_scan_FILE_header(image_info_type *ImageInfo) exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid TIFF file"); return false; } + } else if ((ImageInfo->FileSize > 12) && + (!memcmp(file_header + 4, "ftyp", 4)) && + (exif_read_from_stream_file_looped(ImageInfo->infile, (char*)(file_header + 8), 4) == 4) && + ((!memcmp(file_header + 8, "heic", 4)) || (!memcmp(file_header + 8, "heix", 4)) || (!memcmp(file_header + 8, "mif1", 4)))) { + if (exif_scan_HEIF_header(ImageInfo, file_header)) { + ImageInfo->FileType = IMAGE_FILETYPE_HEIF; + return true; + } else { + exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid HEIF file"); + return false; + } } else { exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "File not supported"); return false; diff --git a/ext/exif/tests/exif029.phpt b/ext/exif/tests/exif029.phpt new file mode 100644 index 0000000000000..f2eadbfe5d2aa --- /dev/null +++ b/ext/exif/tests/exif029.phpt @@ -0,0 +1,145 @@ +--TEST-- +Check for exif_read_data, HEIF with IFD0 and EXIF data in Motorola byte-order. +--EXTENSIONS-- +exif +--INI-- +output_handler= +zlib.output_compression=0 +--FILE-- + +--EXPECTF-- +array(53) { + ["FileName"]=> + string(13) "image029.heic" + ["FileDateTime"]=> + int(%d) + ["FileSize"]=> + int(42199) + ["FileType"]=> + int(20) + ["MimeType"]=> + string(10) "image/heif" + ["SectionsFound"]=> + string(19) "ANY_TAG, IFD0, EXIF" + ["COMPUTED"]=> + array(3) { + ["IsColor"]=> + int(0) + ["ByteOrderMotorola"]=> + int(1) + ["ApertureFNumber"]=> + string(5) "f/1.8" + } + ["Make"]=> + string(5) "Apple" + ["Model"]=> + string(26) "iPhone SE (3rd generation)" + ["Orientation"]=> + int(1) + ["XResolution"]=> + string(4) "72/1" + ["YResolution"]=> + string(4) "72/1" + ["ResolutionUnit"]=> + int(2) + ["Software"]=> + string(6) "17.2.1" + ["DateTime"]=> + string(19) "2024:02:21 16:03:50" + ["HostComputer"]=> + string(26) "iPhone SE (3rd generation)" + ["TileWidth"]=> + int(512) + ["TileLength"]=> + int(512) + ["Exif_IFD_Pointer"]=> + int(264) + ["ExposureTime"]=> + string(4) "1/60" + ["FNumber"]=> + string(3) "9/5" + ["ExposureProgram"]=> + int(2) + ["ISOSpeedRatings"]=> + int(200) + ["ExifVersion"]=> + string(4) "0232" + ["DateTimeOriginal"]=> + string(19) "2024:02:21 16:03:50" + ["DateTimeDigitized"]=> + string(19) "2024:02:21 16:03:50" + ["OffsetTime"]=> + string(6) "+08:00" + ["OffsetTimeOriginal"]=> + string(6) "+08:00" + ["OffsetTimeDigitized"]=> + string(6) "+08:00" + ["ShutterSpeedValue"]=> + string(12) "159921/27040" + ["ApertureValue"]=> + string(11) "54823/32325" + ["BrightnessValue"]=> + string(11) "29968/13467" + ["ExposureBiasValue"]=> + string(3) "0/1" + ["MeteringMode"]=> + int(5) + ["Flash"]=> + int(16) + ["FocalLength"]=> + string(7) "399/100" + ["SubjectLocation"]=> + array(4) { + [0]=> + int(1995) + [1]=> + int(1507) + [2]=> + int(2217) + [3]=> + int(1332) + } + ["MakerNote"]=> + string(9) "Apple iOS" + ["SubSecTimeOriginal"]=> + string(3) "598" + ["SubSecTimeDigitized"]=> + string(3) "598" + ["ColorSpace"]=> + int(65535) + ["ExifImageWidth"]=> + int(4032) + ["ExifImageLength"]=> + int(3024) + ["SensingMethod"]=> + int(2) + ["SceneType"]=> + string(1) "" + ["ExposureMode"]=> + int(0) + ["WhiteBalance"]=> + int(0) + ["DigitalZoomRatio"]=> + string(7) "756/151" + ["FocalLengthIn35mmFilm"]=> + int(140) + ["UndefinedTag:0xA432"]=> + array(4) { + [0]=> + string(15) "4183519/1048501" + [1]=> + string(15) "4183519/1048501" + [2]=> + string(3) "9/5" + [3]=> + string(3) "9/5" + } + ["UndefinedTag:0xA433"]=> + string(5) "Apple" + ["UndefinedTag:0xA434"]=> + string(51) "iPhone SE (3rd generation) back camera 3.99mm f/1.8" + ["UndefinedTag:0xA460"]=> + int(2) +} diff --git a/ext/exif/tests/image029.heic b/ext/exif/tests/image029.heic new file mode 100644 index 0000000000000..99f8186d0723b Binary files /dev/null and b/ext/exif/tests/image029.heic differ diff --git a/ext/standard/tests/image/image_type_to_mime_type_basic.phpt b/ext/standard/tests/image/image_type_to_mime_type_basic.phpt index 6fb5154007642..a393c3e83dc27 100644 --- a/ext/standard/tests/image/image_type_to_mime_type_basic.phpt +++ b/ext/standard/tests/image/image_type_to_mime_type_basic.phpt @@ -21,6 +21,7 @@ $image_types = array ( IMAGETYPE_IFF, IMAGETYPE_WBMP, IMAGETYPE_JPEG2000, + IMAGETYPE_HEIF, IMAGETYPE_XBM, IMAGETYPE_WEBP, IMAGETYPE_HEIF, @@ -50,6 +51,7 @@ string(24) "application/octet-stream" string(9) "image/iff" string(18) "image/vnd.wap.wbmp" string(24) "application/octet-stream" +string(10) "image/heif" string(9) "image/xbm" string(10) "image/webp" string(10) "image/heif"