#include "avifjpeg.h"
#include "avifexif.h"
#include "avifutil.h"
#include <assert.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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
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);
int readLines = 0;
int linesPerCall[3] = { 0, 0, 0 };
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;
}
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 {
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;
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:
return AVIF_TRUE;
}
return AVIF_FALSE;
}
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) {
if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients)) {
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) {
avif->yuvFormat = jpegFormat;
}
if (avif->yuvFormat == jpegFormat) {
cinfo->out_color_space = JCS_YCbCr;
return avifJPEGCopyPixels(avif, cinfo);
}
}
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) {
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)) {
if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients)) {
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);
}
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;
}
}
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) {
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);
}
}
return AVIF_FALSE;
}
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);
}
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
#define AVIF_JPEG_EXIF_HEADER "Exif\0\0"
#define AVIF_JPEG_EXIF_HEADER_LENGTH 6
#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_JPEG_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\""
#define AVIF_JPEG_XMP_NOTE_TAG_LENGTH 24
#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
#define AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP (AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4 + 4)
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));
avifRWData totalXMP = { NULL, 0 };
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, 0xFFFF); }
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; avif->depth = requestedDepth ? requestedDepth : 8;
avif->alphaPremultiplied = AVIF_FALSE;
if (avifJPEGReadCopy(avif, &cinfo)) {
assert(inputFilename); printf("Directly copied JPEG pixel data (no YUV conversion): %s\n", inputFilename);
} else {
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) {
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) {
fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n");
goto cleanup;
}
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; 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]; 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) {
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;
}
}
const size_t extendedXMPSize = marker->data_length - AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP;
const uint32_t totalExtendedXMPSize =
avifJPEGReadUint32BigEndian(&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]);
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);
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;
}
memcpy(&totalXMP.data[standardXMPSize + extendedXMPOffset], &marker->data[AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize);
if (memchr(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize)) {
fprintf(stderr, "XMP extraction failed: overlapping extended XMP segments\n");
goto cleanup;
}
memset(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize);
}
}
if (foundExtendedXMP) {
if (memchr(extendedXMPReadBytes.data, 0, extendedXMPReadBytes.size)) {
fprintf(stderr, "XMP extraction failed: missing extended XMP segments\n");
goto cleanup;
}
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))) {
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;
}
}
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); }
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)) {
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);
const uint8_t orientation = avifImageGetExifOrientationFromIrotImir(avif);
result = avifSetExifOrientation(&exif, orientation);
if (result != AVIF_RESULT_OK) {
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) {
}
if (avif->xmp.data && (avif->xmp.size > 0)) {
if (avif->xmp.size > 65502) {
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;
}