blob: e5cd249d88df021d1a1e1c1ee50d4a63f9178618 [file] [log] [blame]
// Copyright 2020 Joe Drago. All rights reserved.
// SPDX-License-Identifier: BSD-2-Clause
#include "avifjpeg.h"
#include "avifexif.h"
#include "avifutil.h"
#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jpeglib.h"
#include "iccjpeg.h"
#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION)
#include <libxml/parser.h>
#endif
#define AVIF_MIN(a, b) (((a) < (b)) ? (a) : (b))
#define AVIF_MAX(a, b) (((a) > (b)) ? (a) : (b))
struct my_error_mgr
{
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
typedef struct my_error_mgr * my_error_ptr;
static void my_error_exit(j_common_ptr cinfo)
{
my_error_ptr myerr = (my_error_ptr)cinfo->err;
(*cinfo->err->output_message)(cinfo);
longjmp(myerr->setjmp_buffer, 1);
}
#if JPEG_LIB_VERSION >= 70
#define AVIF_LIBJPEG_DCT_v_scaled_size DCT_v_scaled_size
#define AVIF_LIBJPEG_DCT_h_scaled_size DCT_h_scaled_size
#else
#define AVIF_LIBJPEG_DCT_h_scaled_size DCT_scaled_size
#define AVIF_LIBJPEG_DCT_v_scaled_size DCT_scaled_size
#endif
// An internal function used by avifJPEGReadCopy(), this is the shared libjpeg decompression code
// for all paths avifJPEGReadCopy() takes.
static avifBool avifJPEGCopyPixels(avifImage * avif, uint32_t sizeLimit, struct jpeg_decompress_struct * cinfo)
{
cinfo->raw_data_out = TRUE;
jpeg_start_decompress(cinfo);
avif->width = cinfo->image_width;
avif->height = cinfo->image_height;
if ((uint32_t)avif->width > sizeLimit / (uint32_t)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)) {
// 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
// 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_EXPERIMENTAL_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 ((uint32_t)src[0] << 8) | ((uint32_t)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 ((uint32_t)src[0] << 0) | ((uint32_t)src[1] << 8);
}
// Reads 'numBytes' at 'offset', stores them in 'bytes' and increases 'offset'.
static avifBool avifJPEGReadBytes(const avifROData * data, uint8_t * bytes, uint32_t * offset, uint32_t numBytes)
{
if (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, uint32_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 avifJPEGReadU16(const avifROData * data, uint16_t * v, uint32_t * offset, avifBool isBigEndian)
{
uint8_t bytes[2];
AVIF_CHECK(avifJPEGReadBytes(data, bytes, offset, 2));
*v = isBigEndian ? avifJPEGReadUint16BigEndian(bytes) : avifJPEGReadUint16LittleEndian(bytes);
return AVIF_TRUE;
}
static avifBool avifJPEGReadInternal(FILE * f,
const char * inputFilename,
avifImage * avif,
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit);
// Arbitrary max number of jpeg segments to parse before giving up.
#define MAX_JPEG_SEGMENTS 100
// Finds the offset of the first MPF segment. Returns AVIF_TRUE if it was found.
static avifBool avifJPEGFindMpfSegmentOffset(FILE * f, uint32_t * mpfOffset)
{
const long oldOffset = ftell(f);
uint32_t offset = 2; // Skip the 2 byte SOI (Start Of Image) marker.
if (fseek(f, offset, SEEK_SET) != 0) {
return AVIF_FALSE;
}
uint8_t buffer[4];
int numSegments = 0;
while (numSegments < MAX_JPEG_SEGMENTS) {
++numSegments;
// Read the APP<n> segment marker (2 bytes) and the segment size (2 bytes).
if (fread(buffer, 1, 4, f) != 4) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // End of the file reached.
}
offset += 4;
// Total APP<n> segment byte count, including the byte count value (2 bytes), but excluding the 2 byte APP<n> marker itself.
const uint16_t segmentLength = avifJPEGReadUint16BigEndian(&buffer[2]);
if (segmentLength < 2) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // Invalid length.
} else if (segmentLength < 2 + AVIF_JPEG_MPF_HEADER_LENGTH) {
// Cannot be an MPF segment, skip to the next segment.
offset += segmentLength - 2;
if (fseek(f, offset, SEEK_SET) != 0) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE;
}
continue;
}
uint8_t identifier[AVIF_JPEG_MPF_HEADER_LENGTH];
if (fread(identifier, 1, AVIF_JPEG_MPF_HEADER_LENGTH, f) != AVIF_JPEG_MPF_HEADER_LENGTH) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE; // End of the file reached.
}
offset += AVIF_JPEG_MPF_HEADER_LENGTH;
if (buffer[1] == (JPEG_APP0 + 2) && !memcmp(identifier, AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH)) {
// MPF segment found.
*mpfOffset = offset;
fseek(f, oldOffset, SEEK_SET);
return AVIF_TRUE;
}
// Skip to the next segment.
offset += segmentLength - 2 - AVIF_JPEG_MPF_HEADER_LENGTH;
if (fseek(f, offset, SEEK_SET) != 0) {
fseek(f, oldOffset, SEEK_SET);
return AVIF_FALSE;
}
}
return AVIF_FALSE;
}
// Searches for a node called 'nameSpace:nodeName' in the children (or descendants if 'recursive' is set) of 'parentNode'.
// Returns the first such node found (in depth first search). Returns NULL if no such node is found.
static const xmlNode * avifJPEGFindXMLNodeByName(const xmlNode * parentNode, const char * nameSpace, const char * nodeName, avifBool recursive)
{
if (parentNode == NULL) {
return NULL;
}
for (const xmlNode * node = parentNode->children; node != NULL; node = node->next) {
if (node->ns != NULL && !xmlStrcmp(node->ns->href, (const xmlChar *)nameSpace) &&
!xmlStrcmp(node->name, (const xmlChar *)nodeName)) {
return node;
} else if (recursive) {
const xmlNode * descendantNode = avifJPEGFindXMLNodeByName(node, nameSpace, nodeName, recursive);
if (descendantNode != NULL) {
return descendantNode;
}
}
}
return NULL;
}
#define XML_NAME_SPACE_GAIN_MAP "http://ns.adobe.com/hdr-gain-map/1.0/"
#define XML_NAME_SPACE_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
// Finds an 'rdf:Description' node containing a gain map version attribute (hdrgm:Version="1.0").
// Returns NULL if not found.
static const xmlNode * avifJPEGFindGainMapXMPNode(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;
}
// Use XML_PARSE_RECOVER and XML_PARSE_NOERROR to avoid failing/printing errors for invalid XML.
// In particular, if the jpeg files contains extended XMP, avifJPEGReadInternal simply concatenates it to
// standard XMP, which is not a valid XML tree.
// TODO(maryla): better handle extended XMP. If the gain map metadata is in the extended part,
// the current code won't detect it.
#define LIBXML2_XML_PARSING_FLAGS (XML_PARSE_RECOVER | XML_PARSE_NOERROR)
// Returns true if there is an 'rdf:Description' node containing a gain map version attribute (hdrgm:Version="1.0").
// On the main image, this signals that the file also contains a gain map.
// On a subsequent image, this signals that it is a gain map.
static avifBool avifJPEGHasGainMapXMPNode(const uint8_t * xmpData, size_t xmpSize)
{
xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, LIBXML2_XML_PARSING_FLAGS);
if (document == NULL) {
return AVIF_FALSE; // Probably and out of memory error.
}
const xmlNode * rootNode = xmlDocGetRootElement(document);
const xmlNode * node = avifJPEGFindGainMapXMPNode(rootNode);
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)
{
*numValues = 0;
// Search attributes.
for (xmlAttr * prop = descriptionNode->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 *)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 *)XML_NAME_SPACE_GAIN_MAP) &&
!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)
{
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)) {
return AVIF_TRUE; // Property was not found, but it's not an error since they're optional.
}
if (numValues != 1 && numValues != numDoubles) {
return AVIF_FALSE; // Invalid, we expect either 1 or exactly numDoubles values.
}
for (uint32_t i = 0; i < numDoubles; ++i) {
if (i >= numValues) {
// If there is only 1 value, it's copied into the rest of the array.
values[i] = values[i - 1];
} else {
int charsRead;
if (sscanf(textValues[i], "%lf%n", &values[i], &charsRead) < 1) {
return AVIF_FALSE; // Was not able to parse the full string value as a double.
}
// Make sure that remaining characters (if any) are only whitespace.
const int len = (int)strlen(textValues[i]);
while (charsRead < len) {
if (!isspace(textValues[i][charsRead])) {
return AVIF_FALSE; // Invalid character.
}
++charsRead;
}
}
}
return AVIF_TRUE;
}
static inline void SwapDoubles(double * x, double * y)
{
double tmp = *x;
*x = *y;
*y = tmp;
}
// Parses gain map metadata from XMP.
// See https://helpx.adobe.com/camera-raw/using/gain-map.html
// Returns AVIF_TRUE if the gain map metadata was successfully read.
static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMapMetadata * metadata)
{
const xmlNode * descNode = avifJPEGFindGainMapXMPNode(rootNode);
if (descNode == NULL) {
return AVIF_FALSE;
}
avifGainMapMetadataDouble metadataDouble;
// Set default values from Adobe's spec.
metadataDouble.backwardDirection = AVIF_FALSE;
metadataDouble.baseHdrHeadroom = 0.0;
metadataDouble.alternateHdrHeadroom = 1.0;
for (int i = 0; i < 3; ++i) {
metadataDouble.gainMapMin[i] = 0.0;
metadataDouble.gainMapMax[i] = 1.0;
metadataDouble.baseOffset[i] = 1.0 / 64.0;
metadataDouble.alternateOffset[i] = 1.0 / 64.0;
metadataDouble.gainMapGamma[i] = 1.0;
}
// Not in Adobe's spec but both color spaces should be the same so this value doesn't matter.
metadataDouble.useBaseColorSpace = AVIF_TRUE;
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &metadataDouble.baseHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &metadataDouble.alternateHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", metadataDouble.baseOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", metadataDouble.alternateOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", metadataDouble.gainMapMin, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", metadataDouble.gainMapMax, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", metadataDouble.gainMapGamma, /*numDoubles=*/3));
// See inequality requirements in section 'XMP Representation of Gain Map Metadata' of Adobe's gain map specification
// https://helpx.adobe.com/camera-raw/using/gain-map.html
AVIF_CHECK(metadataDouble.alternateHdrHeadroom > metadataDouble.baseHdrHeadroom);
AVIF_CHECK(metadataDouble.baseHdrHeadroom >= 0);
for (int i = 0; i < 3; ++i) {
AVIF_CHECK(metadataDouble.gainMapMax[i] >= metadataDouble.gainMapMin[i]);
AVIF_CHECK(metadataDouble.baseOffset[i] >= 0.0);
AVIF_CHECK(metadataDouble.alternateOffset[i] >= 0.0);
AVIF_CHECK(metadataDouble.gainMapGamma[i] > 0.0);
}
uint32_t numValues;
const char * baseRenditionIsHDR;
if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues)) {
if (!strcmp(baseRenditionIsHDR, "True")) {
metadataDouble.backwardDirection = AVIF_TRUE;
SwapDoubles(&metadataDouble.baseHdrHeadroom, &metadataDouble.alternateHdrHeadroom);
for (int c = 0; c < 3; ++c) {
SwapDoubles(&metadataDouble.baseOffset[c], &metadataDouble.alternateOffset[c]);
}
} else if (!strcmp(baseRenditionIsHDR, "False")) {
metadataDouble.backwardDirection = AVIF_FALSE;
} else {
return AVIF_FALSE; // Unexpected value.
}
}
AVIF_CHECK(avifGainMapMetadataDoubleToFractions(metadata, &metadataDouble));
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, avifGainMapMetadata * metadata)
{
xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, LIBXML2_XML_PARSING_FLAGS);
if (document == NULL) {
return AVIF_FALSE; // Probably an out of memory error.
}
xmlNode * rootNode = xmlDocGetRootElement(document);
const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, metadata);
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
// and https://helpx.adobe.com/camera-raw/using/gain-map.html in particular Figures 1 to 6.
// Returns AVIF_FALSE if no gain map was found.
static avifBool avifJPEGExtractGainMapImageFromMpf(FILE * f,
uint32_t sizeLimit,
const avifROData * segmentData,
avifImage * avif,
avifChromaDownsampling chromaDownsampling)
{
uint32_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));
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) {
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));
offset += 4; // Skip "Dependent image Entry Number" (2 + 2 bytes)
if (imageDataOffset == 0) {
// 0 is a special value which indicates the first image.
// Assume the first image cannot be the gain map and skip it.
continue;
}
// Offsets are relative to the start of the MPF segment. Make them absolute.
imageDataOffset += mpfSegmentOffset;
if (fseek(f, imageDataOffset, SEEK_SET) != 0) {
return AVIF_FALSE;
}
// Read the image and check its XMP to see if it's a gain map.
// NOTE we decode all additional images until a gain map is found, even if some might not
// be gain maps. This could be fixed by having a helper function to get just the XMP without
// decoding the whole image.
if (!avifJPEGReadInternal(f,
"gain map",
avif,
/*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE, // automatic
/*requestedDepth=*/0, // automatic
chromaDownsampling,
/*ignoreColorProfile=*/AVIF_TRUE,
/*ignoreExif=*/AVIF_TRUE,
/*ignoreXMP=*/AVIF_FALSE,
/*ignoreGainMap=*/AVIF_TRUE,
sizeLimit)) {
continue;
}
if (avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size)) {
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://helpx.adobe.com/camera-raw/using/gain-map.html
// Returns AVIF_TRUE if a gain map was found.
static avifBool avifJPEGExtractGainMapImage(FILE * f,
uint32_t sizeLimit,
struct jpeg_decompress_struct * cinfo,
avifGainMap * gainMap,
avifChromaDownsampling chromaDownsampling)
{
const avifROData tagMpf = { (const uint8_t *)AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH };
for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != NULL; marker = marker->next) {
// Note we assume there is only one MPF segment and only look at the first one.
// Otherwise avifJPEGFindMpfSegmentOffset() would have to be modified to take the index of the
// MPF segment whose offset to return.
if ((marker->marker == (JPEG_APP0 + 2)) && (marker->data_length > tagMpf.size) &&
!memcmp(marker->data, tagMpf.data, tagMpf.size)) {
avifImage * image = avifImageCreateEmpty();
// Set jpeg native matrix coefficients to allow copying YUV values directly.
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
assert(avifJPEGHasCompatibleMatrixCoefficients(image->matrixCoefficients));
const avifROData mpfData = { (const uint8_t *)marker->data + tagMpf.size, marker->data_length - tagMpf.size };
if (!avifJPEGExtractGainMapImageFromMpf(f, sizeLimit, &mpfData, image, chromaDownsampling)) {
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;
}
if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, &gainMap->metadata)) {
fprintf(stderr, "Warning: failed to parse gain map metadata\n");
avifImageDestroy(image);
return AVIF_FALSE;
}
gainMap->image = image;
return AVIF_TRUE;
}
}
return AVIF_FALSE;
}
#endif // AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION
// Note on setjmp() and volatile variables:
//
// K & R, The C Programming Language 2nd Ed, p. 254 says:
// ... Accessible objects have the values they had when longjmp was called,
// except that non-volatile automatic variables in the function calling setjmp
// become undefined if they were changed after the setjmp call.
//
// Therefore, 'iccData' is declared as volatile. 'rgb' should be declared as
// volatile, but doing so would be inconvenient (try it) and since it is a
// struct, the compiler is unlikely to put it in a register. 'ret' does not need
// to be declared as volatile because it is not modified between setjmp and
// longjmp. But GCC's -Wclobbered warning may have trouble figuring that out, so
// we preemptively declare it as volatile.
static avifBool avifJPEGReadInternal(FILE * f,
const char * inputFilename,
avifImage * avif,
avifPixelFormat requestedFormat,
uint32_t requestedDepth,
avifChromaDownsampling chromaDownsampling,
avifBool ignoreColorProfile,
avifBool ignoreExif,
avifBool ignoreXMP,
avifBool ignoreGainMap,
uint32_t sizeLimit)
{
volatile avifBool ret = AVIF_FALSE;
uint8_t * volatile iccData = NULL;
avifRGBImage rgb;
memset(&rgb, 0, sizeof(avifRGBImage));
// 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 };
struct my_error_mgr jerr;
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
if (setjmp(jerr.setjmp_buffer)) {
goto cleanup;
}
jpeg_create_decompress(&cinfo);
// See also https://exiftool.org/TagNames/JPEG.html for the meaning of various APP<n> segments.
if (!ignoreExif || !ignoreXMP || !ignoreGainMap) {
// Keep APP1 blocks, for Exif and XMP.
jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF);
}
if (!ignoreGainMap) {
// Keep APP2 blocks, for obtaining ICC and MPF data.
jpeg_save_markers(&cinfo, JPEG_APP0 + 2, /*length_limit=*/0xFFFF);
}
if (!ignoreColorProfile) {
setup_read_icc_profile(&cinfo);
}
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);
jpeg_calc_output_dimensions(&cinfo);
if (cinfo.output_width > sizeLimit / cinfo.output_height) {
fprintf(stderr, "Too big JPEG dimensions (%u x %u > %u px): %s\n", cinfo.output_width, cinfo.output_height, sizeLimit, inputFilename);
goto cleanup;
}
if (!ignoreColorProfile) {
uint8_t * iccDataTmp;
unsigned int iccDataLen;
if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) {
iccData = iccDataTmp;
if (avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen) != AVIF_RESULT_OK) {
fprintf(stderr, "Setting ICC profile failed: %s (out of memory)\n", inputFilename);
goto cleanup;
}
}
}
avif->yuvFormat = requestedFormat; // This may be AVIF_PIXEL_FORMAT_NONE, which is "auto" to avifJPEGReadCopy()
avif->depth = requestedDepth ? requestedDepth : 8;
// JPEG doesn't have alpha. Prevent confusion.
avif->alphaPremultiplied = AVIF_FALSE;
if (avifJPEGReadCopy(avif, sizeLimit, &cinfo)) {
// JPEG pixels were successfully copied without conversion. Notify the enduser.
assert(inputFilename); // JPEG read doesn't support stdin
printf("Directly copied JPEG pixel data (no YUV conversion): %s\n", inputFilename);
} else {
// JPEG pixels could not be copied without conversion. Request (converted) RGB pixels from
// libjpeg and convert to YUV with libavif instead.
cinfo.out_color_space = JCS_RGB;
jpeg_start_decompress(&cinfo);
int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
avif->width = cinfo.output_width;
avif->height = cinfo.output_height;
#if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R)
const avifBool useYCgCoR = (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE ||
avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO);
#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
#if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R)
|| useYCgCoR
#endif
)
? 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) {
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) {
// 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;
}
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 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 + 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;
}
found = AVIF_TRUE;
}
}
}
avifBool readXMP = !ignoreXMP;
#if defined(AVIF_ENABLE_EXPERIMENTAL_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"
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 ||
((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 != (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.
}
#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION)
// The primary XMP block (for the main image) must contain a node with an hdrgm:Version field if and only if a gain map is present.
if (!ignoreGainMap && avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size)) {
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, gainMap, chromaDownsampling)) {
// 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_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION
jpeg_finish_decompress(&cinfo);
ret = AVIF_TRUE;
cleanup:
jpeg_destroy_decompress(&cinfo);
free(iccData);
avifRGBImageFreePixels(&rgb);
avifRWDataFree(&totalXMP);
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 = 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,
requestedFormat,
requestedDepth,
chromaDownsampling,
ignoreColorProfile,
ignoreExif,
ignoreXMP,
ignoreGainMap,
sizeLimit);
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 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;
}