// Copyright 2020 Joe Drago. All rights reserved. // SPDX-License-Identifier: BSD-2-Clause #include "avifjpeg.h" #include "avifexif.h" #include "avifutil.h" #include #include #include #include #include #include #include "jpeglib.h" #include "iccjpeg.h" #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, struct jpeg_decompress_struct * cinfo) { cinfo->raw_data_out = TRUE; jpeg_start_decompress(cinfo); avif->width = cinfo->image_width; avif->height = cinfo->image_height; 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]); } 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, 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, 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, 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)) { // 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, 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, 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, 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, cinfo); } } // A typical RGB->YUV conversion is required. return AVIF_FALSE; } // Reads 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 // 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 "" #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) // 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. avifBool avifJPEGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, uint32_t requestedDepth, avifChromaDownsampling chromaDownsampling, avifBool ignoreColorProfile, avifBool ignoreExif, avifBool ignoreXMP) { volatile avifBool ret = AVIF_FALSE; uint8_t * volatile iccData = NULL; avifRGBImage rgb; memset(&rgb, 0, sizeof(avifRGBImage)); // Standard XMP segment followed by all extended XMP segments. avifRWData totalXMP = { 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 }; FILE * f = fopen(inputFilename, "rb"); if (!f) { fprintf(stderr, "Can't open JPEG file for read: %s\n", inputFilename); return ret; } 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); if (!ignoreExif || !ignoreXMP) { jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF); // Exif/XMP } if (!ignoreColorProfile) { setup_read_icc_profile(&cinfo); } jpeg_stdio_src(&cinfo, f); jpeg_read_header(&cinfo, TRUE); if (!ignoreColorProfile) { uint8_t * iccDataTmp; unsigned int iccDataLen; if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) { iccData = iccDataTmp; 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, &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 defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) const avifBool useYCgCoR = (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE || avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO); #else const avifBool useYCgCoR = AVIF_FALSE; #endif if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { // Identity and YCgCo-R are only valid with YUV444. avif->yuvFormat = (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY || useYCgCoR) ? AVIF_PIXEL_FORMAT_YUV444 : AVIF_APP_DEFAULT_PIXEL_FORMAT; } avif->depth = requestedDepth ? requestedDepth : 8; #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) if (useYCgCoR) { 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 (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; } #endif 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) { const avifROData tagExif = { (const uint8_t *)AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH }; 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 > tagExif.size) && !memcmp(marker->data, tagExif.data, tagExif.size)) { if (found) { // TODO(yguyon): Implement instead of outputting an error. fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n"); goto cleanup; } // Exif orientation, if any, is imported to avif->irot/imir and kept in avif->exif. // libheif has the same behavior, see // https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/heif_enc.cc#L403 if (avifImageSetMetadataExif(avif, marker->data + tagExif.size, marker->data_length - tagExif.size) != AVIF_RESULT_OK) { fprintf(stderr, "Setting Exif metadata failed: %s (out of memory)\n", inputFilename); goto cleanup; } found = AVIF_TRUE; } } } if (!ignoreXMP) { 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" if (((guid[c] < '0') || (guid[c] > '9')) && ((guid[c] < 'A') || (guid[c] > 'F'))) { 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) { fprintf(stderr, "XMP extraction failed: total XMP size is too large\n"); 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 != (totalXMP.size - standardXMPSize)) { fprintf(stderr, "XMP extraction failed: extended XMP total size mismatch\n"); goto cleanup; } } else { memcpy(extendedXMPGUID, guid, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH); if (avifRWDataRealloc(&totalXMP, (size_t)standardXMPSize + totalExtendedXMPSize) != AVIF_RESULT_OK) { fprintf(stderr, "XMP extraction failed: out of memory\n"); goto cleanup; } memcpy(totalXMP.data, standardXMPData, standardXMPSize); // Keep track of the bytes that were set. if (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(&totalXMP.data[standardXMPSize + 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); if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, xmpNote, sizeof(xmpNote))) { // 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; } } // According to Adobe XMP Specification Part 3 section 1.1.3.1: // "A JPEG reader must [...] remove the xmpNote:HasExtendedXMP property." // This constraint is ignored here because leaving the xmpNote:HasExtendedXMP property is rather harmless // and editing XMP metadata is quite involved. avifRWDataFree(&avif->xmp); avif->xmp = totalXMP; totalXMP.data = NULL; totalXMP.size = 0; } 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. } jpeg_finish_decompress(&cinfo); ret = AVIF_TRUE; cleanup: jpeg_destroy_decompress(&cinfo); fclose(f); free(iccData); avifRGBImageFreePixels(&rgb); avifRWDataFree(&totalXMP); avifRWDataFree(&extendedXMPReadBytes); return ret; } 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 rgb; avifRGBImageSetDefaults(&rgb, avif); rgb.format = AVIF_RGB_FORMAT_RGB; rgb.chromaUpsampling = chromaUpsampling; rgb.depth = 8; if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename); goto cleanup; } if (avifImageYUVToRGB(avif, &rgb) != AVIF_RESULT_OK) { fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename); 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 = avif->width; cinfo.image_height = avif->height; cinfo.input_components = 3; cinfo.in_color_space = 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)) { // TODO(yguyon): Use jpeg_write_icc_profile() 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); // Make sure the Exif orientation matches the irot/imir values. // libheif does not have the same behavior. The orientation is applied to samples and orientation data is discarded there, // see https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/encoder_jpeg.cc#L187 const uint8_t orientation = avifImageGetExifOrientationFromIrotImir(avif); result = avifSetExifOrientation(&exif, orientation); if (result != AVIF_RESULT_OK) { // Ignore errors if the orientation is the default one because not being able to set Exif orientation now // means a reader will not be able to parse it later either. if (orientation != 1) { 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); } else if (avifImageGetExifOrientationFromIrotImir(avif) != 1) { // There is no Exif yet, but we need to store the orientation. // TODO(yguyon): Add a valid Exif payload or rotate the samples. } 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] = &rgb.pixels[cinfo.next_scanline * rgb.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(&rgb); return ret; }