Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/src/avifultrahdr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions lib/src/gainmapmetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <algorithm>
#include <cmath>
#include <cstdint>

#include "ultrahdr/gainmapmath.h"
#include "ultrahdr/gainmapmetadata.h"
Expand Down Expand Up @@ -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) {
Expand All @@ -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<int64_t>(from->gainMapMaxN[i]) * from->gainMapMinD[i] <
static_cast<int64_t>(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
Expand All @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/heifultrahdr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
46 changes: 31 additions & 15 deletions lib/src/ultrahdr_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> 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);
}
Expand Down
89 changes: 89 additions & 0 deletions tests/gainmapmetadata_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

#include <gtest/gtest.h>
#include <climits>
#include <vector>

#include "ultrahdr/gainmapmetadata.h"
Expand All @@ -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++) {
Expand Down Expand Up @@ -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
102 changes: 102 additions & 0 deletions tests/ultrahdr_api_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#else
#include <gtest/gtest.h>
#endif
#include <algorithm>
#include <fstream>
#include <vector>
#include <memory>
Expand Down Expand Up @@ -187,6 +188,43 @@ TEST_F(UltraHdrApiTest, JpegEncodeApi2AndDecode) {
// ============================================================================

#if defined(UHDR_ENABLE_HEIF)
static bool setBackwardDirectionFlag(const uhdr_compressed_image_t* image,
std::vector<uint8_t>& modified_image) {
std::unique_ptr<heif_context, decltype(&heif_context_free)> 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<heif_image_handle, decltype(&heif_image_handle_release)> 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<uint8_t> 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<const uint8_t*>(image->data),
static_cast<const uint8_t*>(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);
Expand Down Expand Up @@ -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<uint8_t> 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<uint8_t> 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);
Expand Down
Loading