| // 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 <limits.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->output_width; | |
| avif->height = cinfo->output_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, | |
| avifBool headerOnly); | |
| // 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 xmlNode * avifJPEGFindXMLNodeByName(const xmlNode * parentNode, const char * nameSpace, const char * nodeName, avifBool recursive) | |
| { | |
| if (parentNode == NULL) { | |
| return NULL; | |
| } | |
| for (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) { | |
| 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/" | |
| static xmlDoc * avifJPEGReadXMLMemory(const uint8_t * data, size_t size, const char * url) | |
| { | |
| if (size > INT_MAX) { | |
| return NULL; | |
| } | |
| return xmlReadMemory((const char *)data, (int)size, url, NULL, /*options=*/0); | |
| } | |
| // 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 = avifJPEGReadXMLMemory(xmpData, xmpSize, NULL); | |
| 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((unsigned char)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 = avifJPEGReadXMLMemory(xmpData, xmpSize, NULL); | |
| 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, | |
| /*headerOnly=*/AVIF_FALSE)) { | |
| 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(); | |
| if (!image) { | |
| fprintf(stderr, "Warning: out of memory when reading gain map\n"); | |
| return AVIF_FALSE; | |
| } | |
| // 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 stdin\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 = avifJPEGReadXMLMemory(standardXMPData, standardXMPSize, "standard.xml"); | |
| if (xmpDoc == NULL) { | |
| fprintf(stderr, "XMP extraction failed: invalid standard XMP segment\n"); | |
| isValid = AVIF_FALSE; | |
| goto cleanup_xml; | |
| } | |
| xmlNode * xmpRdf = 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 = avifJPEGReadXMLMemory(extendedXMP.data, extendedXMP.size, "extended.xml"); | |
| if (extendedXMPDoc == NULL) { | |
| fprintf(stderr, "XMP extraction failed: invalid extended XMP segment\n"); | |
| isValid = AVIF_FALSE; | |
| goto cleanup_xml; | |
| } | |
| const xmlNode * extendedXMPRdf = avifJPEGFindXMLNodeByName(xmlDocGetRootElement(extendedXMPDoc), | |
| XML_NAME_SPACE_RDF, | |
| "RDF", | |
| /*recursive=*/AVIF_TRUE); | |
| if (!extendedXMPRdf) { | |
| fprintf(stderr, "XMP extraction failed: cannot find RDF node\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, | |
| avifBool headerOnly) | |
| { | |
| 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; | |
| } | |
| avif->width = cinfo.output_width; | |
| avif->height = cinfo.output_height; | |
| 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 (headerOnly) { | |
| // No real decoding needed. Stop here. | |
| ret = AVIF_TRUE; | |
| goto cleanup; | |
| } | |
| 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); | |
| 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, | |
| /*headerOnly=*/AVIF_FALSE); | |
| if (f && f != stdin) { | |
| fclose(f); | |
| } | |
| return res; | |
| } | |
| avifBool avifJPEGPeek(const char * inputFilename, avifImage * avif) | |
| { | |
| FILE * f = fopen(inputFilename, "rb"); | |
| if (!f) { | |
| fprintf(stderr, "Can't open JPEG file for read: %s\n", inputFilename); | |
| return AVIF_FALSE; | |
| } | |
| const avifBool res = avifJPEGReadInternal(f, | |
| inputFilename, | |
| avif, | |
| AVIF_PIXEL_FORMAT_NONE, | |
| /*requestedDepth=*/0, | |
| AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, | |
| /*ignoreColorProfile=*/AVIF_TRUE, | |
| /*ignoreExif=*/AVIF_TRUE, | |
| /*ignoreXMP=*/AVIF_TRUE, | |
| /*ignoreGainMap=*/AVIF_TRUE, | |
| /*sizeLimit=*/UINT32_MAX, | |
| /*headerOnly=*/AVIF_TRUE); | |
| 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; | |
| } |