diff --git a/lib/src/avifultrahdr.cpp b/lib/src/avifultrahdr.cpp index c1184a91..ec63fe4a 100644 --- a/lib/src/avifultrahdr.cpp +++ b/lib/src/avifultrahdr.cpp @@ -627,7 +627,7 @@ uhdr_error_info_t AvifUltraHdr::decodeAvifUltraHdr(uhdr_compressed_image_t* uhdr if (gainmap_metadata != nullptr || output_ct != UHDR_CT_SRGB) { HEIF_ERR_CHECK(heif_image_handle_get_gain_map_image_handle(base_handle, &gainmap_handle)) status = heif_get_gainmap_metadata(base_handle, &metadata); - if (status.error_code != UHDR_CODEC_OK) return status; + if (status.error_code != UHDR_CODEC_OK) goto CleanUp; if (gainmap_metadata != nullptr) { std::copy(metadata.min_content_boost, metadata.min_content_boost + 3, gainmap_metadata->min_content_boost); diff --git a/lib/src/gainmapmetadata.cpp b/lib/src/gainmapmetadata.cpp index e1b965a8..0c6bedde 100644 --- a/lib/src/gainmapmetadata.cpp +++ b/lib/src/gainmapmetadata.cpp @@ -10,6 +10,7 @@ #include #include +#include #include "ultrahdr/gainmapmath.h" #include "ultrahdr/gainmapmetadata.h" @@ -297,6 +298,14 @@ uhdr_error_info_t uhdr_gainmap_metadata_frac::decodeGainmapMetadata( return status; \ } +static uhdr_error_info_t invalidGainMapMetadata(const char *detail) { + uhdr_error_info_t status; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "%s", detail); + return status; +} + uhdr_error_info_t uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat( const uhdr_gainmap_metadata_frac *from, uhdr_gainmap_metadata_ext_t *to) { if (from == nullptr || to == nullptr) { @@ -316,6 +325,11 @@ uhdr_error_info_t uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat( UHDR_CHECK_NON_ZERO(from->gainMapMinD[i], "gainMapMin denominator"); UHDR_CHECK_NON_ZERO(from->baseOffsetD[i], "baseOffset denominator"); UHDR_CHECK_NON_ZERO(from->alternateOffsetD[i], "alternateOffset denominator"); + if (static_cast(from->gainMapMaxN[i]) * from->gainMapMinD[i] < + static_cast(from->gainMapMinN[i]) * from->gainMapMaxD[i]) { + return invalidGainMapMetadata( + "decoded gain map maximum content boost is less than its minimum"); + } } // jpeg supports only 8 bits per component, applying gainmap in inverse direction is unexpected @@ -342,6 +356,23 @@ uhdr_error_info_t uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat( to->hdr_capacity_min = exp2((float)from->baseHdrHeadroomN / from->baseHdrHeadroomD); to->use_base_cg = from->useBaseColorSpace; + for (int i = 0; i < 3; ++i) { + if (!std::isfinite(to->min_content_boost[i]) || !std::isfinite(to->max_content_boost[i]) || + !std::isfinite(to->gamma[i]) || !std::isfinite(to->offset_sdr[i]) || + !std::isfinite(to->offset_hdr[i])) { + return invalidGainMapMetadata("decoded gain map metadata contains a non-finite value"); + } + if (to->min_content_boost[i] <= 0.0f || to->max_content_boost[i] <= 0.0f) { + return invalidGainMapMetadata("decoded gain map content boost must be positive"); + } + if (to->gamma[i] <= 0.0f) { + return invalidGainMapMetadata("decoded gain map gamma must be positive"); + } + } + if (!std::isfinite(to->hdr_capacity_min) || !std::isfinite(to->hdr_capacity_max) || + to->hdr_capacity_min <= 0.0f || to->hdr_capacity_max <= 0.0f) { + return invalidGainMapMetadata("decoded HDR capacity must be positive and finite"); + } return g_no_error; } diff --git a/lib/src/heifultrahdr.cpp b/lib/src/heifultrahdr.cpp index b6480c68..bf10cdda 100644 --- a/lib/src/heifultrahdr.cpp +++ b/lib/src/heifultrahdr.cpp @@ -627,7 +627,7 @@ uhdr_error_info_t HeifUltraHdr::decodeHeicUltraHdr(uhdr_compressed_image_t* uhdr if (gainmap_metadata != nullptr || output_ct != UHDR_CT_SRGB) { HEIF_ERR_CHECK(heif_image_handle_get_gain_map_image_handle(base_handle, &gainmap_handle)) status = heif_get_gainmap_metadata(base_handle, &metadata); - if (status.error_code != UHDR_CODEC_OK) return status; + if (status.error_code != UHDR_CODEC_OK) goto CleanUp; if (gainmap_metadata != nullptr) { std::copy(metadata.min_content_boost, metadata.min_content_boost + 3, gainmap_metadata->min_content_boost); diff --git a/lib/src/ultrahdr_api.cpp b/lib/src/ultrahdr_api.cpp index 2fd00629..efb56fd1 100644 --- a/lib/src/ultrahdr_api.cpp +++ b/lib/src/ultrahdr_api.cpp @@ -1714,25 +1714,41 @@ uhdr_error_info_t uhdr_dec_probe(uhdr_codec_private_t* dec) { handle->m_gainmap_ht = heif_image_handle_get_height(gainmap_handle); handle->m_gainmap_num_comp = 1; - int meta_len = heif_image_handle_get_gain_map_metadata_size(base_handle); + size_t meta_len = heif_image_handle_get_gain_map_metadata_size(base_handle); if (meta_len > 0) { std::vector meta(meta_len); - heif_image_handle_get_gain_map_metadata(base_handle, meta.data()); - ultrahdr::uhdr_gainmap_metadata_frac frac; - if (ultrahdr::uhdr_gainmap_metadata_frac::decodeGainmapMetadata(meta, &frac).error_code == UHDR_CODEC_OK) { + err = heif_image_handle_get_gain_map_metadata(base_handle, meta.data()); + if (err.code != heif_error_Ok) { + status.error_code = UHDR_CODEC_ERROR; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "%s", err.message); + } else { + ultrahdr::uhdr_gainmap_metadata_frac frac; + status = ultrahdr::uhdr_gainmap_metadata_frac::decodeGainmapMetadata(meta, &frac); ultrahdr::uhdr_gainmap_metadata_ext_t metadata; - ultrahdr::uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&frac, &metadata); - std::copy(metadata.max_content_boost, metadata.max_content_boost + 3, - handle->m_metadata.max_content_boost); - std::copy(metadata.min_content_boost, metadata.min_content_boost + 3, - handle->m_metadata.min_content_boost); - std::copy(metadata.gamma, metadata.gamma + 3, handle->m_metadata.gamma); - std::copy(metadata.offset_sdr, metadata.offset_sdr + 3, handle->m_metadata.offset_sdr); - std::copy(metadata.offset_hdr, metadata.offset_hdr + 3, handle->m_metadata.offset_hdr); - handle->m_metadata.hdr_capacity_min = metadata.hdr_capacity_min; - handle->m_metadata.hdr_capacity_max = metadata.hdr_capacity_max; - handle->m_metadata.use_base_cg = metadata.use_base_cg; + if (status.error_code == UHDR_CODEC_OK) { + status = ultrahdr::uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat( + &frac, &metadata); + } + if (status.error_code == UHDR_CODEC_OK) { + std::copy(metadata.max_content_boost, metadata.max_content_boost + 3, + handle->m_metadata.max_content_boost); + std::copy(metadata.min_content_boost, metadata.min_content_boost + 3, + handle->m_metadata.min_content_boost); + std::copy(metadata.gamma, metadata.gamma + 3, handle->m_metadata.gamma); + std::copy(metadata.offset_sdr, metadata.offset_sdr + 3, + handle->m_metadata.offset_sdr); + std::copy(metadata.offset_hdr, metadata.offset_hdr + 3, + handle->m_metadata.offset_hdr); + handle->m_metadata.hdr_capacity_min = metadata.hdr_capacity_min; + handle->m_metadata.hdr_capacity_max = metadata.hdr_capacity_max; + handle->m_metadata.use_base_cg = metadata.use_base_cg; + } } + } else { + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, "gain map metadata is empty"); } heif_image_handle_release(gainmap_handle); } diff --git a/tests/gainmapmetadata_test.cpp b/tests/gainmapmetadata_test.cpp index 9ffcd78f..24e94ced 100644 --- a/tests/gainmapmetadata_test.cpp +++ b/tests/gainmapmetadata_test.cpp @@ -9,6 +9,7 @@ */ #include +#include #include #include "ultrahdr/gainmapmetadata.h" @@ -35,6 +36,23 @@ void GainMapMetadataTest::TearDown() {} const std::string kIso = "urn:iso:std:iso:ts:21496:-1"; +static uhdr_gainmap_metadata_frac makeValidFractionMetadata() { + uhdr_gainmap_metadata_frac metadata{}; + metadata.baseHdrHeadroomD = 1; + metadata.alternateHdrHeadroomN = 3; + metadata.alternateHdrHeadroomD = 1; + for (int i = 0; i < 3; ++i) { + metadata.gainMapMinD[i] = 1; + metadata.gainMapMaxN[i] = 2; + metadata.gainMapMaxD[i] = 1; + metadata.gainMapGammaN[i] = 1; + metadata.gainMapGammaD[i] = 1; + metadata.baseOffsetD[i] = 1; + metadata.alternateOffsetD[i] = 1; + } + return metadata; +} + TEST_F(GainMapMetadataTest, encodeMetadataThenDecode) { uhdr_gainmap_metadata_ext_t expected("1.0"); for (int i = 0; i < 3; i++) { @@ -112,4 +130,75 @@ TEST_F(GainMapMetadataTest, encodeMetadataThenDecode) { EXPECT_EQ(expected.use_base_cg, decodedUHdrMetadata.use_base_cg); } +TEST_F(GainMapMetadataTest, fractionToFloatRejectsInvalidValues) { + uhdr_gainmap_metadata_ext_t converted; + + uhdr_gainmap_metadata_frac metadata = makeValidFractionMetadata(); + metadata.gainMapGammaD[0] = 0; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + metadata = makeValidFractionMetadata(); + metadata.gainMapGammaN[0] = 0; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + metadata = makeValidFractionMetadata(); + metadata.gainMapMaxN[0] = INT32_MAX; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + metadata = makeValidFractionMetadata(); + metadata.gainMapMinN[0] = INT32_MIN; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + metadata = makeValidFractionMetadata(); + metadata.alternateHdrHeadroomN = UINT32_MAX; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + metadata = makeValidFractionMetadata(); + metadata.gainMapMinN[0] = 2; + metadata.gainMapMaxN[0] = 1; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); + + // These fractions are ordered incorrectly but both round to 1.0f. Validate their exact rational + // values so float precision cannot hide the invalid range. + metadata = makeValidFractionMetadata(); + metadata.gainMapMinN[0] = 16777217; + metadata.gainMapMinD[0] = 16777216; + metadata.gainMapMaxN[0] = 1; + metadata.gainMapMaxD[0] = 1; + EXPECT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_INVALID_PARAM); +} + +TEST_F(GainMapMetadataTest, fractionToFloatAcceptsNegativeOffsets) { + uhdr_gainmap_metadata_frac metadata = makeValidFractionMetadata(); + for (int i = 0; i < 3; ++i) { + metadata.baseOffsetN[i] = -1; + metadata.baseOffsetD[i] = 16; + metadata.alternateOffsetN[i] = -1; + metadata.alternateOffsetD[i] = 16; + } + + uhdr_gainmap_metadata_ext_t converted; + ASSERT_EQ( + uhdr_gainmap_metadata_frac::gainmapMetadataFractionToFloat(&metadata, &converted).error_code, + UHDR_CODEC_OK); + for (int i = 0; i < 3; ++i) { + EXPECT_FLOAT_EQ(converted.offset_sdr[i], -0.0625f); + EXPECT_FLOAT_EQ(converted.offset_hdr[i], -0.0625f); + } +} + } // namespace ultrahdr diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index 04bab5eb..56d1afb9 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -7,6 +7,7 @@ #else #include #endif +#include #include #include #include @@ -187,6 +188,43 @@ TEST_F(UltraHdrApiTest, JpegEncodeApi2AndDecode) { // ============================================================================ #if defined(UHDR_ENABLE_HEIF) +static bool setBackwardDirectionFlag(const uhdr_compressed_image_t* image, + std::vector& modified_image) { + std::unique_ptr context(heif_context_alloc(), + heif_context_free); + if (context == nullptr) return false; + heif_error error = heif_context_read_from_memory_without_copy(context.get(), image->data, + image->data_sz, nullptr); + if (error.code != heif_error_Ok) return false; + + heif_image_handle* raw_base_handle = nullptr; + error = heif_context_get_primary_image_handle(context.get(), &raw_base_handle); + std::unique_ptr base_handle( + raw_base_handle, heif_image_handle_release); + if (error.code != heif_error_Ok || base_handle == nullptr) return false; + + const size_t metadata_size = heif_image_handle_get_gain_map_metadata_size(base_handle.get()); + if (metadata_size <= 4) return false; + std::vector metadata(metadata_size); + error = heif_image_handle_get_gain_map_metadata(base_handle.get(), metadata.data()); + if (error.code != heif_error_Ok) return false; + + modified_image.assign(static_cast(image->data), + static_cast(image->data) + image->data_sz); + auto metadata_pos = + std::search(modified_image.begin(), modified_image.end(), metadata.begin(), metadata.end()); + if (metadata_pos == modified_image.end()) return false; + if (std::search(metadata_pos + metadata.size(), modified_image.end(), metadata.begin(), + metadata.end()) != modified_image.end()) { + return false; + } + + // ISO 21496-1 metadata stores its flags after two 16-bit version fields. + // Set backwardDirection, which this decoder explicitly does not support. + metadata_pos[4] |= 4; + return true; +} + TEST_F(UltraHdrApiTest, HeicEncodeApi0AndDecode) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr); @@ -375,6 +413,70 @@ TEST_F(UltraHdrApiTest, AvifEncodeApi1AndDecode) { uhdr_release_encoder(enc); } +TEST_F(UltraHdrApiTest, HeifAndAvifPropagateGainMapMetadataErrors) { + int tested_formats = 0; + for (uhdr_codec_t codec : {UHDR_CODEC_AVIF, UHDR_CODEC_HEIF}) { + SCOPED_TRACE(codec == UHDR_CODEC_AVIF ? "AVIF" : "HEIF"); + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + + ASSERT_EQ(uhdr_enc_set_raw_image(enc, &mHdrRaw, UHDR_HDR_IMG).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, codec).error_code, UHDR_CODEC_OK); + uhdr_error_info_t enc_status = uhdr_encode(enc); + if (enc_status.error_code != UHDR_CODEC_OK && enc_status.has_detail && + (strstr(enc_status.detail, "Unsupported file-type") != nullptr || + strstr(enc_status.detail, "No encoder") != nullptr)) { + uhdr_release_encoder(enc); + continue; + } + ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK); + ++tested_formats; + + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + std::vector invalid_data; + ASSERT_TRUE(setBackwardDirectionFlag(output, invalid_data)); + + uhdr_compressed_image_t invalid_image = *output; + invalid_image.data = invalid_data.data(); + invalid_image.data_sz = invalid_image.capacity = invalid_data.size(); + + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, &invalid_image).error_code, UHDR_CODEC_OK); + EXPECT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_UNSUPPORTED_FEATURE); + uhdr_release_decoder(dec); + + // Exercise each backend error path directly as well. Sanitizer builds verify that these paths + // release the partially decoded libheif objects before returning the metadata error. + std::vector decoded_data(kImageWidth * kImageHeight * 4); + uhdr_raw_image_t decoded_image{}; + decoded_image.fmt = UHDR_IMG_FMT_32bppRGBA8888; + decoded_image.cg = UHDR_CG_BT_709; + decoded_image.ct = UHDR_CT_SRGB; + decoded_image.range = UHDR_CR_FULL_RANGE; + decoded_image.w = kImageWidth; + decoded_image.h = kImageHeight; + decoded_image.planes[UHDR_PLANE_PACKED] = decoded_data.data(); + decoded_image.stride[UHDR_PLANE_PACKED] = kImageWidth; + uhdr_gainmap_metadata_t metadata{}; + uhdr_error_info_t decode_status; + if (codec == UHDR_CODEC_AVIF) { + AvifUltraHdr avif; + decode_status = avif.decodeAvifUltraHdr(&invalid_image, &decoded_image, FLT_MAX, UHDR_CT_SRGB, + UHDR_IMG_FMT_32bppRGBA8888, nullptr, &metadata); + } else { + HeifUltraHdr heif; + decode_status = heif.decodeHeicUltraHdr(&invalid_image, &decoded_image, FLT_MAX, UHDR_CT_SRGB, + UHDR_IMG_FMT_32bppRGBA8888, nullptr, &metadata); + } + EXPECT_EQ(decode_status.error_code, UHDR_CODEC_UNSUPPORTED_FEATURE); + + uhdr_release_encoder(enc); + } + if (tested_formats == 0) GTEST_SKIP() << "AV1 and HEVC encoder plugins are unavailable"; +} + TEST_F(UltraHdrApiTest, AvifCompressedIntentsUnsupported) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr);