From 53f740b914c913127892fcb3c5c6c6f2fd896d93 Mon Sep 17 00:00:00 2001 From: Greg Benz Date: Wed, 16 Sep 2026 13:46:14 -0500 Subject: [PATCH 1/5] Fix primary JPEG XMP merging across namespace prefixes and reencoding --- lib/include/ultrahdr/jpegr.h | 3 + lib/include/ultrahdr/jpegrutils.h | 5 +- lib/src/jpegr.cpp | 9 + lib/src/jpegrutils.cpp | 543 ++++++++++++++++++++++++++++-- tests/ultrahdr_api_test.cpp | 520 ++++++++++++++++++++++++++++ 5 files changed, 1042 insertions(+), 38 deletions(-) diff --git a/lib/include/ultrahdr/jpegr.h b/lib/include/ultrahdr/jpegr.h index f9047d50..a4da8a83 100644 --- a/lib/include/ultrahdr/jpegr.h +++ b/lib/include/ultrahdr/jpegr.h @@ -351,8 +351,11 @@ class JpegR : public UltraHdr { * \param[in] metadata gainmap metadata descriptor * \param[in, out] dest output image descriptor to store compressed ultrahdr * image + * \param[in] pXmp optional primary-image XMP packet to preserve and + * merge when XMP metadata is enabled * * \return uhdr_error_info_t #UHDR_CODEC_OK if operation succeeds, uhdr_codec_err_t otherwise. + * An unsupported or unmergeable XMP packet returns #UHDR_CODEC_INVALID_PARAM in XMP mode. */ uhdr_error_info_t appendGainMap(uhdr_compressed_image_t* sdr_intent_compressed, uhdr_compressed_image_t* gainmap_compressed, diff --git a/lib/include/ultrahdr/jpegrutils.h b/lib/include/ultrahdr/jpegrutils.h index 8539f269..6c217dd5 100644 --- a/lib/include/ultrahdr/jpegrutils.h +++ b/lib/include/ultrahdr/jpegrutils.h @@ -124,7 +124,10 @@ uhdr_error_info_t getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, uint8_t * * * @param secondary_image_length length of secondary image - * @return XMP metadata in type of string + * @param user_xmp optional XMP packet from the primary image. Its unrelated XML is preserved while + * the encoder-owned primary-image gain-map description is replaced. + * @return XMP metadata in type of string. An empty string means that a supplied packet could not be + * safely merged. */ std::string generateXmpForPrimaryImage(size_t secondary_image_length, uhdr_gainmap_metadata_ext_t& metadata, diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp index d93396a1..ffc2e4ff 100644 --- a/lib/src/jpegr.cpp +++ b/lib/src/jpegr.cpp @@ -1270,6 +1270,15 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr std::string xmp_primary_str; if (kWriteXmpMetadata) { xmp_primary_str = generateXmpForPrimaryImage(secondary_image_size, *metadata, pXmp); + if (pXmp != nullptr && pXmp->data != nullptr && pXmp->data_sz > 0 && + xmp_primary_str.empty()) { + uhdr_error_info_t status; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, + "unable to safely merge supplied XMP metadata into the primary image"); + return status; + } } else if (pXmp != nullptr && pXmp->data != nullptr && pXmp->data_sz > 0) { const std::string kXmpHeader = "http://ns.adobe.com/xap/1.0/"; if (pXmp->data_sz > kXmpHeader.size() && diff --git a/lib/src/jpegrutils.cpp b/lib/src/jpegrutils.cpp index 032b03d2..389d46b5 100644 --- a/lib/src/jpegrutils.cpp +++ b/lib/src/jpegrutils.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include "ultrahdr/ultrahdrcommon.h" #include "ultrahdr/jpegr.h" @@ -424,6 +426,7 @@ class XMPXmlHandler : public XmlHandler { }; // GContainer XMP constants - URI and namespace prefix +const string kRdfUri = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; const string kContainerUri = "http://ns.google.com/photos/1.0/container/"; const string kContainerPrefix = "Container"; @@ -873,12 +876,510 @@ uhdr_error_info_t getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, uint8_t return uhdr_validate_gainmap_metadata_descriptor(metadata); } +namespace { + +constexpr size_t kNoXmpElement = (std::numeric_limits::max)(); + +bool IsXmlWhitespace(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +bool FindXmlMarkupEnd(const string& xml, size_t begin, size_t* end) { + char quote = 0; + for (size_t pos = begin; pos < xml.size(); ++pos) { + const char c = xml[pos]; + if (quote != 0) { + if (c == quote) quote = 0; + } else if (c == '\'' || c == '"') { + quote = c; + } else if (c == '>') { + *end = pos + 1; + return true; + } + } + return false; +} + +struct XmpAttributeSpan { + string qualified_name; + string value; + string uri; + string local_name; + size_t name_begin = 0; + size_t value_end = 0; + bool namespace_declaration = false; +}; + +struct XmpNamespaceDeclaration { + string prefix; + string uri; +}; + +struct XmpElementSpan { + string qualified_name; + string uri; + string local_name; + vector namespace_declarations; + vector attributes; + size_t parent = kNoXmpElement; + size_t start_begin = 0; + size_t start_end = 0; + size_t end_begin = 0; + size_t end_end = 0; + bool self_closing = false; +}; + +class XmpMergeXmlHandler : public XmlHandler { + public: + explicit XmpMergeXmlHandler(const string& xml) : xml_(xml) {} + + DataMatchResult StartElement(const XmlTokenContext& context) override { + if (!FinalizePendingElement()) valid_ = false; + pending_name_.clear(); + pending_attributes_.clear(); + pending_attribute_name_.clear(); + pending_attribute_value_.clear(); + pending_attribute_waiting_value_ = false; + pending_name_begin_ = context.GetTokenRange().GetBegin(); + if (!context.BuildTokenValue(&pending_name_)) valid_ = false; + pending_element_ = true; + return context.GetResult(); + } + + DataMatchResult AttributeName(const XmlTokenContext& context) override { + if (!pending_element_ || pending_attribute_waiting_value_) { + valid_ = false; + return context.GetResult(); + } + pending_attribute_name_.clear(); + pending_attribute_name_begin_ = context.GetTokenRange().GetBegin(); + if (!context.BuildTokenValue(&pending_attribute_name_)) valid_ = false; + pending_attribute_waiting_value_ = true; + return context.GetResult(); + } + + DataMatchResult AttributeValue(const XmlTokenContext& context) override { + if (!pending_element_ || !pending_attribute_waiting_value_) { + valid_ = false; + return context.GetResult(); + } + pending_attribute_value_.clear(); + if (!context.BuildTokenValue(&pending_attribute_value_, true)) valid_ = false; + XmpAttributeSpan attribute; + attribute.qualified_name = pending_attribute_name_; + attribute.value = pending_attribute_value_; + attribute.name_begin = pending_attribute_name_begin_; + attribute.value_end = context.GetTokenRange().GetEnd(); + pending_attributes_.push_back(std::move(attribute)); + pending_attribute_name_.clear(); + pending_attribute_value_.clear(); + pending_attribute_waiting_value_ = false; + return context.GetResult(); + } + + DataMatchResult FinishElement(const XmlTokenContext& context) override { + const bool had_pending_element = pending_element_; + if (!FinalizePendingElement()) valid_ = false; + if (!context.GetTokenRange().IsValid()) { + if (!had_pending_element || !last_element_was_self_closing_) valid_ = false; + last_element_was_self_closing_ = false; + return context.GetResult(); + } + + string closing_name; + if (!context.BuildTokenValue(&closing_name)) valid_ = false; + if (stack_.empty()) { + valid_ = false; + return context.GetResult(); + } + XmpElementSpan& element = elements_[stack_.back()]; + const size_t close_name_begin = context.GetTokenRange().GetBegin(); + const size_t close_begin = xml_.rfind('<', close_name_begin); + size_t close_end = 0; + if (close_begin == string::npos || !FindXmlMarkupEnd(xml_, close_begin, &close_end) || + closing_name != element.qualified_name) { + valid_ = false; + } else { + element.end_begin = close_begin; + element.end_end = close_end; + stack_.pop_back(); + if (stack_.empty()) root_closed_ = true; + } + return context.GetResult(); + } + + DataMatchResult ElementContent(const XmlTokenContext& context) override { + if (!FinalizePendingElement()) valid_ = false; + if (!stack_.empty() && stack_.back() == root_index_) { + string value; + if (!context.BuildTokenValue(&value) || !IsOuterWhitespace(value)) valid_ = false; + } + return context.GetResult(); + } + + DataMatchResult Comment(const XmlTokenContext& context) override { + if (!FinalizePendingElement()) valid_ = false; + return context.GetResult(); + } + + DataMatchResult Cdata(const XmlTokenContext& context) override { + if (!FinalizePendingElement()) valid_ = false; + if (!stack_.empty() && stack_.back() == root_index_) valid_ = false; + return context.GetResult(); + } + + DataMatchResult Pi(const XmlTokenContext& context) override { + if (!FinalizePendingElement()) valid_ = false; + return context.GetResult(); + } + + bool IsComplete() const { + return valid_ && !pending_element_ && stack_.empty() && root_closed_ && + root_index_ != kNoXmpElement; + } + + const vector& elements() const { return elements_; } + size_t root_index() const { return root_index_; } + + private: + bool LookupNamespace(size_t parent, const vector& local, + const string& prefix, string* uri) const { + for (auto it = local.rbegin(); it != local.rend(); ++it) { + if (it->prefix == prefix) { + *uri = it->uri; + return true; + } + } + for (size_t index = parent; index != kNoXmpElement; index = elements_[index].parent) { + const vector& declarations = elements_[index].namespace_declarations; + for (auto it = declarations.rbegin(); it != declarations.rend(); ++it) { + if (it->prefix == prefix) { + *uri = it->uri; + return true; + } + } + } + if (prefix == "xml") { + *uri = "http://www.w3.org/XML/1998/namespace"; + return true; + } + if (prefix == "xmlns") { + *uri = "http://www.w3.org/2000/xmlns/"; + return true; + } + return false; + } + + bool IsOuterWhitespace(const string& value) const { + size_t begin = 0; + if (value.compare(0, 3, "\xef\xbb\xbf") == 0) begin = 3; + for (; begin < value.size(); ++begin) { + if (!IsXmlWhitespace(value[begin])) return false; + } + return true; + } + + bool ResolveName(const string& qualified_name, size_t parent, + const vector& local, bool attribute, + string* uri, string* local_name) const { + const size_t colon = qualified_name.find(':'); + if (colon == string::npos) { + *local_name = qualified_name; + if (attribute) { + uri->clear(); + return true; + } + if (!LookupNamespace(parent, local, "", uri)) uri->clear(); + return true; + } + if (colon == 0 || colon + 1 >= qualified_name.size() || + qualified_name.find(':', colon + 1) != string::npos) { + return false; + } + const string prefix = qualified_name.substr(0, colon); + if (!LookupNamespace(parent, local, prefix, uri)) return false; + *local_name = qualified_name.substr(colon + 1); + return true; + } + + bool FinalizePendingElement() { + if (!pending_element_) return true; + if (pending_name_.empty() || pending_attribute_waiting_value_) { + pending_element_ = false; + return false; + } + const size_t start_begin = xml_.rfind('<', pending_name_begin_); + size_t start_end = 0; + if (start_begin == string::npos || !FindXmlMarkupEnd(xml_, start_begin, &start_end)) { + pending_element_ = false; + return false; + } + const size_t parent = stack_.empty() ? kNoXmpElement : stack_.back(); + vector namespace_declarations; + for (auto& attribute : pending_attributes_) { + string prefix; + if (attribute.qualified_name == "xmlns") { + attribute.namespace_declaration = true; + } else if (attribute.qualified_name.compare(0, 6, "xmlns:") == 0) { + prefix = attribute.qualified_name.substr(6); + if (prefix.empty() || prefix.find(':') != string::npos) { + pending_element_ = false; + return false; + } + attribute.namespace_declaration = true; + } else { + continue; + } + if (attribute.value.find('&') != string::npos) { + pending_element_ = false; + return false; + } + for (const XmpNamespaceDeclaration& declaration : namespace_declarations) { + if (declaration.prefix == prefix) { + pending_element_ = false; + return false; + } + } + XmpNamespaceDeclaration declaration; + declaration.prefix = prefix; + declaration.uri = attribute.value; + namespace_declarations.push_back(std::move(declaration)); + } + + XmpElementSpan element; + element.qualified_name = pending_name_; + element.parent = parent; + element.start_begin = start_begin; + element.start_end = start_end; + element.namespace_declarations = namespace_declarations; + if (!ResolveName(element.qualified_name, parent, namespace_declarations, false, &element.uri, + &element.local_name)) { + pending_element_ = false; + return false; + } + for (auto& attribute : pending_attributes_) { + if (!attribute.namespace_declaration && + !ResolveName(attribute.qualified_name, parent, namespace_declarations, true, + &attribute.uri, &attribute.local_name)) { + pending_element_ = false; + return false; + } + element.attributes.push_back(std::move(attribute)); + } + size_t last = start_end - 1; + while (last > start_begin && IsXmlWhitespace(xml_[last - 1])) --last; + element.self_closing = last > start_begin && xml_[last - 1] == '/'; + element.end_begin = element.self_closing ? start_begin : 0; + element.end_end = element.self_closing ? start_end : 0; + elements_.push_back(std::move(element)); + const size_t index = elements_.size() - 1; + if (parent == kNoXmpElement) { + if (root_index_ != kNoXmpElement) { + pending_element_ = false; + return false; + } + root_index_ = index; + } + last_element_was_self_closing_ = elements_[index].self_closing; + if (!elements_[index].self_closing) stack_.push_back(index); + else if (stack_.empty()) root_closed_ = true; + pending_element_ = false; + return true; + } + + const string& xml_; + vector elements_; + vector stack_; + size_t root_index_ = kNoXmpElement; + bool root_closed_ = false; + bool valid_ = true; + bool pending_element_ = false; + string pending_name_; + size_t pending_name_begin_ = 0; + vector pending_attributes_; + string pending_attribute_name_; + string pending_attribute_value_; + size_t pending_attribute_name_begin_ = 0; + bool pending_attribute_waiting_value_ = false; + bool last_element_was_self_closing_ = false; +}; + +string GeneratePrimaryDescription(size_t secondary_image_length, + uhdr_gainmap_metadata_ext_t& metadata) { + const vector con_dir_seq({kConDirectory, string("rdf:Seq")}); + stringstream ss; + photos_editing_formats::image_io::XmlWriter writer(ss); + writer.StartWritingElement("rdf:Description"); + writer.WriteXmlns("rdf", kRdfUri); + writer.WriteXmlns(kContainerPrefix, kContainerUri); + writer.WriteXmlns(kItemPrefix, kItemUri); + writer.WriteXmlns(kGainMapPrefix, kGainMapUri); + writer.WriteAttributeNameAndValue(kMapVersion, metadata.version); + writer.StartWritingElements(con_dir_seq); + + const size_t item_depth = writer.StartWritingElement("rdf:li"); + writer.WriteAttributeNameAndValue("rdf:parseType", "Resource"); + writer.StartWritingElement(kConItem); + writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary); + writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); + writer.FinishWritingElementsToDepth(item_depth); + + writer.StartWritingElement("rdf:li"); + writer.WriteAttributeNameAndValue("rdf:parseType", "Resource"); + writer.StartWritingElement(kConItem); + writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticGainMap); + writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); + writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length); + writer.FinishWriting(); + return ss.str(); +} + +string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length, + uhdr_gainmap_metadata_ext_t& metadata) { + const string synthetic_open = ""; + const string synthetic_close = ""; + const size_t offset = synthetic_open.size(); + const string parse_xml = synthetic_open + existing_xmp + synthetic_close; + + XmpMergeXmlHandler handler(parse_xml); + MessageHandler message_handler; + unique_ptr rule(new XmlElementRule); + XmlReader reader(&handler, &message_handler); + if (!reader.StartParse(std::move(rule)) || !reader.Parse(parse_xml) || !reader.FinishParse() || + reader.HasErrors() || !handler.IsComplete()) { + return string(); + } + + const vector& elements = handler.elements(); + const size_t synthetic_root = handler.root_index(); + size_t packet_root = kNoXmpElement; + for (size_t index = 0; index < elements.size(); ++index) { + if (elements[index].parent == synthetic_root) { + if (packet_root != kNoXmpElement) return string(); + packet_root = index; + } + } + if (packet_root == kNoXmpElement) return string(); + + size_t rdf_root = kNoXmpElement; + if (elements[packet_root].uri == kRdfUri && elements[packet_root].local_name == "RDF") { + rdf_root = packet_root; + } else if (elements[packet_root].local_name == "xmpmeta" && + elements[packet_root].uri == "adobe:ns:meta/") { + for (size_t index = 0; index < elements.size(); ++index) { + if (elements[index].parent == packet_root && elements[index].uri == kRdfUri && + elements[index].local_name == "RDF") { + if (rdf_root != kNoXmpElement) return string(); + rdf_root = index; + } + } + } else { + return string(); + } + if (rdf_root == kNoXmpElement || elements[rdf_root].self_closing || + elements[rdf_root].end_begin <= elements[rdf_root].start_begin) { + return string(); + } + + vector primary_descriptions; + for (size_t index = 0; index < elements.size(); ++index) { + const XmpElementSpan& element = elements[index]; + if (element.parent != rdf_root || element.uri != kRdfUri || + element.local_name != "Description") { + continue; + } + bool has_about = false; + bool about_empty = true; + bool has_non_primary_id = false; + for (const XmpAttributeSpan& attribute : element.attributes) { + if (attribute.uri == kRdfUri && attribute.local_name == "about") { + has_about = true; + about_empty = attribute.value.empty(); + } else if (attribute.uri == kRdfUri && + (attribute.local_name == "ID" || attribute.local_name == "nodeID")) { + has_non_primary_id = true; + } + } + if (!has_non_primary_id && (!has_about || about_empty)) primary_descriptions.push_back(index); + } + + vector> removals; + for (const size_t description : primary_descriptions) { + for (size_t index = 0; index < elements.size(); ++index) { + const XmpElementSpan& element = elements[index]; + if (element.parent == description && element.uri == kContainerUri && + element.local_name == "Directory") { + removals.emplace_back(element.start_begin, element.end_end); + } + } + for (const XmpAttributeSpan& attribute : elements[description].attributes) { + if (attribute.uri == kContainerUri && attribute.local_name == "Directory") { + removals.emplace_back(attribute.name_begin, attribute.value_end); + } + } + } + for (const size_t description : primary_descriptions) { + const XmpElementSpan& element = elements[description]; + for (const XmpAttributeSpan& attribute : element.attributes) { + if (attribute.uri == kGainMapUri && attribute.local_name == "Version") { + removals.emplace_back(attribute.name_begin, attribute.value_end); + } + } + for (size_t index = 0; index < elements.size(); ++index) { + const XmpElementSpan& child = elements[index]; + if (child.parent == description && child.uri == kGainMapUri && + child.local_name == "Version") { + removals.emplace_back(child.start_begin, child.end_end); + } + } + } + + if (elements[rdf_root].end_begin < offset || + elements[rdf_root].end_begin - offset > existing_xmp.size()) { + return string(); + } + const size_t insertion = elements[rdf_root].end_begin - offset; + for (auto& removal : removals) { + if (removal.first < offset || removal.second < removal.first) return string(); + removal.first -= offset; + removal.second -= offset; + } + sort(removals.begin(), removals.end()); + for (size_t index = 1; index < removals.size(); ++index) { + if (removals[index].first < removals[index - 1].second) return string(); + } + + const string generated_description = + GeneratePrimaryDescription(secondary_image_length, metadata); + string merged; + size_t cursor = 0; + size_t removal_index = 0; + while (removal_index < removals.size() && removals[removal_index].first < insertion) { + const auto [begin, end] = removals[removal_index++]; + if (begin < cursor || end > existing_xmp.size()) return string(); + merged.append(existing_xmp, cursor, begin - cursor); + cursor = end; + } + if (cursor > insertion) return string(); + merged.append(existing_xmp, cursor, insertion - cursor); + merged.append(generated_description); + merged.push_back('\n'); + cursor = insertion; + while (removal_index < removals.size()) { + const auto [begin, end] = removals[removal_index++]; + if (begin < cursor || end > existing_xmp.size()) return string(); + merged.append(existing_xmp, cursor, begin - cursor); + cursor = end; + } + merged.append(existing_xmp, cursor, existing_xmp.size() - cursor); + return merged; +} + +} // namespace + string generateXmpForPrimaryImage(size_t secondary_image_length, uhdr_gainmap_metadata_ext_t& metadata, uhdr_mem_block_t* user_xmp) { - const vector kConDirSeq({kConDirectory, string("rdf:Seq")}); - const vector kLiItem({string("rdf:li"), kConItem}); - if (user_xmp != nullptr && user_xmp->data != nullptr && user_xmp->data_sz > 0) { const std::string kXmpHeader = "http://ns.adobe.com/xap/1.0/"; std::string existing_xmp; @@ -893,42 +1394,10 @@ string generateXmpForPrimaryImage(size_t secondary_image_length, } else { existing_xmp = std::string(static_cast(user_xmp->data), user_xmp->data_sz); } - - std::stringstream desc_ss; - photos_editing_formats::image_io::XmlWriter writer(desc_ss); - writer.StartWritingElement("rdf:Description"); - writer.WriteXmlns(kContainerPrefix, kContainerUri); - writer.WriteXmlns(kItemPrefix, kItemUri); - writer.WriteXmlns(kGainMapPrefix, kGainMapUri); - writer.WriteAttributeNameAndValue(kMapVersion, metadata.version); - - writer.StartWritingElements(kConDirSeq); - - size_t item_depth = writer.StartWritingElement("rdf:li"); - writer.WriteAttributeNameAndValue("rdf:parseType", "Resource"); - writer.StartWritingElement(kConItem); - writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary); - writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); - writer.FinishWritingElementsToDepth(item_depth); - - writer.StartWritingElement("rdf:li"); - writer.WriteAttributeNameAndValue("rdf:parseType", "Resource"); - writer.StartWritingElement(kConItem); - writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticGainMap); - writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg); - writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length); - - writer.FinishWriting(); - - std::string desc_str = desc_ss.str(); - size_t rdf_close_pos = existing_xmp.rfind(""); - if (rdf_close_pos != std::string::npos) { - std::string merged = existing_xmp; - merged.insert(rdf_close_pos, desc_str + "\n"); - return merged; - } + return MergePrimaryXmp(existing_xmp, secondary_image_length, metadata); } + const vector kConDirSeq({kConDirectory, string("rdf:Seq")}); std::stringstream ss; photos_editing_formats::image_io::XmlWriter writer(ss); writer.StartWritingElement("x:xmpmeta"); diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index 152c9eb9..8f60818f 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -7,20 +7,28 @@ #else #include #endif +#include #include #include #include #include #include +#include #include +#include #include #include #include "ultrahdr_api.h" #include "ultrahdr/ultrahdrcommon.h" #include "ultrahdr/jpegr.h" +#include "ultrahdr/jpegencoderhelper.h" #include "ultrahdr/heifultrahdr.h" #include "ultrahdr/avifultrahdr.h" +#include "image_io/base/message_handler.h" +#include "image_io/xml/xml_element_rules.h" +#include "image_io/xml/xml_handler.h" +#include "image_io/xml/xml_reader.h" namespace ultrahdr { @@ -49,6 +57,125 @@ static std::vector makeSmallHdrData() { return data; } +static std::string getXmpPacket(const uhdr_mem_block_t* xmp) { + constexpr std::string_view kXmpNamespace = "http://ns.adobe.com/xap/1.0/"; + if (xmp == nullptr || xmp->data == nullptr) return {}; + const auto* bytes = static_cast(xmp->data); + size_t offset = 0; + if (xmp->data_sz >= kXmpNamespace.size() && + std::string_view(bytes, kXmpNamespace.size()) == kXmpNamespace) { + offset = kXmpNamespace.size(); + if (offset < xmp->data_sz && bytes[offset] == '\0') ++offset; + } + return std::string(bytes + offset, xmp->data_sz - offset); +} + +#if defined(UHDR_WRITE_XMP) +static uhdr_gainmap_metadata_t makeTestGainmapMetadata() { + uhdr_gainmap_metadata_t metadata{}; + for (int channel = 0; channel < 3; ++channel) { + metadata.max_content_boost[channel] = 2.0f; + metadata.min_content_boost[channel] = 1.0f; + metadata.gamma[channel] = 1.0f; + } + metadata.hdr_capacity_min = 1.0f; + metadata.hdr_capacity_max = 2.0f; + metadata.use_base_cg = 1; + return metadata; +} + +struct ContainerDirectoryInfo { + bool parsed = false; + size_t directory_count = 0; + std::vector gainmap_lengths; +}; + +class ContainerDirectoryHandler : public photos_editing_formats::image_io::XmlHandler { + public: + photos_editing_formats::image_io::DataMatchResult StartElement( + const photos_editing_formats::image_io::XmlTokenContext& context) override { + std::string name; + if (context.BuildTokenValue(&name)) { + element_stack_.push_back(name); + current_attribute_.clear(); + if (name == "Container:Directory") ++directory_count_; + if (name == "Container:Item") { + current_item_is_gainmap_ = false; + current_item_length_.reset(); + } + } + return context.GetResult(); + } + + photos_editing_formats::image_io::DataMatchResult AttributeName( + const photos_editing_formats::image_io::XmlTokenContext& context) override { + context.BuildTokenValue(¤t_attribute_); + return context.GetResult(); + } + + photos_editing_formats::image_io::DataMatchResult AttributeValue( + const photos_editing_formats::image_io::XmlTokenContext& context) override { + std::string value; + if (context.BuildTokenValue(&value, true) && !element_stack_.empty() && + element_stack_.back() == "Container:Item") { + if (current_attribute_ == "Item:Semantic") { + current_item_is_gainmap_ = value == "GainMap"; + } else if (current_attribute_ == "Item:Length") { + size_t parsed = 0; + const auto parse_result = + std::from_chars(value.data(), value.data() + value.size(), parsed); + if (parse_result.ec == std::errc() && parse_result.ptr == value.data() + value.size()) { + current_item_length_ = parsed; + } else { + current_item_length_.reset(); + } + } + } + return context.GetResult(); + } + + photos_editing_formats::image_io::DataMatchResult FinishElement( + const photos_editing_formats::image_io::XmlTokenContext& context) override { + if (!element_stack_.empty()) { + if (element_stack_.back() == "Container:Item" && current_item_is_gainmap_ && + current_item_length_.has_value()) { + gainmap_lengths_.push_back(*current_item_length_); + } + element_stack_.pop_back(); + } + current_attribute_.clear(); + return context.GetResult(); + } + + size_t directoryCount() const { return directory_count_; } + const std::vector& gainmapLengths() const { return gainmap_lengths_; } + + private: + std::vector element_stack_; + std::string current_attribute_; + bool current_item_is_gainmap_ = false; + std::optional current_item_length_; + size_t directory_count_ = 0; + std::vector gainmap_lengths_; +}; + +static ContainerDirectoryInfo getContainerDirectoryInfo(std::string_view xmp) { + ContainerDirectoryInfo info; + ContainerDirectoryHandler handler; + photos_editing_formats::image_io::MessageHandler messages; + photos_editing_formats::image_io::XmlReader reader(&handler, &messages); + if (!reader.StartParse(std::make_unique()) || + !reader.Parse(std::string("") + std::string(xmp) + "") || + !reader.FinishParse() || reader.HasErrors()) { + return info; + } + info.parsed = true; + info.directory_count = handler.directoryCount(); + info.gainmap_lengths = handler.gainmapLengths(); + return info; +} +#endif + static void expectEncodedXmpDecodes(uhdr_compressed_image_t* output, size_t min_xmp_size) { ASSERT_NE(output, nullptr); uhdr_codec_private_t* dec = uhdr_create_decoder(); @@ -318,6 +445,399 @@ TEST_F(UltraHdrApiTest, JpegEncodeApi2AndDecode) { uhdr_release_encoder(enc); } +#if defined(UHDR_WRITE_XMP) +TEST_F(UltraHdrApiTest, JpegApi4ExplicitXmpPreservesRdfNamespacePrefixes) { + struct PrefixControl { + const char* name; + const char* packet; + }; + const PrefixControl controls[] = { + {"conventional", + "semantic-probe" + "keep-unrelated" + ""}, + {"alias", + "semantic-probe" + "keep-unrelated" + ""}, + {"default-rdf", + "semantic-probe" + "keep-unrelated" + ""}, + }; + + for (const PrefixControl& control : controls) { + SCOPED_TRACE(control.name); + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(control.packet), strlen(control.packet), + strlen(control.packet)}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t enc_status = uhdr_encode(enc); + ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; + + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + ASSERT_NE(decoded_xmp, nullptr); + ASSERT_NE(decoded_xmp->data, nullptr); + const std::string decoded_packet = getXmpPacket(decoded_xmp); + const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(decoded_packet); + ASSERT_TRUE(directory_info.parsed); + EXPECT_EQ(directory_info.directory_count, 1u); + const size_t generated_description = decoded_packet.rfind("', generated_description); + ASSERT_NE(opening_end, std::string::npos); + const std::string generated_opening = + decoded_packet.substr(generated_description, opening_end - generated_description); + EXPECT_NE(generated_opening.find( + "xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""), + std::string::npos); + EXPECT_NE(decoded_packet.find("semantic-probe"), std::string::npos); + EXPECT_NE(decoded_packet.find("keep-unrelated"), std::string::npos); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); + } +} + +TEST_F(UltraHdrApiTest, JpegApi4XmpMergeReplacesVersionAttributeAndElement) { + struct VersionForm { + const char* name; + const char* packet; + const char* stale_value; + }; + const VersionForm forms[] = { + {"attribute", + "", + "old-attribute"}, + {"element", + "" + "old-element" + "", + "old-element"}, + }; + + for (const VersionForm& form : forms) { + SCOPED_TRACE(form.name); + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(form.packet), strlen(form.packet), + strlen(form.packet)}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t status = uhdr_encode(enc); + ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << status.detail; + + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + ASSERT_NE(decoded_xmp, nullptr); + ASSERT_NE(decoded_xmp->data, nullptr); + const std::string decoded_packet = getXmpPacket(decoded_xmp); + const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(decoded_packet); + ASSERT_TRUE(directory_info.parsed); + EXPECT_EQ(directory_info.directory_count, 1u); + EXPECT_EQ(decoded_packet.find(form.stale_value), std::string::npos); + EXPECT_NE(decoded_packet.find("hdrgm:Version=\"1.0\""), std::string::npos); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); + } +} + +TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDirectory) { + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + uhdr_codec_private_t* first_enc = uhdr_create_encoder(); + ASSERT_NE(first_enc, nullptr); + ASSERT_EQ(uhdr_enc_set_compressed_image(first_enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(first_enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(first_enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_encode(first_enc).error_code, UHDR_CODEC_OK); + uhdr_compressed_image_t* first_output = uhdr_get_encoded_stream(first_enc); + ASSERT_NE(first_output, nullptr); + + uhdr_codec_private_t* first_dec = uhdr_create_decoder(); + ASSERT_NE(first_dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(first_dec, first_output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(first_dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* first_xmp = uhdr_dec_get_xmp(first_dec); + ASSERT_NE(first_xmp, nullptr); + ASSERT_NE(first_xmp->data, nullptr); + uhdr_mem_block_t* first_base = uhdr_dec_get_base_image(first_dec); + ASSERT_NE(first_base, nullptr); + uhdr_mem_block_t* first_gainmap = uhdr_dec_get_gainmap_image(first_dec); + ASSERT_NE(first_gainmap, nullptr); + ASSERT_NE(first_gainmap->data, nullptr); + const std::string first_packet = getXmpPacket(first_xmp); + ASSERT_FALSE(first_packet.empty()); + + JpegEncoderHelper changed_gainmap_encoder; + ASSERT_EQ(changed_gainmap_encoder.compressImage(&mSdrRaw, 50, nullptr, 0).error_code, + UHDR_CODEC_OK); + uhdr_compressed_image_t changed_gainmap = changed_gainmap_encoder.getCompressedImage(); + ASSERT_NE(changed_gainmap.data, nullptr); + ASSERT_NE(changed_gainmap.data_sz, first_gainmap->data_sz); + + uhdr_compressed_image_t base_input{ + first_base->data, first_base->data_sz, first_base->capacity, UHDR_CG_BT_709, UHDR_CT_SRGB, + UHDR_CR_FULL_RANGE}; + uhdr_compressed_image_t gainmap_input = changed_gainmap; + uhdr_gainmap_metadata_t* decoded_metadata = uhdr_dec_get_gainmap_metadata(first_dec); + ASSERT_NE(decoded_metadata, nullptr); + + const std::string xpacket_wrapped = + "\n" + first_packet + + "\n"; + const std::string raw_getter(static_cast(first_xmp->data), first_xmp->data_sz); + const std::string xmp_inputs[] = {raw_getter, xpacket_wrapped}; + for (size_t input_index = 0; input_index < std::size(xmp_inputs); ++input_index) { + SCOPED_TRACE(input_index == 0 ? "raw getter bytes" : "xpacket wrapped XML"); + uhdr_codec_private_t* second_enc = uhdr_create_encoder(); + ASSERT_NE(second_enc, nullptr); + ASSERT_EQ(uhdr_enc_set_compressed_image(second_enc, &base_input, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(second_enc, &gainmap_input, decoded_metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_input{const_cast(xmp_inputs[input_index].data()), + xmp_inputs[input_index].size(), xmp_inputs[input_index].size()}; + ASSERT_EQ(uhdr_enc_set_xmp_data(second_enc, &xmp_input).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(second_enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t second_status = uhdr_encode(second_enc); + ASSERT_EQ(second_status.error_code, UHDR_CODEC_OK) << second_status.detail; + uhdr_compressed_image_t* second_output = uhdr_get_encoded_stream(second_enc); + ASSERT_NE(second_output, nullptr); + + uhdr_codec_private_t* second_dec = uhdr_create_decoder(); + ASSERT_NE(second_dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(second_dec, second_output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(second_dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* second_gainmap = uhdr_dec_get_gainmap_image(second_dec); + ASSERT_NE(second_gainmap, nullptr); + ASSERT_NE(second_gainmap->data, nullptr); + ASSERT_NE(second_gainmap->data_sz, first_gainmap->data_sz); + uhdr_mem_block_t* second_xmp = uhdr_dec_get_xmp(second_dec); + ASSERT_NE(second_xmp, nullptr); + ASSERT_NE(second_xmp->data, nullptr); + const std::string second_packet = getXmpPacket(second_xmp); + const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(second_packet); + EXPECT_TRUE(directory_info.parsed); + EXPECT_EQ(directory_info.directory_count, 1u); + EXPECT_EQ(directory_info.gainmap_lengths.size(), 1u); + if (!directory_info.gainmap_lengths.empty()) { + EXPECT_EQ(directory_info.gainmap_lengths.front(), second_gainmap->data_sz); + } + if (input_index == 1) { + EXPECT_NE(second_packet.find( + ""), + std::string::npos); + EXPECT_NE(second_packet.find(""), std::string::npos); + } + + uhdr_release_decoder(second_dec); + uhdr_release_encoder(second_enc); + } + uhdr_release_decoder(first_dec); + uhdr_release_encoder(first_enc); +} + +TEST_F(UltraHdrApiTest, JpegApi4XmpMergeIgnoresFakeRdfClosersInCommentsAndCdata) { + const std::string user_xmp = + "semantic-probe" + "]]>" + "" + ""; + + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t enc_status = uhdr_encode(enc); + ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + ASSERT_NE(decoded_xmp, nullptr); + ASSERT_NE(decoded_xmp->data, nullptr); + const std::string decoded_packet = getXmpPacket(decoded_xmp); + const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(decoded_packet); + ASSERT_TRUE(directory_info.parsed); + EXPECT_EQ(directory_info.directory_count, 1u); + EXPECT_NE(decoded_packet.find("semantic-probe"), std::string::npos); + EXPECT_NE(decoded_packet.find("]]>"), std::string::npos); + EXPECT_NE(decoded_packet.find(""), std::string::npos); + ASSERT_EQ(directory_info.gainmap_lengths.size(), 1u); + uhdr_mem_block_t* decoded_gainmap = uhdr_dec_get_gainmap_image(dec); + ASSERT_NE(decoded_gainmap, nullptr); + ASSERT_NE(decoded_gainmap->data, nullptr); + EXPECT_EQ(directory_info.gainmap_lengths.front(), decoded_gainmap->data_sz); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); +} + +TEST_F(UltraHdrApiTest, JpegApi4XmpMergeKeepsOwnedPropertiesOnNodeIdDescription) { + const std::string user_xmp = + "" + "keep-id-directory" + "" + "keep-node-directory" + "" + "primary-probe"; + + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t enc_status = uhdr_encode(enc); + ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + ASSERT_NE(decoded_xmp, nullptr); + ASSERT_NE(decoded_xmp->data, nullptr); + const std::string decoded_packet = getXmpPacket(decoded_xmp); + EXPECT_NE(decoded_packet.find("keep-id-directory"), std::string::npos); + EXPECT_NE(decoded_packet.find("keep-node-directory"), std::string::npos); + EXPECT_NE(decoded_packet.find("primary-probe"), std::string::npos); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); +} + +TEST_F(UltraHdrApiTest, JpegApi4RejectsMalformedOrUnmergeableXmp) { + const char* packets[] = { + "", + "" + "", + "" + "unmergeable", + }; + + for (const char* packet : packets) { + SCOPED_TRACE(packet); + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(packet), strlen(packet), strlen(packet)}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + const uhdr_error_info_t status = uhdr_encode(enc); + EXPECT_EQ(status.error_code, UHDR_CODEC_INVALID_PARAM) << status.detail; + uhdr_release_encoder(enc); + } +} +#endif + +#if !defined(UHDR_WRITE_XMP) && defined(UHDR_WRITE_ISO) +TEST_F(UltraHdrApiTest, JpegApi4IsoOnlyPassesThroughXmpExactly) { + const std::string user_xmp = + "iso-passthrough" + ""; + uhdr_gainmap_metadata_t metadata{}; + for (int channel = 0; channel < 3; ++channel) { + metadata.max_content_boost[channel] = 2.0f; + metadata.min_content_boost[channel] = 1.0f; + metadata.gamma[channel] = 1.0f; + } + metadata.hdr_capacity_min = 1.0f; + metadata.hdr_capacity_max = 2.0f; + metadata.use_base_cg = 1; + + uhdr_codec_private_t* enc = uhdr_create_encoder(); + ASSERT_NE(enc, nullptr); + ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, + UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, + UHDR_CODEC_OK); + uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; + ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_encode(enc).error_code, UHDR_CODEC_OK); + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); + ASSERT_NE(output, nullptr); + + uhdr_codec_private_t* dec = uhdr_create_decoder(); + ASSERT_NE(dec, nullptr); + ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); + ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + ASSERT_NE(decoded_xmp, nullptr); + ASSERT_NE(decoded_xmp->data, nullptr); + EXPECT_EQ(getXmpPacket(decoded_xmp), user_xmp); + + uhdr_release_decoder(dec); + uhdr_release_encoder(enc); +} +#endif + TEST_F(UltraHdrApiTest, JpegEncodeWithXmpAndDecode) { uhdr_codec_private_t* enc = uhdr_create_encoder(); ASSERT_NE(enc, nullptr); From 77ab722a1ca77c3420f9b59d7df3ac690e1afdc0 Mon Sep 17 00:00:00 2001 From: Greg Benz Date: Wed, 16 Sep 2026 14:06:30 -0500 Subject: [PATCH 2/5] Simplify XMP merge bookkeeping and regression test setup --- lib/src/jpegrutils.cpp | 85 ++++------- tests/ultrahdr_api_test.cpp | 288 +++++++++++++++--------------------- 2 files changed, 145 insertions(+), 228 deletions(-) diff --git a/lib/src/jpegrutils.cpp b/lib/src/jpegrutils.cpp index 389d46b5..cde05810 100644 --- a/lib/src/jpegrutils.cpp +++ b/lib/src/jpegrutils.cpp @@ -923,7 +923,6 @@ struct XmpElementSpan { vector attributes; size_t parent = kNoXmpElement; size_t start_begin = 0; - size_t start_end = 0; size_t end_begin = 0; size_t end_end = 0; bool self_closing = false; @@ -937,9 +936,7 @@ class XmpMergeXmlHandler : public XmlHandler { if (!FinalizePendingElement()) valid_ = false; pending_name_.clear(); pending_attributes_.clear(); - pending_attribute_name_.clear(); - pending_attribute_value_.clear(); - pending_attribute_waiting_value_ = false; + pending_attribute_index_ = kNoXmpElement; pending_name_begin_ = context.GetTokenRange().GetBegin(); if (!context.BuildTokenValue(&pending_name_)) valid_ = false; pending_element_ = true; @@ -947,33 +944,27 @@ class XmpMergeXmlHandler : public XmlHandler { } DataMatchResult AttributeName(const XmlTokenContext& context) override { - if (!pending_element_ || pending_attribute_waiting_value_) { + if (!pending_element_ || pending_attribute_index_ != kNoXmpElement) { valid_ = false; return context.GetResult(); } - pending_attribute_name_.clear(); - pending_attribute_name_begin_ = context.GetTokenRange().GetBegin(); - if (!context.BuildTokenValue(&pending_attribute_name_)) valid_ = false; - pending_attribute_waiting_value_ = true; + XmpAttributeSpan attribute; + attribute.name_begin = context.GetTokenRange().GetBegin(); + if (!context.BuildTokenValue(&attribute.qualified_name)) valid_ = false; + pending_attributes_.push_back(std::move(attribute)); + pending_attribute_index_ = pending_attributes_.size() - 1; return context.GetResult(); } DataMatchResult AttributeValue(const XmlTokenContext& context) override { - if (!pending_element_ || !pending_attribute_waiting_value_) { + if (!pending_element_ || pending_attribute_index_ == kNoXmpElement) { valid_ = false; return context.GetResult(); } - pending_attribute_value_.clear(); - if (!context.BuildTokenValue(&pending_attribute_value_, true)) valid_ = false; - XmpAttributeSpan attribute; - attribute.qualified_name = pending_attribute_name_; - attribute.value = pending_attribute_value_; - attribute.name_begin = pending_attribute_name_begin_; + XmpAttributeSpan& attribute = pending_attributes_[pending_attribute_index_]; + if (!context.BuildTokenValue(&attribute.value, true)) valid_ = false; attribute.value_end = context.GetTokenRange().GetEnd(); - pending_attributes_.push_back(std::move(attribute)); - pending_attribute_name_.clear(); - pending_attribute_value_.clear(); - pending_attribute_waiting_value_ = false; + pending_attribute_index_ = kNoXmpElement; return context.GetResult(); } @@ -1104,7 +1095,7 @@ class XmpMergeXmlHandler : public XmlHandler { bool FinalizePendingElement() { if (!pending_element_) return true; - if (pending_name_.empty() || pending_attribute_waiting_value_) { + if (pending_name_.empty() || pending_attribute_index_ != kNoXmpElement) { pending_element_ = false; return false; } @@ -1150,7 +1141,6 @@ class XmpMergeXmlHandler : public XmlHandler { element.qualified_name = pending_name_; element.parent = parent; element.start_begin = start_begin; - element.start_end = start_end; element.namespace_declarations = namespace_declarations; if (!ResolveName(element.qualified_name, parent, namespace_declarations, false, &element.uri, &element.local_name)) { @@ -1197,10 +1187,7 @@ class XmpMergeXmlHandler : public XmlHandler { string pending_name_; size_t pending_name_begin_ = 0; vector pending_attributes_; - string pending_attribute_name_; - string pending_attribute_value_; - size_t pending_attribute_name_begin_ = 0; - bool pending_attribute_waiting_value_ = false; + size_t pending_attribute_index_ = kNoXmpElement; bool last_element_was_self_closing_ = false; }; @@ -1305,30 +1292,17 @@ string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length vector> removals; for (const size_t description : primary_descriptions) { - for (size_t index = 0; index < elements.size(); ++index) { - const XmpElementSpan& element = elements[index]; - if (element.parent == description && element.uri == kContainerUri && - element.local_name == "Directory") { - removals.emplace_back(element.start_begin, element.end_end); - } - } - for (const XmpAttributeSpan& attribute : elements[description].attributes) { - if (attribute.uri == kContainerUri && attribute.local_name == "Directory") { + const XmpElementSpan& primary = elements[description]; + for (const XmpAttributeSpan& attribute : primary.attributes) { + if ((attribute.uri == kContainerUri && attribute.local_name == "Directory") || + (attribute.uri == kGainMapUri && attribute.local_name == "Version")) { removals.emplace_back(attribute.name_begin, attribute.value_end); } } - } - for (const size_t description : primary_descriptions) { - const XmpElementSpan& element = elements[description]; - for (const XmpAttributeSpan& attribute : element.attributes) { - if (attribute.uri == kGainMapUri && attribute.local_name == "Version") { - removals.emplace_back(attribute.name_begin, attribute.value_end); - } - } - for (size_t index = 0; index < elements.size(); ++index) { - const XmpElementSpan& child = elements[index]; - if (child.parent == description && child.uri == kGainMapUri && - child.local_name == "Version") { + for (const XmpElementSpan& child : elements) { + if (child.parent == description && + ((child.uri == kContainerUri && child.local_name == "Directory") || + (child.uri == kGainMapUri && child.local_name == "Version"))) { removals.emplace_back(child.start_begin, child.end_end); } } @@ -1343,6 +1317,7 @@ string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length if (removal.first < offset || removal.second < removal.first) return string(); removal.first -= offset; removal.second -= offset; + if (removal.second > insertion) return string(); } sort(removals.begin(), removals.end()); for (size_t index = 1; index < removals.size(); ++index) { @@ -1353,25 +1328,15 @@ string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length GeneratePrimaryDescription(secondary_image_length, metadata); string merged; size_t cursor = 0; - size_t removal_index = 0; - while (removal_index < removals.size() && removals[removal_index].first < insertion) { - const auto [begin, end] = removals[removal_index++]; - if (begin < cursor || end > existing_xmp.size()) return string(); + for (const auto [begin, end] : removals) { + if (begin < cursor || end > insertion || end > existing_xmp.size()) return string(); merged.append(existing_xmp, cursor, begin - cursor); cursor = end; } - if (cursor > insertion) return string(); merged.append(existing_xmp, cursor, insertion - cursor); merged.append(generated_description); merged.push_back('\n'); - cursor = insertion; - while (removal_index < removals.size()) { - const auto [begin, end] = removals[removal_index++]; - if (begin < cursor || end > existing_xmp.size()) return string(); - merged.append(existing_xmp, cursor, begin - cursor); - cursor = end; - } - merged.append(existing_xmp, cursor, existing_xmp.size() - cursor); + merged.append(existing_xmp, insertion, existing_xmp.size() - insertion); return merged; } diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index 8f60818f..1ef50bc7 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -70,7 +70,6 @@ static std::string getXmpPacket(const uhdr_mem_block_t* xmp) { return std::string(bytes + offset, xmp->data_sz - offset); } -#if defined(UHDR_WRITE_XMP) static uhdr_gainmap_metadata_t makeTestGainmapMetadata() { uhdr_gainmap_metadata_t metadata{}; for (int channel = 0; channel < 3; ++channel) { @@ -84,6 +83,85 @@ static uhdr_gainmap_metadata_t makeTestGainmapMetadata() { return metadata; } +using EncoderPtr = std::unique_ptr; +using DecoderPtr = std::unique_ptr; + +static EncoderPtr makeEncoder() { + return EncoderPtr(uhdr_create_encoder(), &uhdr_release_encoder); +} + +static DecoderPtr makeDecoder() { + return DecoderPtr(uhdr_create_decoder(), &uhdr_release_decoder); +} + +// Configures the API-4 inputs used by the focused JPEG XMP tests. The XMP block is optional for +// the first encode in the getter/setter re-encode test, which relies on generated XMP. +static uhdr_error_info_t configureJpegGainmapEncoder(uhdr_codec_private_t* encoder, + uhdr_compressed_image_t* base_image, + uhdr_compressed_image_t* gainmap_image, + uhdr_gainmap_metadata_t* metadata, + uhdr_mem_block_t* xmp) { + uhdr_error_info_t status = + uhdr_enc_set_compressed_image(encoder, base_image, UHDR_BASE_IMG); + if (status.error_code != UHDR_CODEC_OK) return status; + status = uhdr_enc_set_gainmap_image(encoder, gainmap_image, metadata); + if (status.error_code != UHDR_CODEC_OK) return status; + if (xmp != nullptr) { + status = uhdr_enc_set_xmp_data(encoder, xmp); + if (status.error_code != UHDR_CODEC_OK) return status; + } + return uhdr_enc_set_output_format(encoder, UHDR_CODEC_JPG); +} + +struct JpegXmpRoundTrip { + EncoderPtr encoder; + DecoderPtr decoder; + + JpegXmpRoundTrip() + : encoder(nullptr, &uhdr_release_encoder), decoder(nullptr, &uhdr_release_decoder) {} +}; + +static testing::AssertionResult encodeAndProbeJpegXmp( + uhdr_compressed_image_t* base_image, uhdr_compressed_image_t* gainmap_image, + uhdr_gainmap_metadata_t* metadata, uhdr_mem_block_t* xmp, JpegXmpRoundTrip& round_trip) { + round_trip.encoder = makeEncoder(); + if (round_trip.encoder == nullptr) { + return testing::AssertionFailure() << "uhdr_create_encoder returned nullptr"; + } + + const uhdr_error_info_t setup_status = configureJpegGainmapEncoder( + round_trip.encoder.get(), base_image, gainmap_image, metadata, xmp); + if (setup_status.error_code != UHDR_CODEC_OK) { + return testing::AssertionFailure() << "JPEG XMP encoder setup failed: " + << setup_status.detail; + } + + const uhdr_error_info_t encode_status = uhdr_encode(round_trip.encoder.get()); + if (encode_status.error_code != UHDR_CODEC_OK) { + return testing::AssertionFailure() << "JPEG XMP encode failed: " << encode_status.detail; + } + uhdr_compressed_image_t* output = uhdr_get_encoded_stream(round_trip.encoder.get()); + if (output == nullptr) { + return testing::AssertionFailure() << "uhdr_get_encoded_stream returned nullptr"; + } + + round_trip.decoder = makeDecoder(); + if (round_trip.decoder == nullptr) { + return testing::AssertionFailure() << "uhdr_create_decoder returned nullptr"; + } + uhdr_error_info_t probe_status = uhdr_dec_set_image(round_trip.decoder.get(), output); + if (probe_status.error_code != UHDR_CODEC_OK) { + return testing::AssertionFailure() << "JPEG XMP decoder setup failed: " + << probe_status.detail; + } + probe_status = uhdr_dec_probe(round_trip.decoder.get()); + if (probe_status.error_code != UHDR_CODEC_OK) { + return testing::AssertionFailure() << "JPEG XMP probe failed: " << probe_status.detail; + } + return testing::AssertionSuccess(); +} + +#if defined(UHDR_WRITE_XMP) struct ContainerDirectoryInfo { bool parsed = false; size_t directory_count = 0; @@ -474,27 +552,13 @@ TEST_F(UltraHdrApiTest, JpegApi4ExplicitXmpPreservesRdfNamespacePrefixes) { for (const PrefixControl& control : controls) { SCOPED_TRACE(control.name); - uhdr_codec_private_t* enc = uhdr_create_encoder(); - ASSERT_NE(enc, nullptr); uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_block{const_cast(control.packet), strlen(control.packet), strlen(control.packet)}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t enc_status = uhdr_encode(enc); - ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; - - uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); - ASSERT_NE(output, nullptr); - uhdr_codec_private_t* dec = uhdr_create_decoder(); - ASSERT_NE(dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + JpegXmpRoundTrip round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block, + round_trip)); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); ASSERT_NE(decoded_xmp, nullptr); ASSERT_NE(decoded_xmp->data, nullptr); const std::string decoded_packet = getXmpPacket(decoded_xmp); @@ -512,9 +576,6 @@ TEST_F(UltraHdrApiTest, JpegApi4ExplicitXmpPreservesRdfNamespacePrefixes) { std::string::npos); EXPECT_NE(decoded_packet.find("semantic-probe"), std::string::npos); EXPECT_NE(decoded_packet.find("keep-unrelated"), std::string::npos); - - uhdr_release_decoder(dec); - uhdr_release_encoder(enc); } } @@ -542,27 +603,13 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeReplacesVersionAttributeAndElement) { for (const VersionForm& form : forms) { SCOPED_TRACE(form.name); - uhdr_codec_private_t* enc = uhdr_create_encoder(); - ASSERT_NE(enc, nullptr); uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_block{const_cast(form.packet), strlen(form.packet), strlen(form.packet)}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t status = uhdr_encode(enc); - ASSERT_EQ(status.error_code, UHDR_CODEC_OK) << status.detail; - - uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); - ASSERT_NE(output, nullptr); - uhdr_codec_private_t* dec = uhdr_create_decoder(); - ASSERT_NE(dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + JpegXmpRoundTrip round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block, + round_trip)); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); ASSERT_NE(decoded_xmp, nullptr); ASSERT_NE(decoded_xmp->data, nullptr); const std::string decoded_packet = getXmpPacket(decoded_xmp); @@ -571,35 +618,20 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeReplacesVersionAttributeAndElement) { EXPECT_EQ(directory_info.directory_count, 1u); EXPECT_EQ(decoded_packet.find(form.stale_value), std::string::npos); EXPECT_NE(decoded_packet.find("hdrgm:Version=\"1.0\""), std::string::npos); - - uhdr_release_decoder(dec); - uhdr_release_encoder(enc); } } TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDirectory) { uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - uhdr_codec_private_t* first_enc = uhdr_create_encoder(); - ASSERT_NE(first_enc, nullptr); - ASSERT_EQ(uhdr_enc_set_compressed_image(first_enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(first_enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(first_enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_encode(first_enc).error_code, UHDR_CODEC_OK); - uhdr_compressed_image_t* first_output = uhdr_get_encoded_stream(first_enc); - ASSERT_NE(first_output, nullptr); - - uhdr_codec_private_t* first_dec = uhdr_create_decoder(); - ASSERT_NE(first_dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(first_dec, first_output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(first_dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* first_xmp = uhdr_dec_get_xmp(first_dec); + JpegXmpRoundTrip first_round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, nullptr, + first_round_trip)); + uhdr_mem_block_t* first_xmp = uhdr_dec_get_xmp(first_round_trip.decoder.get()); ASSERT_NE(first_xmp, nullptr); ASSERT_NE(first_xmp->data, nullptr); - uhdr_mem_block_t* first_base = uhdr_dec_get_base_image(first_dec); + uhdr_mem_block_t* first_base = uhdr_dec_get_base_image(first_round_trip.decoder.get()); ASSERT_NE(first_base, nullptr); - uhdr_mem_block_t* first_gainmap = uhdr_dec_get_gainmap_image(first_dec); + uhdr_mem_block_t* first_gainmap = uhdr_dec_get_gainmap_image(first_round_trip.decoder.get()); ASSERT_NE(first_gainmap, nullptr); ASSERT_NE(first_gainmap->data, nullptr); const std::string first_packet = getXmpPacket(first_xmp); @@ -616,7 +648,8 @@ TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDire first_base->data, first_base->data_sz, first_base->capacity, UHDR_CG_BT_709, UHDR_CT_SRGB, UHDR_CR_FULL_RANGE}; uhdr_compressed_image_t gainmap_input = changed_gainmap; - uhdr_gainmap_metadata_t* decoded_metadata = uhdr_dec_get_gainmap_metadata(first_dec); + uhdr_gainmap_metadata_t* decoded_metadata = + uhdr_dec_get_gainmap_metadata(first_round_trip.decoder.get()); ASSERT_NE(decoded_metadata, nullptr); const std::string xpacket_wrapped = @@ -626,30 +659,17 @@ TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDire const std::string xmp_inputs[] = {raw_getter, xpacket_wrapped}; for (size_t input_index = 0; input_index < std::size(xmp_inputs); ++input_index) { SCOPED_TRACE(input_index == 0 ? "raw getter bytes" : "xpacket wrapped XML"); - uhdr_codec_private_t* second_enc = uhdr_create_encoder(); - ASSERT_NE(second_enc, nullptr); - ASSERT_EQ(uhdr_enc_set_compressed_image(second_enc, &base_input, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(second_enc, &gainmap_input, decoded_metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_input{const_cast(xmp_inputs[input_index].data()), xmp_inputs[input_index].size(), xmp_inputs[input_index].size()}; - ASSERT_EQ(uhdr_enc_set_xmp_data(second_enc, &xmp_input).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(second_enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t second_status = uhdr_encode(second_enc); - ASSERT_EQ(second_status.error_code, UHDR_CODEC_OK) << second_status.detail; - uhdr_compressed_image_t* second_output = uhdr_get_encoded_stream(second_enc); - ASSERT_NE(second_output, nullptr); - - uhdr_codec_private_t* second_dec = uhdr_create_decoder(); - ASSERT_NE(second_dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(second_dec, second_output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(second_dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* second_gainmap = uhdr_dec_get_gainmap_image(second_dec); + JpegXmpRoundTrip second_round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&base_input, &gainmap_input, decoded_metadata, &xmp_input, + second_round_trip)); + uhdr_mem_block_t* second_gainmap = + uhdr_dec_get_gainmap_image(second_round_trip.decoder.get()); ASSERT_NE(second_gainmap, nullptr); ASSERT_NE(second_gainmap->data, nullptr); ASSERT_NE(second_gainmap->data_sz, first_gainmap->data_sz); - uhdr_mem_block_t* second_xmp = uhdr_dec_get_xmp(second_dec); + uhdr_mem_block_t* second_xmp = uhdr_dec_get_xmp(second_round_trip.decoder.get()); ASSERT_NE(second_xmp, nullptr); ASSERT_NE(second_xmp->data, nullptr); const std::string second_packet = getXmpPacket(second_xmp); @@ -666,12 +686,7 @@ TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDire std::string::npos); EXPECT_NE(second_packet.find(""), std::string::npos); } - - uhdr_release_decoder(second_dec); - uhdr_release_encoder(second_enc); } - uhdr_release_decoder(first_dec); - uhdr_release_encoder(first_enc); } TEST_F(UltraHdrApiTest, JpegApi4XmpMergeIgnoresFakeRdfClosersInCommentsAndCdata) { @@ -683,26 +698,12 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeIgnoresFakeRdfClosersInCommentsAndCdata) "" ""; - uhdr_codec_private_t* enc = uhdr_create_encoder(); - ASSERT_NE(enc, nullptr); uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t enc_status = uhdr_encode(enc); - ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; - uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); - ASSERT_NE(output, nullptr); - - uhdr_codec_private_t* dec = uhdr_create_decoder(); - ASSERT_NE(dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + JpegXmpRoundTrip round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block, + round_trip)); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); ASSERT_NE(decoded_xmp, nullptr); ASSERT_NE(decoded_xmp->data, nullptr); const std::string decoded_packet = getXmpPacket(decoded_xmp); @@ -713,13 +714,10 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeIgnoresFakeRdfClosersInCommentsAndCdata) EXPECT_NE(decoded_packet.find("]]>"), std::string::npos); EXPECT_NE(decoded_packet.find(""), std::string::npos); ASSERT_EQ(directory_info.gainmap_lengths.size(), 1u); - uhdr_mem_block_t* decoded_gainmap = uhdr_dec_get_gainmap_image(dec); + uhdr_mem_block_t* decoded_gainmap = uhdr_dec_get_gainmap_image(round_trip.decoder.get()); ASSERT_NE(decoded_gainmap, nullptr); ASSERT_NE(decoded_gainmap->data, nullptr); EXPECT_EQ(directory_info.gainmap_lengths.front(), decoded_gainmap->data_sz); - - uhdr_release_decoder(dec); - uhdr_release_encoder(enc); } TEST_F(UltraHdrApiTest, JpegApi4XmpMergeKeepsOwnedPropertiesOnNodeIdDescription) { @@ -733,35 +731,18 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeKeepsOwnedPropertiesOnNodeIdDescription) "" "primary-probe"; - uhdr_codec_private_t* enc = uhdr_create_encoder(); - ASSERT_NE(enc, nullptr); uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t enc_status = uhdr_encode(enc); - ASSERT_EQ(enc_status.error_code, UHDR_CODEC_OK) << enc_status.detail; - uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); - ASSERT_NE(output, nullptr); - - uhdr_codec_private_t* dec = uhdr_create_decoder(); - ASSERT_NE(dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + JpegXmpRoundTrip round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block, + round_trip)); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); ASSERT_NE(decoded_xmp, nullptr); ASSERT_NE(decoded_xmp->data, nullptr); const std::string decoded_packet = getXmpPacket(decoded_xmp); EXPECT_NE(decoded_packet.find("keep-id-directory"), std::string::npos); EXPECT_NE(decoded_packet.find("keep-node-directory"), std::string::npos); EXPECT_NE(decoded_packet.find("primary-probe"), std::string::npos); - - uhdr_release_decoder(dec); - uhdr_release_encoder(enc); } TEST_F(UltraHdrApiTest, JpegApi4RejectsMalformedOrUnmergeableXmp) { @@ -777,19 +758,15 @@ TEST_F(UltraHdrApiTest, JpegApi4RejectsMalformedOrUnmergeableXmp) { for (const char* packet : packets) { SCOPED_TRACE(packet); - uhdr_codec_private_t* enc = uhdr_create_encoder(); + EncoderPtr enc = makeEncoder(); ASSERT_NE(enc, nullptr); uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); uhdr_mem_block_t xmp_block{const_cast(packet), strlen(packet), strlen(packet)}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - const uhdr_error_info_t status = uhdr_encode(enc); + const uhdr_error_info_t setup_status = configureJpegGainmapEncoder( + enc.get(), &mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block); + ASSERT_EQ(setup_status.error_code, UHDR_CODEC_OK) << setup_status.detail; + const uhdr_error_info_t status = uhdr_encode(enc.get()); EXPECT_EQ(status.error_code, UHDR_CODEC_INVALID_PARAM) << status.detail; - uhdr_release_encoder(enc); } } #endif @@ -801,40 +778,15 @@ TEST_F(UltraHdrApiTest, JpegApi4IsoOnlyPassesThroughXmpExactly) { "xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">iso-passthrough" ""; - uhdr_gainmap_metadata_t metadata{}; - for (int channel = 0; channel < 3; ++channel) { - metadata.max_content_boost[channel] = 2.0f; - metadata.min_content_boost[channel] = 1.0f; - metadata.gamma[channel] = 1.0f; - } - metadata.hdr_capacity_min = 1.0f; - metadata.hdr_capacity_max = 2.0f; - metadata.use_base_cg = 1; - - uhdr_codec_private_t* enc = uhdr_create_encoder(); - ASSERT_NE(enc, nullptr); - ASSERT_EQ(uhdr_enc_set_compressed_image(enc, &mSdrCompressed, UHDR_BASE_IMG).error_code, - UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_gainmap_image(enc, &mSdrCompressed, &metadata).error_code, - UHDR_CODEC_OK); + uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); uhdr_mem_block_t xmp_block{const_cast(user_xmp.data()), user_xmp.size(), user_xmp.size()}; - ASSERT_EQ(uhdr_enc_set_xmp_data(enc, &xmp_block).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_enc_set_output_format(enc, UHDR_CODEC_JPG).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_encode(enc).error_code, UHDR_CODEC_OK); - uhdr_compressed_image_t* output = uhdr_get_encoded_stream(enc); - ASSERT_NE(output, nullptr); - - uhdr_codec_private_t* dec = uhdr_create_decoder(); - ASSERT_NE(dec, nullptr); - ASSERT_EQ(uhdr_dec_set_image(dec, output).error_code, UHDR_CODEC_OK); - ASSERT_EQ(uhdr_dec_probe(dec).error_code, UHDR_CODEC_OK); - uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(dec); + JpegXmpRoundTrip round_trip; + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, &xmp_block, + round_trip)); + uhdr_mem_block_t* decoded_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); ASSERT_NE(decoded_xmp, nullptr); ASSERT_NE(decoded_xmp->data, nullptr); EXPECT_EQ(getXmpPacket(decoded_xmp), user_xmp); - - uhdr_release_decoder(dec); - uhdr_release_encoder(enc); } #endif From 7a27202465c9447fc4003e63f90891010d3156ad Mon Sep 17 00:00:00 2001 From: Greg Benz Date: Wed, 16 Sep 2026 15:29:54 -0500 Subject: [PATCH 3/5] Verify inherited JPEG XMP reuse and fix test includes --- CMakeLists.txt | 1 + tests/ultrahdr_api_test.cpp | 121 +++++++++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 37c184fd..191300da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -823,6 +823,7 @@ if(UHDR_BUILD_TESTS) target_include_directories(ultrahdr_unit_test PRIVATE ${PRIVATE_INCLUDE_DIR} ${GTEST_INCLUDE_DIRS} + "${THIRD_PARTY_DIR}/image_io/includes" ) if(UHDR_BUILD_FUZZERS) target_link_options(ultrahdr_unit_test PRIVATE -fsanitize=fuzzer-no-link) diff --git a/tests/ultrahdr_api_test.cpp b/tests/ultrahdr_api_test.cpp index 1ef50bc7..91c2395f 100644 --- a/tests/ultrahdr_api_test.cpp +++ b/tests/ultrahdr_api_test.cpp @@ -94,8 +94,8 @@ static DecoderPtr makeDecoder() { return DecoderPtr(uhdr_create_decoder(), &uhdr_release_decoder); } -// Configures the API-4 inputs used by the focused JPEG XMP tests. The XMP block is optional for -// the first encode in the getter/setter re-encode test, which relies on generated XMP. +// Configures the API-4 inputs used by the focused JPEG XMP tests. The XMP block is optional when +// an API-4 re-encode should reuse XMP embedded in its compressed base image. static uhdr_error_info_t configureJpegGainmapEncoder(uhdr_codec_private_t* encoder, uhdr_compressed_image_t* base_image, uhdr_compressed_image_t* gainmap_image, @@ -622,10 +622,24 @@ TEST_F(UltraHdrApiTest, JpegApi4XmpMergeReplacesVersionAttributeAndElement) { } TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDirectory) { + const std::string descriptive_seed = + "" + "retained-description" + "" + "stale-container-element" + "stale-hdrgm-element" + ""; + uhdr_mem_block_t descriptive_seed_block{const_cast(descriptive_seed.data()), + descriptive_seed.size(), descriptive_seed.size()}; uhdr_gainmap_metadata_t metadata = makeTestGainmapMetadata(); JpegXmpRoundTrip first_round_trip; - ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, nullptr, - first_round_trip)); + ASSERT_TRUE(encodeAndProbeJpegXmp(&mSdrCompressed, &mSdrCompressed, &metadata, + &descriptive_seed_block, first_round_trip)); uhdr_mem_block_t* first_xmp = uhdr_dec_get_xmp(first_round_trip.decoder.get()); ASSERT_NE(first_xmp, nullptr); ASSERT_NE(first_xmp->data, nullptr); @@ -636,6 +650,47 @@ TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDire ASSERT_NE(first_gainmap->data, nullptr); const std::string first_packet = getXmpPacket(first_xmp); ASSERT_FALSE(first_packet.empty()); + const char* stale_sentinels[] = { + "stale-container-attribute", "stale-container-element", "stale-hdrgm-attribute", + "stale-hdrgm-element", + }; + + auto checkOutput = [&](JpegXmpRoundTrip& round_trip, bool expect_changed_gainmap, + const char* expected_description, const char* absent_description, + std::string* packet_out) { + uhdr_mem_block_t* output_gainmap = uhdr_dec_get_gainmap_image(round_trip.decoder.get()); + ASSERT_NE(output_gainmap, nullptr); + ASSERT_NE(output_gainmap->data, nullptr); + if (expect_changed_gainmap) { + EXPECT_NE(output_gainmap->data_sz, first_gainmap->data_sz); + } + + uhdr_mem_block_t* output_xmp = uhdr_dec_get_xmp(round_trip.decoder.get()); + ASSERT_NE(output_xmp, nullptr); + ASSERT_NE(output_xmp->data, nullptr); + const std::string output_packet = getXmpPacket(output_xmp); + ASSERT_FALSE(output_packet.empty()); + EXPECT_NE(output_packet.find(expected_description), std::string::npos); + if (absent_description != nullptr) { + EXPECT_EQ(output_packet.find(absent_description), std::string::npos); + } + for (const char* stale : stale_sentinels) { + EXPECT_EQ(output_packet.find(stale), std::string::npos) << stale; + } + + const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(output_packet); + ASSERT_TRUE(directory_info.parsed); + EXPECT_EQ(directory_info.directory_count, 1u); + ASSERT_EQ(directory_info.gainmap_lengths.size(), 1u); + EXPECT_EQ(directory_info.gainmap_lengths.front(), output_gainmap->data_sz); + ASSERT_EQ(uhdr_decode(round_trip.decoder.get()).error_code, UHDR_CODEC_OK); + if (packet_out != nullptr) *packet_out = output_packet; + }; + + uhdr_gainmap_metadata_t* decoded_metadata = + uhdr_dec_get_gainmap_metadata(first_round_trip.decoder.get()); + ASSERT_NE(decoded_metadata, nullptr); + checkOutput(first_round_trip, false, "retained-description", nullptr, nullptr); JpegEncoderHelper changed_gainmap_encoder; ASSERT_EQ(changed_gainmap_encoder.compressImage(&mSdrRaw, 50, nullptr, 0).error_code, @@ -648,39 +703,45 @@ TEST_F(UltraHdrApiTest, JpegApi4GetterSetterReencodeUsesSingleCurrentGainmapDire first_base->data, first_base->data_sz, first_base->capacity, UHDR_CG_BT_709, UHDR_CT_SRGB, UHDR_CR_FULL_RANGE}; uhdr_compressed_image_t gainmap_input = changed_gainmap; - uhdr_gainmap_metadata_t* decoded_metadata = - uhdr_dec_get_gainmap_metadata(first_round_trip.decoder.get()); - ASSERT_NE(decoded_metadata, nullptr); const std::string xpacket_wrapped = "\n" + first_packet + "\n"; const std::string raw_getter(static_cast(first_xmp->data), first_xmp->data_sz); - const std::string xmp_inputs[] = {raw_getter, xpacket_wrapped}; - for (size_t input_index = 0; input_index < std::size(xmp_inputs); ++input_index) { - SCOPED_TRACE(input_index == 0 ? "raw getter bytes" : "xpacket wrapped XML"); - uhdr_mem_block_t xmp_input{const_cast(xmp_inputs[input_index].data()), - xmp_inputs[input_index].size(), xmp_inputs[input_index].size()}; + const std::string replacement_xmp = + "" + "replacement-description"; + struct ReencodeControl { + const char* name; + const std::string* xmp; + const char* expected_description; + const char* absent_description; + bool expect_xpacket_wrapper; + }; + const ReencodeControl controls[] = { + {"raw getter bytes", &raw_getter, "retained-description", nullptr, false}, + {"xpacket wrapped XML", &xpacket_wrapped, "retained-description", nullptr, true}, + {"no XMP setter", nullptr, "retained-description", nullptr, false}, + {"explicit replacement", &replacement_xmp, "replacement-description", + "retained-description", false}, + }; + for (const ReencodeControl& control : controls) { + SCOPED_TRACE(control.name); + uhdr_mem_block_t xmp_input{}; + if (control.xmp != nullptr) { + xmp_input = {const_cast(control.xmp->data()), control.xmp->size(), + control.xmp->size()}; + } JpegXmpRoundTrip second_round_trip; - ASSERT_TRUE(encodeAndProbeJpegXmp(&base_input, &gainmap_input, decoded_metadata, &xmp_input, + ASSERT_TRUE(encodeAndProbeJpegXmp(&base_input, &gainmap_input, decoded_metadata, + control.xmp != nullptr ? &xmp_input : nullptr, second_round_trip)); - uhdr_mem_block_t* second_gainmap = - uhdr_dec_get_gainmap_image(second_round_trip.decoder.get()); - ASSERT_NE(second_gainmap, nullptr); - ASSERT_NE(second_gainmap->data, nullptr); - ASSERT_NE(second_gainmap->data_sz, first_gainmap->data_sz); - uhdr_mem_block_t* second_xmp = uhdr_dec_get_xmp(second_round_trip.decoder.get()); - ASSERT_NE(second_xmp, nullptr); - ASSERT_NE(second_xmp->data, nullptr); - const std::string second_packet = getXmpPacket(second_xmp); - const ContainerDirectoryInfo directory_info = getContainerDirectoryInfo(second_packet); - EXPECT_TRUE(directory_info.parsed); - EXPECT_EQ(directory_info.directory_count, 1u); - EXPECT_EQ(directory_info.gainmap_lengths.size(), 1u); - if (!directory_info.gainmap_lengths.empty()) { - EXPECT_EQ(directory_info.gainmap_lengths.front(), second_gainmap->data_sz); - } - if (input_index == 1) { + std::string second_packet; + checkOutput(second_round_trip, true, control.expected_description, + control.absent_description, &second_packet); + if (control.expect_xpacket_wrapper) { EXPECT_NE(second_packet.find( ""), std::string::npos); From b252467c38c5abd0724bd7bf2fe5f9fd81e889a2 Mon Sep 17 00:00:00 2001 From: Greg Benz Date: Wed, 16 Sep 2026 17:51:47 -0500 Subject: [PATCH 4/5] Avoid copying XMP removal ranges in structured binding --- lib/src/jpegrutils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/jpegrutils.cpp b/lib/src/jpegrutils.cpp index cde05810..511cea13 100644 --- a/lib/src/jpegrutils.cpp +++ b/lib/src/jpegrutils.cpp @@ -1328,7 +1328,7 @@ string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length GeneratePrimaryDescription(secondary_image_length, metadata); string merged; size_t cursor = 0; - for (const auto [begin, end] : removals) { + for (const auto& [begin, end] : removals) { if (begin < cursor || end > insertion || end > existing_xmp.size()) return string(); merged.append(existing_xmp, cursor, begin - cursor); cursor = end; From c27ef261845cec9edd2b7864b623cc7c6a73b551 Mon Sep 17 00:00:00 2001 From: Dichen Zhang Date: Fri, 18 Sep 2026 23:46:38 +0000 Subject: [PATCH 5/5] Fix build errors from duplicated hunks in XMP merge change --- lib/src/jpegr.cpp | 22 ++++++++++------------ lib/src/jpegrutils.cpp | 5 ----- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp index 87654882..e801c653 100644 --- a/lib/src/jpegr.cpp +++ b/lib/src/jpegr.cpp @@ -1269,18 +1269,8 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr // Prepare and write XMP std::string xmp_primary_str; - if (kWriteXmpMetadata) { - xmp_primary_str = generateXmpForPrimaryImage(secondary_image_size, *metadata, pXmp); - if (pXmp != nullptr && pXmp->data != nullptr && pXmp->data_sz > 0 && - xmp_primary_str.empty()) { - uhdr_error_info_t status; - status.error_code = UHDR_CODEC_INVALID_PARAM; - status.has_detail = 1; - snprintf(status.detail, sizeof status.detail, - "unable to safely merge supplied XMP metadata into the primary image"); - return status; - } - } else if (pXmp != nullptr && pXmp->data != nullptr && pXmp->data_sz > 0) { + std::string user_xmp_str; + if (pXmp != nullptr && pXmp->data != nullptr && pXmp->data_sz > 0) { const std::string kXmpHeader = "http://ns.adobe.com/xap/1.0/"; if (pXmp->data_sz > kXmpHeader.size() && memcmp(pXmp->data, kXmpHeader.c_str(), kXmpHeader.size()) == 0) { @@ -1302,6 +1292,14 @@ uhdr_error_info_t JpegR::appendGainMap(uhdr_compressed_image_t* sdr_intent_compr uhdr_mem_block_t user_block{const_cast(user_xmp_str.data()), user_xmp_str.size(), user_xmp_str.size()}; xmp_primary_str = generateXmpForPrimaryImage(secondary_image_size, *metadata, &user_block); + if (xmp_primary_str.empty()) { + uhdr_error_info_t status; + status.error_code = UHDR_CODEC_INVALID_PARAM; + status.has_detail = 1; + snprintf(status.detail, sizeof status.detail, + "unable to safely merge supplied XMP metadata into the primary image"); + return status; + } } else { xmp_primary_str = user_xmp_str; } diff --git a/lib/src/jpegrutils.cpp b/lib/src/jpegrutils.cpp index 3138a21a..2b41c306 100644 --- a/lib/src/jpegrutils.cpp +++ b/lib/src/jpegrutils.cpp @@ -1342,10 +1342,6 @@ string MergePrimaryXmp(const string& existing_xmp, size_t secondary_image_length } // namespace -string generateXmpForPrimaryImage(size_t secondary_image_length, - uhdr_gainmap_metadata_ext_t& metadata, - uhdr_mem_block_t* user_xmp) { - /* * Computes 128-bit MD5 digest formatted as a 32-character uppercase hexadecimal GUID. * @@ -1499,7 +1495,6 @@ string generateXmpForPrimaryImage(size_t secondary_image_length, return MergePrimaryXmp(existing_xmp, secondary_image_length, metadata); } - const vector kConDirSeq({kConDirectory, string("rdf:Seq")}); std::stringstream ss; photos_editing_formats::image_io::XmlWriter writer(ss); writer.StartWritingElement("x:xmpmeta");