diff --git a/.gitignore b/.gitignore index f7687219..3ea725a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ build* third_party/googletest third_party/turbojpeg third_party/benchmark -tests/data \ No newline at end of file +tests/data +!tests/data/ +tests/data/* +!tests/data/gainmap_hevc_16x16.heic +!tests/data/GAINMAP_FIXTURE_README.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 16928b74..b5f748e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -546,7 +546,7 @@ file(GLOB UHDR_TEST_SRCS_LIST "${TESTS_DIR}/*.cpp") file(GLOB UHDR_BM_SRCS_LIST "${BENCHMARK_DIR}/*.cpp") file(GLOB IMAGE_IO_SRCS_LIST "${THIRD_PARTY_DIR}/image_io/src/**/*.cc") -set(PRIVATE_INCLUDE_DIR ${SOURCE_DIR}/include/ ${JPEG_INCLUDE_DIRS}) +set(PRIVATE_INCLUDE_DIR ${SOURCE_DIR}/include/ ${EXPORT_INCLUDE_DIR} ${JPEG_INCLUDE_DIRS}) set(PRIVATE_LINK_LIBS ${JPEG_LIBRARIES} Threads::Threads) if(UHDR_ENABLE_SMPTE2094_50) list(APPEND PRIVATE_LINK_LIBS smpte2094_50_utils) @@ -556,11 +556,13 @@ if(UHDR_ENABLE_GLES) list(APPEND PRIVATE_LINK_LIBS ${EGL_LIBRARIES} ${OPENGLES3_LIBRARIES}) endif() if(UHDR_ENABLE_HEIF) + set(LIBHEIF_SYSTEM_INCOMPATIBLE FALSE) if(NOT UHDR_BUILD_DEPS) find_package(libheif QUIET) if(LIBHEIF_FOUND AND NOT LIBHEIF_HAS_GAIN_MAP) - message(WARNING "System libheif found but lacks ISO 21496-1 gain map API (PR 1503). " - "Switching to bundled dependency build.") + message(WARNING "System libheif found but lacks the required ISO 21496-1 gain map API. " + "HEIF/AVIF support will be disabled because UHDR_BUILD_DEPS is OFF.") + set(LIBHEIF_SYSTEM_INCOMPATIBLE TRUE) set(LIBHEIF_FOUND FALSE) endif() endif() @@ -642,14 +644,26 @@ if(UHDR_ENABLE_HEIF) INTERFACE_LINK_LIBRARIES "${CMAKE_DL_LIBS}" ) endif() + set(LIBHEIF_HAS_GAIN_MAP TRUE) + set(LIBHEIF_HAS_ITEM_API TRUE) set(LIBHEIF_FOUND TRUE) endif() if(LIBHEIF_FOUND) message(STATUS "Found libheif: ${LIBHEIF_LIBRARIES}") add_compile_options(-DUHDR_ENABLE_HEIF) + if(LIBHEIF_HAS_ITEM_API) + message(STATUS "libheif item inspection API - Available") + add_compile_options(-DUHDR_HAS_HEIF_ITEM_API) + else() + message(WARNING "libheif item inspection API unavailable. HEIF/AVIF encoding and decoding " + "remain enabled, but uhdr_is_supported_gainmap_image() returns 0 for " + "HEIF/AVIF.") + endif() list(APPEND PRIVATE_LINK_LIBS libheif::heif) else() - message(WARNING "libheif not found. Building without HEIF/AVIF support.") + if(NOT LIBHEIF_SYSTEM_INCOMPATIBLE) + message(WARNING "libheif not found. Building without HEIF/AVIF support.") + endif() set(UHDR_ENABLE_HEIF FALSE) endif() endif() diff --git a/cmake/Findlibheif.cmake b/cmake/Findlibheif.cmake index d4ff65ab..4c67f319 100644 --- a/cmake/Findlibheif.cmake +++ b/cmake/Findlibheif.cmake @@ -8,11 +8,13 @@ # Result Variables: # LIBHEIF_FOUND - True if libheif was found # LIBHEIF_HAS_GAIN_MAP - True if libheif includes ISO 21496-1 gain map APIs +# LIBHEIF_HAS_ITEM_API - True if libheif exposes item inspection APIs # LIBHEIF_VERSION - Version of libheif found # LIBHEIF_INCLUDE_DIRS - Include directories for libheif # LIBHEIF_LIBRARIES - Libraries needed to link against libheif include(CheckCXXSymbolExists) +include(CheckCXXSourceCompiles) include(FindPackageHandleStandardArgs) set(LIBHEIF_TARGET "") @@ -91,7 +93,8 @@ if(LIBHEIF_TARGET) list(APPEND CMAKE_REQUIRED_INCLUDES ${LIBHEIF_INCLUDE_DIR}) endif() if(LIBHEIF_DEFS) - set(CMAKE_REQUIRED_DEFINITIONS "-D${LIBHEIF_DEFS}") + set(CMAKE_REQUIRED_DEFINITIONS ${LIBHEIF_DEFS}) + list(TRANSFORM CMAKE_REQUIRED_DEFINITIONS PREPEND "-D") endif() # Perform a compile-only check to avoid linking transitive dependencies @@ -104,6 +107,32 @@ if(LIBHEIF_TARGET) "libheif/heif.h" LIBHEIF_HAS_GAIN_MAP ) + check_cxx_source_compiles( + [=[ +#include "libheif/heif.h" +#include "libheif/heif_items.h" + +int main() { + const heif_context* context = nullptr; + const heif_image_handle* handle = nullptr; + const heif_item_id item_id = heif_image_handle_get_item_id(handle); + const uint32_t item_type = heif_item_get_item_type(context, item_id); + const int aux_filter = 0; + heif_item_id auxiliary_ids[1] = {}; + heif_image_handle* auxiliary_handle = nullptr; + const char* auxiliary_type = nullptr; + + (void)heif_image_handle_get_number_of_auxiliary_images(handle, aux_filter); + (void)heif_image_handle_get_list_of_auxiliary_image_IDs(handle, aux_filter, auxiliary_ids, 1); + (void)heif_image_handle_get_auxiliary_type(handle, &auxiliary_type); + (void)heif_image_handle_release_auxiliary_type(handle, &auxiliary_type); + (void)heif_image_handle_get_auxiliary_image_handle(handle, item_id, &auxiliary_handle); + (void)item_type; + return 0; +} +]=] + LIBHEIF_HAS_ITEM_API + ) set(CMAKE_TRY_COMPILE_TARGET_TYPE ${_saved_try_compile_target_type}) unset(CMAKE_REQUIRED_INCLUDES) diff --git a/fuzzer/ultrahdr_dec_fuzzer.cpp b/fuzzer/ultrahdr_dec_fuzzer.cpp index e1008b47..33c93144 100644 --- a/fuzzer/ultrahdr_dec_fuzzer.cpp +++ b/fuzzer/ultrahdr_dec_fuzzer.cpp @@ -118,6 +118,7 @@ void UltraHdrDecFuzzer::process() { } extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + (void)uhdr_is_supported_gainmap_image(data, size); UltraHdrDecFuzzer fuzzHandle(data, size); fuzzHandle.process(); return 0; diff --git a/lib/include/ultrahdr/jpegr.h b/lib/include/ultrahdr/jpegr.h index abf32933..72d03121 100644 --- a/lib/include/ultrahdr/jpegr.h +++ b/lib/include/ultrahdr/jpegr.h @@ -221,6 +221,11 @@ class JpegR : public UltraHdr { uhdr_error_info_t getJPEGRInfo(uhdr_compressed_image_t* uhdr_compressed_img, jr_info_ptr uhdr_image_info); + // Internal fast-probe variant. Compressed JPEG payloads are borrowed while parsing and the MPF + // directory must identify the exact secondary JPEG range. + uhdr_error_info_t getJPEGRInfoForProbe(uhdr_compressed_image_t* uhdr_compressed_img, + jr_info_ptr uhdr_image_info); + /* \brief Alias of Encode API-0. * * \deprecated This function is deprecated. Use its alias @@ -276,6 +281,10 @@ class JpegR : public UltraHdr { status_t getJPEGRInfo(jr_compressed_ptr jpegr_image_ptr, jr_info_ptr jpegr_image_info_ptr); private: + uhdr_error_info_t getJPEGRInfo(uhdr_compressed_image_t* uhdr_compressed_img, + jr_info_ptr uhdr_image_info, bool copy_image_data, + bool require_mpf_association); + /*!\brief compress gainmap image * * \param[in] gainmap_img gainmap image descriptor @@ -296,7 +305,8 @@ class JpegR : public UltraHdr { */ uhdr_error_info_t extractPrimaryImageAndGainMap(uhdr_compressed_image_t* jpegr_image, uhdr_compressed_image_t* primary_image, - uhdr_compressed_image_t* gainmap_image); + uhdr_compressed_image_t* gainmap_image, + bool require_mpf_association = false); /*!\brief This function parses the bitstream and returns metadata that is useful for actual * decoding. This does not decode the image. That is handled by decompressImage(). @@ -310,7 +320,7 @@ class JpegR : public UltraHdr { */ uhdr_error_info_t parseJpegInfo(uhdr_compressed_image_t* jpeg_image, j_info_ptr image_info, unsigned int* img_width = nullptr, - unsigned int* img_height = nullptr); + unsigned int* img_height = nullptr, bool copy_image_data = true); /*!\brief This method takes compressed sdr intent, compressed gainmap coefficient, gainmap * metadata and creates a ultrahdr image. This is done by first generating XMP packet from gainmap diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp index 4ed12ff0..ad286900 100644 --- a/lib/src/jpegr.cpp +++ b/lib/src/jpegr.cpp @@ -1438,14 +1438,27 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr uhdr_error_info_t JpegR::getJPEGRInfo(uhdr_compressed_image_t* uhdr_compressed_img, jr_info_ptr uhdr_image_info) { + return getJPEGRInfo(uhdr_compressed_img, uhdr_image_info, true, false); +} + +uhdr_error_info_t JpegR::getJPEGRInfoForProbe(uhdr_compressed_image_t* uhdr_compressed_img, + jr_info_ptr uhdr_image_info) { + return getJPEGRInfo(uhdr_compressed_img, uhdr_image_info, false, true); +} + +uhdr_error_info_t JpegR::getJPEGRInfo(uhdr_compressed_image_t* uhdr_compressed_img, + jr_info_ptr uhdr_image_info, bool copy_image_data, + bool require_mpf_association) { uhdr_compressed_image_t primary_image, gainmap; - UHDR_ERR_CHECK(extractPrimaryImageAndGainMap(uhdr_compressed_img, &primary_image, &gainmap)) + UHDR_ERR_CHECK(extractPrimaryImageAndGainMap(uhdr_compressed_img, &primary_image, &gainmap, + require_mpf_association)) UHDR_ERR_CHECK(parseJpegInfo(&primary_image, uhdr_image_info->primaryImgInfo, - &uhdr_image_info->width, &uhdr_image_info->height)) + &uhdr_image_info->width, &uhdr_image_info->height, copy_image_data)) if (uhdr_image_info->gainmapImgInfo != nullptr) { - UHDR_ERR_CHECK(parseJpegInfo(&gainmap, uhdr_image_info->gainmapImgInfo)) + UHDR_ERR_CHECK( + parseJpegInfo(&gainmap, uhdr_image_info->gainmapImgInfo, nullptr, nullptr, copy_image_data)) } return g_no_error; @@ -1861,9 +1874,137 @@ uhdr_error_info_t UltraHdr::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ return g_no_error; } +namespace { + +bool readMpfU16(const uint8_t* data, size_t size, size_t offset, bool big_endian, uint16_t* value) { + if (offset > size || size - offset < sizeof(uint16_t)) return false; + *value = big_endian ? static_cast((data[offset] << 8) | data[offset + 1]) + : static_cast(data[offset] | (data[offset + 1] << 8)); + return true; +} + +bool readMpfU32(const uint8_t* data, size_t size, size_t offset, bool big_endian, uint32_t* value) { + if (offset > size || size - offset < sizeof(uint32_t)) return false; + if (big_endian) { + *value = (static_cast(data[offset]) << 24) | + (static_cast(data[offset + 1]) << 16) | + (static_cast(data[offset + 2]) << 8) | data[offset + 3]; + } else { + *value = data[offset] | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | + (static_cast(data[offset + 3]) << 24); + } + return true; +} + +bool hasValidMpfAssociation(const uhdr_compressed_image_t* image, const JpegInfo& info) { + const auto& image_ranges = info.GetImageRanges(); + if (image_ranges.size() != 2) return false; + const JpegSegmentInfo mpf = info.GetSegmentInfo(0, kMpf); + if (!mpf.IsValid() || image->data == nullptr) return false; + const DataRange& mpf_range = mpf.GetDataRange(); + if (mpf_range.GetEnd() > image->data_sz || mpf_range.GetBegin() > mpf_range.GetEnd()) + return false; + const size_t mpf_end = mpf_range.GetEnd(); + + const uint8_t* bytes = static_cast(image->data); + constexpr size_t kJpegSegmentHeaderSize = 4; + if (mpf_range.GetBegin() > mpf_end || + mpf_end - mpf_range.GetBegin() < kJpegSegmentHeaderSize + sizeof(kMpfSig)) { + return false; + } + const size_t signature = mpf_range.GetBegin() + kJpegSegmentHeaderSize; + if (memcmp(bytes + signature, kMpfSig, sizeof(kMpfSig)) != 0) return false; + + const size_t tiff = signature + sizeof(kMpfSig); + if (tiff > mpf_end || mpf_end - tiff < kMpEndianSize + sizeof(uint32_t)) { + return false; + } + const bool big_endian = memcmp(bytes + tiff, kMpBigEndian, kMpEndianSize) == 0; + if (!big_endian && memcmp(bytes + tiff, kMpLittleEndian, kMpEndianSize) != 0) return false; + + uint32_t ifd_offset = 0; + if (!readMpfU32(bytes, mpf_end, tiff + kMpEndianSize, big_endian, &ifd_offset) || + ifd_offset > mpf_end - tiff) { + return false; + } + const size_t ifd = tiff + ifd_offset; + uint16_t tag_count = 0; + if (!readMpfU16(bytes, mpf_end, ifd, big_endian, &tag_count) || tag_count > 64) { + return false; + } + + size_t entries_offset = 0; + uint32_t entries_size = 0; + uint32_t image_count = 0; + bool has_valid_version = false; + bool has_image_count = false; + bool has_mp_entries = false; + size_t tag = ifd + sizeof(uint16_t); + for (uint16_t i = 0; i < tag_count; ++i, tag += kTagSize) { + uint16_t id = 0; + uint16_t type = 0; + uint32_t count = 0; + uint32_t value = 0; + if (!readMpfU16(bytes, mpf_end, tag, big_endian, &id) || + !readMpfU16(bytes, mpf_end, tag + 2, big_endian, &type) || + !readMpfU32(bytes, mpf_end, tag + 4, big_endian, &count) || + !readMpfU32(bytes, mpf_end, tag + 8, big_endian, &value)) { + return false; + } + if (id == kVersionTag) { + if (has_valid_version || type != kVersionType || count != kVersionCount || + memcmp(bytes + tag + 8, kVersionExpected, kVersionSize) != 0) { + return false; + } + has_valid_version = true; + } else if (id == kNumberOfImagesTag) { + if (has_image_count || type != kTypeLong || count != kNumberOfImagesCount) return false; + has_image_count = true; + image_count = value; + } else if (id == kMPEntryTag) { + if (has_mp_entries || type != kTypeUndefined || count < 2 * kMPEntrySize) return false; + has_mp_entries = true; + entries_offset = value; + entries_size = count; + } + } + if (!has_valid_version || !has_image_count || !has_mp_entries || image_count != 2 || + entries_size != image_count * kMPEntrySize || + entries_offset > mpf_end - tiff || mpf_end - (tiff + entries_offset) < entries_size) { + return false; + } + + const size_t primary_entry = tiff + entries_offset; + const size_t secondary_entry = tiff + entries_offset + kMPEntrySize; + uint32_t primary_attributes = 0; + uint32_t primary_offset = 0; + uint32_t secondary_attributes = 0; + uint32_t secondary_size = 0; + uint32_t secondary_offset = 0; + constexpr uint32_t kMpfFormatAndTypeMask = 0x0fffffff; + if (!readMpfU32(bytes, mpf_end, primary_entry, big_endian, &primary_attributes) || + !readMpfU32(bytes, mpf_end, primary_entry + 8, big_endian, &primary_offset) || + !readMpfU32(bytes, mpf_end, secondary_entry, big_endian, &secondary_attributes) || + !readMpfU32(bytes, mpf_end, secondary_entry + 4, big_endian, &secondary_size) || + !readMpfU32(bytes, mpf_end, secondary_entry + 8, big_endian, &secondary_offset) || + (primary_attributes & kMpfFormatAndTypeMask) != kMPEntryAttributeTypePrimary || + (secondary_attributes & kMpfFormatAndTypeMask) != kMPEntryAttributeFormatJpeg || + primary_offset != 0 || secondary_offset > image->data_sz - tiff) { + return false; + } + // Only the secondary entry is needed to associate the gain-map JPEG. Some Apple-authored files + // use legacy primary-size bookkeeping, so do not reject an otherwise exact association for it. + return tiff + secondary_offset == image_ranges[1].GetBegin() && + secondary_size == image_ranges[1].GetLength(); +} + +} // namespace + uhdr_error_info_t JpegR::extractPrimaryImageAndGainMap(uhdr_compressed_image_t* jpegr_image, uhdr_compressed_image_t* primary_image, - uhdr_compressed_image_t* gainmap_image) { + uhdr_compressed_image_t* gainmap_image, + bool require_mpf_association) { MessageHandler msg_handler; msg_handler.SetMessageWriter(make_unique(AlogMessageWriter())); @@ -1916,6 +2057,15 @@ uhdr_error_info_t JpegR::extractPrimaryImageAndGainMap(uhdr_compressed_image_t* return status; } + if (require_mpf_association && !hasValidMpfAssociation(jpegr_image, jpeg_info)) { + uhdr_error_info_t status; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, + "input uhdr image does not contain a valid MPF gain map association"); + return status; + } + if (gainmap_image != nullptr) { gainmap_image->data = static_cast(jpegr_image->data) + image_ranges[1].GetBegin(); gainmap_image->data_sz = image_ranges[1].GetLength(); @@ -1931,7 +2081,8 @@ uhdr_error_info_t JpegR::extractPrimaryImageAndGainMap(uhdr_compressed_image_t* } uhdr_error_info_t JpegR::parseJpegInfo(uhdr_compressed_image_t* jpeg_image, j_info_ptr image_info, - unsigned int* img_width, unsigned int* img_height) { + unsigned int* img_width, unsigned int* img_height, + bool copy_image_data) { JpegDecoderHelper jpeg_dec_obj; UHDR_ERR_CHECK(jpeg_dec_obj.parseImage(jpeg_image->data, jpeg_image->data_sz)) unsigned int imgWidth, imgHeight, numComponents; @@ -1943,8 +2094,10 @@ uhdr_error_info_t JpegR::parseJpegInfo(uhdr_compressed_image_t* jpeg_image, j_in image_info->width = imgWidth; image_info->height = imgHeight; image_info->numComponents = numComponents; - image_info->imgData.resize(jpeg_image->data_sz, 0); - memcpy(static_cast(image_info->imgData.data()), jpeg_image->data, jpeg_image->data_sz); + if (copy_image_data) { + image_info->imgData.resize(jpeg_image->data_sz, 0); + memcpy(static_cast(image_info->imgData.data()), jpeg_image->data, jpeg_image->data_sz); + } if (jpeg_dec_obj.getICCSize() != 0) { image_info->iccData.resize(jpeg_dec_obj.getICCSize(), 0); memcpy(static_cast(image_info->iccData.data()), jpeg_dec_obj.getICCPtr(), diff --git a/lib/src/ultrahdr_api.cpp b/lib/src/ultrahdr_api.cpp index f5f6cb8e..bbd3f699 100644 --- a/lib/src/ultrahdr_api.cpp +++ b/lib/src/ultrahdr_api.cpp @@ -8,8 +8,11 @@ * except according to those terms. */ +#include #include #include +#include +#include #include "ultrahdr_api.h" #include "ultrahdr/ultrahdrcommon.h" @@ -21,13 +24,9 @@ #include "ultrahdr/heifultrahdr.h" #include "ultrahdr/avifultrahdr.h" #include "ultrahdr/gainmapmetadata.h" +#if defined(UHDR_HAS_HEIF_ITEM_API) +#include "libheif/heif_items.h" #endif - - -#ifdef UHDR_ENABLE_HEIF -#include "ultrahdr/heifultrahdr.h" -#include "ultrahdr/avifultrahdr.h" -#include "ultrahdr/gainmapmetadata.h" #endif #include "ultrahdr/jpegrutils.h" @@ -506,6 +505,238 @@ uhdr_error_info_t uhdr_validate_gainmap_metadata_descriptor(uhdr_gainmap_metadat } // namespace ultrahdr +namespace { + +bool inspect_jpeg_gainmap(const void* data, size_t size) { + uhdr_compressed_image_t image{}; + image.data = const_cast(data); + image.data_sz = size; + image.capacity = size; + image.cg = UHDR_CG_UNSPECIFIED; + image.ct = UHDR_CT_UNSPECIFIED; + image.range = UHDR_CR_UNSPECIFIED; + + ultrahdr::jpeg_info_struct primary; + ultrahdr::jpeg_info_struct gainmap; + ultrahdr::jpegr_info_struct info; + info.primaryImgInfo = &primary; + info.gainmapImgInfo = &gainmap; + + ultrahdr::JpegR jpegr; + if (jpegr.getJPEGRInfoForProbe(&image, &info).error_code != UHDR_CODEC_OK) return false; + + ultrahdr::uhdr_gainmap_metadata_ext_t metadata; + return jpegr + .parseGainMapMetadata(gainmap.isoData.data(), gainmap.isoData.size(), + gainmap.xmpData.data(), gainmap.xmpData.size(), + primary.exifData.data(), primary.exifData.size(), &metadata) + .error_code == UHDR_CODEC_OK; +} + +#if defined(UHDR_ENABLE_HEIF) && defined(UHDR_HAS_HEIF_ITEM_API) + +constexpr size_t kMaxProbeMetadataSize = 64 * 1024; +constexpr int kMaxProbeAuxiliaryImages = 16; + +struct HeifContextDeleter { + void operator()(heif_context* context) const { heif_context_free(context); } +}; + +struct HeifHandleDeleter { + void operator()(heif_image_handle* handle) const { heif_image_handle_release(handle); } +}; + +bool is_supported_heif_dimensions(const heif_image_handle* handle) { + const int width = heif_image_handle_get_width(handle); + const int height = heif_image_handle_get_height(handle); + return width > 0 && height > 0 && width <= ultrahdr::kMaxWidth && height <= ultrahdr::kMaxHeight; +} + +bool is_supported_heif_layout(const heif_image_handle* handle, bool allow_monochrome) { + heif_colorspace colorspace; + heif_chroma chroma; + const heif_error error = + heif_image_handle_get_preferred_decoding_colorspace(handle, &colorspace, &chroma); + if (error.code != heif_error_Ok) return false; + if (colorspace == heif_colorspace_monochrome) { + return allow_monochrome && chroma == heif_chroma_monochrome; + } + if (colorspace == heif_colorspace_RGB) return true; + return colorspace == heif_colorspace_YCbCr && + (chroma == heif_chroma_420 || chroma == heif_chroma_422 || chroma == heif_chroma_444); +} + +bool get_direct_image_decoder_format(const heif_context* context, const heif_image_handle* handle, + heif_compression_format* format) { + const uint32_t type = heif_item_get_item_type(context, heif_image_handle_get_item_id(handle)); + if (type == heif_fourcc('a', 'v', '0', '1')) { + *format = heif_compression_AV1; + return true; + } + if (type == heif_fourcc('h', 'v', 'c', '1') || type == heif_fourcc('h', 'e', 'v', '1')) { + *format = heif_compression_HEVC; + return true; + } + return false; +} + +bool is_known_alpha_auxiliary_type(const char* type) { + return type != nullptr && + (strcmp(type, "urn:mpeg:avc:2015:auxid:1") == 0 || + strcmp(type, "urn:mpeg:hevc:2015:auxid:1") == 0 || + strcmp(type, "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha") == 0); +} + +bool inspect_heif_alpha_dependencies(const heif_context* context, + const heif_image_handle* handle) { + if (!heif_image_handle_has_alpha_channel(handle)) return true; + + const int auxiliary_count = heif_image_handle_get_number_of_auxiliary_images(handle, 0); + if (auxiliary_count <= 0 || auxiliary_count > kMaxProbeAuxiliaryImages) return false; + + std::vector auxiliary_ids(auxiliary_count); + if (heif_image_handle_get_list_of_auxiliary_image_IDs(handle, 0, auxiliary_ids.data(), + auxiliary_count) != auxiliary_count) { + return false; + } + + bool found_alpha = false; + for (heif_item_id auxiliary_id : auxiliary_ids) { + heif_image_handle* auxiliary_raw = nullptr; + if (heif_image_handle_get_auxiliary_image_handle(handle, auxiliary_id, &auxiliary_raw).code != + heif_error_Ok || + auxiliary_raw == nullptr) { + return false; + } + std::unique_ptr auxiliary(auxiliary_raw); + + const char* auxiliary_type = nullptr; + const heif_error type_error = + heif_image_handle_get_auxiliary_type(auxiliary.get(), &auxiliary_type); + const bool type_resolved = type_error.code == heif_error_Ok && auxiliary_type != nullptr; + const bool is_alpha = type_resolved && is_known_alpha_auxiliary_type(auxiliary_type); + heif_image_handle_release_auxiliary_type(auxiliary.get(), &auxiliary_type); + if (!type_resolved) return false; + if (!is_alpha) continue; + if (found_alpha) return false; + found_alpha = true; + + heif_compression_format auxiliary_format; + if (!get_direct_image_decoder_format(context, auxiliary.get(), &auxiliary_format) || + !heif_have_decoder_for_format(auxiliary_format)) { + return false; + } + + // Alpha is supported as a direct independent dependency, allowing monochrome layouts. Do not + // follow an alpha dependency's own auxiliary graph. + if (heif_image_handle_has_alpha_channel(auxiliary.get()) || + !is_supported_heif_dimensions(auxiliary.get()) || + !is_supported_heif_layout(auxiliary.get(), true)) { + return false; + } + } + + return found_alpha; +} + +bool inspect_heif_gainmap(const void* data, size_t size) { + const int sniff_size = + static_cast((std::min)(size, static_cast((std::numeric_limits::max)()))); + const heif_filetype_result filetype = + heif_check_filetype(static_cast(data), sniff_size); + if (filetype != heif_filetype_yes_supported && filetype != heif_filetype_maybe) return false; + + std::unique_ptr context(heif_context_alloc()); + if (context == nullptr) return false; + if (heif_context_read_from_memory_without_copy(context.get(), data, size, nullptr).code != + heif_error_Ok) { + return false; + } + + heif_image_handle* primary_raw = nullptr; + if (heif_context_get_primary_image_handle(context.get(), &primary_raw).code != heif_error_Ok || + primary_raw == nullptr) { + return false; + } + std::unique_ptr primary(primary_raw); + if (!heif_image_handle_is_primary_image(primary.get())) return false; + + heif_image_handle* gainmap_raw = nullptr; + if (heif_image_handle_get_gain_map_image_handle(primary.get(), &gainmap_raw).code != + heif_error_Ok || + gainmap_raw == nullptr) { + return false; + } + std::unique_ptr gainmap(gainmap_raw); + + const size_t metadata_size = heif_image_handle_get_gain_map_metadata_size(primary.get()); + if (metadata_size == 0 || metadata_size > kMaxProbeMetadataSize) return false; + std::vector metadata_bytes(metadata_size); + if (heif_image_handle_get_gain_map_metadata(primary.get(), metadata_bytes.data()).code != + heif_error_Ok) { + return false; + } + ultrahdr::uhdr_gainmap_metadata_frac metadata_fraction; + ultrahdr::uhdr_gainmap_metadata_ext_t metadata; + if (ultrahdr::uhdr_gainmap_metadata_frac::decodeGainmapMetadata(metadata_bytes, + &metadata_fraction) + .error_code != UHDR_CODEC_OK || + ultrahdr::uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata_fraction, + &metadata) + .error_code != UHDR_CODEC_OK) { + return false; + } + + if (!is_supported_heif_dimensions(primary.get()) || + !is_supported_heif_dimensions(gainmap.get()) || + !is_supported_heif_layout(primary.get(), false) || + !is_supported_heif_layout(gainmap.get(), true)) { + return false; + } + + heif_compression_format primary_format; + heif_compression_format gainmap_format; + if (!get_direct_image_decoder_format(context.get(), primary.get(), &primary_format) || + !get_direct_image_decoder_format(context.get(), gainmap.get(), &gainmap_format)) { + return false; + } + if (!heif_have_decoder_for_format(primary_format) || + !heif_have_decoder_for_format(gainmap_format)) { + return false; + } + return inspect_heif_alpha_dependencies(context.get(), primary.get()) && + inspect_heif_alpha_dependencies(context.get(), gainmap.get()); +} + +#endif // defined(UHDR_ENABLE_HEIF) && defined(UHDR_HAS_HEIF_ITEM_API) + +bool inspect_gainmap_image(const void* data, size_t size) { + if (data == nullptr || size == 0) return false; + const uint8_t* bytes = static_cast(data); + if (size >= 3 && memcmp(bytes, "\377\330\377", 3) == 0) { + return inspect_jpeg_gainmap(data, size); + } +#if defined(UHDR_ENABLE_HEIF) && defined(UHDR_HAS_HEIF_ITEM_API) + return inspect_heif_gainmap(data, size); +#else + return false; +#endif +} + +int inspect_gainmap_image_for_c_api(const void* data, size_t size) { +#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) + try { + return inspect_gainmap_image(data, size); + } catch (...) { + return 0; + } +#else + return inspect_gainmap_image(data, size); +#endif +} + +} // namespace + uhdr_codec_private::~uhdr_codec_private() { for (auto it : m_effects) delete it; m_effects.clear(); @@ -1517,6 +1748,10 @@ int is_uhdr_image(void* data, int size) { return 1; } +UHDR_EXTERN int uhdr_is_supported_gainmap_image(const void* data, size_t size) { + return inspect_gainmap_image_for_c_api(data, size); +} + uhdr_codec_private_t* uhdr_create_decoder(void) { uhdr_decoder_private* handle = new uhdr_decoder_private(); diff --git a/tests/data/GAINMAP_FIXTURE_README.md b/tests/data/GAINMAP_FIXTURE_README.md new file mode 100644 index 00000000..89a3d36c --- /dev/null +++ b/tests/data/GAINMAP_FIXTURE_README.md @@ -0,0 +1,18 @@ +# Generated routing-probe fixture: `gainmap_hevc_16x16.heic` + +This synthetic fixture was generated during test development with this repository's public HEIF +encoder from programmatically initialized, uniform 16x16 P010 HLG pixels. No third-party source +image was used. The encoder used its default gain-map scale, quality, and metadata settings. The +result contains direct-coded HEVC primary and gain-map items plus ISO 21496-1 metadata and exists to +test both sides of runtime decoder availability. It was generated from libultrahdr revision +`418b6b361e252a91c435a56cf386afb37d7d1c9d`; the temporary generation helper was not retained. + +SHA-256: `6dd810ee6d118d3ce876936d9b265338b94d44cde187bce2ba384ce93edb1b8e` + +## Fixture-specific license + +The license below applies specifically to `gainmap_hevc_16x16.heic` and this provenance document; +it is an explicit exception to the general CC-BY-4.0 license used by the pre-existing files in this +directory. + +SPDX-License-Identifier: Apache-2.0 OR MIT diff --git a/tests/data/gainmap_hevc_16x16.heic b/tests/data/gainmap_hevc_16x16.heic new file mode 100644 index 00000000..9158af25 Binary files /dev/null and b/tests/data/gainmap_hevc_16x16.heic differ diff --git a/tests/jpegr_test.cpp b/tests/jpegr_test.cpp index 8748dac1..73d75623 100644 --- a/tests/jpegr_test.cpp +++ b/tests/jpegr_test.cpp @@ -1722,6 +1722,7 @@ TEST(JpegRTest, decodeApple) { std::string content((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())); ASSERT_GT(content.size(), 4u); ASSERT_EQ(is_uhdr_image(content.data(), content.size()), 1); + ASSERT_EQ(uhdr_is_supported_gainmap_image(content.data(), content.size()), 1); uhdrCompressedImg.data = content.data(); uhdrCompressedImg.data_sz = content.size(); diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index f4b2edc9..f8b9d213 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -19,14 +19,19 @@ #include "ultrahdr/ultrahdrcommon.h" #include "ultrahdr/jpegr.h" #include "ultrahdr/gainmapmath.h" +#include "ultrahdr/gainmapmetadata.h" #include "ultrahdr/heifultrahdr.h" #include "ultrahdr/avifultrahdr.h" +#if defined(UHDR_ENABLE_HEIF) +#include "libheif/heif.h" +#endif namespace ultrahdr { static const char* kYCbCrP010FileName = "raw_p010_image.p010"; static const char* kYCbCr420FileName = "raw_yuv420_image.yuv420"; static const char* kSdrJpgFileName = "jpeg_image.jpg"; +static const char* kHevcGainmapFileName = "gainmap_hevc_16x16.heic"; static const size_t kImageWidth = 1280; static const size_t kImageHeight = 720; @@ -53,6 +58,370 @@ static bool loadFile(const char* filename, std::vector& buffer) { return false; } +static bool readMpfU16ForTest(const std::vector& data, size_t offset, bool big_endian, + uint16_t* value) { + if (offset > data.size() || data.size() - offset < 2) return false; + *value = big_endian ? static_cast((data[offset] << 8) | data[offset + 1]) + : static_cast(data[offset] | (data[offset + 1] << 8)); + return true; +} + +static bool readMpfU32ForTest(const std::vector& data, size_t offset, bool big_endian, + uint32_t* value) { + if (offset > data.size() || data.size() - offset < 4) return false; + if (big_endian) { + *value = (static_cast(data[offset]) << 24) | + (static_cast(data[offset + 1]) << 16) | + (static_cast(data[offset + 2]) << 8) | data[offset + 3]; + } else { + *value = data[offset] | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | + (static_cast(data[offset + 3]) << 24); + } + return true; +} + +static void writeMpfU16ForTest(std::vector& data, size_t offset, bool big_endian, + uint16_t value) { + data[offset] = static_cast(value >> (big_endian ? 8 : 0)); + data[offset + 1] = static_cast(value >> (big_endian ? 0 : 8)); +} + +static void writeMpfU32ForTest(std::vector& data, size_t offset, bool big_endian, + uint32_t value) { + for (int i = 0; i < 4; ++i) { + const int shift = big_endian ? 24 - 8 * i : 8 * i; + data[offset + i] = static_cast(value >> shift); + } +} + +struct MpfEntryLocations { + bool big_endian; + size_t signature; + size_t ifd; + size_t number_of_images_tag; + size_t mp_entry_tag; + size_t mp_entry_value; + size_t version_id; + size_t version_type; + size_t version_count; + size_t version_value; + size_t count; + size_t primary_attributes; + size_t primary_offset; + size_t secondary_attributes; + size_t secondary_offset; +}; + +static bool findMpfEntryLocations(const std::vector& data, MpfEntryLocations* locations) { + const uint8_t signature[] = {'M', 'P', 'F', 0}; + const auto found = + std::search(data.begin(), data.end(), std::begin(signature), std::end(signature)); + if (found == data.end()) return false; + const size_t signature_offset = static_cast(found - data.begin()); + const size_t tiff = signature_offset + sizeof signature; + if (tiff > data.size() || data.size() - tiff < 8) return false; + const bool big_endian = data[tiff] == 'M' && data[tiff + 1] == 'M'; + if (!big_endian && !(data[tiff] == 'I' && data[tiff + 1] == 'I')) return false; + + uint32_t ifd_relative_offset = 0; + if (!readMpfU32ForTest(data, tiff + 4, big_endian, &ifd_relative_offset) || + ifd_relative_offset > data.size() - tiff) { + return false; + } + const size_t ifd = tiff + ifd_relative_offset; + uint16_t tag_count = 0; + if (!readMpfU16ForTest(data, ifd, big_endian, &tag_count)) return false; + bool found_version = false; + bool found_number_of_images = false; + bool found_entries = false; + locations->big_endian = big_endian; + locations->signature = signature_offset; + locations->ifd = ifd; + for (uint16_t i = 0; i < tag_count; ++i) { + const size_t tag = ifd + 2 + 12 * i; + uint16_t id = 0; + uint32_t entries_offset = 0; + if (!readMpfU16ForTest(data, tag, big_endian, &id) || + !readMpfU32ForTest(data, tag + 8, big_endian, &entries_offset)) { + return false; + } + if (id == 0xb000) { + locations->version_id = tag; + locations->version_type = tag + 2; + locations->version_count = tag + 4; + locations->version_value = tag + 8; + found_version = true; + continue; + } + if (id == 0xb001) { + locations->number_of_images_tag = tag; + found_number_of_images = true; + continue; + } + if (id != 0xb002) continue; + if (entries_offset > data.size() - tiff || data.size() - (tiff + entries_offset) < 32) { + return false; + } + const size_t entries = tiff + entries_offset; + locations->count = tag + 4; + locations->mp_entry_tag = tag; + locations->mp_entry_value = tag + 8; + locations->primary_attributes = entries; + locations->primary_offset = entries + 8; + locations->secondary_attributes = entries + 16; + locations->secondary_offset = entries + 24; + found_entries = true; + } + return found_version && found_number_of_images && found_entries; +} + +static bool insertDuplicateMpfTag(std::vector& data, uint16_t tag_id) { + MpfEntryLocations locations; + if (!findMpfEntryLocations(data, &locations) || locations.signature < 2) return false; + + uint16_t tag_count = 0; + uint16_t app2_length = 0; + uint32_t entries_offset = 0; + uint32_t secondary_offset = 0; + if (!readMpfU16ForTest(data, locations.ifd, locations.big_endian, &tag_count) || + !readMpfU16ForTest(data, locations.signature - 2, true, &app2_length) || + !readMpfU32ForTest(data, locations.mp_entry_value, locations.big_endian, &entries_offset) || + !readMpfU32ForTest(data, locations.secondary_offset, locations.big_endian, + &secondary_offset)) { + return false; + } + const size_t source_tag = + tag_id == 0xb001 ? locations.number_of_images_tag : locations.mp_entry_tag; + const size_t insertion = locations.ifd + 2 + static_cast(tag_count) * 12; + if (source_tag > data.size() || data.size() - source_tag < 12 || insertion > data.size() || + app2_length > UINT16_MAX - 12) { + return false; + } + + const std::vector duplicate(data.begin() + source_tag, data.begin() + source_tag + 12); + data.insert(data.begin() + insertion, duplicate.begin(), duplicate.end()); + writeMpfU16ForTest(data, locations.ifd, locations.big_endian, tag_count + 1); + writeMpfU16ForTest(data, locations.signature - 2, true, app2_length + 12); + writeMpfU32ForTest(data, locations.mp_entry_value, locations.big_endian, entries_offset + 12); + if (tag_id == 0xb002) { + writeMpfU32ForTest(data, insertion + 8, locations.big_endian, entries_offset + 12); + } + writeMpfU32ForTest(data, locations.secondary_offset + 12, locations.big_endian, + secondary_offset + 12); + return true; +} + +#if defined(UHDR_ENABLE_HEIF) +static void expectSupportedHeifGainmap(const void* data, size_t size, bool decoder_available) { +#if defined(UHDR_HAS_HEIF_ITEM_API) + EXPECT_EQ(uhdr_is_supported_gainmap_image(data, size), decoder_available ? 1 : 0); +#else + // The legacy HEIF gain-map APIs keep encode/decode coverage available, but routing requires + // the optional item-inspection API. The documented fallback is an unsupported routing result. + EXPECT_EQ(uhdr_is_supported_gainmap_image(data, size), 0); + (void)decoder_available; +#endif +} + +static heif_error writeHeifToVector([[maybe_unused]] heif_context* context, const void* data, + size_t size, void* userdata) { + auto* output = static_cast*>(userdata); + const uint8_t* bytes = static_cast(data); + output->insert(output->end(), bytes, bytes + size); + return {heif_error_Ok, heif_suberror_Unspecified, nullptr}; +} + +static bool encodeOrdinaryAvif(std::vector& output) { + constexpr int width = 16; + constexpr int height = 16; + heif_context* context = heif_context_alloc(); + heif_image* image = nullptr; + heif_encoder* encoder = nullptr; + heif_image_handle* handle = nullptr; + bool success = false; + if (context == nullptr) return false; + + if (heif_image_create(width, height, heif_colorspace_YCbCr, heif_chroma_420, &image).code != + heif_error_Ok || + heif_image_add_plane(image, heif_channel_Y, width, height, 8).code != heif_error_Ok || + heif_image_add_plane(image, heif_channel_Cb, width / 2, height / 2, 8).code != + heif_error_Ok || + heif_image_add_plane(image, heif_channel_Cr, width / 2, height / 2, 8).code != + heif_error_Ok) { + goto cleanup; + } + for (const heif_channel channel : {heif_channel_Y, heif_channel_Cb, heif_channel_Cr}) { + int stride = 0; + uint8_t* plane = heif_image_get_plane(image, channel, &stride); + const int plane_height = channel == heif_channel_Y ? height : height / 2; + if (plane == nullptr || stride <= 0) goto cleanup; + std::fill(plane, plane + stride * plane_height, 128); + } + if (heif_context_get_encoder_for_format(context, heif_compression_AV1, &encoder).code != + heif_error_Ok || + heif_context_encode_image(context, image, encoder, nullptr, &handle).code != heif_error_Ok) { + goto cleanup; + } + { + heif_writer writer{1, writeHeifToVector}; + success = heif_context_write(context, &writer, &output).code == heif_error_Ok; + } + +cleanup: + if (handle != nullptr) heif_image_handle_release(handle); + if (encoder != nullptr) heif_encoder_release(encoder); + if (image != nullptr) heif_image_release(image); + heif_context_free(context); + return success; +} + +using HeifContextPtr = std::unique_ptr; +using HeifEncoderPtr = std::unique_ptr; +using HeifImagePtr = std::unique_ptr; +using HeifHandlePtr = + std::unique_ptr; +using HeifOptionsPtr = + std::unique_ptr; +using HeifProfilePtr = + std::unique_ptr; + +static bool fillHeifPlane(heif_image* image, heif_channel channel, int width, int height, + uint8_t value) { + int stride = 0; + uint8_t* plane = heif_image_get_plane(image, channel, &stride); + if (plane == nullptr || stride < width) return false; + for (int y = 0; y < height; ++y) { + std::fill(plane + y * stride, plane + y * stride + width, value); + } + return true; +} + +static HeifProfilePtr makeNclxProfile(uint16_t primaries, uint16_t transfer, uint16_t matrix) { + HeifProfilePtr profile(heif_nclx_color_profile_alloc(), heif_nclx_color_profile_free); + if (profile == nullptr || + heif_nclx_color_profile_set_color_primaries(profile.get(), primaries).code != + heif_error_Ok || + heif_nclx_color_profile_set_transfer_characteristics(profile.get(), transfer).code != + heif_error_Ok || + heif_nclx_color_profile_set_matrix_coefficients(profile.get(), matrix).code != + heif_error_Ok) { + return HeifProfilePtr(nullptr, heif_nclx_color_profile_free); + } + profile->full_range_flag = 1; + return profile; +} + +static bool encodeGainmapSideAlphaAvif(std::vector& output) { + constexpr int base_width = 64; + constexpr int base_height = 64; + constexpr int gainmap_width = 32; + constexpr int gainmap_height = 32; + + HeifContextPtr context(heif_context_alloc(), heif_context_free); + if (context == nullptr) return false; + + heif_image* base_raw = nullptr; + if (heif_image_create(base_width, base_height, heif_colorspace_YCbCr, heif_chroma_420, + &base_raw) + .code != heif_error_Ok) { + return false; + } + HeifImagePtr base(base_raw, heif_image_release); + if (heif_image_add_plane(base.get(), heif_channel_Y, base_width, base_height, 8).code != + heif_error_Ok || + heif_image_add_plane(base.get(), heif_channel_Cb, base_width / 2, base_height / 2, 8) + .code != heif_error_Ok || + heif_image_add_plane(base.get(), heif_channel_Cr, base_width / 2, base_height / 2, 8) + .code != heif_error_Ok || + !fillHeifPlane(base.get(), heif_channel_Y, base_width, base_height, 128) || + !fillHeifPlane(base.get(), heif_channel_Cb, base_width / 2, base_height / 2, 128) || + !fillHeifPlane(base.get(), heif_channel_Cr, base_width / 2, base_height / 2, 128)) { + return false; + } + + heif_image* gainmap_raw = nullptr; + if (heif_image_create(gainmap_width, gainmap_height, heif_colorspace_monochrome, + heif_chroma_monochrome, &gainmap_raw) + .code != heif_error_Ok) { + return false; + } + HeifImagePtr gainmap(gainmap_raw, heif_image_release); + if (heif_image_add_plane(gainmap.get(), heif_channel_Y, gainmap_width, gainmap_height, 8) + .code != heif_error_Ok || + heif_image_add_plane(gainmap.get(), heif_channel_Alpha, gainmap_width, gainmap_height, 8) + .code != heif_error_Ok || + !fillHeifPlane(gainmap.get(), heif_channel_Y, gainmap_width, gainmap_height, 96) || + !fillHeifPlane(gainmap.get(), heif_channel_Alpha, gainmap_width, gainmap_height, 128)) { + return false; + } + heif_image_set_premultiplied_alpha(gainmap.get(), 0); + + HeifProfilePtr base_profile = + makeNclxProfile(heif_color_primaries_ITU_R_BT_709_5, + heif_transfer_characteristic_ITU_R_BT_709_5, + heif_matrix_coefficients_ITU_R_BT_709_5); + HeifProfilePtr gainmap_profile = + makeNclxProfile(heif_color_primaries_unspecified, heif_transfer_characteristic_unspecified, + heif_matrix_coefficients_ITU_R_BT_601_6); + HeifProfilePtr derived_profile = + makeNclxProfile(heif_color_primaries_ITU_R_BT_2020_2_and_2100_0, + heif_transfer_characteristic_ITU_R_BT_2100_0_HLG, + heif_matrix_coefficients_ITU_R_BT_2020_2_non_constant_luminance); + if (base_profile == nullptr || gainmap_profile == nullptr || derived_profile == nullptr || + heif_image_set_nclx_color_profile(base.get(), base_profile.get()).code != heif_error_Ok || + heif_image_set_nclx_color_profile(gainmap.get(), gainmap_profile.get()).code != + heif_error_Ok) { + return false; + } + + heif_encoder* encoder_raw = nullptr; + if (heif_context_get_encoder_for_format(context.get(), heif_compression_AV1, &encoder_raw) + .code != heif_error_Ok) { + return false; + } + HeifEncoderPtr encoder(encoder_raw, heif_encoder_release); + if (heif_encoder_set_lossy_quality(encoder.get(), 100).code != heif_error_Ok) return false; + + HeifOptionsPtr options(heif_encoding_options_alloc(), heif_encoding_options_free); + if (options == nullptr) return false; + options->save_alpha_channel = 1; + options->output_nclx_profile = base_profile.get(); + + heif_image_handle* base_handle_raw = nullptr; + if (heif_context_encode_image(context.get(), base.get(), encoder.get(), options.get(), + &base_handle_raw) + .code != heif_error_Ok) { + return false; + } + HeifHandlePtr base_handle(base_handle_raw, heif_image_handle_release); + + uhdr_gainmap_metadata_frac metadata; + metadata.alternateHdrHeadroomN = 2; + std::vector iso_metadata; + if (uhdr_gainmap_metadata_frac::encodeGainmapMetadata(&metadata, iso_metadata).error_code != + UHDR_CODEC_OK) { + return false; + } + + options->output_nclx_profile = gainmap_profile.get(); + if (heif_encoder_set_lossy_quality(encoder.get(), 100).code != heif_error_Ok) return false; + heif_image_handle* gainmap_handle_raw = nullptr; + if (heif_context_encode_gain_map_image( + context.get(), base_handle.get(), encoder.get(), gainmap.get(), options.get(), + iso_metadata.data(), static_cast(iso_metadata.size()), derived_profile.get(), + &gainmap_handle_raw) + .code != heif_error_Ok) { + return false; + } + HeifHandlePtr gainmap_handle(gainmap_handle_raw, heif_image_handle_release); + + heif_writer writer{1, writeHeifToVector}; + return heif_context_write(context.get(), &writer, &output).code == heif_error_Ok && + !output.empty(); +} +#endif + static void expectGainMapsEqual(const uhdr_raw_image_t* expected, const uhdr_raw_image_t* actual) { ASSERT_NE(expected, nullptr); @@ -162,6 +531,16 @@ static heif_transfer_characteristics getPrimaryImageTransfer( } #endif +TEST_F(UltraHdrApiTest, RoutingProbeRejectsInvalidInputAndOrdinaryJpeg) { + EXPECT_EQ(uhdr_is_supported_gainmap_image(nullptr, 1), 0); + EXPECT_EQ(uhdr_is_supported_gainmap_image(mSdrJpgData.data(), 0), 0); + EXPECT_EQ(is_uhdr_image(mSdrJpgData.data(), static_cast(mSdrJpgData.size())), 0); + EXPECT_EQ(uhdr_is_supported_gainmap_image(mSdrJpgData.data(), mSdrJpgData.size()), 0); + + const uint8_t truncated_jpeg[] = {0xff, 0xd8, 0xff}; + EXPECT_EQ(uhdr_is_supported_gainmap_image(truncated_jpeg, sizeof truncated_jpeg), 0); +} + TEST_F(UltraHdrApiTest, InvalidCompressedImageIntentDoesNotMutateEncoder) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr); @@ -210,6 +589,96 @@ TEST_F(UltraHdrApiTest, JpegEncodeApi0AndDecode) { uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); + EXPECT_EQ(is_uhdr_image(output->data, static_cast(output->data_sz)), 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(output->data, output->data_sz), 1); + + std::vector broken_association(static_cast(output->data), + static_cast(output->data) + output->data_sz); + MpfEntryLocations locations; + ASSERT_TRUE(findMpfEntryLocations(broken_association, &locations)); + uint32_t secondary_offset = 0; + ASSERT_TRUE(readMpfU32ForTest(broken_association, locations.secondary_offset, + locations.big_endian, &secondary_offset)); + writeMpfU32ForTest(broken_association, locations.secondary_offset, locations.big_endian, + secondary_offset + 1); + // is_uhdr_image() retains its historical decoder-probe behavior. The stricter MPF association + // check belongs to the new routing predicate. + EXPECT_EQ(is_uhdr_image(broken_association.data(), static_cast(broken_association.size())), + 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(broken_association.data(), broken_association.size()), + 0); + + std::vector malformed_table(static_cast(output->data), + static_cast(output->data) + output->data_sz); + ASSERT_TRUE(findMpfEntryLocations(malformed_table, &locations)); + writeMpfU32ForTest(malformed_table, locations.count, locations.big_endian, 0xffffffffu); + EXPECT_EQ(is_uhdr_image(malformed_table.data(), static_cast(malformed_table.size())), 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(malformed_table.data(), malformed_table.size()), 0); + + std::vector displaced_signature(static_cast(output->data), + static_cast(output->data) + output->data_sz); + ASSERT_TRUE(findMpfEntryLocations(displaced_signature, &locations)); + ASSERT_GE(locations.signature, 2u); + uint16_t app2_length = 0; + ASSERT_TRUE(readMpfU16ForTest(displaced_signature, locations.signature - 2, true, &app2_length)); + displaced_signature[locations.signature + 3] = 'x'; + const size_t displaced_mpf_offset = locations.signature + 4; + displaced_signature.insert(displaced_signature.begin() + displaced_mpf_offset, 4, uint8_t{0}); + displaced_signature[displaced_mpf_offset] = 'M'; + displaced_signature[displaced_mpf_offset + 1] = 'P'; + displaced_signature[displaced_mpf_offset + 2] = 'F'; + writeMpfU16ForTest(displaced_signature, locations.signature - 2, true, app2_length + 4); + EXPECT_EQ(is_uhdr_image(displaced_signature.data(), static_cast(displaced_signature.size())), + 1); + EXPECT_EQ( + uhdr_is_supported_gainmap_image(displaced_signature.data(), displaced_signature.size()), 0); + + for (const uint16_t duplicate_tag : {uint16_t{0xb001}, uint16_t{0xb002}}) { + std::vector duplicate_mandatory_tag( + static_cast(output->data), + static_cast(output->data) + output->data_sz); + ASSERT_TRUE(insertDuplicateMpfTag(duplicate_mandatory_tag, duplicate_tag)); + EXPECT_EQ(is_uhdr_image(duplicate_mandatory_tag.data(), + static_cast(duplicate_mandatory_tag.size())), + 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(duplicate_mandatory_tag.data(), + duplicate_mandatory_tag.size()), + 0); + } + + ASSERT_TRUE(findMpfEntryLocations(malformed_table, &locations)); + struct VersionMutation { + size_t offset; + bool is_u16; + }; + for (const VersionMutation mutation : { + VersionMutation{locations.version_id, true}, + VersionMutation{locations.version_type, true}, + VersionMutation{locations.version_count, false}, + VersionMutation{locations.version_value, false}, + }) { + std::vector malformed_version(static_cast(output->data), + static_cast(output->data) + output->data_sz); + if (mutation.is_u16) { + writeMpfU16ForTest(malformed_version, mutation.offset, locations.big_endian, 0); + } else { + writeMpfU32ForTest(malformed_version, mutation.offset, locations.big_endian, 0); + } + EXPECT_EQ(is_uhdr_image(malformed_version.data(), static_cast(malformed_version.size())), + 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(malformed_version.data(), malformed_version.size()), + 0); + } + + for (const size_t invalid_field : + {locations.primary_attributes, locations.primary_offset, locations.secondary_attributes}) { + std::vector malformed_entry(static_cast(output->data), + static_cast(output->data) + output->data_sz); + ASSERT_TRUE(findMpfEntryLocations(malformed_entry, &locations)); + writeMpfU32ForTest(malformed_entry, invalid_field, locations.big_endian, 1); + EXPECT_EQ(is_uhdr_image(malformed_entry.data(), static_cast(malformed_entry.size())), 1); + EXPECT_EQ(uhdr_is_supported_gainmap_image(malformed_entry.data(), malformed_entry.size()), 0); + } // Decode stream uhdr_codec_private_t* dec = uhdr_create_decoder(); @@ -327,6 +796,82 @@ static bool setBackwardDirectionFlag(const uhdr_compressed_image_t* image, return true; } +TEST_F(UltraHdrApiTest, RoutingProbeReflectsHevcDecoderAvailability) { + std::vector hevc_gainmap; + ASSERT_TRUE(loadFile(kHevcGainmapFileName, hevc_gainmap)); + + EXPECT_EQ(is_uhdr_image(hevc_gainmap.data(), static_cast(hevc_gainmap.size())), 1); + const bool have_hevc_decoder = heif_have_decoder_for_format(heif_compression_HEVC); + expectSupportedHeifGainmap(hevc_gainmap.data(), hevc_gainmap.size(), have_hevc_decoder); + + if (have_hevc_decoder) { + uhdr_compressed_image_t input{}; + input.data = hevc_gainmap.data(); + input.data_sz = hevc_gainmap.size(); + input.capacity = hevc_gainmap.size(); + input.cg = UHDR_CG_UNSPECIFIED; + input.ct = UHDR_CT_UNSPECIFIED; + input.range = UHDR_CR_UNSPECIFIED; + uhdr_codec_private_t* decoder = uhdr_create_decoder(); + ASSERT_NE(decoder, nullptr); + EXPECT_EQ(uhdr_dec_set_image(decoder, &input).error_code, UHDR_CODEC_OK); + EXPECT_EQ(uhdr_dec_set_out_color_transfer(decoder, UHDR_CT_HLG).error_code, UHDR_CODEC_OK); + EXPECT_EQ(uhdr_dec_set_out_img_format(decoder, UHDR_IMG_FMT_32bppRGBA1010102).error_code, + UHDR_CODEC_OK); + EXPECT_EQ(uhdr_dec_probe(decoder).error_code, UHDR_CODEC_OK); + EXPECT_EQ(uhdr_decode(decoder).error_code, UHDR_CODEC_OK); + uhdr_release_decoder(decoder); + } +} + +TEST_F(UltraHdrApiTest, RoutingProbeRejectsOrdinaryAvifButLegacyPredicateRemainsCompatible) { + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + GTEST_SKIP() << "AV1 encoder plugin not available in environment"; + } + std::vector ordinary_avif; + ASSERT_TRUE(encodeOrdinaryAvif(ordinary_avif)); + ASSERT_FALSE(ordinary_avif.empty()); + + // The historical decoder-probe implementation returns 1 for a parseable HEIF/AVIF container + // even when it has no gain map. Keep that compatibility behavior explicit here. + EXPECT_EQ(is_uhdr_image(ordinary_avif.data(), static_cast(ordinary_avif.size())), 1); + expectSupportedHeifGainmap(ordinary_avif.data(), ordinary_avif.size(), false); +} + +TEST(UltraHdrRoutingTest, GainmapSideAlphaIsSupported) { + if (!heif_have_encoder_for_format(heif_compression_AV1)) { + GTEST_SKIP() << "AV1 encoder plugin not available in environment"; + } + + std::vector encoded; + ASSERT_TRUE(encodeGainmapSideAlphaAvif(encoded)); + + HeifContextPtr context(heif_context_alloc(), heif_context_free); + ASSERT_NE(context, nullptr); + ASSERT_EQ(heif_context_read_from_memory_without_copy(context.get(), encoded.data(), + encoded.size(), nullptr) + .code, + heif_error_Ok); + + heif_image_handle* primary_raw = nullptr; + ASSERT_EQ(heif_context_get_primary_image_handle(context.get(), &primary_raw).code, + heif_error_Ok); + HeifHandlePtr primary(primary_raw, heif_image_handle_release); + ASSERT_NE(primary, nullptr); + + heif_image_handle* gainmap_raw = nullptr; + ASSERT_EQ(heif_image_handle_get_gain_map_image_handle(primary.get(), &gainmap_raw).code, + heif_error_Ok); + HeifHandlePtr gainmap(gainmap_raw, heif_image_handle_release); + ASSERT_NE(gainmap, nullptr); + + EXPECT_EQ(heif_image_handle_has_alpha_channel(primary.get()), 0); + EXPECT_EQ(heif_image_handle_has_alpha_channel(gainmap.get()), 1); + EXPECT_EQ(heif_image_handle_is_premultiplied_alpha(gainmap.get()), 0); + expectSupportedHeifGainmap(encoded.data(), encoded.size(), + heif_have_decoder_for_format(heif_compression_AV1)); +} + namespace { constexpr size_t kAlphaTestWidth = 64; @@ -542,6 +1087,10 @@ TEST_F(UltraHdrApiTest, HeicEncodeApi0AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); + EXPECT_EQ(is_uhdr_image(output->data, static_cast(output->data_sz)), 1); + expectSupportedHeifGainmap(output->data, output->data_sz, + heif_have_decoder_for_format(heif_compression_HEVC)); + // Decode HEIC stream uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -609,6 +1158,9 @@ TEST_F(UltraHdrApiTest, HeicEncodeApi1AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); EXPECT_EQ(getPrimaryImageTransfer(output), heif_transfer_characteristic_IEC_61966_2_1); + EXPECT_EQ(is_uhdr_image(output->data, static_cast(output->data_sz)), 1); + expectSupportedHeifGainmap(output->data, output->data_sz, + heif_have_decoder_for_format(heif_compression_HEVC)); uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -683,6 +1235,10 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi0AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); + EXPECT_EQ(is_uhdr_image(output->data, static_cast(output->data_sz)), 1); + expectSupportedHeifGainmap(output->data, output->data_sz, + heif_have_decoder_for_format(heif_compression_AV1)); + // Decode AVIF stream uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -746,6 +1302,9 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi1AndDecode) { ASSERT_NE(output, nullptr); ASSERT_GT(output->data_sz, 0u); EXPECT_EQ(getPrimaryImageTransfer(output), heif_transfer_characteristic_IEC_61966_2_1); + EXPECT_EQ(is_uhdr_image(output->data, static_cast(output->data_sz)), 1); + expectSupportedHeifGainmap(output->data, output->data_sz, + heif_have_decoder_for_format(heif_compression_AV1)); uhdr_codec_private_t* dec = uhdr_create_decoder(); ASSERT_NE(dec, nullptr); @@ -972,6 +1531,12 @@ TEST_F(UltraHdrApiTest, AvifPreservesRawAlpha) { GTEST_SKIP() << "AV1 encoder plugin not available in environment: " << status.detail; } ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << (status.has_detail ? status.detail : "no detail"); + // This fixture has straight alpha on the primary AVIF item. Its gain-map item is ordinary + // monochrome and is not alpha-bearing. When item inspection is available, the routing assertion + // covers the primary-alpha form; the helper below also performs a real SDR decode to RGBA. + EXPECT_EQ(is_uhdr_image(encoded.data(), static_cast(encoded.size())), 1); + expectSupportedHeifGainmap(encoded.data(), encoded.size(), + heif_have_decoder_for_format(heif_compression_AV1)); EXPECT_TRUE(decodedAlphaMatches(encoded)); expectRawAlphaPreserved(UHDR_CODEC_AVIF, &images.hdrHalfFloatDesc); images.makeHdr1010102AlphaOpaque(); @@ -987,6 +1552,9 @@ TEST_F(UltraHdrApiTest, HeifPreservesRawAlpha) { GTEST_SKIP() << "HEVC encoder plugin not available in environment: " << status.detail; } ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << (status.has_detail ? status.detail : "no detail"); + EXPECT_EQ(is_uhdr_image(encoded.data(), static_cast(encoded.size())), 1); + expectSupportedHeifGainmap(encoded.data(), encoded.size(), + heif_have_decoder_for_format(heif_compression_HEVC)); EXPECT_TRUE(decodedAlphaMatches(encoded)); expectRawAlphaPreserved(UHDR_CODEC_HEIF, &images.hdrHalfFloatDesc); images.makeHdr1010102AlphaOpaque(); diff --git a/ultrahdr_api.h b/ultrahdr_api.h index 39e529f5..4a112359 100644 --- a/ultrahdr_api.h +++ b/ultrahdr_api.h @@ -614,9 +614,46 @@ UHDR_EXTERN void uhdr_reset_encoder(uhdr_codec_private_t* enc); * @returns 1 if the input data has a primary image, gain map image and gain map metadata. 0 if any * errors are encountered during parsing process or if the image does not have primary * image or gainmap image or gainmap metadata + * + * This is a structural check and does not guarantee that the current runtime can decode the image. + * The legacy implementation retains its historical decoder-probe behavior. */ UHDR_EXTERN int is_uhdr_image(void* data, int size); +/*!\brief Check whether this runtime supports routing a complete gain-map image to libultrahdr. + * + * This function validates the gain-map structure and metadata without decoding pixels. For + * HEIF/AVIF inputs, the current runtime-support policy is conservative: the primary and gain-map + * items must each be directly coded as AV1 (av01) or HEVC (hvc1/hev1), and any directly associated + * alpha dependency must also be directly coded and use a supported color layout (including + * monochrome for alpha). Derived-image layouts, unresolved dependencies, and nested alpha + * dependencies are rejected. The primary and gain-map items must use dimensions and color layouts + * supported by libultrahdr. Their required decoder families must all be available. Other + * structurally valid HEIF/AVIF gain-map structures may return 0. If the build lacks libheif item + * inspection, this function returns 0 for HEIF/AVIF; existing encode/decode APIs and JPEG routing + * remain available. A positive result is a routing hint, not a guarantee that the compressed + * payload is complete or that every payload or codec profile will decode successfully. + * + * Because this function includes runtime and layout checks, a structurally recognized image may + * return 1 from is_uhdr_image() but 0 here. + * This function is the strict MPF validator for JPEG routing and covers the fields needed to + * associate the exact secondary image; unrelated primary-image size bookkeeping is tolerated for + * compatibility with existing Apple-authored files. The legacy is_uhdr_image() probe is not + * replaced by this stricter MPF validation. + * + * The input buffer is borrowed for the duration of the call and is not modified or retained. The + * complete encoded stream and embedded JPEG payloads are not duplicated; parsers may allocate + * storage for container structures and metadata. + * + * @param[in] data pointer to a complete input compressed stream + * @param[in] size size of the compressed stream + * + * @returns 1 if the image has a supported gain-map structure and the required decoder families are + * available. 0 for ordinary images, malformed gain-map structure or metadata, unsupported + * HEIF/AVIF item or layout forms, unavailable decoder families, or parsing errors. + */ +UHDR_EXTERN int uhdr_is_supported_gainmap_image(const void* data, size_t size); + /*!\brief Create a new decoder instance. The instance is initialized with default settings. * To override the settings use uhdr_dec_set_*() *