1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801
|
// Copyright 2020 Joe Drago. All rights reserved.
// SPDX-License-Identifier: BSD-2-Clause
#include "avifjpeg.h"
#include "avifexif.h"
#include "avifutil.h"
#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jpeglib.h"
#include "iccjpeg.h"
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
#include <libxml/parser.h>
#endif
#define AVIF_MIN(a, b) (((a) < (b)) ? (a) : (b))
#define AVIF_MAX(a, b) (((a) > (b)) ? (a) : (b))
struct my_error_mgr
{
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
typedef struct my_error_mgr * my_error_ptr;
static void my_error_exit(j_common_ptr cinfo)
{
my_error_ptr myerr = (my_error_ptr)cinfo->err;
(*cinfo->err->output_message)(cinfo);
longjmp(myerr->setjmp_buffer, 1);
}
#if JPEG_LIB_VERSION >= 70
#define AVIF_LIBJPEG_DCT_v_scaled_size DCT_v_scaled_size
#define AVIF_LIBJPEG_DCT_h_scaled_size DCT_h_scaled_size
#else
#define AVIF_LIBJPEG_DCT_h_scaled_size DCT_scaled_size
#define AVIF_LIBJPEG_DCT_v_scaled_size DCT_scaled_size
#endif
// An internal function used by avifJPEGReadCopy(), this is the shared libjpeg decompression code
// for all paths avifJPEGReadCopy() takes.
static avifBool avifJPEGCopyPixels(avifImage * avif, uint32_t sizeLimit, struct jpeg_decompress_struct * cinfo)
{
cinfo->raw_data_out = TRUE;
jpeg_start_decompress(cinfo);
avif->width = cinfo->image_width;
avif->height = cinfo->image_height;
if (avif->width > sizeLimit / avif->height) {
return AVIF_FALSE;
}
JSAMPIMAGE buffer = (*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_IMAGE, sizeof(JSAMPARRAY) * cinfo->num_components);
// lines of output image to be read per jpeg_read_raw_data call
int readLines = 0;
// lines of samples to be read per call (for each channel)
int linesPerCall[3] = { 0, 0, 0 };
// expected count of sample lines (for each channel)
int targetRead[3] = { 0, 0, 0 };
for (int i = 0; i < cinfo->num_components; ++i) {
jpeg_component_info * comp = &cinfo->comp_info[i];
linesPerCall[i] = comp->v_samp_factor * comp->AVIF_LIBJPEG_DCT_v_scaled_size;
targetRead[i] = comp->downsampled_height;
buffer[i] = (*cinfo->mem->alloc_sarray)((j_common_ptr)cinfo,
JPOOL_IMAGE,
comp->width_in_blocks * comp->AVIF_LIBJPEG_DCT_h_scaled_size,
linesPerCall[i]);
readLines = AVIF_MAX(readLines, linesPerCall[i]);
}
avifImageFreePlanes(avif, AVIF_PLANES_ALL); // Free planes in case they were already allocated.
if (avifImageAllocatePlanes(avif, AVIF_PLANES_YUV) != AVIF_RESULT_OK) {
return AVIF_FALSE;
}
// destination avif channel for each jpeg channel
avifChannelIndex targetChannel[3] = { AVIF_CHAN_Y, AVIF_CHAN_Y, AVIF_CHAN_Y };
if (cinfo->jpeg_color_space == JCS_YCbCr) {
targetChannel[0] = AVIF_CHAN_Y;
targetChannel[1] = AVIF_CHAN_U;
targetChannel[2] = AVIF_CHAN_V;
} else if (cinfo->jpeg_color_space == JCS_GRAYSCALE) {
targetChannel[0] = AVIF_CHAN_Y;
} else {
// cinfo->jpeg_color_space == JCS_RGB
targetChannel[0] = AVIF_CHAN_V;
targetChannel[1] = AVIF_CHAN_Y;
targetChannel[2] = AVIF_CHAN_U;
}
int workComponents = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? 1 : cinfo->num_components;
// count of already-read lines (for each channel)
int alreadyRead[3] = { 0, 0, 0 };
while (cinfo->output_scanline < cinfo->output_height) {
jpeg_read_raw_data(cinfo, buffer, readLines);
for (int i = 0; i < workComponents; ++i) {
int linesRead = AVIF_MIN(targetRead[i] - alreadyRead[i], linesPerCall[i]);
for (int j = 0; j < linesRead; ++j) {
memcpy(&avif->yuvPlanes[targetChannel[i]][avif->yuvRowBytes[targetChannel[i]] * (alreadyRead[i] + j)],
buffer[i][j],
avif->yuvRowBytes[targetChannel[i]]);
}
alreadyRead[i] += linesPerCall[i];
}
}
return AVIF_TRUE;
}
static avifBool avifJPEGHasCompatibleMatrixCoefficients(avifMatrixCoefficients matrixCoefficients)
{
switch (matrixCoefficients) {
case AVIF_MATRIX_COEFFICIENTS_BT470BG:
case AVIF_MATRIX_COEFFICIENTS_BT601:
// JPEG always uses [Kr:0.299, Kb:0.114], which matches these MCs.
return AVIF_TRUE;
}
return AVIF_FALSE;
}
// This attempts to copy the internal representation of the JPEG directly into avifImage without
// YUV->RGB conversion. If it returns AVIF_FALSE, a typical RGB->YUV conversion is required.
static avifBool avifJPEGReadCopy(avifImage * avif, uint32_t sizeLimit, struct jpeg_decompress_struct * cinfo)
{
if ((avif->depth != 8) || (avif->yuvRange != AVIF_RANGE_FULL)) {
return AVIF_FALSE;
}
if (cinfo->jpeg_color_space == JCS_YCbCr) {
// Import from YUV: must use compatible matrixCoefficients.
if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients)) {
// YUV->YUV: require precise match for pixel format.
avifPixelFormat jpegFormat = AVIF_PIXEL_FORMAT_NONE;
if (cinfo->comp_info[0].h_samp_factor == 1 && cinfo->comp_info[0].v_samp_factor == 1 &&
cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 &&
cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) {
jpegFormat = AVIF_PIXEL_FORMAT_YUV444;
} else if (cinfo->comp_info[0].h_samp_factor == 2 && cinfo->comp_info[0].v_samp_factor == 1 &&
cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 &&
cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) {
jpegFormat = AVIF_PIXEL_FORMAT_YUV422;
} else if (cinfo->comp_info[0].h_samp_factor == 2 && cinfo->comp_info[0].v_samp_factor == 2 &&
cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 &&
cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) {
jpegFormat = AVIF_PIXEL_FORMAT_YUV420;
}
if (jpegFormat != AVIF_PIXEL_FORMAT_NONE) {
if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
// The requested format is "auto": Adopt JPEG's internal format.
avif->yuvFormat = jpegFormat;
}
if (avif->yuvFormat == jpegFormat) {
cinfo->out_color_space = JCS_YCbCr;
return avifJPEGCopyPixels(avif, sizeLimit, cinfo);
}
}
// YUV->Grayscale: subsample Y plane not allowed.
if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && (cinfo->comp_info[0].h_samp_factor == cinfo->max_h_samp_factor &&
cinfo->comp_info[0].v_samp_factor == cinfo->max_v_samp_factor)) {
cinfo->out_color_space = JCS_YCbCr;
return avifJPEGCopyPixels(avif, sizeLimit, cinfo);
}
}
} else if (cinfo->jpeg_color_space == JCS_GRAYSCALE) {
// Import from Grayscale: subsample not allowed.
if ((cinfo->comp_info[0].h_samp_factor == cinfo->max_h_samp_factor &&
cinfo->comp_info[0].v_samp_factor == cinfo->max_v_samp_factor)) {
// Import to YUV/Grayscale: must use compatible matrixCoefficients.
if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients) ||
avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
// Grayscale->Grayscale: direct copy.
if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE)) {
avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400;
cinfo->out_color_space = JCS_GRAYSCALE;
return avifJPEGCopyPixels(avif, sizeLimit, cinfo);
}
// Grayscale->YUV: copy Y, fill UV with monochrome value.
if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV422) ||
(avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV420)) {
cinfo->out_color_space = JCS_GRAYSCALE;
if (!avifJPEGCopyPixels(avif, sizeLimit, cinfo)) {
return AVIF_FALSE;
}
uint32_t uvHeight = avifImagePlaneHeight(avif, AVIF_CHAN_U);
memset(avif->yuvPlanes[AVIF_CHAN_U], 128, (size_t)avif->yuvRowBytes[AVIF_CHAN_U] * uvHeight);
memset(avif->yuvPlanes[AVIF_CHAN_V], 128, (size_t)avif->yuvRowBytes[AVIF_CHAN_V] * uvHeight);
return AVIF_TRUE;
}
}
// Grayscale->RGB: copy Y to G, duplicate to B and R.
if ((avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) &&
((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE))) {
avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
cinfo->out_color_space = JCS_GRAYSCALE;
if (!avifJPEGCopyPixels(avif, sizeLimit, cinfo)) {
return AVIF_FALSE;
}
memcpy(avif->yuvPlanes[AVIF_CHAN_U], avif->yuvPlanes[AVIF_CHAN_Y], (size_t)avif->yuvRowBytes[AVIF_CHAN_U] * avif->height);
memcpy(avif->yuvPlanes[AVIF_CHAN_V], avif->yuvPlanes[AVIF_CHAN_Y], (size_t)avif->yuvRowBytes[AVIF_CHAN_V] * avif->height);
return AVIF_TRUE;
}
}
} else if (cinfo->jpeg_color_space == JCS_RGB) {
// RGB->RGB: subsample not allowed.
if ((avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) &&
((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE)) &&
(cinfo->comp_info[0].h_samp_factor == 1 && cinfo->comp_info[0].v_samp_factor == 1 &&
cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 &&
cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1)) {
avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
cinfo->out_color_space = JCS_RGB;
return avifJPEGCopyPixels(avif, sizeLimit, cinfo);
}
}
// A typical RGB->YUV conversion is required.
return AVIF_FALSE;
}
// Reads a 4-byte unsigned integer in big-endian format from the raw bitstream src.
static uint32_t avifJPEGReadUint32BigEndian(const uint8_t * src)
{
return ((uint32_t)src[0] << 24) | ((uint32_t)src[1] << 16) | ((uint32_t)src[2] << 8) | ((uint32_t)src[3] << 0);
}
// Returns the pointer in str to the first occurrence of substr. Returns NULL if substr cannot be found in str.
static const uint8_t * avifJPEGFindSubstr(const uint8_t * str, size_t strLength, const uint8_t * substr, size_t substrLength)
{
for (size_t index = 0; index + substrLength <= strLength; ++index) {
if (!memcmp(&str[index], substr, substrLength)) {
return &str[index];
}
}
return NULL;
}
#define AVIF_JPEG_MAX_MARKER_DATA_LENGTH 65533
// Exif tag
#define AVIF_JPEG_EXIF_HEADER "Exif\0\0"
#define AVIF_JPEG_EXIF_HEADER_LENGTH 6
// XMP tags
#define AVIF_JPEG_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0"
#define AVIF_JPEG_STANDARD_XMP_TAG_LENGTH 29
#define AVIF_JPEG_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0"
#define AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH 35
#define AVIF_EXIF_APPLE_MAKER_NOTES_HEADER "Apple iOS\0\0\1MM"
#define AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH 14
// MPF tag (Multi-Picture Format)
#define AVIF_JPEG_MPF_HEADER "MPF\0"
#define AVIF_JPEG_MPF_HEADER_LENGTH 4
// One way of storing the Extended XMP GUID (generated by a camera for example).
#define AVIF_JPEG_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
#define AVIF_JPEG_XMP_NOTE_TAG_LENGTH 24
// Another way of storing the Extended XMP GUID (generated by exiftool for example).
#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>"
#define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24
#define AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH 32
// Offset in APP1 segment (skip tag + guid + size + offset).
#define AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP (AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
#define AVIF_CHECK(A) \
do { \
if (!(A)) \
return AVIF_FALSE; \
} while (0)
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
// Reads a 4-byte unsigned integer in little-endian format from the raw bitstream src.
static uint32_t avifJPEGReadUint32LittleEndian(const uint8_t * src)
{
return ((uint32_t)src[0] << 0) | ((uint32_t)src[1] << 8) | ((uint32_t)src[2] << 16) | ((uint32_t)src[3] << 24);
}
// Reads a 2-byte unsigned integer in big-endian format from the raw bitstream src.
static uint16_t avifJPEGReadUint16BigEndian(const uint8_t * src)
{
return (uint16_t)((src[0] << 8) | (src[1] << 0));
}
// Reads a 2-byte unsigned integer in little-endian format from the raw bitstream src.
static uint16_t avifJPEGReadUint16LittleEndian(const uint8_t * src)
{
return (uint16_t)((src[0] << 0) | (src[1] << 8));
}
// Reads 'numBytes' at 'offset', stores them in 'bytes' and increases 'offset'.
static avifBool avifJPEGReadBytes(const avifROData * data, uint8_t * bytes, size_t * offset, uint32_t numBytes)
{
if ((UINT32_MAX - *offset) < numBytes || data->size < (*offset + numBytes)) {
return AVIF_FALSE;
}
memcpy(bytes, &data->data[*offset], numBytes);
*offset += numBytes;
return AVIF_TRUE;
}
static avifBool avifJPEGReadU32(const avifROData * data, uint32_t * v, size_t * offset, avifBool isBigEndian)
{
uint8_t bytes[4];
AVIF_CHECK(avifJPEGReadBytes(data, bytes, offset, 4));
*v = isBigEndian ? avifJPEGReadUint32BigEndian(bytes) : avifJPEGReadUint32LittleEndian(bytes);
return AVIF_TRUE;
}
static avifBool avifJPEGReadS32(const avifROData * data, int32_t * v, size_t * offset, avifBool isBigEndian)
{
uint32_t u;
AVIF_CHECK(avifJPEGReadU32(data, &u, offset, isBigEndian));
*v = (int32_t)u;
return AVIF_TRUE;
}
static avifBool avifJPEGReadU16(const avifROData * data, uint16_t * v, size_t * offset, avifBool isBigEndian)
{
uint8_t bytes[2];
AVIF_CHECK(avifJPEGReadBytes(data, bytes, offset, 2));
*v = isBigEndian ? avifJPEGReadUint16BigEndian(bytes) : avifJPEGReadUint16LittleEndian(bytes);
return AVIF_TRUE;
}
// Searches for the HDR headroom in the Exif metadata for JPEGs captured on iPhones.
// Returns false in case of reading error or if the headroom could not be found.
// References:
// https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
// https://www.media.mit.edu/pia/Research/deepview/exif.html
// https://www.cipa.jp/std/documents/download_e.html?CIPA_DC-008-2024-E
// https://exiftool.org/TagNames/EXIF.html
// Exif metadata consists of a list of IFDs (Image File Directory), each containing a list of tags.
// The first IFD (IFD0) is expected to contain a tag called ExifOffset (id 0x8769) which contains
// the offset to another IFD, the Exif IFD.
// The Exif IFD is expected to contain a tag called MakerNotes (id 0x927c) which contains an offset
// to proprietary notes data specific to the camera vendor. In the case of Apple, it consists of a
// header starting with 'Apple iOS' etc. followed by another IFD. This last IFD contains the tags
// 33 and 48 which are used to compute the headroom.
avifBool avifGetExifAppleHeadroom(const avifROData * exif, double * altHeadroom)
{
*altHeadroom = 0.0f;
size_t offset = 0;
const avifResult result = avifGetExifTiffHeaderOffset(exif->data, exif->size, &offset);
if (result != AVIF_RESULT_OK) {
return AVIF_FALSE; // Couldn't find the TIFF header
}
avifBool isBigEndian = (exif->data[offset] == 'M');
offset += 4; // Skip the TIFF header.
uint32_t offsetToIfd;
AVIF_CHECK(avifJPEGReadU32(exif, &offsetToIfd, &offset, isBigEndian));
avifBool inAppleMakerNotes = AVIF_FALSE;
// According to the Skia implementation, "Many images have a maker33 but not a maker48."
// We assume the missing value (if any) to be zero.
avifBool hasMaker33Or48 = AVIF_FALSE;
double maker33 = 0.0;
double maker48 = 0.0;
int numIfds = 0;
const int maxIfds = 3; // Prevent infinite looping caused by malformed data.
while (offsetToIfd != 0 && numIfds++ < maxIfds) {
offset = offsetToIfd;
avifBool offsetToNextIfdAlreadySet = AVIF_FALSE;
uint16_t fieldCount;
AVIF_CHECK(avifJPEGReadU16(exif, &fieldCount, &offset, isBigEndian));
for (uint16_t field = 0; field < fieldCount; ++field) {
uint16_t tagId;
uint16_t dataFormat;
uint32_t numComponents;
uint32_t tagData;
AVIF_CHECK(avifJPEGReadU16(exif, &tagId, &offset, isBigEndian));
AVIF_CHECK(avifJPEGReadU16(exif, &dataFormat, &offset, isBigEndian));
AVIF_CHECK(avifJPEGReadU32(exif, &numComponents, &offset, isBigEndian));
AVIF_CHECK(avifJPEGReadU32(exif, &tagData, &offset, isBigEndian));
if (tagId == 0x8769) { // Exif Offset (offset to a sub IFD)
// Move back to just before the tagData which contains the offset of the Exif IFD.
offset -= 4;
break;
} else if (tagId == 0x927c) { // Maker Notes
size_t makerNotesOffset = tagData;
uint8_t makerTag[AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH];
AVIF_CHECK(avifJPEGReadBytes(exif, makerTag, &makerNotesOffset, AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH));
// From https://exiftool.org/makernote_types.html
// Apple Maker Notes contain a header (below) followed by an IFD.
if (!memcmp(&makerTag, AVIF_EXIF_APPLE_MAKER_NOTES_HEADER, AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH)) {
if (makerNotesOffset > UINT32_MAX) {
return AVIF_FALSE;
}
offsetToIfd = (uint32_t)makerNotesOffset;
inAppleMakerNotes = AVIF_TRUE;
offsetToNextIfdAlreadySet = AVIF_TRUE;
// Apple Maker Notes are always big endian, regardless of the endianness of the top level Exif.
isBigEndian = AVIF_TRUE;
break;
}
} else if (inAppleMakerNotes && (tagId == 33 || tagId == 48) && dataFormat == 10) {
// Offsets in the Apple Maker Notes are relative to the Maker Notes field.
if (offsetToIfd < AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH ||
((uint64_t)offsetToIfd - AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH + tagData) > SIZE_MAX) {
return AVIF_FALSE; // Avoid under/over flow.
}
size_t tmpOffset = (size_t)offsetToIfd - AVIF_EXIF_APPLE_MAKER_NOTES_HEADER_LENGTH + (size_t)tagData;
int32_t numerator;
uint32_t denominator;
AVIF_CHECK(avifJPEGReadS32(exif, &numerator, &tmpOffset, isBigEndian));
AVIF_CHECK(avifJPEGReadU32(exif, &denominator, &tmpOffset, isBigEndian));
if (denominator == 0) {
return AVIF_FALSE;
}
const double v = (double)numerator / denominator;
if (tagId == 33) {
maker33 = v;
} else {
maker48 = v;
}
hasMaker33Or48 = AVIF_TRUE;
}
}
if (!offsetToNextIfdAlreadySet) {
AVIF_CHECK(avifJPEGReadU32(exif, &offsetToIfd, &offset, isBigEndian));
}
}
if (!hasMaker33Or48) {
return AVIF_FALSE;
}
// From https://developer.apple.com/documentation/appkit/applying-apple-hdr-effect-to-your-photos
double stops;
if (maker33 < 1.0) {
if (maker48 <= 0.01) {
stops = -20.0 * maker48 + 1.8;
} else {
stops = -0.101 * maker48 + 1.601;
}
} else {
if (maker48 <= 0.01) {
stops = -70.0 * maker48 + 3.0;
} else {
stops = -0.303 * maker48 + 2.303;
}
}
*altHeadroom = stops;
return AVIF_TRUE;
}
static avifBool avifJPEGReadInternal(FILE * f,
const char * inputFilename,
avifImage * avif,
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit);
// Arbitrary max number of jpeg segments to parse before giving up.
#define MAX_JPEG_SEGMENTS 100
// Finds the offset of the first MPF segment. Returns AVIF_TRUE if it was found.
static avifBool avifJPEGFindMpfSegmentOffset(FILE * f, uint32_t * mpfOffset)
{
const long oldOffset = ftell(f);
if (oldOffset < 0) {
return AVIF_FALSE;
}
uint32_t offset = 2; // Skip the 2 byte SOI (Start Of Image) marker.
if (fseek(f, offset, SEEK_SET) != 0) {
return AVIF_FALSE;
}
uint8_t buffer[4];
int numSegments = 0;
while (numSegments < MAX_JPEG_SEGMENTS) {
++numSegments;
// Read the APP<n> segment marker (2 bytes) and the segment size (2 bytes).
if (fread(buffer, 1, 4, f) != 4) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // End of the file reached.
}
offset += 4;
// Total APP<n> segment byte count, including the byte count value (2 bytes), but excluding the 2 byte APP<n> marker itself.
const uint16_t segmentLength = avifJPEGReadUint16BigEndian(&buffer[2]);
if (segmentLength < 2) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // Invalid length.
} else if (segmentLength < 2 + AVIF_JPEG_MPF_HEADER_LENGTH) {
// Cannot be an MPF segment, skip to the next segment.
offset += segmentLength - 2;
if (fseek(f, offset, SEEK_SET) != 0) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE;
}
continue;
}
uint8_t identifier[AVIF_JPEG_MPF_HEADER_LENGTH];
if (fread(identifier, 1, AVIF_JPEG_MPF_HEADER_LENGTH, f) != AVIF_JPEG_MPF_HEADER_LENGTH) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // End of the file reached.
}
offset += AVIF_JPEG_MPF_HEADER_LENGTH;
if (buffer[1] == (JPEG_APP0 + 2) && !memcmp(identifier, AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH)) {
// MPF segment found.
*mpfOffset = offset;
fseek(f, oldOffset, SEEK_SET);
return AVIF_TRUE;
}
// Skip to the next segment.
offset += segmentLength - 2 - AVIF_JPEG_MPF_HEADER_LENGTH;
if (fseek(f, offset, SEEK_SET) != 0) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE;
}
}
return AVIF_FALSE;
}
// Searches for a node called 'nameSpace:nodeName' in the children (or descendants if 'recursive' is set) of 'parentNode'.
// Returns the first such node found (in depth first search). Returns NULL if no such node is found.
static const xmlNode * avifJPEGFindXMLNodeByName(const xmlNode * parentNode, const char * nameSpace, const char * nodeName, avifBool recursive)
{
if (parentNode == NULL) {
return NULL;
}
for (const xmlNode * node = parentNode->children; node != NULL; node = node->next) {
if (node->ns != NULL && !xmlStrcmp(node->ns->href, (const xmlChar *)nameSpace) &&
!xmlStrcmp(node->name, (const xmlChar *)nodeName)) {
return node;
} else if (recursive) {
const xmlNode * descendantNode = avifJPEGFindXMLNodeByName(node, nameSpace, nodeName, recursive);
if (descendantNode != NULL) {
return descendantNode;
}
}
}
return NULL;
}
#define XML_NAME_SPACE_GAIN_MAP "http://ns.adobe.com/hdr-gain-map/1.0/"
#define XML_NAME_SPACE_APPLE_GAIN_MAP "http://ns.apple.com/HDRGainMap/1.0/"
#define XML_NAME_SPACE_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
#define XML_NAME_SPACE_XMP_NOTE "http://ns.adobe.com/xmp/note/"
// Finds an 'rdf:Description' node containing a gain map version attribute (hdrgm:Version="1.0").
// Returns NULL if not found.
static const xmlNode * avifJPEGFindIsoGainMapXMPNode(const xmlNode * rootNode)
{
// See XMP specification https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart1.pdf
// ISO 16684-1:2011 7.1 "For this serialization, a single XMP packet shall be serialized using a single rdf:RDF XML element."
// 7.3 "Other XML elements may appear around the rdf:RDF element."
const xmlNode * rdfNode = avifJPEGFindXMLNodeByName(rootNode, XML_NAME_SPACE_RDF, "RDF", /*recursive=*/AVIF_TRUE);
if (rdfNode == NULL) {
return NULL;
}
for (const xmlNode * node = rdfNode->children; node != NULL; node = node->next) {
// Loop through rdf:Description children.
// 7.4 "A single XMP packet shall be serialized using a single rdf:RDF XML element. The rdf:RDF element content
// shall consist of only zero or more rdf:Description elements."
if (node->ns && !xmlStrcmp(node->ns->href, (const xmlChar *)XML_NAME_SPACE_RDF) &&
!xmlStrcmp(node->name, (const xmlChar *)"Description")) {
// Look for the gain map version attribute: hdrgm:Version="1.0"
for (xmlAttr * prop = node->properties; prop != NULL; prop = prop->next) {
if (prop->ns && !xmlStrcmp(prop->ns->href, (const xmlChar *)XML_NAME_SPACE_GAIN_MAP) &&
!xmlStrcmp(prop->name, (const xmlChar *)"Version") && prop->children != NULL &&
!xmlStrcmp(prop->children->content, (const xmlChar *)"1.0")) {
return node;
}
}
}
}
return NULL;
}
// Finds an 'rdf:Description' node containing a <HDRGainMap:HDRGainMapVersion> child.
static const xmlNode * avifJPEGFindAppleGainMapXMPNode(const xmlNode * rootNode)
{
// See XMP specification https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart1.pdf
// ISO 16684-1:2011 7.1 "For this serialization, a single XMP packet shall be serialized using a single rdf:RDF XML element."
// 7.3 "Other XML elements may appear around the rdf:RDF element."
const xmlNode * rdfNode = avifJPEGFindXMLNodeByName(rootNode, XML_NAME_SPACE_RDF, "RDF", /*recursive=*/AVIF_TRUE);
if (rdfNode == NULL) {
return NULL;
}
for (const xmlNode * node = rdfNode->children; node != NULL; node = node->next) {
// Loop through rdf:Description children.
// 7.4 "A single XMP packet shall be serialized using a single rdf:RDF XML element. The rdf:RDF element content
// shall consist of only zero or more rdf:Description elements."
if (node->ns && !xmlStrcmp(node->ns->href, (const xmlChar *)XML_NAME_SPACE_RDF) &&
!xmlStrcmp(node->name, (const xmlChar *)"Description")) {
// Look for a <HDRGainMap:HDRGainMapVersion> child.
for (const xmlNode * child = node->children; child != NULL; child = child->next) {
if (child->ns && !xmlStrcmp(child->ns->href, (const xmlChar *)XML_NAME_SPACE_APPLE_GAIN_MAP) &&
!xmlStrcmp(child->name, (const xmlChar *)"HDRGainMapVersion")) {
return node;
}
}
}
}
return NULL;
}
static const xmlNode * avifJPEGFindGainMapXMPNode(const xmlNode * rootNode, avifBool * isAppleGainMap)
{
if (isAppleGainMap) {
*isAppleGainMap = AVIF_FALSE;
}
const xmlNode * node = avifJPEGFindIsoGainMapXMPNode(rootNode);
if (node) {
return node;
}
node = avifJPEGFindAppleGainMapXMPNode(rootNode);
if (node) {
if (isAppleGainMap) {
*isAppleGainMap = AVIF_TRUE;
}
return node;
}
return NULL;
}
// Returns true if there is an 'rdf:Description' node containing a gain map version attribute
// (ISO style) or child element (Apple style).
// On the main image, this signals that the file also contains a gain map (for ISO gain maps). Apple style gain maps
// do not have gain map XMP on the main image.
// On a subsequent image, this signals that it is a gain map.
// If not null, isAppleGainMap is set to AVIF_TRUE for an Apple style gain map, and AVIF_FALSE for an ISO gain map.
static avifBool avifJPEGHasGainMapXMPNode(const uint8_t * xmpData, size_t xmpSize, avifBool * isAppleGainMap)
{
xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, /*options=*/0);
if (document == NULL) {
return AVIF_FALSE; // Probably and out of memory error.
}
const xmlNode * rootNode = xmlDocGetRootElement(document);
const xmlNode * node = avifJPEGFindGainMapXMPNode(rootNode, isAppleGainMap);
const avifBool found = (node != NULL);
xmlFreeDoc(document);
return found;
}
// Finds the value of a gain map metadata property, that can be either stored as an attribute of 'descriptionNode'
// (which should point to a <rdf:Description> node) or as a child node.
// 'maxValues' is the maximum number of expected values, and the size of the 'values' array. 'numValues' is set to the number
// of values actually found (which may be smaller or larger, but only up to 'maxValues' are stored in 'values').
// Returns AVIF_TRUE if the property was found.
static avifBool avifJPEGFindGainMapProperty(const xmlNode * descriptionNode,
const char * propertyName,
uint32_t maxValues,
const char * values[],
uint32_t * numValues,
const char * nameSpace)
{
*numValues = 0;
// Search attributes.
for (xmlAttr * prop = descriptionNode->properties; prop != NULL; prop = prop->next) {
if (prop->ns && !xmlStrcmp(prop->ns->href, (const xmlChar *)nameSpace) &&
!xmlStrcmp(prop->name, (const xmlChar *)propertyName) && prop->children != NULL && prop->children->content != NULL) {
// Properties should have just one child containing the property's value
// (in fact the 'children' field is documented as "the value of the property").
values[0] = (const char *)prop->children->content;
*numValues = 1;
return AVIF_TRUE;
}
}
// Search child nodes.
for (const xmlNode * node = descriptionNode->children; node != NULL; node = node->next) {
if (node->ns && !xmlStrcmp(node->ns->href, (const xmlChar *)nameSpace) &&
!xmlStrcmp(node->name, (const xmlChar *)propertyName) && node->children) {
// Multiple values can be specified with a Seq tag: <rdf:Seq><rdf:li>value1</rdf:li><rdf:li>value2</rdf:li>...</rdf:Seq>
const xmlNode * seq = avifJPEGFindXMLNodeByName(node, XML_NAME_SPACE_RDF, "Seq", /*recursive=*/AVIF_FALSE);
if (seq) {
for (xmlNode * seqChild = seq->children; seqChild; seqChild = seqChild->next) {
if (!xmlStrcmp(seqChild->name, (const xmlChar *)"li") && seqChild->children != NULL &&
seqChild->children->content != NULL) {
if (*numValues < maxValues) {
values[*numValues] = (const char *)seqChild->children->content;
}
++(*numValues);
}
}
return *numValues > 0 ? AVIF_TRUE : AVIF_FALSE;
} else if (node->children->next == NULL && node->children->type == XML_TEXT_NODE) { // Only one child and it's text.
values[0] = (const char *)node->children->content;
*numValues = 1;
return AVIF_TRUE;
}
// We found a tag for this property but no valid content.
return AVIF_FALSE;
}
}
return AVIF_FALSE; // Property not found.
}
// Up to 3 values per property (one for each RGB channel).
#define GAIN_MAP_PROPERTY_MAX_VALUES 3
// Looks for a given gain map property's double value(s), and if found, stores them in 'values'.
// The 'values' array should have size at least 'numDoubles', and should be initialized with default
// values for this property, since the array will be left untouched if the property is not found.
// Returns AVIF_TRUE if the property was successfully parsed, or if it was not found, since all properties
// are optional. Returns AVIF_FALSE in case of error (invalid metadata XMP).
static avifBool avifJPEGFindGainMapPropertyDoubles(const xmlNode * descriptionNode,
const char * propertyName,
double * values,
uint32_t numDoubles,
const char * nameSpace)
{
assert(numDoubles <= GAIN_MAP_PROPERTY_MAX_VALUES);
const char * textValues[GAIN_MAP_PROPERTY_MAX_VALUES];
uint32_t numValues;
if (!avifJPEGFindGainMapProperty(descriptionNode, propertyName, /*maxValues=*/numDoubles, &textValues[0], &numValues, nameSpace)) {
return AVIF_TRUE; // Property was not found, but it's not an error since they're all optional.
}
if (numValues != 1 && numValues != numDoubles) {
return AVIF_FALSE; // Invalid, we expect either 1 or exactly numDoubles values.
}
for (uint32_t i = 0; i < numDoubles; ++i) {
if (i >= numValues) {
// If there is only 1 value, it's copied into the rest of the array.
values[i] = values[i - 1];
} else {
int charsRead;
if (sscanf(textValues[i], "%lf%n", &values[i], &charsRead) < 1) {
return AVIF_FALSE; // Was not able to parse the full string value as a double.
}
// Make sure that remaining characters (if any) are only whitespace.
const int len = (int)strlen(textValues[i]);
while (charsRead < len) {
if (!isspace(textValues[i][charsRead])) {
return AVIF_FALSE; // Invalid character.
}
++charsRead;
}
}
}
return AVIF_TRUE;
}
static inline void SwapDoubles(double * x, double * y)
{
double tmp = *x;
*x = *y;
*y = tmp;
}
static avifBool avifJPEGParseGainMapXMPPropertiesAppleFormat(const xmlNode * descNode, avifGainMap * gainMap)
{
double hdrHeadroomLinear = 1.0;
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRGainMapHeadroom", &hdrHeadroomLinear, /*numDoubles=*/1, XML_NAME_SPACE_APPLE_GAIN_MAP));
if (hdrHeadroomLinear <= 0) {
return AVIF_FALSE;
}
const double hdrHeadroom = log2(hdrHeadroomLinear);
avifSignedFraction hdrHeadroomSFraction;
AVIF_CHECK(avifDoubleToSignedFraction(hdrHeadroom, &hdrHeadroomSFraction));
for (int i = 0; i < 3; ++i) {
gainMap->gainMapMin[i].n = 0; // Min = 0 (log2)
gainMap->gainMapMin[i].d = 1;
gainMap->gainMapMax[i] = hdrHeadroomSFraction;
gainMap->gainMapGamma[i].n = 1; // Gamma = 1.
gainMap->gainMapGamma[i].d = 1;
gainMap->baseOffset[i].n = 0; // Base offset = 0.
gainMap->baseOffset[i].d = 1;
gainMap->alternateOffset[i].n = 0; // Alternate offset = 0.
gainMap->alternateOffset[i].d = 1;
}
gainMap->baseHdrHeadroom.n = 0; // Base headroom = 0 (SDR)
gainMap->baseHdrHeadroom.d = 1;
AVIF_CHECK(avifDoubleToUnsignedFraction(hdrHeadroom, &gainMap->alternateHdrHeadroom));
return AVIF_TRUE;
}
// Parses gain map metadata from XMP.
// See https://developer.android.com/media/platform/hdr-image-format
// Returns AVIF_TRUE if the gain map metadata was successfully read.
static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMap * gainMap, avifBool * isAppleGainMap)
{
const xmlNode * descNode = avifJPEGFindGainMapXMPNode(rootNode, isAppleGainMap);
if (descNode == NULL) {
return AVIF_FALSE;
}
if (*isAppleGainMap) {
return avifJPEGParseGainMapXMPPropertiesAppleFormat(descNode, gainMap);
}
double baseHdrHeadroom = 0.0;
double alternateHdrHeadroom = 1.0;
double gainMapMin[3] = { 0.0, 0.0, 0.0 };
double gainMapMax[3] = { 1.0, 1.0, 1.0 };
double gainMapGamma[3] = { 1.0, 1.0, 1.0 };
double baseOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
double alternateOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
const char * ns = XML_NAME_SPACE_GAIN_MAP;
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &baseHdrHeadroom, /*numDoubles=*/1, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &alternateHdrHeadroom, /*numDoubles=*/1, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", baseOffset, /*numDoubles=*/3, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", alternateOffset, /*numDoubles=*/3, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", gainMapMin, /*numDoubles=*/3, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", gainMapMax, /*numDoubles=*/3, ns));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", gainMapGamma, /*numDoubles=*/3, ns));
AVIF_CHECK(alternateHdrHeadroom > baseHdrHeadroom);
AVIF_CHECK(baseHdrHeadroom >= 0);
for (int i = 0; i < 3; ++i) {
AVIF_CHECK(gainMapMax[i] >= gainMapMin[i]);
AVIF_CHECK(baseOffset[i] >= 0.0);
AVIF_CHECK(alternateOffset[i] >= 0.0);
AVIF_CHECK(gainMapGamma[i] > 0.0);
}
uint32_t numValues;
const char * baseRenditionIsHDR;
if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues, ns)) {
if (!strcmp(baseRenditionIsHDR, "True")) {
SwapDoubles(&baseHdrHeadroom, &alternateHdrHeadroom);
for (int c = 0; c < 3; ++c) {
SwapDoubles(&baseOffset[c], &alternateOffset[c]);
}
} else if (!strcmp(baseRenditionIsHDR, "False")) {
} else {
return AVIF_FALSE; // Unexpected value.
}
}
for (int i = 0; i < 3; ++i) {
AVIF_CHECK(avifDoubleToSignedFraction(gainMapMin[i], &gainMap->gainMapMin[i]));
AVIF_CHECK(avifDoubleToSignedFraction(gainMapMax[i], &gainMap->gainMapMax[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(gainMapGamma[i], &gainMap->gainMapGamma[i]));
AVIF_CHECK(avifDoubleToSignedFraction(baseOffset[i], &gainMap->baseOffset[i]));
AVIF_CHECK(avifDoubleToSignedFraction(alternateOffset[i], &gainMap->alternateOffset[i]));
}
AVIF_CHECK(avifDoubleToUnsignedFraction(baseHdrHeadroom, &gainMap->baseHdrHeadroom));
AVIF_CHECK(avifDoubleToUnsignedFraction(alternateHdrHeadroom, &gainMap->alternateHdrHeadroom));
// Not in the XMP metadata but both color spaces should be the same so this value doesn't matter.
gainMap->useBaseColorSpace = AVIF_TRUE;
return AVIF_TRUE;
}
// Parses gain map metadata from an XMP payload.
// Returns AVIF_TRUE if the gain map metadata was successfully read.
avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap, avifBool * isAppleGainMap)
{
xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, /*options=*/0);
if (document == NULL) {
return AVIF_FALSE; // Probably an out of memory error.
}
xmlNode * rootNode = xmlDocGetRootElement(document);
const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, gainMap, isAppleGainMap);
xmlFreeDoc(document);
return res;
}
// Parses an MPF (Multi-Picture File) JPEG metadata segment to find the location of other
// images, and decodes the gain map image (as determined by having gain map XMP metadata) into 'avif'.
// See CIPA DC-007-Translation-2021 Multi-Picture Format at https://www.cipa.jp/e/std/std-sec.html,
// (in particular Figures 1 to 6) and https://developer.android.com/media/platform/hdr-image-format.
// Returns AVIF_FALSE if no gain map was found.
static avifBool avifJPEGExtractGainMapImageFromMpf(FILE * f,
uint32_t sizeLimit,
const avifROData * segmentData,
avifImage * avif,
avifChromaDownsampling chromaDownsampling)
{
size_t offset = 0;
const uint8_t littleEndian[4] = { 0x49, 0x49, 0x2A, 0x00 }; // "II*\0"
const uint8_t bigEndian[4] = { 0x4D, 0x4D, 0x00, 0x2A }; // "MM\0*"
uint8_t endiannessTag[4];
AVIF_CHECK(avifJPEGReadBytes(segmentData, endiannessTag, &offset, 4));
avifBool isBigEndian;
if (!memcmp(endiannessTag, bigEndian, 4)) {
isBigEndian = AVIF_TRUE;
} else if (!memcmp(endiannessTag, littleEndian, 4)) {
isBigEndian = AVIF_FALSE;
} else {
return AVIF_FALSE; // Invalid endianness tag.
}
uint32_t offsetToFirstIfd;
AVIF_CHECK(avifJPEGReadU32(segmentData, &offsetToFirstIfd, &offset, isBigEndian));
if (offsetToFirstIfd < offset) {
return AVIF_FALSE;
}
offset = offsetToFirstIfd;
// Read MP (Multi-Picture) tags.
uint16_t mpTagCount;
AVIF_CHECK(avifJPEGReadU16(segmentData, &mpTagCount, &offset, isBigEndian));
// See also https://www.media.mit.edu/pia/Research/deepview/exif.html
uint32_t numImages = 0;
uint32_t mpEntryOffset = 0;
for (int mpTagIdx = 0; mpTagIdx < mpTagCount; ++mpTagIdx) {
uint16_t tagId;
AVIF_CHECK(avifJPEGReadU16(segmentData, &tagId, &offset, isBigEndian));
if (UINT32_MAX - offset < 2 + 4) {
return AVIF_FALSE;
}
offset += 2; // Skip data format.
offset += 4; // Skip num components.
uint8_t valueBytes[4];
AVIF_CHECK(avifJPEGReadBytes(segmentData, valueBytes, &offset, 4));
const uint32_t value = isBigEndian ? avifJPEGReadUint32BigEndian(valueBytes) : avifJPEGReadUint32LittleEndian(valueBytes);
switch (tagId) { // MPFVersion
case 45056: // MPFVersion
if (memcmp(valueBytes, "0100", 4)) {
// Unexpected version.
return AVIF_FALSE;
}
break;
case 45057: // NumberOfImages
numImages = value;
break;
case 45058: // MPEntry
mpEntryOffset = value;
break;
case 45059: // ImageUIDList, unused
case 45060: // TotalFrames, unused
default:
break;
}
}
if (numImages < 2 || mpEntryOffset < offset) {
return AVIF_FALSE;
}
offset = mpEntryOffset;
uint32_t mpfSegmentOffset;
AVIF_CHECK(avifJPEGFindMpfSegmentOffset(f, &mpfSegmentOffset));
for (uint32_t imageIdx = 0; imageIdx < numImages; ++imageIdx) {
if (UINT32_MAX - offset < 4) {
return AVIF_FALSE;
}
offset += 4; // Skip "Individual Image Attribute"
uint32_t imageSize;
AVIF_CHECK(avifJPEGReadU32(segmentData, &imageSize, &offset, isBigEndian));
uint32_t imageDataOffset;
AVIF_CHECK(avifJPEGReadU32(segmentData, &imageDataOffset, &offset, isBigEndian));
if (UINT32_MAX - offset < 4) {
return AVIF_FALSE;
}
offset += 4; // Skip "Dependent image Entry Number" (2 + 2 bytes)
if (imageDataOffset == 0) {
// 0 is a special value which indicates the first image.
// Assume the first image cannot be the gain map and skip it.
continue;
}
// Offsets are relative to the start of the MPF segment. Make them absolute.
imageDataOffset += mpfSegmentOffset;
if (fseek(f, imageDataOffset, SEEK_SET) != 0) {
return AVIF_FALSE;
}
// Read the image and check its XMP to see if it's a gain map.
// NOTE we decode all additional images until a gain map is found, even if some might not
// be gain maps. This could be fixed by having a helper function to get just the XMP without
// decoding the whole image.
if (!avifJPEGReadInternal(f,
"gain map",
avif,
/*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE, // automatic
/*requestedDepth=*/0, // automatic
chromaDownsampling,
/*ignoreColorProfile=*/AVIF_TRUE,
/*ignoreExif=*/AVIF_TRUE,
/*ignoreXMP=*/AVIF_FALSE,
/*ignoreGainMap=*/AVIF_TRUE,
sizeLimit)) {
continue;
}
if (avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size, NULL)) {
return AVIF_TRUE;
}
}
return AVIF_FALSE;
}
// Returns AVIF_TRUE if the file contains a Multi Picture Format segment.
static avifBool hasMpfSegment(struct jpeg_decompress_struct * cinfo)
{
const avifROData tagMpf = { (const uint8_t *)AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH };
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != NULL; marker = marker->next) {
if ((marker->marker == (JPEG_APP0 + 2)) && (marker->data_length > tagMpf.size) &&
!memcmp(marker->data, tagMpf.data, tagMpf.size)) {
return AVIF_TRUE;
}
}
return AVIF_FALSE;
}
// Tries to find and decode a gain map image and its metadata.
// Looks for an MPF (Multi-Picture Format) segment then loops through the linked images to see
// if one of them has gain map XMP metadata.
// See CIPA DC-007-Translation-2021 Multi-Picture Format at https://www.cipa.jp/e/std/std-sec.html
// and https://developer.android.com/media/platform/hdr-image-format
// Returns AVIF_TRUE if a gain map was found.
static avifBool avifJPEGExtractGainMapImage(FILE * f,
uint32_t sizeLimit,
struct jpeg_decompress_struct * cinfo,
avifImage * baseImage,
avifGainMap * gainMap,
avifChromaDownsampling chromaDownsampling,
avifBool expectIsoGainMap)
{
const avifROData tagMpf = { (const uint8_t *)AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH };
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != NULL; marker = marker->next) {
// Note we assume there is only one MPF segment and only look at the first one.
// Otherwise avifJPEGFindMpfSegmentOffset() would have to be modified to take the index of the
// MPF segment whose offset to return.
if ((marker->marker == (JPEG_APP0 + 2)) && (marker->data_length > tagMpf.size) &&
!memcmp(marker->data, tagMpf.data, tagMpf.size)) {
avifImage * image = avifImageCreateEmpty();
// Set jpeg native matrix coefficients to allow copying YUV values directly.
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
assert(avifJPEGHasCompatibleMatrixCoefficients(image->matrixCoefficients));
const avifROData mpfData = { (const uint8_t *)marker->data + tagMpf.size, marker->data_length - tagMpf.size };
if (!avifJPEGExtractGainMapImageFromMpf(f, sizeLimit, &mpfData, image, chromaDownsampling)) {
if (f == stdin) {
// Not supported because fseek doesn't work on stdin.
fprintf(stderr, "Warning: gain map transcoding is not supported with sdtin\n");
} else if (expectIsoGainMap) {
fprintf(stderr, "Note: XMP metadata indicated the presence of a gain map, but it could not be found or decoded\n");
}
avifImageDestroy(image);
return AVIF_FALSE;
}
avifBool isAppleGainMap;
if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, gainMap, &isAppleGainMap)) {
fprintf(stderr, "Warning: failed to parse gain map XMP metadata\n");
avifImageDestroy(image);
return AVIF_FALSE;
}
if (isAppleGainMap && gainMap->alternateHdrHeadroom.n == 0) {
// Look for the headroom in the Exif metadata if it wasn't in the XMP.
// Newer images have it in the XMP, but for older versions it's only in Exif.
const avifROData exif = { baseImage->exif.data, baseImage->exif.size };
double headroom;
if (baseImage->exif.size == 0 || !avifGetExifAppleHeadroom(&exif, &headroom) || headroom <= 0.0 ||
!avifDoubleToUnsignedFraction(headroom, &gainMap->alternateHdrHeadroom) ||
!avifDoubleToSignedFraction(headroom, &gainMap->gainMapMax[0])) {
fprintf(stderr, "Warning: could not find headroom in Exif or XMP metadata\n");
avifImageDestroy(image);
return AVIF_FALSE;
}
gainMap->gainMapMax[1] = gainMap->gainMapMax[0];
gainMap->gainMapMax[2] = gainMap->gainMapMax[0];
}
gainMap->image = image;
return AVIF_TRUE;
}
}
return AVIF_FALSE;
}
// Merges the standard XMP data with the extended XMP data.
// Returns AVIF_FALSE if an error occurred.
static avifBool avifJPEGMergeXMP(const uint8_t * standardXMPData,
uint32_t standardXMPSize,
const avifRWData extendedXMP,
avifBool foundAlternativeXMPNote,
avifRWData * xmp)
{
// Initialize the XMP RDF.
avifBool isValid = AVIF_TRUE;
xmlDoc * extendedXMPDoc = NULL;
xmlChar * xmlBuff = NULL;
xmlDoc * xmpDoc = xmlReadMemory((const char *)standardXMPData, (int)standardXMPSize, "standard.xml", NULL, /*options=*/0);
xmlNode * xmpRdf = (xmlNode *)avifJPEGFindXMLNodeByName(xmlDocGetRootElement(xmpDoc),
XML_NAME_SPACE_RDF,
"RDF",
/*recursive=*/AVIF_TRUE);
if (!xmpRdf) {
fprintf(stderr, "XMP extraction failed: cannot find RDF node\n");
isValid = AVIF_FALSE;
goto cleanup_xml;
}
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "A JPEG reader must [...] remove the xmpNote:HasExtendedXMP property."
avifBool foundHasExtendedXMP = AVIF_FALSE;
xmlNode * descNode = xmpRdf->children;
while (!foundHasExtendedXMP && descNode != NULL) {
if (descNode->type == XML_ELEMENT_NODE && descNode->ns != NULL &&
xmlStrcmp(descNode->ns->href, (const xmlChar *)XML_NAME_SPACE_RDF) == 0 &&
xmlStrcmp(descNode->name, (const xmlChar *)"Description") == 0) {
// Remove the HasExtendedXMP property.
if (foundAlternativeXMPNote) {
xmlNodePtr cur = descNode->children;
while (cur != NULL) {
if (cur->type == XML_ELEMENT_NODE && cur->ns != NULL && xmlStrcmp(cur->name, (const xmlChar *)"HasExtendedXMP") == 0 &&
xmlStrcmp(cur->ns->href, (const xmlChar *)XML_NAME_SPACE_XMP_NOTE) == 0) {
// We must Unlink and Free the node.
xmlUnlinkNode(cur);
xmlFreeNode(cur);
foundHasExtendedXMP = AVIF_TRUE;
break;
}
cur = cur->next;
}
} else {
xmlAttrPtr attr = xmlHasNsProp(descNode, (const xmlChar *)"HasExtendedXMP", (const xmlChar *)XML_NAME_SPACE_XMP_NOTE);
if (attr) {
xmlRemoveProp(attr);
foundHasExtendedXMP = AVIF_TRUE;
break;
}
}
}
// Check next sibling in case there are multiple Descriptions.
descNode = descNode->next;
}
if (!foundHasExtendedXMP) {
fprintf(stderr, "XMP extraction failed: cannot find HasExtendedXMP property\n");
isValid = AVIF_FALSE;
goto cleanup_xml;
}
// Read the extended XMP.
extendedXMPDoc = xmlReadMemory((const char *)extendedXMP.data,
(int)extendedXMP.size,
"extended.xml",
NULL,
/*options=*/0);
const xmlNode * extendedXMPRdf = avifJPEGFindXMLNodeByName(xmlDocGetRootElement(extendedXMPDoc),
XML_NAME_SPACE_RDF,
"RDF",
/*recursive=*/AVIF_TRUE);
if (!extendedXMPRdf) {
fprintf(stderr, "XMP extraction failed: invalid standard XMP segment\n");
isValid = AVIF_FALSE;
goto cleanup_xml;
}
// Copy the extended nodes over.
xmlNode * cur = extendedXMPRdf->xmlChildrenNode;
while (cur != NULL) {
// Copy the child.
xmlNode * childCopy = xmlDocCopyNode(cur, xmpDoc, 1);
xmlAddChild(xmpRdf, childCopy);
cur = cur->next;
}
// Dump the new XMP to avif->xmp.
int buffer_size;
xmlDocDumpFormatMemory(xmpDoc, &xmlBuff, &buffer_size, 1);
if (xmlBuff == NULL) {
fprintf(stderr, "Error: Could not dump XML to memory.\n");
isValid = AVIF_FALSE;
goto cleanup_xml;
}
avifRWDataFree(xmp);
if (avifRWDataRealloc(xmp, (size_t)buffer_size) != AVIF_RESULT_OK) {
fprintf(stderr, "XMP copy failed: out of memory\n");
isValid = AVIF_FALSE;
goto cleanup_xml;
}
memcpy(xmp->data, xmlBuff, buffer_size);
cleanup_xml:
xmlFreeDoc(xmpDoc);
xmlFreeDoc(extendedXMPDoc);
xmlFree(xmlBuff);
return isValid;
}
#endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION
// Note on setjmp() and volatile variables:
//
// K & R, The C Programming Language 2nd Ed, p. 254 says:
// ... Accessible objects have the values they had when longjmp was called,
// except that non-volatile automatic variables in the function calling setjmp
// become undefined if they were changed after the setjmp call.
//
// Therefore, 'iccData' is declared as volatile. 'rgb' should be declared as
// volatile, but doing so would be inconvenient (try it) and since it is a
// struct, the compiler is unlikely to put it in a register. 'ret' does not need
// to be declared as volatile because it is not modified between setjmp and
// longjmp. But GCC's -Wclobbered warning may have trouble figuring that out, so
// we preemptively declare it as volatile.
static avifBool avifJPEGReadInternal(FILE * f,
const char * inputFilename,
avifImage * avif,
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit)
{
volatile avifBool ret = AVIF_FALSE;
uint8_t * volatile iccData = NULL;
avifRGBImage rgb;
memset(&rgb, 0, sizeof(avifRGBImage));
// Extended XMP after concatenation of all extended XMP segments.
avifRWData extendedXMP = { NULL, 0 };
// Each byte set to 0 is a missing byte. Each byte set to 1 was read and copied to totalXMP.
avifRWData extendedXMPReadBytes = { NULL, 0 };
struct my_error_mgr jerr;
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
if (setjmp(jerr.setjmp_buffer)) {
goto cleanup;
}
jpeg_create_decompress(&cinfo);
// See also https://exiftool.org/TagNames/JPEG.html for the meaning of various APP<n> segments.
if (!ignoreExif || !ignoreXMP || !ignoreGainMap) {
// Keep APP1 blocks, for Exif and XMP.
jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF);
}
if (!ignoreGainMap) {
// Keep APP2 blocks, for obtaining ICC and MPF data.
jpeg_save_markers(&cinfo, JPEG_APP0 + 2, /*length_limit=*/0xFFFF);
}
if (!ignoreColorProfile) {
setup_read_icc_profile(&cinfo);
}
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);
jpeg_calc_output_dimensions(&cinfo);
if (cinfo.output_width > sizeLimit / cinfo.output_height) {
fprintf(stderr, "Too big JPEG dimensions (%u x %u > %u px): %s\n", cinfo.output_width, cinfo.output_height, sizeLimit, inputFilename);
goto cleanup;
}
if (!ignoreColorProfile) {
uint8_t * iccDataTmp;
unsigned int iccDataLen;
if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
iccData = iccDataTmp;
const avifBool isGray = (cinfo.jpeg_color_space == JCS_GRAYSCALE);
if (!isGray && (requestedFormat == AVIF_PIXEL_FORMAT_YUV400)) {
fprintf(stderr,
"The image contains a color ICC profile which is incompatible with the requested output "
"format YUV400 (grayscale). Pass --ignore-icc to discard the ICC profile.\n");
goto cleanup;
}
if (isGray && requestedFormat != AVIF_PIXEL_FORMAT_YUV400) {
fprintf(stderr,
"The image contains a gray ICC profile which is incompatible with the requested output "
"format YUV (color). Pass --ignore-icc to discard the ICC profile.\n");
goto cleanup;
}
if (avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting ICC profile failed: %s (out of memory)\n", inputFilename);
goto cleanup;
}
}
}
avif->yuvFormat = requestedFormat; // This may be AVIF_PIXEL_FORMAT_NONE, which is "auto" to avifJPEGReadCopy()
avif->depth = requestedDepth ? requestedDepth : 8;
// JPEG doesn't have alpha. Prevent confusion.
avif->alphaPremultiplied = AVIF_FALSE;
if (avifJPEGReadCopy(avif, sizeLimit, &cinfo)) {
// JPEG pixels were successfully copied without conversion. Notify the enduser.
assert(inputFilename); // JPEG read doesn't support stdin
printf("Directly copied JPEG pixel data (no YUV conversion): %s\n", inputFilename);
} else {
// JPEG pixels could not be copied without conversion. Request (converted) RGB pixels from
// libjpeg and convert to YUV with libavif instead.
cinfo.out_color_space = JCS_RGB;
jpeg_start_decompress(&cinfo);
int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
avif->width = cinfo.output_width;
avif->height = cinfo.output_height;
if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) {
fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with JPEG because it has an even bit depth.\n");
goto cleanup;
}
if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) {
// Identity and YCgCo-R are only valid with YUV444.
avif->yuvFormat = (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY ||
avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE)
? AVIF_PIXEL_FORMAT_YUV444
: AVIF_APP_DEFAULT_PIXEL_FORMAT;
}
avif->depth = requestedDepth ? requestedDepth : 8;
if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) {
if (requestedDepth && requestedDepth != 10) {
fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it uses 2 extra bits.\n", requestedDepth);
goto cleanup;
}
avif->depth = 10;
}
avifRGBImageSetDefaults(&rgb, avif);
rgb.format = AVIF_RGB_FORMAT_RGB;
rgb.chromaDownsampling = chromaDownsampling;
rgb.depth = 8;
if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) {
fprintf(stderr, "Conversion to YUV failed: %s (out of memory)\n", inputFilename);
goto cleanup;
}
int row = 0;
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, buffer, 1);
uint8_t * pixelRow = &rgb.pixels[row * rgb.rowBytes];
memcpy(pixelRow, buffer[0], rgb.rowBytes);
++row;
}
if (avifImageRGBToYUV(avif, &rgb) != AVIF_RESULT_OK) {
fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename);
goto cleanup;
}
}
if (!ignoreExif) {
avifBool found = AVIF_FALSE;
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXIF_HEADER_LENGTH) &&
!memcmp(marker->data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH)) {
if (found) {
fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n");
goto cleanup;
}
if (marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH > sizeLimit) {
fprintf(stderr,
"Setting Exif metadata failed: Exif size is too large (%u > %u bytes): %s\n",
marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH,
sizeLimit,
inputFilename);
goto cleanup;
}
// Exif orientation, if any, is imported to avif->irot/imir, and the Exif data is saved to avif->exif.
if (avifImageSetMetadataExif(avif,
marker->data + AVIF_JPEG_EXIF_HEADER_LENGTH,
marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting Exif metadata failed: %s (out of memory)\n", inputFilename);
goto cleanup;
}
// Set the Exif orientation to 1 (no transformation).
// ISO/IEC 23000-22:2024 (MIAF), Section 7.3.10.1:
// There should be no image transformations expressed by Exif (rotation,
// mirroring, etc.) indicated in the Exif metadata, in files encoded according
// to this document.
// Do not check for errors, it's a "should" so ok to do on a best-effort basis.
// Moreover it should only fail if the Exif is marlformed or there is no orientation
// tag to begin with.
(void)avifSetExifOrientation(&avif->exif, 1);
found = AVIF_TRUE;
}
}
}
avifBool readXMP = !ignoreXMP;
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
readXMP = readXMP || !ignoreGainMap; // Gain map metadata is in XMP.
#endif
if (readXMP) {
const uint8_t * standardXMPData = NULL;
uint32_t standardXMPSize = 0; // At most 64kB as defined by Adobe XMP Specification Part 3.
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_STANDARD_XMP_TAG_LENGTH) &&
!memcmp(marker->data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH)) {
if (standardXMPData) {
fprintf(stderr, "XMP extraction failed: invalid multiple standard XMP segments\n");
goto cleanup;
}
standardXMPData = marker->data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH;
standardXMPSize = (uint32_t)(marker->data_length - AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
}
}
avifBool foundExtendedXMP = AVIF_FALSE;
uint8_t extendedXMPGUID[AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments.
for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) {
if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH) &&
!memcmp(marker->data, AVIF_JPEG_EXTENDED_XMP_TAG, AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH)) {
if (!standardXMPData) {
fprintf(stderr, "XMP extraction failed: extended XMP segment found, missing standard XMP segment\n");
goto cleanup;
}
if (marker->data_length < AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP) {
fprintf(stderr, "XMP extraction failed: truncated extended XMP segment\n");
goto cleanup;
}
const uint8_t * guid = &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH];
for (size_t c = 0; c < AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH; ++c) {
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination"
// Also allow lowercase since some cameras use lowercase. https://github.com/AOMediaCodec/libavif/issues/2755
if (!isxdigit(guid[c])) {
fprintf(stderr, "XMP extraction failed: invalid XMP segment GUID\n");
goto cleanup;
}
}
// Size of the current extended segment.
const size_t extendedXMPSize = marker->data_length - AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP;
// Expected size of the sum of all extended segments.
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "full length of the ExtendedXMP serialization as a 32-bit unsigned integer"
const uint32_t totalExtendedXMPSize =
avifJPEGReadUint32BigEndian(&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]);
// Offset in totalXMP after standardXMP.
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "offset of this portion as a 32-bit unsigned integer"
const uint32_t extendedXMPOffset = avifJPEGReadUint32BigEndian(
&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4]);
if (((uint64_t)standardXMPSize + totalExtendedXMPSize) > SIZE_MAX ||
((uint64_t)standardXMPSize + totalExtendedXMPSize) > sizeLimit) {
fprintf(stderr,
"XMP extraction failed: total XMP size is too large (%u + %u > %u bytes): %s\n",
standardXMPSize,
totalExtendedXMPSize,
sizeLimit,
inputFilename);
goto cleanup;
}
if ((extendedXMPSize == 0) || (((uint64_t)extendedXMPOffset + extendedXMPSize) > totalExtendedXMPSize)) {
fprintf(stderr, "XMP extraction failed: invalid extended XMP segment size or offset\n");
goto cleanup;
}
if (foundExtendedXMP) {
if (memcmp(guid, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH)) {
fprintf(stderr, "XMP extraction failed: extended XMP segment GUID mismatch\n");
goto cleanup;
}
if (totalExtendedXMPSize != extendedXMP.size) {
fprintf(stderr, "XMP extraction failed: extended XMP total size mismatch\n");
goto cleanup;
}
} else {
memcpy(extendedXMPGUID, guid, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
// Allocate the extended XMP and keep track of the bytes that were set.
if (avifRWDataRealloc(&extendedXMP, (size_t)totalExtendedXMPSize) != AVIF_RESULT_OK ||
avifRWDataRealloc(&extendedXMPReadBytes, totalExtendedXMPSize) != AVIF_RESULT_OK) {
fprintf(stderr, "XMP extraction failed: out of memory\n");
goto cleanup;
}
memset(extendedXMPReadBytes.data, 0, extendedXMPReadBytes.size);
foundExtendedXMP = AVIF_TRUE;
}
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "A robust JPEG reader should tolerate the marker segments in any order."
memcpy(&extendedXMP.data[extendedXMPOffset], &marker->data[AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
// Make sure no previously read data was overwritten by the current segment.
if (memchr(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize)) {
fprintf(stderr, "XMP extraction failed: overlapping extended XMP segments\n");
goto cleanup;
}
// Keep track of the bytes that were set.
memset(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize);
}
}
if (foundExtendedXMP) {
// Make sure there is no missing byte.
if (memchr(extendedXMPReadBytes.data, 0, extendedXMPReadBytes.size)) {
fprintf(stderr, "XMP extraction failed: missing extended XMP segments\n");
goto cleanup;
}
// According to Adobe XMP Specification Part 3 section 1.1.3.1:
// "A reader must incorporate only ExtendedXMP blocks whose GUID matches the value of xmpNote:HasExtendedXMP."
uint8_t xmpNote[AVIF_JPEG_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
memcpy(xmpNote, AVIF_JPEG_XMP_NOTE_TAG, AVIF_JPEG_XMP_NOTE_TAG_LENGTH);
memcpy(xmpNote + AVIF_JPEG_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
avifBool foundAlternativeXMPNote;
if (avifJPEGFindSubstr(standardXMPData, standardXMPSize, xmpNote, sizeof(xmpNote))) {
foundAlternativeXMPNote = AVIF_FALSE;
} else {
// Try the alternative before returning an error.
uint8_t alternativeXmpNote[AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH];
memcpy(alternativeXmpNote, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH);
memcpy(alternativeXmpNote + AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH);
if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, alternativeXmpNote, sizeof(alternativeXmpNote))) {
fprintf(stderr, "XMP extraction failed: standard and extended XMP GUID mismatch\n");
goto cleanup;
}
foundAlternativeXMPNote = AVIF_TRUE;
}
(void)foundAlternativeXMPNote;
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
if (!avifJPEGMergeXMP(standardXMPData, standardXMPSize, extendedXMP, foundAlternativeXMPNote, &avif->xmp)) {
goto cleanup;
}
#else
fprintf(stderr, "WARNING: must be compiled with libxml2 to copy extended XMP properly\n");
avifRWDataFree(&avif->xmp);
if (avifRWDataRealloc(&avif->xmp, (size_t)standardXMPSize + extendedXMP.size) != AVIF_RESULT_OK) {
fprintf(stderr, "XMP copy failed: out of memory\n");
goto cleanup;
}
memcpy(avif->xmp.data, standardXMPData, standardXMPSize);
memcpy(avif->xmp.data + standardXMPSize, extendedXMP.data, extendedXMP.size);
#endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION
} else if (standardXMPData) {
if (avifImageSetMetadataXMP(avif, standardXMPData, standardXMPSize) != AVIF_RESULT_OK) {
fprintf(stderr, "XMP extraction failed: out of memory\n");
goto cleanup;
}
}
avifImageFixXMP(avif); // Remove one trailing null character if any.
}
#if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
if (!ignoreGainMap && hasMpfSegment(&cinfo)) {
avifBool expectIsoGainMap = AVIF_FALSE;
avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size, &expectIsoGainMap);
avifGainMap * gainMap = avifGainMapCreate();
if (gainMap == NULL) {
fprintf(stderr, "Creating gain map failed: out of memory\n");
goto cleanup;
}
// Ignore the return value: continue even if we fail to find/parse/decode the gain map.
if (avifJPEGExtractGainMapImage(f, sizeLimit, &cinfo, avif, gainMap, chromaDownsampling, expectIsoGainMap)) {
// Since jpeg doesn't provide this metadata, assume the values are the same as the base image
// with a PQ transfer curve.
gainMap->altColorPrimaries = avif->colorPrimaries;
gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ;
gainMap->altMatrixCoefficients = avif->matrixCoefficients;
gainMap->altDepth = 8;
gainMap->altPlaneCount =
(avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && gainMap->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3;
if (avif->icc.size > 0) {
// The base image's ICC should also apply to the alternage image.
if (avifRWDataSet(&gainMap->altICC, avif->icc.data, avif->icc.size) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting gain map ICC profile failed: out of memory\n");
goto cleanup;
}
}
avif->gainMap = gainMap;
} else {
avifGainMapDestroy(gainMap);
}
}
if (avif->xmp.size > 0 && ignoreXMP) {
// Clear XMP in case we read it for something else (like gain map).
if (avifImageSetMetadataXMP(avif, NULL, 0) != AVIF_RESULT_OK) {
assert(AVIF_FALSE);
}
}
#endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION
jpeg_finish_decompress(&cinfo);
ret = AVIF_TRUE;
cleanup:
jpeg_destroy_decompress(&cinfo);
free(iccData);
avifRGBImageFreePixels(&rgb);
avifRWDataFree(&extendedXMP);
avifRWDataFree(&extendedXMPReadBytes);
return ret;
}
avifBool avifJPEGRead(const char * inputFilename,
avifImage * avif,
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit)
{
FILE * f;
if (inputFilename) {
f = fopen(inputFilename, "rb");
if (!f) {
fprintf(stderr, "Can't open JPEG file for read: %s\n", inputFilename);
return AVIF_FALSE;
}
} else {
f = stdin;
inputFilename = "(stdin)";
}
const avifBool res = avifJPEGReadInternal(f,
inputFilename,
avif,
requestedFormat,
requestedDepth,
chromaDownsampling,
ignoreColorProfile,
ignoreExif,
ignoreXMP,
ignoreGainMap,
sizeLimit);
if (f && f != stdin) {
fclose(f);
}
return res;
}
avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling)
{
avifBool ret = AVIF_FALSE;
FILE * f = NULL;
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
JSAMPROW row_pointer[1];
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
avifRGBImage rgbData;
avifRGBImageSetDefaults(&rgbData, avif);
rgbData.format = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_RGB_FORMAT_GRAY : AVIF_RGB_FORMAT_RGB;
rgbData.chromaUpsampling = chromaUpsampling;
rgbData.depth = 8;
if (avifRGBImageAllocatePixels(&rgbData) != AVIF_RESULT_OK) {
fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename);
goto cleanup;
}
if (avifImageYUVToRGB(avif, &rgbData) != AVIF_RESULT_OK) {
fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename);
goto cleanup;
}
// rgbView is a view on rgbData. avifApplyTransforms() may modify rgbData.
avifRGBImage rgbView;
avifResult transformResult = avifApplyTransforms(&rgbView, &rgbData, avif);
if (transformResult != AVIF_RESULT_OK) {
if (transformResult == AVIF_RESULT_INVALID_ARGUMENT) {
fprintf(stderr, "Warning, ignoring invalid transforms (clap/irot/imir)\n");
} else {
fprintf(stderr, "Failed to apply transforms: %s\n", avifResultToString(transformResult));
goto cleanup;
}
}
f = fopen(outputFilename, "wb");
if (!f) {
fprintf(stderr, "Can't open JPEG file for write: %s\n", outputFilename);
goto cleanup;
}
jpeg_stdio_dest(&cinfo, f);
cinfo.image_width = rgbView.width;
cinfo.image_height = rgbView.height;
const avifBool isGray = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400;
cinfo.input_components = isGray ? 1 : 3;
cinfo.in_color_space = isGray ? JCS_GRAYSCALE : JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, jpegQuality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
if (avif->icc.data && (avif->icc.size > 0)) {
// Note: jpeg_write_icc_profile() could be used instead.
write_icc_profile(&cinfo, avif->icc.data, (unsigned int)avif->icc.size);
}
if (avif->exif.data && (avif->exif.size > 0)) {
size_t exifTiffHeaderOffset;
avifResult result = avifGetExifTiffHeaderOffset(avif->exif.data, avif->exif.size, &exifTiffHeaderOffset);
if (result != AVIF_RESULT_OK) {
fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
goto cleanup;
}
avifRWData exif = { NULL, 0 };
if (avifRWDataRealloc(&exif, AVIF_JPEG_EXIF_HEADER_LENGTH + avif->exif.size - exifTiffHeaderOffset) != AVIF_RESULT_OK) {
fprintf(stderr, "Error writing JPEG metadata: out of memory\n");
goto cleanup;
}
memcpy(exif.data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH);
memcpy(exif.data + AVIF_JPEG_EXIF_HEADER_LENGTH, avif->exif.data + exifTiffHeaderOffset, avif->exif.size - exifTiffHeaderOffset);
// We already rotated the pixels if necessary in avifApplyTransforms(), so we set the orientation to 1 (no rotation, no mirror).
result = avifSetExifOrientation(&exif, 1);
if (result != AVIF_RESULT_OK) {
if (result == AVIF_RESULT_INVALID_EXIF_PAYLOAD || result == AVIF_RESULT_NOT_IMPLEMENTED) {
// Either the Exif is invalid, or it doesn't have an orientation field.
// If it's invalid, we can consider it as equivalent to not having an orientation.
// In both cases, we can ignore the error.
} else {
fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result));
avifRWDataFree(&exif);
goto cleanup;
}
}
avifROData remainingExif = { exif.data, exif.size };
while (remainingExif.size > AVIF_JPEG_MAX_MARKER_DATA_LENGTH) {
jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, AVIF_JPEG_MAX_MARKER_DATA_LENGTH);
remainingExif.data += AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
remainingExif.size -= AVIF_JPEG_MAX_MARKER_DATA_LENGTH;
}
jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, (unsigned int)remainingExif.size);
avifRWDataFree(&exif);
}
if (avif->xmp.data && (avif->xmp.size > 0)) {
// See XMP specification part 3.
if (avif->xmp.size > 65502) {
// libheif just refuses to export JPEG with long XMP, see
// https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/encoder_jpeg.cc#L227
// But libheif also ignores extended XMP at reading, so converting a JPEG with extended XMP to HEIC and back to JPEG
// works, with the extended XMP part dropped, even if it had fit into a single JPEG marker.
// In libavif the whole XMP payload is dropped if it exceeds a single JPEG marker size limit, with a warning.
// The advantage is that it keeps the whole XMP payload, including the extended part, if it fits into a single JPEG
// marker. This is acceptable because section 1.1.3.1 of XMP specification part 3 says
// "It is unusual for XMP to exceed 65502 bytes; typically, it is around 2 KB."
fprintf(stderr, "Warning writing JPEG metadata: XMP payload is too big and was dropped\n");
} else {
avifRWData xmp = { NULL, 0 };
if (avifRWDataRealloc(&xmp, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH + avif->xmp.size) != AVIF_RESULT_OK) {
fprintf(stderr, "Error writing JPEG metadata: out of memory\n");
goto cleanup;
}
memcpy(xmp.data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH);
memcpy(xmp.data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH, avif->xmp.data, avif->xmp.size);
jpeg_write_marker(&cinfo, JPEG_APP0 + 1, xmp.data, (unsigned int)xmp.size);
avifRWDataFree(&xmp);
}
}
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &rgbView.pixels[cinfo.next_scanline * rgbView.rowBytes];
(void)jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
jpeg_finish_compress(&cinfo);
ret = AVIF_TRUE;
printf("Wrote JPEG: %s\n", outputFilename);
cleanup:
if (f) {
fclose(f);
}
jpeg_destroy_compress(&cinfo);
avifRGBImageFreePixels(&rgbData);
return ret;
}
|