From c90b0948886072bdd5adf23950871d9185dc8cf2 Mon Sep 17 00:00:00 2001 From: maryla-uc <91276487+maryla-uc@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:13:29 +0200 Subject: [PATCH] Add support for reading jpeg gain maps and converting them to AVIF. (#1565) Needs libxml2 and AVIF_ENABLE_EXPERIMENTA_GAIN_MAP to be turned on. The parsing code has three parts: - MPF (Multi-Picture Format) metadata parsing, to find the offset(s) of the additional image(s) in the file. These offsets are relative fo the position of the MPF segment. - JPEG metadata segment parsing, to find the offset of the MPF segment, in order to make the image offsets absolute (unfortunately jpeglib does not provide this information) - XMP parsing (using libxml2) for the gain map metadata --- CHANGELOG.md | 3 + CMakeLists.txt | 39 ++ apps/avifenc.c | 21 +- apps/shared/avifjpeg.c | 663 +++++++++++++++++- apps/shared/avifjpeg.h | 8 +- apps/shared/avifutil.c | 3 +- apps/shared/avifutil.h | 5 + ext/libxml2.cmd | 14 + include/avif/gainmap.h | 2 +- tests/CMakeLists.txt | 11 + tests/data/README.md | 49 ++ .../data/paris_exif_xmp_gainmap_bigendian.jpg | Bin 0 -> 47579 bytes .../paris_exif_xmp_gainmap_littleendian.jpg | Bin 0 -> 47579 bytes tests/gtest/are_images_equal.cc | 5 +- tests/gtest/avifjpeggainmaptest.cc | 94 +++ tests/gtest/aviflosslesstest.cc | 3 +- tests/gtest/avifpng16bittest.cc | 3 +- tests/gtest/aviftest_helpers.cc | 5 +- tests/gtest/aviftest_helpers.h | 3 +- 19 files changed, 901 insertions(+), 30 deletions(-) create mode 100755 ext/libxml2.cmd create mode 100644 tests/data/paris_exif_xmp_gainmap_bigendian.jpg create mode 100644 tests/data/paris_exif_xmp_gainmap_littleendian.jpg create mode 100644 tests/gtest/avifjpeggainmaptest.cc diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8aac5619..0c4cddb94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 in the future. Files created now might not decode in a future version. This feature is off by default and must be enabled with the AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP compilation flag. +* Add experimental support for converting jpeg files with gain maps to AVIF + files with gain maps. Requires libxml2, and the AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP + compilation flag. * Add the headerFormat member of new type avifHeaderFormat to avifEncoder. * Add experimental API for reading and writing "avir"-branded AVIF files behind the compilation flag AVIF_ENABLE_EXPERIMENTAL_AVIR. diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f3d5cd29e..3559321d29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,7 @@ if(AVIF_LOCAL_ZLIBPNG) set(ZLIB_LIBRARY zlibstatic) endif() + option(AVIF_LOCAL_JPEG "Build jpeg by providing your own copy inside the ext subdir." OFF) if(AVIF_LOCAL_JPEG) add_subdirectory(ext/libjpeg) @@ -139,6 +140,7 @@ if(AVIF_LOCAL_JPEG) set(JPEG_LIBRARY jpeg PARENT_SCOPE) endif() endif() + option(AVIF_LOCAL_LIBYUV "Build libyuv by providing your own copy inside the ext subdir." OFF) if(AVIF_LOCAL_LIBYUV) set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libyuv/build/${AVIF_LIBRARY_PREFIX}yuv${AVIF_LIBRARY_SUFFIX}") @@ -188,6 +190,7 @@ if(libyuv_FOUND) set(AVIF_PLATFORM_INCLUDES ${AVIF_PLATFORM_INCLUDES} ${LIBYUV_INCLUDE_DIR}) set(AVIF_PLATFORM_LIBRARIES ${AVIF_PLATFORM_LIBRARIES} ${LIBYUV_LIBRARY}) endif(libyuv_FOUND) + option(AVIF_LOCAL_LIBSHARPYUV "Build libsharpyuv by providing your own copy inside the ext subdir." OFF) if(AVIF_LOCAL_LIBSHARPYUV) set(LIB_FILENAME "${CMAKE_CURRENT_SOURCE_DIR}/ext/libwebp/build/libsharpyuv${AVIF_LIBRARY_SUFFIX}") @@ -213,6 +216,28 @@ if(libsharpyuv_FOUND) else(libsharpyuv_FOUND) message(STATUS "libavif: libsharpyuv not found") endif(libsharpyuv_FOUND) + +option(AVIF_LOCAL_LIBXML2 "Build libxml2 by providing your own copy inside the ext subdir. \ +libxml2 is used when AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP is ON" OFF +) +if(AVIF_LOCAL_LIBXML2) + set(LIB_FILENAME + "${CMAKE_CURRENT_SOURCE_DIR}/ext/libxml2/install.libavif/lib/${AVIF_LIBRARY_PREFIX}xml2${AVIF_LIBRARY_SUFFIX}" + ) + if(NOT EXISTS "${LIB_FILENAME}") + message(FATAL_ERROR "libavif: ${LIB_FILENAME} is missing, bailing out") + endif() + if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + set(LIBXML2_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/libxml2/install.libavif/include/libxml2") + set(LIBXML2_LIBRARY ${LIB_FILENAME}) + else() + set(LIBXML2_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/libxml2/install.libavif/include/libxml2" PARENT_SCOPE) + set(LIBXML2_LIBRARY ${LIB_FILENAME} PARENT_SCOPE) + endif() + set(LIBXML2_FOUND TRUE) +else() + find_package(LibXml2 QUIET) # not required +endif() # --------------------------------------------------------------------------------------- # Enable all warnings @@ -637,6 +662,17 @@ if(AVIF_BUILD_APPS OR (AVIF_BUILD_TESTS AND AVIF_ENABLE_GTEST)) avif_apps PRIVATE $ ${PNG_PNG_INCLUDE_DIR} ${JPEG_INCLUDE_DIR} INTERFACE apps/shared ) + + if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + if(LIBXML2_FOUND) + set(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION TRUE) + add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) + target_link_libraries(avif_apps ${LIBXML2_LIBRARY}) + target_include_directories(avif_apps PRIVATE ${LIBXML2_INCLUDE_DIR}) + else() + message(STATUS "libavif: libxml2 not found; avifenc will ignore any gain map in jpeg files") + endif() + endif() endif() if(AVIF_BUILD_APPS) @@ -780,6 +816,9 @@ if(WIN32) if(AVIF_LOCAL_JPEG) avif_set_folder_safe(jpeg "ext/libjpeg") endif() + if(AVIF_LOCAL_LIBXML2) + avif_set_folder_safe(xml2 "ext/libxml2") + endif() endif() add_subdirectory(contrib) diff --git a/apps/avifenc.c b/apps/avifenc.c index 034a3dcb74..8a786dc555 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c @@ -54,6 +54,7 @@ typedef struct avifBool ignoreExif; avifBool ignoreXMP; avifBool ignoreColorProfile; + avifBool ignoreGainMap; // only relevant when compiled with AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION // This holds the output timing for image sequences. The timescale member in this struct will // become the timescale set on avifEncoder, and the duration member will be the default duration @@ -209,6 +210,10 @@ static void syntaxLong(void) printf(" --ignore-exif : If the input file contains embedded Exif metadata, ignore it (no-op if absent)\n"); printf(" --ignore-xmp : If the input file contains embedded XMP metadata, ignore it (no-op if absent)\n"); printf(" --ignore-profile,--ignore-icc : If the input file contains an embedded color profile, ignore it (no-op if absent)\n"); +#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) + printf(" --ignore-gain-map : If the input file contains an embedded gain map, ignore it (no-op if absent)\n"); + // TODO(maryla): add quality setting for the gain map. +#endif printf(" --pasp H,V : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n"); printf(" --crop CROPX,CROPY,CROPW,CROPH : Add clap property (clean aperture), but calculated from a crop rectangle\n"); printf(" --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in num/denom pairs)\n"); @@ -482,6 +487,7 @@ static avifBool avifInputReadImage(avifInput * input, avifBool ignoreExif, avifBool ignoreXMP, avifBool allowChangingCicp, + avifBool ignoreGainMap, avifImage * image, const avifInputFileSettings ** settings, uint32_t * outDepth, @@ -571,6 +577,7 @@ static avifBool avifInputReadImage(avifInput * input, ignoreExif, ignoreXMP, allowChangingCicp, + ignoreGainMap, dstImage, dstDepth, dstSourceTiming, @@ -601,6 +608,7 @@ static avifBool avifInputReadImage(avifInput * input, ignoreExif, ignoreXMP, allowChangingCicp, + ignoreGainMap, image, settings, outDepth, @@ -895,12 +903,14 @@ static avifBool avifEncodeRestOfImageSequence(avifEncoder * encoder, // Ignore ICC, Exif and XMP because only the metadata of the first frame is taken into // account by the libavif API. + // Ignore gain map as it's not supported for sequences. if (!avifInputReadImage(input, imageIndex, /*ignoreColorProfile=*/AVIF_TRUE, /*ignoreExif=*/AVIF_TRUE, /*ignoreXMP=*/AVIF_TRUE, /*allowChangingCicp=*/AVIF_FALSE, + /*ignoreGainMap=*/AVIF_TRUE, nextImage, &nextSettings, /*outDepth=*/NULL, @@ -1279,6 +1289,7 @@ int main(int argc, char * argv[]) settings.ignoreExif = AVIF_FALSE; settings.ignoreXMP = AVIF_FALSE; settings.ignoreColorProfile = AVIF_FALSE; + settings.ignoreGainMap = AVIF_FALSE; settings.cicpExplicitlySet = AVIF_FALSE; avifInputFileSettings pendingSettings; @@ -1684,6 +1695,10 @@ int main(int argc, char * argv[]) settings.ignoreXMP = AVIF_TRUE; } else if (!strcmp(arg, "--ignore-profile") || !strcmp(arg, "--ignore-icc")) { settings.ignoreColorProfile = AVIF_TRUE; +#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) + } else if (!strcmp(arg, "--ignore-gain-map")) { + settings.ignoreGainMap = AVIF_TRUE; +#endif } else if (!strcmp(arg, "--pasp")) { NEXTARG(); settings.paspCount = parseU32List(settings.paspValues, arg); @@ -2031,12 +2046,16 @@ int main(int argc, char * argv[]) uint32_t sourceDepth = 0; avifBool sourceWasRGB = AVIF_FALSE; avifAppSourceTiming firstSourceTiming; + const avifBool isImageSequence = (settings.gridDimsCount == 0) && (input.filesCount > 1); + // Gain maps are not supported for animations or layered images. + const avifBool ignoreGainMap = settings.ignoreGainMap || isImageSequence || settings.progressive; if (!avifInputReadImage(&input, /*imageIndex=*/0, settings.ignoreColorProfile, settings.ignoreExif, settings.ignoreXMP, /*allowChangingCicp=*/!settings.cicpExplicitlySet, + ignoreGainMap, image, /*settings=*/NULL, // Must use the setting for first input &sourceDepth, @@ -2276,6 +2295,7 @@ int main(int argc, char * argv[]) /*ignoreExif=*/AVIF_TRUE, /*ignoreXMP=*/AVIF_TRUE, /*allowChangingCicp=*/AVIF_FALSE, + settings.ignoreGainMap, cellImage, /*settings=*/NULL, /*outDepth=*/NULL, @@ -2327,7 +2347,6 @@ int main(int argc, char * argv[]) printf("Encoded successfully.\n"); printf(" * Color AV1 total size: %" AVIF_FMT_ZU " bytes\n", ioStats.colorOBUSize); printf(" * Alpha AV1 total size: %" AVIF_FMT_ZU " bytes\n", ioStats.alphaOBUSize); - const avifBool isImageSequence = (settings.gridDimsCount == 0) && (input.filesCount > 1); if (isImageSequence) { if (settings.repetitionCount == AVIF_REPETITION_COUNT_INFINITE) { printf(" * Repetition Count: Infinite\n"); diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c index de079a8197..d8526000f5 100644 --- a/apps/shared/avifjpeg.c +++ b/apps/shared/avifjpeg.c @@ -6,6 +6,8 @@ #include "avifutil.h" #include +#include +#include #include #include #include @@ -16,6 +18,11 @@ #include "iccjpeg.h" +#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) +#include "avif/gainmap.h" +#include +#endif + #define AVIF_MIN(a, b) (((a) < (b)) ? (a) : (b)) #define AVIF_MAX(a, b) (((a) > (b)) ? (a) : (b)) @@ -70,6 +77,7 @@ static avifBool avifJPEGCopyPixels(avifImage * avif, struct jpeg_decompress_stru 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; } @@ -225,7 +233,7 @@ static avifBool avifJPEGReadCopy(avifImage * avif, struct jpeg_decompress_struct return AVIF_FALSE; } -// Reads 4-byte unsigned integer in big-endian format from the raw bitstream src. +// 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); @@ -254,6 +262,10 @@ static const uint8_t * avifJPEGFindSubstr(const uint8_t * str, size_t strLength, #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 @@ -266,6 +278,578 @@ static const uint8_t * avifJPEGFindSubstr(const uint8_t * str, size_t strLength, // 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); + +// 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 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 segment byte count, including the byte count value (2 bytes), but excluding the 2 byte APP 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 probably 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; // Out of memory? + } + 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 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: value1value2... + 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, + avifBool log2Encoded, + 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 { + double valueD; + int charsRead; + if (sscanf(textValues[i], "%lf%n", &valueD, &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; + } + if (log2Encoded) { + valueD = exp2(valueD); + } + values[i] = valueD; + } + } + + return AVIF_TRUE; +} + +// 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.baseRenditionIsHDR = AVIF_FALSE; + metadataDouble.hdrCapacityMin = 1.0; // exp2(0) = 1 + metadataDouble.hdrCapacityMax = exp2(1); + for (int i = 0; i < 3; ++i) { + metadataDouble.gainMapMin[i] = 1.0; // exp2(0) = 1 + metadataDouble.gainMapMax[i] = exp2(1.0); + metadataDouble.offsetSdr[i] = 1.0 / 64.0; + metadataDouble.offsetHdr[i] = 1.0 / 64.0; + metadataDouble.gainMapGamma[i] = 1.0; + } + + uint32_t numValues; + const char * baseRenditionIsHDR; + if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues)) { + if (!strcmp(baseRenditionIsHDR, "True")) { + metadata->baseRenditionIsHDR = AVIF_TRUE; + } else if (!strcmp(baseRenditionIsHDR, "False")) { + metadata->baseRenditionIsHDR = AVIF_FALSE; + } else { + return AVIF_FALSE; // Unexpected value. + } + } + + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, + "HDRCapacityMin", + /*log2Encoded=*/AVIF_TRUE, + &metadataDouble.hdrCapacityMin, + /*numDoubles=*/1)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, + "HDRCapacityMax", + /*log2Encoded=*/AVIF_TRUE, + &metadataDouble.hdrCapacityMax, + /*numDoubles=*/1)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", /*log2Encoded=*/AVIF_FALSE, metadataDouble.offsetSdr, /*numDoubles=*/3)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", /*log2Encoded=*/AVIF_FALSE, metadataDouble.offsetHdr, /*numDoubles=*/3)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, + "GainMapMin", + /*log2Encoded=*/AVIF_TRUE, + metadataDouble.gainMapMin, + /*numDoubles=*/3)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, + "GainMapMax", + /*log2Encoded=*/AVIF_TRUE, + metadataDouble.gainMapMax, + /*numDoubles=*/3)); + AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, + "Gamma", + /*log2Encoded=*/AVIF_FALSE, + metadataDouble.gainMapGamma, + /*numDoubles=*/3)); + // Change gammma to 1.0/gamma (we are using a different convention from the metadata XMP). + for (int i = 0; i < 3; ++i) { + if (metadataDouble.gainMapGamma[i] == 0) { + return AVIF_FALSE; // Invalid value, the spec says that gamma should be > 0 + } + metadataDouble.gainMapGamma[i] = 1.0 / metadataDouble.gainMapGamma[i]; + } + // Should have HDRCapacityMax > HDRCapacityMin. + if (metadataDouble.hdrCapacityMax <= (double)metadataDouble.hdrCapacityMin) { + return AVIF_FALSE; + } + // Should have GainMapMax >= GainMapMin. + for (int i = 0; i < 3; ++i) { + if (metadataDouble.gainMapMax[i] < (double)metadataDouble.gainMapMin[i]) { + return AVIF_FALSE; + } + } + + 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. +static 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); + 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, + const char * inputFilename, + const avifROData * segmentData, + avifImage * avif, + avifPixelFormat requestedFormat, + uint32_t requestedDepth, + 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, + inputFilename, + avif, + requestedFormat, + requestedDepth, + chromaDownsampling, + /*ignoreColorProfile=*/AVIF_TRUE, + /*ignoreExif=*/AVIF_TRUE, + /*ignoreXMP=*/AVIF_FALSE, + /*ignoreGainMap=*/AVIF_TRUE)) { + 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, + const char * inputFilename, + struct jpeg_decompress_struct * cinfo, + avifGainMap * gainMap, + avifPixelFormat requestedFormat, + uint32_t requestedDepth, + 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(); + + const avifROData mpfData = { (const uint8_t *)marker->data + tagMpf.size, marker->data_length - tagMpf.size }; + if (!avifJPEGExtractGainMapImageFromMpf(f, inputFilename, &mpfData, image, requestedFormat, requestedDepth, 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: @@ -280,14 +864,16 @@ static const uint8_t * avifJPEGFindSubstr(const uint8_t * str, size_t strLength, // 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) +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) { volatile avifBool ret = AVIF_FALSE; uint8_t * volatile iccData = NULL; @@ -300,12 +886,6 @@ avifBool avifJPEGRead(const char * inputFilename, // 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); @@ -316,9 +896,16 @@ avifBool avifJPEGRead(const char * inputFilename, jpeg_create_decompress(&cinfo); - if (!ignoreExif || !ignoreXMP) { - jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF); // Exif/XMP + // See also https://exiftool.org/TagNames/JPEG.html for the meaning of various APP 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); } @@ -429,7 +1016,12 @@ avifBool avifJPEGRead(const char * inputFilename, } } } - if (!ignoreXMP) { + + 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) { @@ -568,11 +1160,23 @@ avifBool avifJPEGRead(const char * inputFilename, } 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)) { + // Ignore the return value: continue even if we fail to find/parse/decode the gain map. + avifJPEGExtractGainMapImage(f, inputFilename, &cinfo, &avif->gainMap, requestedFormat, requestedDepth, chromaDownsampling); + } + + if (avif->xmp.size > 0 && ignoreXMP) { + // Clear XMP in case we read it for something else (like gain map). + avifImageSetMetadataXMP(avif, NULL, 0); + } +#endif // AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION jpeg_finish_decompress(&cinfo); ret = AVIF_TRUE; cleanup: jpeg_destroy_decompress(&cinfo); - fclose(f); free(iccData); avifRGBImageFreePixels(&rgb); avifRWDataFree(&totalXMP); @@ -580,6 +1184,27 @@ avifBool avifJPEGRead(const char * inputFilename, return ret; } +avifBool avifJPEGRead(const char * inputFilename, + avifImage * avif, + avifPixelFormat requestedFormat, + uint32_t requestedDepth, + avifChromaDownsampling chromaDownsampling, + avifBool ignoreColorProfile, + avifBool ignoreExif, + avifBool ignoreXMP, + avifBool ignoreGainMap) +{ + 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); + fclose(f); + return res; +} + avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling) { avifBool ret = AVIF_FALSE; diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h index dce833b3b0..00e622b51d 100644 --- a/apps/shared/avifjpeg.h +++ b/apps/shared/avifjpeg.h @@ -10,6 +10,11 @@ extern "C" { #endif +// Decodes the jpeg file at path 'inputFilename' into 'avif'. +// 'ignoreGainMap' is only relevant for jpeg files that have a gain map +// and only if AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION is ON +// (requires AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP and libxml2). Otherwise +// it has no effect. avifBool avifJPEGRead(const char * inputFilename, avifImage * avif, avifPixelFormat requestedFormat, @@ -17,7 +22,8 @@ avifBool avifJPEGRead(const char * inputFilename, avifChromaDownsampling chromaDownsampling, avifBool ignoreColorProfile, avifBool ignoreExif, - avifBool ignoreXMP); + avifBool ignoreXMP, + avifBool ignoreGainMap); avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling); #ifdef __cplusplus diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c index 91aae6fb31..7a32fc7a28 100644 --- a/apps/shared/avifutil.c +++ b/apps/shared/avifutil.c @@ -247,6 +247,7 @@ avifAppFileFormat avifReadImage(const char * filename, avifBool ignoreExif, avifBool ignoreXMP, avifBool allowChangingCicp, + avifBool ignoreGainMap, avifImage * image, uint32_t * outDepth, avifAppSourceTiming * sourceTiming, @@ -261,7 +262,7 @@ avifAppFileFormat avifReadImage(const char * filename, *outDepth = image->depth; } } else if (format == AVIF_APP_FILE_FORMAT_JPEG) { - if (!avifJPEGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP)) { + if (!avifJPEGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP, ignoreGainMap)) { return AVIF_APP_FILE_FORMAT_UNKNOWN; } if (outDepth) { diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h index d6cf65ffa4..9a39b7367b 100644 --- a/apps/shared/avifutil.h +++ b/apps/shared/avifutil.h @@ -60,6 +60,10 @@ struct y4mFrameIterator; // Reads an image from a file with the requested format and depth. // In case of a y4m file, sourceTiming and frameIter can be set. // Returns AVIF_APP_FILE_FORMAT_UNKNOWN in case of error. +// 'ignoreGainMap' is only relevant for jpeg files that have a gain map +// and only if AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION is ON +// (requires AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP and libxml2). Otherwise +// it has no effect. avifAppFileFormat avifReadImage(const char * filename, avifPixelFormat requestedFormat, int requestedDepth, @@ -68,6 +72,7 @@ avifAppFileFormat avifReadImage(const char * filename, avifBool ignoreExif, avifBool ignoreXMP, avifBool allowChangingCicp, + avifBool ignoreGainMap, avifImage * image, uint32_t * outDepth, avifAppSourceTiming * sourceTiming, diff --git a/ext/libxml2.cmd b/ext/libxml2.cmd new file mode 100755 index 0000000000..ea39c7e3e7 --- /dev/null +++ b/ext/libxml2.cmd @@ -0,0 +1,14 @@ +: # If you want to use a local build of libxml2, you must clone the libxml2 repo in this directory first, then enable CMake's AVIF_LOCAL_LIBXML2 option. +: # The git tag below is known to work, and will occasionally be updated. Feel free to use a more recent commit. + +: # The odd choice of comment style in this file is to try to share this script between *nix and win32. + +: # libxml2 is released under the MIT License. + +git clone -b v2.11.5 --depth 1 https://gitlab.gnome.org/GNOME/libxml2.git + +mkdir -p libxml2/build.libavif +cmake libxml2 -B libxml2/build.libavif/ -G Ninja -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=libxml2/install.libavif \ + -DLIBXML2_WITH_PYTHON=OFF -DLIBXML2_WITH_ZLIB=OFF -DLIBXML2_WITH_LZMA=OFF +ninja -C libxml2/build.libavif +ninja -C libxml2/build.libavif install diff --git a/include/avif/gainmap.h b/include/avif/gainmap.h index b3b0376aee..1e7500faea 100644 --- a/include/avif/gainmap.h +++ b/include/avif/gainmap.h @@ -33,7 +33,7 @@ typedef struct avifGainMapMetadataDouble // Converts a avifGainMapMetadataDouble to avifGainMapMetadata by converting double values // to the closest uint32_t fractions. // Returns AVIF_FALSE if some field values are < 0 or > UINT32_MAX. -avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src); +AVIF_API avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src); #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 64b9f1fc69..7f13d95d8f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -134,6 +134,13 @@ if(AVIF_ENABLE_GTEST) target_link_libraries(avifgainmaptest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) target_include_directories(avifgainmaptest PRIVATE ${GTEST_INCLUDE_DIRS}) add_test(NAME avifgainmaptest COMMAND avifgainmaptest) + + if(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) + add_executable(avifjpeggainmaptest gtest/avifjpeggainmaptest.cc) + target_link_libraries(avifjpeggainmaptest aviftest_helpers ${GTEST_BOTH_LIBRARIES}) + target_include_directories(avifjpeggainmaptest PRIVATE ${GTEST_INCLUDE_DIRS}) + add_test(NAME avifjpeggainmaptest COMMAND avifjpeggainmaptest ${CMAKE_CURRENT_SOURCE_DIR}/data/) + endif() endif() add_executable(avifgridapitest gtest/avifgridapitest.cc) @@ -343,6 +350,10 @@ if(AVIF_CODEC_AVM) endif() if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) set_tests_properties(avifgainmaptest PROPERTIES DISABLED True) + + if(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION) + set_tests_properties(avifjpeggainmaptest PROPERTIES DISABLED True) + endif() endif() endif() diff --git a/tests/data/README.md b/tests/data/README.md index 10f0adc3e0..3d5b879268 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -234,6 +234,55 @@ License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LIC Source: Encoded from `paris_icc_exif_xmp.png` using `avifenc -s 10` at commit ed52c1b. +### File [paris_exif_xmp_gainmap_littleendian.jpg](paris_exif_xmp_gainmap_littleendian.jpg) + +![](paris_exif_xmp_gainmap_littleendian.jpg) + +License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE) + +Source: Based on paris_exif_xmp_icc.jpg with ICC stripped out and a gain map added. +Contains a MPF (Multi-Picture Format) segment with metadata pointing to a second image +at offset 33487. The MPF metadata is in little endian order, as signaled by the four bytes +'II*\0'. + +| address | marker | length | data | +|--------:|-------------|-------:|----------------------------------------------| +| 0 | 0xffd8 SOI | | | +| 2 | 0xffe0 APP0 | 16 | `JFIF.....,.,.` | +| 20 | 0xffe1 APP1 | 838 | `Exif..II*......................` | +| 1156 | 0xffe1 APP1 | 2808 | `http://ns.adobe.com/xap/1.0/.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 aHVSAxtbzBva^-N_#yBBKKWh8GzWq0k4Fg#K literal 0 HcmV?d00001 diff --git a/tests/data/paris_exif_xmp_gainmap_littleendian.jpg b/tests/data/paris_exif_xmp_gainmap_littleendian.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d59a03a10a4aec2d3d6499721f95dbccf20c6cf6 GIT binary patch literal 47579 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 NPDs*^+WxO^{|zh218)ET literal 0 HcmV?d00001 diff --git a/tests/gtest/are_images_equal.cc b/tests/gtest/are_images_equal.cc index 039ed248cb..8ce1ae6b26 100644 --- a/tests/gtest/are_images_equal.cc +++ b/tests/gtest/are_images_equal.cc @@ -35,8 +35,9 @@ int main(int argc, char** argv) { /*ignoreColorProfile==*/AVIF_FALSE, /*ignoreExif=*/AVIF_FALSE, /*ignoreXMP=*/AVIF_FALSE, /*allowChangingCicp=*/AVIF_TRUE, - decoded[i].get(), &depth[i], nullptr, - nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { + // TODO(maryla): also compare gain maps. + /*ignoreGainMap=*/AVIF_TRUE, decoded[i].get(), &depth[i], + nullptr, nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { std::cerr << "Image " << argv[i + 1] << " cannot be read." << std::endl; return 2; } diff --git a/tests/gtest/avifjpeggainmaptest.cc b/tests/gtest/avifjpeggainmaptest.cc new file mode 100644 index 0000000000..ce974cb2cc --- /dev/null +++ b/tests/gtest/avifjpeggainmaptest.cc @@ -0,0 +1,94 @@ +// Copyright 2023 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "gtest/gtest.h" + +namespace libavif { +namespace { + +// Used to pass the data folder path to the GoogleTest suites. +const char* data_path = nullptr; + +//------------------------------------------------------------------------------ + +TEST(JpegTest, ReadJpegWithGainMap) { + for (const char* filename : {"paris_exif_xmp_gainmap_bigendian.jpg", + "paris_exif_xmp_gainmap_littleendian.jpg"}) { + SCOPED_TRACE(filename); + + const testutil::AvifImagePtr image = + testutil::ReadImage(data_path, filename, AVIF_PIXEL_FORMAT_YUV444, 8, + AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, + /*ignore_icc=*/false, /*ignore_exif=*/false, + /*ignore_xmp=*/true, /*allow_changing_cicp=*/true, + /*ignore_gain_map=*/false); + ASSERT_NE(image, nullptr); + ASSERT_NE(image->gainMap.image, nullptr); + EXPECT_EQ(image->gainMap.image->width, 512u); + EXPECT_EQ(image->gainMap.image->height, 384u); + // Since ignore_xmp is true, there should be no XMP, even if it had to + // be read to parse the gain map. + EXPECT_EQ(image->xmp.size, 0u); + + const auto& m = image->gainMap.metadata; + EXPECT_FALSE(m.baseRenditionIsHDR); + const double kEpsilon = 1e-8; + EXPECT_NEAR(static_cast(m.hdrCapacityMinN) / m.hdrCapacityMinD, 1, + kEpsilon); + EXPECT_NEAR(static_cast(m.hdrCapacityMaxN) / m.hdrCapacityMaxD, + exp2(3.5), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMinN[0]) / m.gainMapMinD[0], + exp2(0), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMinN[1]) / m.gainMapMinD[1], + exp2(0), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMinN[2]) / m.gainMapMinD[2], + exp2(0), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMaxN[0]) / m.gainMapMaxD[0], + exp2(3.5), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMaxN[1]) / m.gainMapMaxD[1], + exp2(3.6), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapMaxN[2]) / m.gainMapMaxD[2], + exp2(3.7), kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapGammaN[0]) / m.gainMapGammaD[0], + 1.0, kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapGammaN[1]) / m.gainMapGammaD[1], + 1.0, kEpsilon); + EXPECT_NEAR(static_cast(m.gainMapGammaN[2]) / m.gainMapGammaD[2], + 1.0, kEpsilon); + } +} + +TEST(JpegTest, IgnoreGainMap) { + const testutil::AvifImagePtr image = testutil::ReadImage( + data_path, "paris_exif_xmp_gainmap_littleendian.jpg", + AVIF_PIXEL_FORMAT_YUV444, 8, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, + /*ignore_icc=*/false, /*ignore_exif=*/false, + /*ignore_xmp=*/false, /*allow_changing_cicp=*/true, + /*ignore_gain_map=*/true); + ASSERT_NE(image, nullptr); + EXPECT_EQ(image->gainMap.image, nullptr); + // Check there is xmp since ignore_xmp is false (just making sure that + // ignore_gain_map=true has no impact on this). + EXPECT_GT(image->xmp.size, 0u); +} + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace libavif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + if (argc != 2) { + std::cerr << "There must be exactly one argument containing the path to " + "the test data folder" + << std::endl; + return 1; + } + libavif::data_path = argv[1]; + return RUN_ALL_TESTS(); +} diff --git a/tests/gtest/aviflosslesstest.cc b/tests/gtest/aviflosslesstest.cc index c2faefc650..b75a5c650d 100644 --- a/tests/gtest/aviflosslesstest.cc +++ b/tests/gtest/aviflosslesstest.cc @@ -37,7 +37,8 @@ TEST(BasicTest, EncodeDecodeMatrixCoefficients) { /*requestedDepth=*/0, /*chromaDownsampling=*/AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/false, /*ignoreExif=*/false, - /*ignoreXMP=*/false, /*allowChangingCicp=*/true, image.get(), + /*ignoreXMP=*/false, /*allowChangingCicp=*/true, + /*ignoreGainMap=*/true, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr); #if defined(AVIF_ENABLE_EXPERIMENTAL_YCGCO_R) diff --git a/tests/gtest/avifpng16bittest.cc b/tests/gtest/avifpng16bittest.cc index fb5c5fc9ff..1f31018085 100644 --- a/tests/gtest/avifpng16bittest.cc +++ b/tests/gtest/avifpng16bittest.cc @@ -33,7 +33,8 @@ testutil::AvifImagePtr ReadImageLosslessBitDepth(const std::string& path, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, /*ignoreColorProfile=*/true, /*ignoreExif=*/true, /*ignoreXMP=*/true, - /*allowChangingCicp=*/true, image.get(), &output_depth, + /*allowChangingCicp=*/true, /*ignoreGainMap=*/true, + image.get(), &output_depth, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { return {nullptr, nullptr}; diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc index 6d081c30b2..ab7496c511 100644 --- a/tests/gtest/aviftest_helpers.cc +++ b/tests/gtest/aviftest_helpers.cc @@ -400,13 +400,14 @@ AvifImagePtr ReadImage(const char* folder_path, const char* file_name, avifPixelFormat requested_format, int requested_depth, avifChromaDownsampling chroma_downsampling, avifBool ignore_icc, avifBool ignore_exif, - avifBool ignore_xmp, avifBool allow_changing_cicp) { + avifBool ignore_xmp, avifBool allow_changing_cicp, + avifBool ignore_gain_map) { testutil::AvifImagePtr image(avifImageCreateEmpty(), avifImageDestroy); if (!image || avifReadImage((std::string(folder_path) + file_name).c_str(), requested_format, requested_depth, chroma_downsampling, ignore_icc, ignore_exif, ignore_xmp, allow_changing_cicp, - image.get(), + ignore_gain_map, image.get(), /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { return {nullptr, nullptr}; diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h index 91299c3f47..10c0c59746 100644 --- a/tests/gtest/aviftest_helpers.h +++ b/tests/gtest/aviftest_helpers.h @@ -119,7 +119,8 @@ AvifImagePtr ReadImage( avifChromaDownsampling chroma_downsampling = AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, avifBool ignore_icc = false, avifBool ignore_exif = false, - avifBool ignore_xmp = false, avifBool allow_changing_cicp = true); + avifBool ignore_xmp = false, avifBool allow_changing_cicp = true, + avifBool ignore_gain_map = false); // Convenient wrapper around avifPNGWrite() for debugging purposes. // Do not remove. bool WriteImage(const avifImage* image, const char* file_path);