Skip to content

Commit 64f7790

Browse files
committed
Fix GH-20863: read HEIC/HEIF dimensions for grid and Exif-only images
Camera HEIC files are grid images whose primary item holds the full-size ispe, but libavifinfo dropped that association once tile items filled its 32-entry ipma table, so the dimensions never resolved. Reserve ipma slots for the primary item so its association survives. Some files also omit any bit-depth property, so libavifinfo reports nothing even with a valid ispe; for those, fall back to the dimensions in the embedded Exif item. Fixes GH-20863
1 parent 332baba commit 64f7790

3 files changed

Lines changed: 460 additions & 12 deletions

File tree

ext/standard/image.c

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,8 @@ PHPAPI const int php_tiff_bytes_per_format[] = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8,
711711
/* compressed images only */
712712
#define TAG_COMP_IMAGEWIDTH 0xA002
713713
#define TAG_COMP_IMAGEHEIGHT 0xA003
714+
/* pointer to the Exif sub-IFD */
715+
#define TAG_EXIF_IFD_POINTER 0x8769
714716

715717
#define TAG_FMT_BYTE 1
716718
#define TAG_FMT_STRING 2
@@ -1171,6 +1173,369 @@ static void php_avif_stream_skip(void* stream, size_t num_bytes) {
11711173
}
11721174
/* }}} */
11731175

1176+
#define HEIF_META_MAX_SIZE (1 * 1024 * 1024)
1177+
#define HEIF_EXIF_MAX_SIZE (128 * 1024)
1178+
1179+
/* {{{ php_heif_buf_get */
1180+
static uint64_t php_heif_buf_get(const unsigned char *buf, unsigned int size)
1181+
{
1182+
uint64_t value = 0;
1183+
while (size-- > 0) {
1184+
value = (value << 8) | *buf++;
1185+
}
1186+
return value;
1187+
}
1188+
/* }}} */
1189+
1190+
/* {{{ php_heif_exif_ifd_dims */
1191+
static bool php_heif_exif_ifd_dims(const unsigned char *tiff, size_t tiff_len,
1192+
int motorola, uint32_t ifd_off, size_t *width, size_t *height,
1193+
uint32_t *sub_ifd)
1194+
{
1195+
if (ifd_off > tiff_len || tiff_len - ifd_off < 2) {
1196+
return false;
1197+
}
1198+
unsigned int num_entries = php_ifd_get16u((void *) (tiff + ifd_off), motorola);
1199+
if ((size_t) num_entries > (tiff_len - ifd_off - 2) / 12) {
1200+
return false;
1201+
}
1202+
for (unsigned int i = 0; i < num_entries; i++) {
1203+
const unsigned char *entry = tiff + ifd_off + 2 + i * 12;
1204+
unsigned int tag = php_ifd_get16u((void *) entry, motorola);
1205+
unsigned int type = php_ifd_get16u((void *) (entry + 2), motorola);
1206+
size_t value;
1207+
1208+
switch (type) {
1209+
case TAG_FMT_BYTE:
1210+
value = entry[8];
1211+
break;
1212+
case TAG_FMT_USHORT:
1213+
value = php_ifd_get16u((void *) (entry + 8), motorola);
1214+
break;
1215+
case TAG_FMT_ULONG:
1216+
value = php_ifd_get32u((void *) (entry + 8), motorola);
1217+
break;
1218+
default:
1219+
continue;
1220+
}
1221+
switch (tag) {
1222+
case TAG_IMAGEWIDTH:
1223+
case TAG_COMP_IMAGEWIDTH:
1224+
*width = value;
1225+
break;
1226+
case TAG_IMAGEHEIGHT:
1227+
case TAG_COMP_IMAGEHEIGHT:
1228+
*height = value;
1229+
break;
1230+
case TAG_EXIF_IFD_POINTER:
1231+
*sub_ifd = (uint32_t) value;
1232+
break;
1233+
}
1234+
}
1235+
return true;
1236+
}
1237+
/* }}} */
1238+
1239+
/* {{{ php_heif_exif_tiff_dims */
1240+
static bool php_heif_exif_tiff_dims(const unsigned char *tiff, size_t tiff_len,
1241+
size_t *out_w, size_t *out_h)
1242+
{
1243+
int motorola;
1244+
if (tiff_len < 8) {
1245+
return false;
1246+
}
1247+
if (!memcmp(tiff, "MM", 2)) {
1248+
motorola = 1;
1249+
} else if (!memcmp(tiff, "II", 2)) {
1250+
motorola = 0;
1251+
} else {
1252+
return false;
1253+
}
1254+
1255+
uint32_t ifd0 = php_ifd_get32u((void *) (tiff + 4), motorola);
1256+
size_t w0 = 0, h0 = 0;
1257+
uint32_t sub_ifd = 0;
1258+
if (!php_heif_exif_ifd_dims(tiff, tiff_len, motorola, ifd0, &w0, &h0, &sub_ifd)) {
1259+
return false;
1260+
}
1261+
1262+
size_t ws = 0, hs = 0;
1263+
if (sub_ifd != 0) {
1264+
uint32_t ignore = 0;
1265+
php_heif_exif_ifd_dims(tiff, tiff_len, motorola, sub_ifd, &ws, &hs, &ignore);
1266+
}
1267+
1268+
size_t w = ws ? ws : w0;
1269+
size_t h = hs ? hs : h0;
1270+
if (w == 0 || h == 0) {
1271+
return false;
1272+
}
1273+
*out_w = w;
1274+
*out_h = h;
1275+
return true;
1276+
}
1277+
/* }}} */
1278+
1279+
/* {{{ php_heif_find_exif_item */
1280+
static bool php_heif_find_exif_item(const unsigned char *meta, size_t meta_len,
1281+
uint64_t *file_off, uint64_t *length)
1282+
{
1283+
uint32_t exif_id = 0;
1284+
bool have_exif_id = false;
1285+
size_t p = 0;
1286+
1287+
const unsigned char *iinf = NULL, *iloc = NULL;
1288+
size_t iinf_len = 0, iloc_len = 0;
1289+
while (p + 8 <= meta_len) {
1290+
uint64_t box_size = php_heif_buf_get(meta + p, 4);
1291+
size_t hdr = 8;
1292+
if (box_size == 1) {
1293+
if (p + 16 > meta_len) {
1294+
break;
1295+
}
1296+
box_size = php_heif_buf_get(meta + p + 8, 8);
1297+
hdr = 16;
1298+
} else if (box_size == 0) {
1299+
box_size = meta_len - p;
1300+
}
1301+
if (box_size < hdr || box_size > meta_len - p) {
1302+
break;
1303+
}
1304+
if (!memcmp(meta + p + 4, "iinf", 4)) {
1305+
iinf = meta + p + hdr;
1306+
iinf_len = (size_t) box_size - hdr;
1307+
} else if (!memcmp(meta + p + 4, "iloc", 4)) {
1308+
iloc = meta + p + hdr;
1309+
iloc_len = (size_t) box_size - hdr;
1310+
}
1311+
p += (size_t) box_size;
1312+
}
1313+
if (iinf == NULL || iloc == NULL) {
1314+
return false;
1315+
}
1316+
1317+
if (iinf_len < 6) {
1318+
return false;
1319+
}
1320+
unsigned int iinf_ver = iinf[0];
1321+
size_t q = 4;
1322+
unsigned int count_bytes = (iinf_ver == 0) ? 2 : 4;
1323+
if (q + count_bytes > iinf_len) {
1324+
return false;
1325+
}
1326+
uint32_t entry_count = (uint32_t) php_heif_buf_get(iinf + q, count_bytes);
1327+
q += count_bytes;
1328+
for (uint32_t i = 0; i < entry_count && q + 8 <= iinf_len; i++) {
1329+
uint64_t isz = php_heif_buf_get(iinf + q, 4);
1330+
if (isz < 12 || isz > iinf_len - q) {
1331+
break;
1332+
}
1333+
if (!memcmp(iinf + q + 4, "infe", 4)) {
1334+
unsigned int infe_ver = iinf[q + 8];
1335+
if (infe_ver == 2 && isz >= 20) {
1336+
uint32_t iid = (uint32_t) php_heif_buf_get(iinf + q + 12, 2);
1337+
if (!memcmp(iinf + q + 16, "Exif", 4)) {
1338+
exif_id = iid;
1339+
have_exif_id = true;
1340+
}
1341+
} else if (infe_ver >= 3 && isz >= 24) {
1342+
uint32_t iid = (uint32_t) php_heif_buf_get(iinf + q + 12, 4);
1343+
if (!memcmp(iinf + q + 18, "Exif", 4)) {
1344+
exif_id = iid;
1345+
have_exif_id = true;
1346+
}
1347+
}
1348+
}
1349+
q += (size_t) isz;
1350+
}
1351+
if (!have_exif_id) {
1352+
return false;
1353+
}
1354+
1355+
if (iloc_len < 8) {
1356+
return false;
1357+
}
1358+
unsigned int iloc_ver = iloc[0];
1359+
size_t r = 4;
1360+
unsigned int offset_size = (iloc[r] >> 4) & 0xf;
1361+
unsigned int length_size = iloc[r] & 0xf;
1362+
unsigned int base_offset_size = (iloc[r + 1] >> 4) & 0xf;
1363+
unsigned int index_size = (iloc_ver == 1 || iloc_ver == 2) ? (iloc[r + 1] & 0xf) : 0;
1364+
r += 2;
1365+
unsigned int item_count_bytes = (iloc_ver < 2) ? 2 : 4;
1366+
if (r + item_count_bytes > iloc_len) {
1367+
return false;
1368+
}
1369+
uint32_t item_count = (uint32_t) php_heif_buf_get(iloc + r, item_count_bytes);
1370+
r += item_count_bytes;
1371+
unsigned int id_bytes = (iloc_ver < 2) ? 2 : 4;
1372+
1373+
for (uint32_t i = 0; i < item_count; i++) {
1374+
if (r + id_bytes > iloc_len) {
1375+
return false;
1376+
}
1377+
uint32_t item_id = (uint32_t) php_heif_buf_get(iloc + r, id_bytes);
1378+
r += id_bytes;
1379+
unsigned int construction_method = 0;
1380+
if (iloc_ver == 1 || iloc_ver == 2) {
1381+
if (r + 2 > iloc_len) {
1382+
return false;
1383+
}
1384+
construction_method = php_heif_buf_get(iloc + r, 2) & 0xf;
1385+
r += 2;
1386+
}
1387+
if (r + 2 > iloc_len) {
1388+
return false;
1389+
}
1390+
r += 2;
1391+
if (r + base_offset_size > iloc_len) {
1392+
return false;
1393+
}
1394+
uint64_t base_offset = php_heif_buf_get(iloc + r, base_offset_size);
1395+
r += base_offset_size;
1396+
if (r + 2 > iloc_len) {
1397+
return false;
1398+
}
1399+
uint32_t extent_count = (uint32_t) php_heif_buf_get(iloc + r, 2);
1400+
r += 2;
1401+
for (uint32_t e = 0; e < extent_count; e++) {
1402+
if (index_size) {
1403+
if (r + index_size > iloc_len) {
1404+
return false;
1405+
}
1406+
r += index_size;
1407+
}
1408+
if (r + offset_size + length_size > iloc_len) {
1409+
return false;
1410+
}
1411+
uint64_t ext_off = php_heif_buf_get(iloc + r, offset_size);
1412+
r += offset_size;
1413+
uint64_t ext_len = php_heif_buf_get(iloc + r, length_size);
1414+
r += length_size;
1415+
if (item_id == exif_id && construction_method == 0 && e == 0) {
1416+
if (ext_off > UINT64_MAX - base_offset) {
1417+
return false;
1418+
}
1419+
*file_off = base_offset + ext_off;
1420+
*length = ext_len;
1421+
return true;
1422+
}
1423+
}
1424+
}
1425+
return false;
1426+
}
1427+
/* }}} */
1428+
1429+
/* {{{ php_handle_heif_exif */
1430+
static struct php_gfxinfo *php_handle_heif_exif(php_stream *stream)
1431+
{
1432+
struct php_gfxinfo *result = NULL;
1433+
unsigned char header[8];
1434+
1435+
if (php_stream_rewind(stream)) {
1436+
return NULL;
1437+
}
1438+
1439+
uint64_t meta_hdr = 0, meta_size = 0;
1440+
for (;;) {
1441+
if (php_stream_read(stream, (char *) header, 8) != 8) {
1442+
return NULL;
1443+
}
1444+
uint64_t box_size = php_heif_buf_get(header, 4);
1445+
uint64_t hdr = 8;
1446+
if (box_size == 1) {
1447+
unsigned char ext[8];
1448+
if (php_stream_read(stream, (char *) ext, 8) != 8) {
1449+
return NULL;
1450+
}
1451+
box_size = php_heif_buf_get(ext, 8);
1452+
hdr = 16;
1453+
} else if (box_size == 0) {
1454+
zend_off_t cur = php_stream_tell(stream);
1455+
if (cur < 0 || php_stream_seek(stream, 0, SEEK_END)) {
1456+
return NULL;
1457+
}
1458+
zend_off_t end = php_stream_tell(stream);
1459+
if (end < cur || php_stream_seek(stream, cur, SEEK_SET)) {
1460+
return NULL;
1461+
}
1462+
box_size = (uint64_t) (end - cur) + hdr;
1463+
}
1464+
if (box_size < hdr) {
1465+
return NULL;
1466+
}
1467+
if (!memcmp(header + 4, "meta", 4)) {
1468+
meta_hdr = hdr;
1469+
meta_size = box_size;
1470+
break;
1471+
}
1472+
if (php_stream_seek(stream, (zend_off_t) (box_size - hdr), SEEK_CUR)) {
1473+
return NULL;
1474+
}
1475+
}
1476+
1477+
if (meta_size <= meta_hdr + 4) {
1478+
return NULL;
1479+
}
1480+
uint64_t meta_body_len = meta_size - meta_hdr - 4;
1481+
if (meta_body_len == 0 || meta_body_len > HEIF_META_MAX_SIZE) {
1482+
return NULL;
1483+
}
1484+
if (php_stream_seek(stream, 4, SEEK_CUR)) {
1485+
return NULL;
1486+
}
1487+
1488+
unsigned char *meta = emalloc((size_t) meta_body_len);
1489+
if (php_stream_read(stream, (char *) meta, (size_t) meta_body_len) != (ssize_t) meta_body_len) {
1490+
efree(meta);
1491+
return NULL;
1492+
}
1493+
1494+
uint64_t exif_off = 0, exif_len = 0;
1495+
bool found = php_heif_find_exif_item(meta, (size_t) meta_body_len, &exif_off, &exif_len);
1496+
efree(meta);
1497+
if (!found || exif_len < 4) {
1498+
return NULL;
1499+
}
1500+
1501+
if (php_stream_seek(stream, (zend_off_t) exif_off, SEEK_SET)) {
1502+
return NULL;
1503+
}
1504+
unsigned char prefix[4];
1505+
if (php_stream_read(stream, (char *) prefix, 4) != 4) {
1506+
return NULL;
1507+
}
1508+
uint64_t tiff_skip = php_heif_buf_get(prefix, 4);
1509+
if (tiff_skip > exif_len - 4) {
1510+
return NULL;
1511+
}
1512+
uint64_t tiff_len = exif_len - 4 - tiff_skip;
1513+
if (tiff_len < 8) {
1514+
return NULL;
1515+
}
1516+
if (tiff_len > HEIF_EXIF_MAX_SIZE) {
1517+
tiff_len = HEIF_EXIF_MAX_SIZE;
1518+
}
1519+
if (tiff_skip && php_stream_seek(stream, (zend_off_t) tiff_skip, SEEK_CUR)) {
1520+
return NULL;
1521+
}
1522+
unsigned char *tiff = emalloc((size_t) tiff_len);
1523+
if (php_stream_read(stream, (char *) tiff, (size_t) tiff_len) != (ssize_t) tiff_len) {
1524+
efree(tiff);
1525+
return NULL;
1526+
}
1527+
1528+
size_t w = 0, h = 0;
1529+
if (php_heif_exif_tiff_dims(tiff, (size_t) tiff_len, &w, &h)) {
1530+
result = ecalloc(1, sizeof(struct php_gfxinfo));
1531+
result->width = (unsigned int) w;
1532+
result->height = (unsigned int) h;
1533+
}
1534+
efree(tiff);
1535+
return result;
1536+
}
1537+
/* }}} */
1538+
11741539
/* {{{ php_handle_avif
11751540
* Parse AVIF features
11761541
*
@@ -1192,6 +1557,8 @@ static struct php_gfxinfo *php_handle_avif(php_stream * stream) {
11921557
result->height = features.height;
11931558
result->bits = features.bit_depth;
11941559
result->channels = features.num_channels;
1560+
} else {
1561+
result = php_handle_heif_exif(stream);
11951562
}
11961563
return result;
11971564
}

0 commit comments

Comments
 (0)