diff --git a/fuzzer/ultrahdr_dec_fuzzer.cpp b/fuzzer/ultrahdr_dec_fuzzer.cpp index e1008b47..45ef120c 100644 --- a/fuzzer/ultrahdr_dec_fuzzer.cpp +++ b/fuzzer/ultrahdr_dec_fuzzer.cpp @@ -69,6 +69,14 @@ void UltraHdrDecFuzzer::process() { (void)is_uhdr_image(buffer.data(), buffer.size()); + uhdr_mem_block_t stripped_size{nullptr, 0, 0}; + if (uhdr_strip_gain_map(&jpegImgR, &stripped_size).error_code == UHDR_CODEC_OK && + stripped_size.data_sz > 0 && stripped_size.data_sz <= buffer.size() + 65536) { + std::vector stripped_buf(stripped_size.data_sz); + uhdr_mem_block_t stripped_out{stripped_buf.data(), 0, stripped_buf.size()}; + (void)uhdr_strip_gain_map(&jpegImgR, &stripped_out); + } + uhdr_codec_private_t* dec_handle = uhdr_create_decoder(); if (dec_handle) { ON_ERR(uhdr_dec_set_image(dec_handle, &jpegImgR)) @@ -106,6 +114,7 @@ void UltraHdrDecFuzzer::process() { uhdr_dec_get_exif(dec_handle); uhdr_dec_get_icc(dec_handle); + uhdr_dec_get_xmp(dec_handle); uhdr_dec_get_base_image(dec_handle); uhdr_dec_get_gainmap_image(dec_handle); uhdr_dec_get_gainmap_metadata(dec_handle); diff --git a/fuzzer/ultrahdr_enc_fuzzer.cpp b/fuzzer/ultrahdr_enc_fuzzer.cpp index 92794682..71e3576f 100644 --- a/fuzzer/ultrahdr_enc_fuzzer.cpp +++ b/fuzzer/ultrahdr_enc_fuzzer.cpp @@ -427,6 +427,13 @@ void UltraHdrEncFuzzer::process() { if (status.error_code == UHDR_CODEC_OK) { auto output = uhdr_get_encoded_stream(enc_handle); if (output != nullptr) { + uhdr_mem_block_t stripped_size{nullptr, 0, 0}; + if (uhdr_strip_gain_map(output, &stripped_size).error_code == UHDR_CODEC_OK && + stripped_size.data_sz > 0) { + std::vector stripped_buf(stripped_size.data_sz); + uhdr_mem_block_t stripped_out{stripped_buf.data(), 0, stripped_buf.size()}; + ON_ERR(uhdr_strip_gain_map(output, &stripped_out)) + } uhdr_codec_private_t* dec_handle = uhdr_create_decoder(); if (dec_handle) { ON_ERR(uhdr_dec_set_image(dec_handle, output)) @@ -438,6 +445,7 @@ void UltraHdrEncFuzzer::process() { else ON_ERR(uhdr_dec_set_out_img_format(dec_handle, UHDR_IMG_FMT_32bppRGBA1010102)) ON_ERR(uhdr_decode(dec_handle)) + uhdr_dec_get_xmp(dec_handle); uhdr_release_decoder(dec_handle); } } diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp index 6c282909..fe5efa3b 100644 --- a/lib/src/jpegr.cpp +++ b/lib/src/jpegr.cpp @@ -1285,6 +1285,7 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr } } + bool spill_user_xmp_to_extended = false; if (kWriteXmpMetadata) { if (user_xmp_str.empty()) { xmp_primary_str = generateXmpForPrimaryImage(secondary_image_size, *metadata, nullptr); @@ -1300,15 +1301,21 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr "unable to safely merge supplied XMP metadata into the primary image"); return status; } + if (xmp_primary_str.size() > kMaxStandardXmpPayload) { + xmp_primary_str = user_xmp_str; + spill_user_xmp_to_extended = true; + } } else { xmp_primary_str = user_xmp_str; + spill_user_xmp_to_extended = true; } } else if (!user_xmp_str.empty()) { xmp_primary_str = user_xmp_str; + spill_user_xmp_to_extended = xmp_primary_str.size() > kMaxStandardXmpPayload; } if (!xmp_primary_str.empty()) { - if (xmp_primary_str.size() <= kMaxStandardXmpPayload) { + if (!spill_user_xmp_to_extended && xmp_primary_str.size() <= kMaxStandardXmpPayload) { // Write single standard XMP segment const size_t length = 2 + xmpNameSpaceLength + xmp_primary_str.size(); const uint8_t lengthH = ((length >> 8) & 0xff); diff --git a/lib/src/ultrahdr_api.cpp b/lib/src/ultrahdr_api.cpp index 90440a96..fd5e9e12 100644 --- a/lib/src/ultrahdr_api.cpp +++ b/lib/src/ultrahdr_api.cpp @@ -18,13 +18,6 @@ #include "ultrahdr/editorhelper.h" #include "ultrahdr/jpegr.h" -#ifdef UHDR_ENABLE_HEIF -#include "ultrahdr/heifultrahdr.h" -#include "ultrahdr/avifultrahdr.h" -#include "ultrahdr/gainmapmetadata.h" -#endif - - #ifdef UHDR_ENABLE_HEIF #include "ultrahdr/heifultrahdr.h" #include "ultrahdr/avifultrahdr.h" @@ -1319,14 +1312,10 @@ uhdr_error_info_t uhdr_encode(uhdr_codec_private_t* enc) { const bool mayWriteXmp = !handle->m_xmp.empty() || !handle->m_compressed_images.empty(); size_t xmpAllowance = 0; if (mayWriteXmp) { - if (handle->m_xmp.size() <= ultrahdr::kMaxStandardXmpPayload) { - xmpAllowance = ultrahdr::kJpegAppSegmentTotalMaxBytes; - } else { - const size_t xmp_sz = handle->m_xmp.size(); - const size_t num_chunks = - (xmp_sz + ultrahdr::kExtendedXmpMaxChunkSize - 1) / ultrahdr::kExtendedXmpMaxChunkSize; - xmpAllowance = ultrahdr::kJpegAppSegmentTotalMaxBytes + xmp_sz + num_chunks * 79; - } + const size_t xmp_sz = handle->m_xmp.size(); + const size_t num_chunks = + (xmp_sz + ultrahdr::kExtendedXmpMaxChunkSize - 1) / ultrahdr::kExtendedXmpMaxChunkSize; + xmpAllowance = ultrahdr::kJpegAppSegmentTotalMaxBytes + xmp_sz + num_chunks * 79; } auto addXmpAllowance = [xmpAllowance](size_t base_size, size_t* output_size) { if (xmpAllowance > (std::numeric_limits::max)() - base_size) return false; diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index 745b950b..8c9f3006 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -1121,6 +1121,96 @@ TEST_F(UltraHdrApiTest, StripGainMapInvalidParamsAndCorruptInput) { EXPECT_EQ(uhdr_strip_gain_map(&no_sos_in, &out).error_code, UHDR_CODEC_INVALID_PARAM); } +TEST_F(UltraHdrApiTest, StripGainMapOnAppleGainMapFixtures) { + for (const char* filename : {"apple_gainmap_new.jpg", "apple_gainmap_old.jpg"}) { + SCOPED_TRACE(filename); + std::vector apple_data; + ASSERT_TRUE(loadFile(filename, apple_data)); + ASSERT_EQ(is_uhdr_image(apple_data.data(), static_cast(apple_data.size())), 1); + + uhdr_compressed_image_t apple_in{apple_data.data(), apple_data.size(), apple_data.size(), + UHDR_CG_DISPLAY_P3, UHDR_CT_SRGB, UHDR_CR_FULL_RANGE}; + uhdr_mem_block_t size_query{nullptr, 0, 0}; + ASSERT_EQ(uhdr_strip_gain_map(&apple_in, &size_query).error_code, UHDR_CODEC_OK); + ASSERT_GT(size_query.data_sz, 0u); + ASSERT_LT(size_query.data_sz, apple_data.size()); + + std::vector stripped_buf(size_query.data_sz); + uhdr_mem_block_t stripped_block{stripped_buf.data(), 0, stripped_buf.size()}; + ASSERT_EQ(uhdr_strip_gain_map(&apple_in, &stripped_block).error_code, UHDR_CODEC_OK); + EXPECT_EQ(stripped_block.data_sz, size_query.data_sz); + EXPECT_EQ(is_uhdr_image(stripped_block.data, static_cast(stripped_block.data_sz)), 0); + + // Primary entropy-coded scan (including DRI and RST0..RST7 markers) must be bit-identical. + const std::vector orig_scan = + extractPrimaryScanBytes(apple_data.data(), apple_data.size()); + const std::vector stripped_scan = + extractPrimaryScanBytes(stripped_block.data, stripped_block.data_sz); + ASSERT_FALSE(orig_scan.empty()); + EXPECT_EQ(orig_scan, stripped_scan); + + // Verify EXIF and ICC profile are preserved and stripped JPEG decodes cleanly. + JpegDecoderHelper orig_decoder; + ASSERT_EQ(orig_decoder.parseImage(apple_data.data(), apple_data.size()).error_code, + UHDR_CODEC_OK); + JpegDecoderHelper stripped_decoder; + ASSERT_EQ(stripped_decoder + .decompressImage(stripped_block.data, stripped_block.data_sz, DECODE_TO_RGB_CS) + .error_code, + UHDR_CODEC_OK); + ASSERT_EQ(stripped_decoder.getEXIFSize(), orig_decoder.getEXIFSize()); + EXPECT_EQ(memcmp(stripped_decoder.getEXIFPtr(), orig_decoder.getEXIFPtr(), + orig_decoder.getEXIFSize()), + 0); + ASSERT_EQ(stripped_decoder.getICCSize(), orig_decoder.getICCSize()); + EXPECT_EQ(memcmp(stripped_decoder.getICCPtr(), orig_decoder.getICCPtr(), + orig_decoder.getICCSize()), + 0); + } +} + +TEST_F(UltraHdrApiTest, NearLimitStandardXmpSpillsCleanlyToExtendedXmp) { + // 65,200 bytes is <= kMaxStandardXmpPayload (65,503 B), but when UHDR_WRITE_XMP is enabled, + // merging the ~600-byte Ultra HDR container directory pushes it over 65,503 B into Extended XMP. + constexpr size_t kNearLimitSize = 65200; + const std::string near_limit_xmp = makeLargeXmp(kNearLimitSize); + uhdr_mem_block_t xmp_block{const_cast(near_limit_xmp.data()), near_limit_xmp.size(), + near_limit_xmp.size()}; + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + + EncoderPtr enc = makeEncoder(); + ASSERT_NE(enc, nullptr); + ASSERT_EQ(configureJpegGainmapEncoder(enc.get(), &mSdrCompressed, &mSdrCompressed, &metadata, + &xmp_block) + .error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_encode(enc.get()).error_code, UHDR_CODEC_OK); + uhdr_compressed_image_t* uhdr_stream = uhdr_get_encoded_stream(enc.get()); + ASSERT_NE(uhdr_stream, nullptr); + + // Verify uhdr_dec_get_xmp recovers the exact 65,200-byte user XMP packet. + DecoderPtr dec = makeDecoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec.get(), uhdr_stream).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec.get()).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec.get()); + ASSERT_NE(decoded_xmp, nullptr); + EXPECT_EQ(getXmpPacket(decoded_xmp), near_limit_xmp); + + // Verify uhdr_strip_gain_map also preserves the 65,200-byte user XMP packet cleanly. + std::vector stripped_buf(uhdr_stream->data_sz); + uhdr_mem_block_t stripped_block{stripped_buf.data(), 0, stripped_buf.size()}; + ASSERT_EQ(uhdr_strip_gain_map(uhdr_stream, &stripped_block).error_code, UHDR_CODEC_OK); + EXPECT_EQ(is_uhdr_image(stripped_block.data, static_cast(stripped_block.data_sz)), 0); + + JpegDecoderHelper stripped_decoder; + ASSERT_EQ(stripped_decoder.parseImage(stripped_block.data, stripped_block.data_sz).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t stripped_xmp_mem{stripped_decoder.getXMPPtr(), stripped_decoder.getXMPSize(), + stripped_decoder.getXMPSize()}; + EXPECT_EQ(getXmpPacket(&stripped_xmp_mem), near_limit_xmp); +} + TEST_F(UltraHdrApiTest, JpegEncodeWithXmpAndDecode) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr);