diff --git a/ext/exif/exif.c b/ext/exif/exif.c index 859324b1b96f5..c5c7cdc1ee43c 100644 --- a/ext/exif/exif.c +++ b/ext/exif/exif.c @@ -1284,6 +1284,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 @@ -4281,11 +4293,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]; bool ret = false; ImageInfo->FileType = IMAGE_FILETYPE_UNKNOWN; @@ -4334,6 +4463,16 @@ static bool exif_scan_FILE_header(image_info_type *ImageInfo) } else { exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid TIFF file"); } + } 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; + ret = true; + } else { + exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "Invalid HEIF file"); + } } else { exif_error_docref(NULL EXIFERR_CC, ImageInfo, E_WARNING, "File not supported"); return false; diff --git a/ext/exif/tests/exif028.phpt b/ext/exif/tests/exif028.phpt new file mode 100644 index 0000000000000..7c4894aa430c4 --- /dev/null +++ b/ext/exif/tests/exif028.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) "test4pix.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" + ["UndefinedTag:0x9010"]=> + string(6) "+08:00" + ["UndefinedTag:0x9011"]=> + string(6) "+08:00" + ["UndefinedTag:0x9012"]=> + 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/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index e7f4ff8844714..44fed4668c325 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -646,6 +646,11 @@ * @cvalue IMAGE_FILETYPE_AVIF */ const IMAGETYPE_AVIF = UNKNOWN; +/** + * @var int + * @cvalue IMAGE_FILETYPE_HEIF + */ +const IMAGETYPE_HEIF = UNKNOWN; /** * @var int * @cvalue IMAGE_FILETYPE_UNKNOWN diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 3d92288643159..ec7420e2cec2a 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -3665,6 +3665,7 @@ static void register_basic_functions_symbols(int module_number) REGISTER_LONG_CONSTANT("IMAGETYPE_ICO", IMAGE_FILETYPE_ICO, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMAGETYPE_WEBP", IMAGE_FILETYPE_WEBP, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMAGETYPE_AVIF", IMAGE_FILETYPE_AVIF, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("IMAGETYPE_HEIF", IMAGE_FILETYPE_HEIF, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMAGETYPE_UNKNOWN", IMAGE_FILETYPE_UNKNOWN, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMAGETYPE_COUNT", IMAGE_FILETYPE_COUNT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("INFO_GENERAL", PHP_INFO_GENERAL, CONST_PERSISTENT); diff --git a/ext/standard/image.c b/ext/standard/image.c index 177e87d21b599..99413260875ba 100644 --- a/ext/standard/image.c +++ b/ext/standard/image.c @@ -51,6 +51,10 @@ PHPAPI const char php_sig_iff[4] = {'F','O','R','M'}; PHPAPI const char php_sig_ico[4] = {(char)0x00, (char)0x00, (char)0x01, (char)0x00}; PHPAPI const char php_sig_riff[4] = {'R', 'I', 'F', 'F'}; PHPAPI const char php_sig_webp[4] = {'W', 'E', 'B', 'P'}; +PHPAPI const char php_sig_ftyp[4] = {'f', 't', 'y', 'p'}; +PHPAPI const char php_sig_mif1[4] = {'m', 'i', 'f', '1'}; +PHPAPI const char php_sig_heic[4] = {'h', 'e', 'i', 'c'}; +PHPAPI const char php_sig_heix[4] = {'h', 'e', 'i', 'x'}; /* REMEMBER TO ADD MIME-TYPE TO FUNCTION php_image_type_to_mime_type */ /* PCX must check first 64bytes and byte 0=0x0a and byte2 < 0x06 */ @@ -1249,6 +1253,8 @@ PHPAPI char * php_image_type_to_mime_type(int image_type) return "image/webp"; case IMAGE_FILETYPE_AVIF: return "image/avif"; + case IMAGE_FILETYPE_HEIF: + return "image/heif"; default: case IMAGE_FILETYPE_UNKNOWN: return "application/octet-stream"; /* suppose binary format */ @@ -1334,6 +1340,10 @@ PHP_FUNCTION(image_type_to_extension) case IMAGE_FILETYPE_AVIF: imgext = ".avif"; break; + case IMAGE_FILETYPE_HEIF: + imgext = ".heif"; + break; + break; } if (imgext) { @@ -1418,6 +1428,11 @@ PHPAPI int php_getimagetype(php_stream *stream, const char *input, char *filetyp return IMAGE_FILETYPE_JP2; } + if (twelve_bytes_read && !memcmp(filetype + 4, php_sig_ftyp, 4) && + (!memcmp(filetype + 8, php_sig_mif1, 4) || !memcmp(filetype + 8, php_sig_heic, 4) || !memcmp(filetype + 8, php_sig_heix, 4))) { + return IMAGE_FILETYPE_HEIF; + } + if (!php_stream_rewind(stream) && php_is_image_avif(stream)) { return IMAGE_FILETYPE_AVIF; } @@ -1510,6 +1525,11 @@ static void php_getimagesize_from_stream(php_stream *stream, char *input, zval * case IMAGE_FILETYPE_AVIF: result = php_handle_avif(stream); break; + case IMAGE_FILETYPE_HEIF: + if (!php_stream_rewind(stream)) { + result = php_handle_avif(stream); + } + break; default: case IMAGE_FILETYPE_UNKNOWN: break; diff --git a/ext/standard/php_image.h b/ext/standard/php_image.h index a41273e6745ae..f0f3edb7283eb 100644 --- a/ext/standard/php_image.h +++ b/ext/standard/php_image.h @@ -44,6 +44,7 @@ typedef enum IMAGE_FILETYPE_ICO, IMAGE_FILETYPE_WEBP, IMAGE_FILETYPE_AVIF, + IMAGE_FILETYPE_HEIF, /* WHEN EXTENDING: PLEASE ALSO REGISTER IN basic_function.stub.php */ IMAGE_FILETYPE_COUNT } image_filetype; diff --git a/ext/standard/tests/image/getimagesize.phpt b/ext/standard/tests/image/getimagesize.phpt index 472be1d25e366..7e3cbde99bf9b 100644 --- a/ext/standard/tests/image/getimagesize.phpt +++ b/ext/standard/tests/image/getimagesize.phpt @@ -23,7 +23,7 @@ GetImageSize() var_dump($result); ?> --EXPECT-- -array(17) { +array(18) { ["test-1pix.bmp"]=> array(6) { [0]=> @@ -216,6 +216,23 @@ array(17) { ["mime"]=> string(9) "image/gif" } + ["test4pix.heic"]=> + array(7) { + [0]=> + int(924) + [1]=> + int(941) + [2]=> + int(20) + [3]=> + string(24) "width="924" height="941"" + ["bits"]=> + int(8) + ["channels"]=> + int(3) + ["mime"]=> + string(10) "image/heif" + } ["test4pix.iff"]=> array(6) { [0]=> diff --git a/ext/standard/tests/image/image_type_to_mime_type.phpt b/ext/standard/tests/image/image_type_to_mime_type.phpt index 5877efe531fd9..0102955babe73 100644 --- a/ext/standard/tests/image/image_type_to_mime_type.phpt +++ b/ext/standard/tests/image/image_type_to_mime_type.phpt @@ -24,7 +24,7 @@ image_type_to_mime_type() var_dump($result); ?> --EXPECT-- -array(17) { +array(18) { ["test-1pix.bmp"]=> string(9) "image/bmp" ["test12pix.webp"]=> @@ -49,6 +49,8 @@ array(17) { string(10) "image/webp" ["test4pix.gif"]=> string(9) "image/gif" + ["test4pix.heic"]=> + string(10) "image/heif" ["test4pix.iff"]=> string(9) "image/iff" ["test4pix.png"]=> 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 bf5a1ee250990..f6478868f2d28 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 ); @@ -49,6 +50,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" diff --git a/ext/standard/tests/image/image_type_to_mime_type_variation3.phpt b/ext/standard/tests/image/image_type_to_mime_type_variation3.phpt index fc17cb5ecd947..6626dc5a07c03 100644 --- a/ext/standard/tests/image/image_type_to_mime_type_variation3.phpt +++ b/ext/standard/tests/image/image_type_to_mime_type_variation3.phpt @@ -75,4 +75,7 @@ string\(10\) "image\/webp" string\(10\) "image\/avif" -- Iteration 20 -- +string\(10\) "image\/heif" + +-- Iteration 21 -- string\(24\) "application\/octet-stream" diff --git a/ext/standard/tests/image/test4pix.heic b/ext/standard/tests/image/test4pix.heic new file mode 100644 index 0000000000000..99f8186d0723b Binary files /dev/null and b/ext/standard/tests/image/test4pix.heic differ