@@ -127,6 +127,11 @@ namespace {
127127 std::string_view uri;
128128 };
129129
130+ struct PortableCustomNsDecl final {
131+ std::string prefix;
132+ std::string uri;
133+ };
134+
130135 static void emit_xmp_packet_begin (SpanWriter* w,
131136 std::span<const XmpNsDecl> decls) noexcept
132137 {
@@ -1316,6 +1321,48 @@ namespace {
13161321 return false ;
13171322 }
13181323
1324+ static bool xmp_namespace_uri_is_xml_attr_safe (std::string_view uri) noexcept
1325+ {
1326+ if (uri.empty ()) {
1327+ return false ;
1328+ }
1329+ for (size_t i = 0 ; i < uri.size (); ++i) {
1330+ const unsigned char c = static_cast <unsigned char >(uri[i]);
1331+ if (c < 0x20U || c > 0x7EU || c == ' "' || c == ' &' || c == ' <'
1332+ || c == ' >' ) {
1333+ return false ;
1334+ }
1335+ }
1336+ return true ;
1337+ }
1338+
1339+ static bool portable_custom_ns_prefix_for_uri (
1340+ std::string_view ns, std::span<const PortableCustomNsDecl> decls,
1341+ std::string_view* out_prefix) noexcept
1342+ {
1343+ if (!out_prefix) {
1344+ return false ;
1345+ }
1346+ *out_prefix = {};
1347+ for (size_t i = 0 ; i < decls.size (); ++i) {
1348+ if (decls[i].uri == ns) {
1349+ *out_prefix = decls[i].prefix ;
1350+ return true ;
1351+ }
1352+ }
1353+ return false ;
1354+ }
1355+
1356+ static bool portable_ns_to_prefix (
1357+ std::string_view ns, std::span<const PortableCustomNsDecl> decls,
1358+ std::string_view* out_prefix) noexcept
1359+ {
1360+ if (xmp_ns_to_portable_prefix (ns, out_prefix)) {
1361+ return true ;
1362+ }
1363+ return portable_custom_ns_prefix_for_uri (ns, decls, out_prefix);
1364+ }
1365+
13191366 static std::string_view
13201367 portable_property_name_for_existing_xmp (std::string_view prefix,
13211368 std::string_view name) noexcept
@@ -3174,7 +3221,8 @@ namespace {
31743221 }
31753222
31763223 static bool process_portable_existing_xmp_entry (
3177- const ByteArena& arena, const Entry& e, uint32_t order, SpanWriter* w,
3224+ const ByteArena& arena, std::span<const PortableCustomNsDecl> decls,
3225+ const Entry& e, uint32_t order, SpanWriter* w,
31783226 PortablePropertyOwnerMap* claims,
31793227 std::vector<PortableIndexedProperty>* indexed) noexcept
31803228 {
@@ -3188,7 +3236,7 @@ namespace {
31883236 const std::string_view name
31893237 = arena_string (arena, e.key .data .xmp_property .property_path );
31903238 std::string_view prefix;
3191- if (!xmp_ns_to_portable_prefix (ns, &prefix)) {
3239+ if (!portable_ns_to_prefix (ns, decls , &prefix)) {
31923240 return false ;
31933241 }
31943242
@@ -3240,6 +3288,70 @@ namespace {
32403288 return false ;
32413289 }
32423290
3291+ static void collect_portable_custom_ns_decls (
3292+ const ByteArena& arena, std::span<const Entry> entries,
3293+ const XmpPortableOptions& options,
3294+ std::vector<PortableCustomNsDecl>* out) noexcept
3295+ {
3296+ if (!out) {
3297+ return ;
3298+ }
3299+ out->clear ();
3300+ if (!options.include_existing_xmp
3301+ || options.existing_namespace_policy
3302+ != XmpExistingNamespacePolicy::PreserveCustom) {
3303+ return ;
3304+ }
3305+
3306+ uint32_t next_index = 1U ;
3307+ for (size_t i = 0 ; i < entries.size (); ++i) {
3308+ const Entry& e = entries[i];
3309+ if (e.key .kind != MetaKeyKind::XmpProperty) {
3310+ continue ;
3311+ }
3312+
3313+ const std::string_view ns
3314+ = arena_string (arena, e.key .data .xmp_property .schema_ns );
3315+ std::string_view prefix;
3316+ if (xmp_ns_to_portable_prefix (ns, &prefix)
3317+ || !xmp_namespace_uri_is_xml_attr_safe (ns)
3318+ || portable_custom_ns_prefix_for_uri (
3319+ ns, std::span<const PortableCustomNsDecl>(out->data (),
3320+ out->size ()),
3321+ &prefix)) {
3322+ continue ;
3323+ }
3324+
3325+ const std::string_view name
3326+ = arena_string (arena, e.key .data .xmp_property .property_path );
3327+ std::string_view portable_name;
3328+ if (is_simple_xmp_property_name (name)) {
3329+ portable_name = portable_property_name_for_existing_xmp (
3330+ " omns" , name);
3331+ } else {
3332+ std::string_view base_name;
3333+ uint32_t index = 0U ;
3334+ if (!parse_indexed_xmp_property_name (name, &base_name,
3335+ &index)) {
3336+ continue ;
3337+ }
3338+ portable_name = portable_property_name_for_existing_xmp (
3339+ " omns" , base_name);
3340+ }
3341+ if (portable_name.empty ()
3342+ || xmp_property_is_nonportable_blob (" omns" , portable_name)
3343+ || !portable_scalar_like_value_supported (arena, e.value )) {
3344+ continue ;
3345+ }
3346+
3347+ PortableCustomNsDecl decl;
3348+ decl.prefix = " omns" + std::to_string (next_index);
3349+ decl.uri .assign (ns.data (), ns.size ());
3350+ out->push_back (std::move (decl));
3351+ next_index += 1U ;
3352+ }
3353+ }
3354+
32433355 static bool
32443356 process_portable_exif_entry (const ByteArena& arena,
32453357 std::span<const Entry> entries, const Entry& e,
@@ -3600,6 +3712,7 @@ namespace {
36003712
36013713 static void emit_portable_pass (
36023714 PortablePassKind pass, const ByteArena& arena,
3715+ std::span<const PortableCustomNsDecl> custom_decls,
36033716 std::span<const Entry> entries, const XmpPortableOptions& options,
36043717 SpanWriter* w, PortablePropertyOwnerMap* claims,
36053718 std::vector<PortableIndexedProperty>* indexed, uint32_t * emitted,
@@ -3640,7 +3753,8 @@ namespace {
36403753 continue ;
36413754 }
36423755 if (process_portable_existing_xmp_entry (
3643- arena, e, static_cast <uint32_t >(i), w, claims,
3756+ arena, custom_decls, e, static_cast <uint32_t >(i), w,
3757+ claims,
36443758 indexed)) {
36453759 *emitted += 1U ;
36463760 }
@@ -3677,12 +3791,24 @@ dump_xmp_portable(const MetaStore& store, std::span<std::byte> out,
36773791 XmpNsDecl { " photoshop" , kXmpNsPhotoshop },
36783792 XmpNsDecl { " Iptc4xmpCore" , kXmpNsIptc4xmpCore },
36793793 };
3680- emit_xmp_packet_begin (&w, std::span<const XmpNsDecl>(kDecls .data (),
3681- kDecls .size ()));
3682-
36833794 const ByteArena& arena = store.arena ();
36843795 const std::span<const Entry> es = store.entries ();
36853796
3797+ std::vector<PortableCustomNsDecl> custom_decls;
3798+ collect_portable_custom_ns_decls (arena, es, options, &custom_decls);
3799+
3800+ std::vector<XmpNsDecl> decls;
3801+ decls.reserve (kDecls .size () + custom_decls.size ());
3802+ for (size_t i = 0 ; i < kDecls .size (); ++i) {
3803+ decls.push_back (kDecls [i]);
3804+ }
3805+ for (size_t i = 0 ; i < custom_decls.size (); ++i) {
3806+ decls.push_back (XmpNsDecl { custom_decls[i].prefix ,
3807+ custom_decls[i].uri });
3808+ }
3809+ emit_xmp_packet_begin (&w, std::span<const XmpNsDecl>(decls.data (),
3810+ decls.size ()));
3811+
36863812 std::vector<PortableIndexedProperty> indexed;
36873813 indexed.reserve (128 );
36883814 PortablePropertyOwnerMap claims;
@@ -3692,26 +3818,53 @@ dump_xmp_portable(const MetaStore& store, std::span<std::byte> out,
36923818 uint32_t iptc_order = 0U ;
36933819
36943820 if (options.conflict_policy == XmpConflictPolicy::ExistingWins) {
3695- emit_portable_pass (PortablePassKind::ExistingXmp, arena, es, options,
3696- &w, &claims, &indexed, &emitted, &iptc_order);
3697- emit_portable_pass (PortablePassKind::Exif, arena, es, options, &w,
3698- &claims, &indexed, &emitted, &iptc_order);
3699- emit_portable_pass (PortablePassKind::Iptc, arena, es, options, &w,
3700- &claims, &indexed, &emitted, &iptc_order);
3821+ emit_portable_pass (PortablePassKind::ExistingXmp, arena,
3822+ std::span<const PortableCustomNsDecl>(
3823+ custom_decls.data (), custom_decls.size ()),
3824+ es, options, &w, &claims, &indexed, &emitted,
3825+ &iptc_order);
3826+ emit_portable_pass (PortablePassKind::Exif, arena,
3827+ std::span<const PortableCustomNsDecl>(
3828+ custom_decls.data (), custom_decls.size ()),
3829+ es, options, &w, &claims, &indexed, &emitted,
3830+ &iptc_order);
3831+ emit_portable_pass (PortablePassKind::Iptc, arena,
3832+ std::span<const PortableCustomNsDecl>(
3833+ custom_decls.data (), custom_decls.size ()),
3834+ es, options, &w, &claims, &indexed, &emitted,
3835+ &iptc_order);
37013836 } else if (options.conflict_policy == XmpConflictPolicy::GeneratedWins) {
3702- emit_portable_pass (PortablePassKind::Exif, arena, es, options, &w,
3703- &claims, &indexed, &emitted, &iptc_order);
3704- emit_portable_pass (PortablePassKind::Iptc, arena, es, options, &w,
3705- &claims, &indexed, &emitted, &iptc_order);
3706- emit_portable_pass (PortablePassKind::ExistingXmp, arena, es, options,
3707- &w, &claims, &indexed, &emitted, &iptc_order);
3837+ emit_portable_pass (PortablePassKind::Exif, arena,
3838+ std::span<const PortableCustomNsDecl>(
3839+ custom_decls.data (), custom_decls.size ()),
3840+ es, options, &w, &claims, &indexed, &emitted,
3841+ &iptc_order);
3842+ emit_portable_pass (PortablePassKind::Iptc, arena,
3843+ std::span<const PortableCustomNsDecl>(
3844+ custom_decls.data (), custom_decls.size ()),
3845+ es, options, &w, &claims, &indexed, &emitted,
3846+ &iptc_order);
3847+ emit_portable_pass (PortablePassKind::ExistingXmp, arena,
3848+ std::span<const PortableCustomNsDecl>(
3849+ custom_decls.data (), custom_decls.size ()),
3850+ es, options, &w, &claims, &indexed, &emitted,
3851+ &iptc_order);
37083852 } else {
3709- emit_portable_pass (PortablePassKind::Exif, arena, es, options, &w,
3710- &claims, &indexed, &emitted, &iptc_order);
3711- emit_portable_pass (PortablePassKind::ExistingXmp, arena, es, options,
3712- &w, &claims, &indexed, &emitted, &iptc_order);
3713- emit_portable_pass (PortablePassKind::Iptc, arena, es, options, &w,
3714- &claims, &indexed, &emitted, &iptc_order);
3853+ emit_portable_pass (PortablePassKind::Exif, arena,
3854+ std::span<const PortableCustomNsDecl>(
3855+ custom_decls.data (), custom_decls.size ()),
3856+ es, options, &w, &claims, &indexed, &emitted,
3857+ &iptc_order);
3858+ emit_portable_pass (PortablePassKind::ExistingXmp, arena,
3859+ std::span<const PortableCustomNsDecl>(
3860+ custom_decls.data (), custom_decls.size ()),
3861+ es, options, &w, &claims, &indexed, &emitted,
3862+ &iptc_order);
3863+ emit_portable_pass (PortablePassKind::Iptc, arena,
3864+ std::span<const PortableCustomNsDecl>(
3865+ custom_decls.data (), custom_decls.size ()),
3866+ es, options, &w, &claims, &indexed, &emitted,
3867+ &iptc_order);
37153868 }
37163869
37173870 emit_portable_indexed_groups (&w, arena, &indexed,
@@ -3805,6 +3958,8 @@ make_xmp_sidecar_options(const XmpSidecarRequest& request) noexcept
38053958 options.portable .include_exif = request.include_exif ;
38063959 options.portable .include_iptc = request.include_iptc ;
38073960 options.portable .include_existing_xmp = request.include_existing_xmp ;
3961+ options.portable .existing_namespace_policy
3962+ = request.portable_existing_namespace_policy ;
38083963 options.portable .conflict_policy = request.portable_conflict_policy ;
38093964 options.portable .exiftool_gpsdatetime_alias
38103965 = request.portable_exiftool_gpsdatetime_alias ;
0 commit comments