Skip to content

Commit

Permalink
draft support retrieve Exif from heif file
Browse files Browse the repository at this point in the history
Signed-off-by: Benstone Zhang <benstonezhang@gmail.com>
  • Loading branch information
benstonezhang committed Feb 23, 2024
1 parent e72f0c8 commit 37b5208
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 3 deletions.
141 changes: 140 additions & 1 deletion ext/exif/exif.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -4285,11 +4297,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; i<item_count; i++, p2 += 16) {
if (php_ifd_get16u(p2, 1) == exif_id) {
pos->offset = 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;
Expand Down Expand Up @@ -4338,6 +4467,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;
Expand Down
145 changes: 145 additions & 0 deletions ext/exif/tests/exif028.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php
var_dump(exif_read_data(__DIR__.'/../../standard/tests/image/test4pix.heic'));
?>
--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)
}
5 changes: 5 additions & 0 deletions ext/standard/basic_functions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,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
Expand Down
1 change: 1 addition & 0 deletions ext/standard/basic_functions_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions ext/standard/image.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ext/standard/php_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 37b5208

Please sign in to comment.