Refactor SkJpegXmp into SkXmp which is more generic.

Useful for formats other than jpeg that would use the same
gainmap xmp format for example.

Change-Id: Id1e69032a4566e96fdc4d0038ee6a6b39ea7ed95
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/699656
Reviewed-by: Christopher Cameron <ccameron@google.com>
Commit-Queue: Christopher Cameron <ccameron@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/gn/codec.gni b/gn/codec.gni
index 4afeaef..68a195f 100644
--- a/gn/codec.gni
+++ b/gn/codec.gni
@@ -28,6 +28,7 @@
   "$_src/codec/SkSampler.h",
   "$_src/codec/SkSwizzler.cpp",
   "$_src/codec/SkSwizzler.h",
+  "$_src/codec/SkXmp.cpp",
 ]
 
 # List generated by Bazel rules:
diff --git a/gn/core.gni b/gn/core.gni
index 10fb13b..a59729c 100644
--- a/gn/core.gni
+++ b/gn/core.gni
@@ -159,6 +159,7 @@
   "$_include/private/SkPathRef.h",
   "$_include/private/SkShadowFlags.h",
   "$_include/private/SkWeakRefCnt.h",
+  "$_include/private/SkXmp.h",
   "$_include/private/base/SingleOwner.h",
   "$_include/private/base/SkAPI.h",
   "$_include/private/base/SkAlign.h",
diff --git a/gn/tests.gni b/gn/tests.gni
index 7059fd3..8398ac7 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -276,6 +276,7 @@
   "$_tests/SkUTFTest.cpp",
   "$_tests/SkVMTest.cpp",
   "$_tests/SkVxTest.cpp",
+  "$_tests/SkXmpTest.cpp",
   "$_tests/Skbug12214.cpp",
   "$_tests/Skbug5221.cpp",
   "$_tests/Skbug6389.cpp",
@@ -472,4 +473,7 @@
 
 tests_sources += ganesh_tests_sources
 
-jpeg_gainmap_tests_sources = [ "$_tests/JpegGainmapTest.cpp" ]
+jpeg_gainmap_tests_sources = [
+  "$_tests/JpegGainmapTest.cpp",
+  "$_tests/SkJpegXmpTest.cpp",
+]
diff --git a/include/private/BUILD.bazel b/include/private/BUILD.bazel
index b60a407..6bff980 100644
--- a/include/private/BUILD.bazel
+++ b/include/private/BUILD.bazel
@@ -29,6 +29,7 @@
         "SkPathRef.h",
         "SkShadowFlags.h",
         "SkWeakRefCnt.h",
+        "SkXmp.h",
         ":sksl_private_hdrs",
         "//include/private/base:private_hdrs",
         "//include/private/chromium:private_hdrs",
diff --git a/include/private/SkXmp.h b/include/private/SkXmp.h
new file mode 100644
index 0000000..e45e5c7
--- /dev/null
+++ b/include/private/SkXmp.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkXmp_DEFINED
+#define SkXmp_DEFINED
+
+#include "include/core/SkRefCnt.h"
+#include "include/private/base/SkAPI.h"
+
+class SkData;
+struct SkGainmapInfo;
+
+#include <cstddef>
+#include <memory>
+
+/*
+ * An interface to extract information from XMP metadata.
+ */
+class SK_API SkXmp {
+public:
+    virtual ~SkXmp() = default;
+
+    // Create from XMP data.
+    static std::unique_ptr<SkXmp> Make(sk_sp<SkData> xmpData);
+    // Create from standard XMP + extended XMP data, see XMP Specification Part 3: Storage in files,
+    // Section 1.1.3.1: Extended XMP in JPEG
+    static std::unique_ptr<SkXmp> Make(sk_sp<SkData> xmpStandard, sk_sp<SkData> xmpExtended);
+
+    // Extract HDRGM gainmap parameters.
+    virtual bool getGainmapInfoHDRGM(SkGainmapInfo* info) const = 0;
+
+    // Extract HDRGainMap gainmap parameters.
+    virtual bool getGainmapInfoHDRGainMap(SkGainmapInfo* info) const = 0;
+
+    // If this includes GContainer metadata and the GContainer contains an item with semantic
+    // GainMap and Mime of image/jpeg, then return true, and populate |offset| and |size| with
+    // that item's offset (from the end of the primary JPEG image's EndOfImage), and the size of
+    // the gainmap.
+    virtual bool getContainerGainmapLocation(size_t* offset, size_t* size) const = 0;
+
+    // Return the GUID of an Extended XMP if present, or null otherwise.
+    virtual const char* getExtendedXmpGuid() const = 0;
+};
+
+#endif
diff --git a/public.bzl b/public.bzl
index 11bffe4..36c771f 100644
--- a/public.bzl
+++ b/public.bzl
@@ -244,6 +244,7 @@
     "include/private/SkSLSampleUsage.h",
     "include/private/SkShadowFlags.h",
     "include/private/SkWeakRefCnt.h",
+    "include/private/SkXmp.h",
     "include/private/base/SingleOwner.h",
     "include/private/base/SkAPI.h",
     "include/private/base/SkAlign.h",
diff --git a/src/BUILD.bazel b/src/BUILD.bazel
index c196107..d227e88 100644
--- a/src/BUILD.bazel
+++ b/src/BUILD.bazel
@@ -36,6 +36,7 @@
         "//src/shaders:srcs",
         "//src/text:srcs",
         "//src/utils:srcs",
+        "//src/xml:srcs",
     ] + select({
         "//src/gpu:has_gpu_backend": [
             "//src/gpu:srcs",
@@ -48,7 +49,6 @@
     }) + select({
         "//src/svg:enable_svg_canvas_true": [
             "//src/svg:srcs",
-            "//src/xml:srcs",
         ],
         "//conditions:default": [],
     }) + select({
@@ -95,6 +95,7 @@
         "//src/sksl/tracing:skopts_hdrs",
         "//src/text:private_hdrs",
         "//src/utils:private_hdrs",
+        "//src/xml:private_hdrs",
     ] + select({
         "//src/gpu:has_gpu_backend": [
             "//src/gpu:private_hdrs",
@@ -107,7 +108,6 @@
     }) + select({
         "//src/svg:enable_svg_canvas_true": [
             "//src/svg:private_hdrs",
-            "//src/xml:private_hdrs",
         ],
         "//conditions:default": [],
     }) + select({
@@ -132,6 +132,7 @@
         "//src/encode:deps",
         "//src/opts:deps",
         "//src/ports:deps",
+        "//src/xml:deps",
     ] + select({
         "//src/gpu:has_gpu_backend": ["//src/gpu:deps"],
         "//conditions:default": [],
@@ -139,7 +140,6 @@
         "//src/sksl:needs_sksl": ["//src/sksl:deps"],
         "//conditions:default": [],
     }) + select({
-        "//src/svg:enable_svg_canvas_true": ["//src/xml:deps"],
         "//conditions:default": [],
     }) + select({
         "//src/pdf:enable_pdf_backend_true": ["//src/pdf:deps"],
diff --git a/src/codec/BUILD.bazel b/src/codec/BUILD.bazel
index 8e64881..60b6c8b 100644
--- a/src/codec/BUILD.bazel
+++ b/src/codec/BUILD.bazel
@@ -30,6 +30,7 @@
     "SkSampler.h",
     "SkSwizzler.cpp",
     "SkSwizzler.h",
+    "SkXmp.cpp",
 ]
 
 split_srcs_and_hdrs(
@@ -204,9 +205,9 @@
 skia_filegroup(
     name = "srcs",
     srcs = [
+        ":core_srcs",
         ":decode_android_srcs",
         ":decode_bmp_srcs",
-        ":core_srcs",
     ] + select_multi(
         {
             ":avif_decode_codec": [":decode_avif_srcs"],
@@ -224,9 +225,9 @@
 skia_filegroup(
     name = "private_hdrs",
     srcs = [
+        ":core_hdrs",
         ":decode_android_hdrs",
         ":decode_bmp_hdrs",
-        ":core_hdrs",
     ] + select({
         ":needs_jpeg_priv": ["SkJpegPriv.h"],  # used by src/encode/SkJPEGWriteUtility
         "//conditions:default": [],
diff --git a/src/codec/SkJpegCodec.cpp b/src/codec/SkJpegCodec.cpp
index a88e73b..830ab1c 100644
--- a/src/codec/SkJpegCodec.cpp
+++ b/src/codec/SkJpegCodec.cpp
@@ -31,6 +31,7 @@
 
 #ifdef SK_CODEC_DECODES_JPEG_GAINMAPS
 #include "include/private/SkGainmapInfo.h"
+#include "include/private/SkXmp.h"
 #include "src/codec/SkJpegMultiPicture.h"
 #include "src/codec/SkJpegSegmentScan.h"
 #include "src/codec/SkJpegXmp.h"
@@ -1079,14 +1080,14 @@
 
 #ifdef SK_CODEC_DECODES_JPEG_GAINMAPS
 // Collect and parse the primary and extended XMP metadata.
-static std::unique_ptr<SkJpegXmp> get_xmp_metadata(const SkJpegMarkerList& markerList) {
+static std::unique_ptr<SkXmp> get_xmp_metadata(const SkJpegMarkerList& markerList) {
     std::vector<sk_sp<SkData>> decoderApp1Params;
     for (const auto& marker : markerList) {
         if (marker.fMarker == kXMPMarker) {
             decoderApp1Params.push_back(marker.fData);
         }
     }
-    return SkJpegXmp::Make(decoderApp1Params);
+    return SkJpegMakeXmp(decoderApp1Params);
 }
 
 // Extract the SkJpegMultiPictureParameters from this image (if they exist). If |sourceMgr| and
@@ -1174,7 +1175,7 @@
         }
         app1Params.push_back(std::move(parameters));
     }
-    auto xmp = SkJpegXmp::Make(app1Params);
+    auto xmp = SkJpegMakeXmp(app1Params);
     if (!xmp) {
         return false;
     }
@@ -1213,7 +1214,7 @@
                              SkGainmapInfo* info,
                              std::unique_ptr<SkStream>* gainmapImageStream) {
     // The GContainer and APP15-based HDRGM formats require XMP metadata. Extract it now.
-    std::unique_ptr<SkJpegXmp> xmp = get_xmp_metadata(markerList);
+    std::unique_ptr<SkXmp> xmp = get_xmp_metadata(markerList);
 
     // Let |base_image_info| be the HDRGM gainmap information found in the base image (if any).
     SkGainmapInfo base_image_info;
diff --git a/src/codec/SkJpegXmp.cpp b/src/codec/SkJpegXmp.cpp
index 4b9b08d..118cd62 100644
--- a/src/codec/SkJpegXmp.cpp
+++ b/src/codec/SkJpegXmp.cpp
@@ -16,15 +16,10 @@
 
 #include <string>
 
-SkJpegXmp::SkJpegXmp() = default;
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// XMP JPEG extraction helper functions
-
 constexpr size_t kGuidAsciiSize = 32;
 
 /*
- * Extract standard XMP metadata.
+ * Extract standard XMP metadata. The decoderApp1Params must outlive the returned SkData.
  *
  * See XMP Specification Part 3: Storage in files, Section 1.1.3: JPEG.
  */
@@ -65,11 +60,11 @@
     constexpr size_t kHeaderSize = kSigSize + kGuidAsciiSize + kFullLengthSize + kOffsetSize;
 
     // Validate the provided ASCII guid.
-    SkMD5::Digest guidAsDigest;
     if (strlen(guidAscii) != kGuidAsciiSize) {
         SkCodecPrintf("Invalid ASCII GUID size.\n");
         return nullptr;
     }
+    SkMD5::Digest guidAsDigest;
     for (size_t i = 0; i < kGuidAsciiSize; ++i) {
         uint8_t digit = 0;
         if (guidAscii[i] >= '0' && guidAscii[i] <= '9') {
@@ -182,359 +177,19 @@
     return xmpExtendedData;
 }
 
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// XMP parsing helper functions
-
-const char* kXmlnsPrefix = "xmlns:";
-const size_t kXmlnsPrefixLength = 6;
-
-static const char* get_namespace_prefix(const char* name) {
-    if (strlen(name) <= kXmlnsPrefixLength) {
-        return nullptr;
-    }
-    return name + kXmlnsPrefixLength;
-}
-
-/*
- * Given a node, see if that node has only one child with the indicated name. If so, see if that
- * child has only a single child of its own, and that child is text. If all of that is the case
- * then return the text, otherwise return nullptr.
- *
- * In the following example, innerText will be returned.
- *    <node><childName>innerText</childName></node>
- *
- * In the following examples, nullptr will be returned (because there are multiple children with
- * childName in the first case, and because the child has children of its own in the second).
- *    <node><childName>innerTextA</childName><childName>innerTextB</childName></node>
- *    <node><childName>innerText<otherGrandChild/></childName></node>
- */
-static const char* get_unique_child_text(const SkDOM& dom,
-                                         const SkDOM::Node* node,
-                                         const std::string& childName) {
-    // Fail if there are multiple children with childName.
-    if (dom.countChildren(node, childName.c_str()) != 1) {
-        return nullptr;
-    }
-    const auto* child = dom.getFirstChild(node, childName.c_str());
-    if (!child) {
-        return nullptr;
-    }
-    // Fail if the child has any children besides text.
-    if (dom.countChildren(child) != 1) {
-        return nullptr;
-    }
-    const auto* grandChild = dom.getFirstChild(child);
-    if (dom.getType(grandChild) != SkDOM::kText_Type) {
-        return nullptr;
-    }
-    // Return the text.
-    return dom.getName(grandChild);
-}
-
-/*
- * Given a node, find a child node of the specified type.
- *
- * If there exists a child node with name |prefix| + ":" + |type|, then return that child.
- *
- * If there exists a child node with name "rdf:type" that has attribute "rdf:resource" with value
- * of |type|, then if there also exists a child node with name "rdf:value" with attribute
- * "rdf:parseType" of "Resource", then return that child node with name "rdf:value". See Example
- * 3 in section 7.9.2.5: RDF Typed Nodes.
- * TODO(ccameron): This should also accept a URI for the type.
- */
-static const SkDOM::Node* get_typed_child(const SkDOM* dom,
-                                          const SkDOM::Node* node,
-                                          const std::string& prefix,
-                                          const std::string& type) {
-    const auto name = prefix + std::string(":") + type;
-    const SkDOM::Node* child = dom->getFirstChild(node, name.c_str());
-    if (child) {
-        return child;
-    }
-
-    const SkDOM::Node* typeChild = dom->getFirstChild(node, "rdf:type");
-    if (!typeChild) {
-        return nullptr;
-    }
-    const char* typeChildResource = dom->findAttr(typeChild, "rdf:resource");
-    if (!typeChildResource || typeChildResource != type) {
-        return nullptr;
-    }
-
-    const SkDOM::Node* valueChild = dom->getFirstChild(node, "rdf:value");
-    if (!valueChild) {
-        return nullptr;
-    }
-    const char* valueChildParseType = dom->findAttr(valueChild, "rdf:parseType");
-    if (!valueChildParseType || strcmp(valueChildParseType, "Resource") != 0) {
-        return nullptr;
-    }
-    return valueChild;
-}
-
-/*
- * Given a node, return its value for the specified attribute.
- *
- * This will first look for an attribute with the name |prefix| + ":" + |key|, and return the value
- * for that attribute.
- *
- * This will then look for a child node of name |prefix| + ":" + |key|, and return the field value
- * for that child.
- */
-static const char* get_attr(const SkDOM* dom,
-                            const SkDOM::Node* node,
-                            const std::string& prefix,
-                            const std::string& key) {
-    const auto name = prefix + ":" + key;
-    const char* attr = dom->findAttr(node, name.c_str());
-    if (attr) {
-        return attr;
-    }
-    return get_unique_child_text(*dom, node, name);
-}
-
-// Perform get_attr and parse the result as a bool.
-static bool get_attr_bool(const SkDOM* dom,
-                          const SkDOM::Node* node,
-                          const std::string& prefix,
-                          const std::string& key,
-                          bool* outValue) {
-    const char* attr = get_attr(dom, node, prefix, key);
-    if (!attr) {
-        return false;
-    }
-    switch (SkParse::FindList(attr, "False,True")) {
-        case 0:
-            *outValue = false;
-            return true;
-        case 1:
-            *outValue = true;
-            return true;
-        default:
-            break;
-    }
-    return false;
-}
-
-// Perform get_attr and parse the result as an int32_t.
-static bool get_attr_int32(const SkDOM* dom,
-                           const SkDOM::Node* node,
-                           const std::string& prefix,
-                           const std::string& key,
-                           int32_t* value) {
-    const char* attr = get_attr(dom, node, prefix, key);
-    if (!attr) {
-        return false;
-    }
-    if (!SkParse::FindS32(attr, value)) {
-        return false;
-    }
-    return true;
-}
-
-// Perform get_attr and parse the result as a float.
-static bool get_attr_float(const SkDOM* dom,
-                           const SkDOM::Node* node,
-                           const std::string& prefix,
-                           const std::string& key,
-                           float* outValue) {
-    const char* attr = get_attr(dom, node, prefix, key);
-    if (!attr) {
-        return false;
-    }
-    SkScalar value = 0.f;
-    if (SkParse::FindScalar(attr, &value)) {
-        *outValue = value;
-        return true;
-    }
-    return false;
-}
-
-// Perform get_attr and parse the result as three comma-separated floats. Return the result as an
-// SkColor4f with the alpha component set to 1.
-static bool get_attr_float3_as_list(const SkDOM* dom,
-                                    const SkDOM::Node* node,
-                                    const std::string& prefix,
-                                    const std::string& key,
-                                    SkColor4f* outValue) {
-    const auto name = prefix + ":" + key;
-
-    // Fail if there are multiple children with childName.
-    if (dom->countChildren(node, name.c_str()) != 1) {
-        return false;
-    }
-    // Find the child.
-    const auto* child = dom->getFirstChild(node, name.c_str());
-    if (!child) {
-        return false;
-    }
-
-    // Search for the rdf:Seq child.
-    const auto* seq = dom->getFirstChild(child, "rdf:Seq");
-    if (!seq) {
-        return false;
-    }
-
-    size_t count = 0;
-    SkScalar values[3] = {0.f, 0.f, 0.f};
-    for (const auto* liNode = dom->getFirstChild(seq, "rdf:li"); liNode;
-         liNode = dom->getNextSibling(liNode, "rdf:li")) {
-        if (count > 2) {
-            SkCodecPrintf("Too many items in list.\n");
-            return false;
-        }
-        if (dom->countChildren(liNode) != 1) {
-            SkCodecPrintf("Item can only have one child.\n");
-            return false;
-        }
-        const auto* liTextNode = dom->getFirstChild(liNode);
-        if (dom->getType(liTextNode) != SkDOM::kText_Type) {
-            SkCodecPrintf("Item's only child must be text.\n");
-            return false;
-        }
-        const char* liText = dom->getName(liTextNode);
-        if (!liText) {
-            SkCodecPrintf("Failed to get item's text.\n");
-            return false;
-        }
-        if (!SkParse::FindScalar(liText, values + count)) {
-            SkCodecPrintf("Failed to parse item's text to float.\n");
-            return false;
-        }
-        count += 1;
-    }
-    if (count < 3) {
-        SkCodecPrintf("List didn't have enough items.\n");
-        return false;
-    }
-    *outValue = {values[0], values[1], values[2], 1.f};
-    return true;
-}
-
-static bool get_attr_float3(const SkDOM* dom,
-                            const SkDOM::Node* node,
-                            const std::string& prefix,
-                            const std::string& key,
-                            SkColor4f* outValue) {
-    if (get_attr_float3_as_list(dom, node, prefix, key, outValue)) {
-        return true;
-    }
-    SkScalar value = -1.0;
-    if (get_attr_float(dom, node, prefix, key, &value)) {
-        *outValue = {value, value, value, 1.f};
-        return true;
-    }
-    return false;
-}
-
-static void find_uri_namespaces(const SkDOM& dom,
-                                const SkDOM::Node* node,
-                                size_t count,
-                                const char* uris[],
-                                const char* outNamespaces[]) {
-    // Search all attributes for xmlns:NAMESPACEi="URIi".
-    for (const auto* attr = dom.getFirstAttr(node); attr; attr = dom.getNextAttr(node, attr)) {
-        const char* attrName = dom.getAttrName(node, attr);
-        const char* attrValue = dom.getAttrValue(node, attr);
-        if (!attrName || !attrValue) {
-            continue;
-        }
-        // Make sure the name starts with "xmlns:".
-        if (strlen(attrName) <= kXmlnsPrefixLength) {
-            continue;
-        }
-        if (memcmp(attrName, kXmlnsPrefix, kXmlnsPrefixLength) != 0) {
-            continue;
-        }
-        // Search for a requested URI that matches.
-        for (size_t i = 0; i < count; ++i) {
-            if (strcmp(attrValue, uris[i]) != 0) {
-                continue;
-            }
-            outNamespaces[i] = attrName;
-        }
-    }
-}
-
-// See SkJpegXmp::findUriNamespaces. This function has the same behavior, but only searches
-// a single SkDOM.
-static const SkDOM::Node* find_uri_namespaces(const SkDOM& dom,
-                                              size_t count,
-                                              const char* uris[],
-                                              const char* outNamespaces[]) {
-    const SkDOM::Node* root = dom.getRootNode();
-    if (!root) {
-        return nullptr;
-    }
-
-    // Ensure that the root node identifies itself as XMP metadata.
-    const char* rootName = dom.getName(root);
-    if (!rootName || strcmp(rootName, "x:xmpmeta") != 0) {
-        return nullptr;
-    }
-
-    //  Iterate the children with name rdf:RDF.
-    const char* kRdf = "rdf:RDF";
-    for (const auto* rdf = dom.getFirstChild(root, kRdf); rdf;
-         rdf = dom.getNextSibling(rdf, kRdf)) {
-        std::vector<const char*> rdfNamespaces(count, nullptr);
-        find_uri_namespaces(dom, rdf, count, uris, rdfNamespaces.data());
-
-        // Iterate the children with name rdf::Description.
-        const char* kDesc = "rdf:Description";
-        for (const auto* desc = dom.getFirstChild(rdf, kDesc); desc;
-             desc = dom.getNextSibling(desc, kDesc)) {
-            std::vector<const char*> descNamespaces = rdfNamespaces;
-            find_uri_namespaces(dom, desc, count, uris, descNamespaces.data());
-
-            // If we have a match for all the requested URIs, return.
-            bool foundAllUris = true;
-            for (size_t i = 0; i < count; ++i) {
-                if (!descNamespaces[i]) {
-                    foundAllUris = false;
-                    break;
-                }
-            }
-            if (foundAllUris) {
-                for (size_t i = 0; i < count; ++i) {
-                    outNamespaces[i] = descNamespaces[i];
-                }
-                return desc;
-            }
-        }
-    }
-    return nullptr;
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// SkJpegXmp
-
-std::unique_ptr<SkJpegXmp> SkJpegXmp::Make(const std::vector<sk_sp<SkData>>& decoderApp1Params) {
+std::unique_ptr<SkXmp> SkJpegMakeXmp(const std::vector<sk_sp<SkData>>& decoderApp1Params) {
     auto xmpStandard = read_xmp_standard(decoderApp1Params);
     if (!xmpStandard) {
         return nullptr;
     }
 
-    std::unique_ptr<SkJpegXmp> xmp(new SkJpegXmp);
-    auto xmpStandardStream = SkMemoryStream::Make(xmpStandard);
-    if (!xmp->fStandardDOM.build(*xmpStandardStream)) {
-        SkCodecPrintf("Failed to parse XMP standard metadata.\n");
+    std::unique_ptr<SkXmp> xmp = SkXmp::Make(xmpStandard);
+    if (!xmp) {
         return nullptr;
     }
 
-    // See if there is a note indicating extended XMP. If we encounter any errors in retrieving
-    // the extended XMP, return just the standard XMP.
-    const char* namespaces[1] = {nullptr};
-    const char* uris[1] = {"http://ns.adobe.com/xmp/note/"};
-    const auto* extendedNode = find_uri_namespaces(xmp->fStandardDOM, 1, uris, namespaces);
-    if (!extendedNode) {
-        return xmp;
-    }
-    const auto xmpNotePrefix = get_namespace_prefix(namespaces[0]);
-
     // Extract the GUID (the MD5 hash) of the extended metadata.
-    const char* extendedGuid =
-            get_attr(&xmp->fStandardDOM, extendedNode, xmpNotePrefix, "HasExtendedXMP");
+    const char* extendedGuid = xmp->getExtendedXmpGuid();
     if (!extendedGuid) {
         return xmp;
     }
@@ -546,257 +201,5 @@
         return xmp;
     }
 
-    // Parse the extended metadata.
-    auto xmpExtendedStream = SkMemoryStream::Make(xmpExtended);
-    if (xmp->fExtendedDOM.build(*xmpExtendedStream)) {
-        SkCodecPrintf("Failed to parse extended XMP metadata.\n");
-        return xmp;
-    }
-
-    return xmp;
-}
-
-bool SkJpegXmp::findUriNamespaces(size_t count,
-                                  const char* uris[],
-                                  const char* outNamespaces[],
-                                  const SkDOM** outDom,
-                                  const SkDOM::Node** outNode) const {
-    // See XMP Specification Part 3: Storage in files, Section 1.1.3.1: Extended XMP in JPEG:
-    // A JPEG reader must recompose the StandardXMP and ExtendedXMP into a single data model tree
-    // containing all of the XMP for the JPEG file, and remove the xmpNote:HasExtendedXMP property.
-    // This code does not do that. Instead, it maintains the two separate trees and searches them
-    // sequentially.
-    *outNode = find_uri_namespaces(fStandardDOM, count, uris, outNamespaces);
-    if (*outNode) {
-        *outDom = &fStandardDOM;
-        return true;
-    }
-    *outNode = find_uri_namespaces(fExtendedDOM, count, uris, outNamespaces);
-    if (*outNode) {
-        *outDom = &fExtendedDOM;
-        return true;
-    }
-    *outDom = nullptr;
-    return false;
-}
-
-bool SkJpegXmp::getContainerGainmapLocation(size_t* outOffset, size_t* outSize) const {
-    // Find a node that matches the requested namespaces and URIs.
-    const char* namespaces[2] = {nullptr, nullptr};
-    const char* uris[2] = {"http://ns.google.com/photos/1.0/container/",
-                           "http://ns.google.com/photos/1.0/container/item/"};
-    const SkDOM* dom = nullptr;
-    const SkDOM::Node* node = nullptr;
-    if (!findUriNamespaces(2, uris, namespaces, &dom, &node)) {
-        return false;
-    }
-    const char* containerPrefix = get_namespace_prefix(namespaces[0]);
-    const char* itemPrefix = get_namespace_prefix(namespaces[1]);
-
-    // The node must have a Container:Directory child.
-    const auto* directory = get_typed_child(dom, node, containerPrefix, "Directory");
-    if (!directory) {
-        SkCodecPrintf("Missing Container Directory");
-        return false;
-    }
-
-    // That Container:Directory must have a sequence of  items.
-    const auto* seq = dom->getFirstChild(directory, "rdf:Seq");
-    if (!seq) {
-        SkCodecPrintf("Missing rdf:Seq");
-        return false;
-    }
-
-    // Iterate through the items in the Container:Directory's sequence. Keep a running sum of the
-    // Item:Length of all items that appear before the GainMap.
-    bool isFirstItem = true;
-    size_t offset = 0;
-    for (const auto* li = dom->getFirstChild(seq, "rdf:li"); li;
-         li = dom->getNextSibling(li, "rdf:li")) {
-        // Each list item must contain a Container:Item.
-        const auto* item = get_typed_child(dom, li, containerPrefix, "Item");
-        if (!item) {
-            SkCodecPrintf("List item does not have container Item.\n");
-            return false;
-        }
-        // A Semantic is required for every item.
-        const char* itemSemantic = get_attr(dom, item, itemPrefix, "Semantic");
-        if (!itemSemantic) {
-            SkCodecPrintf("Item is missing Semantic.\n");
-            return false;
-        }
-        // A Mime is required for every item.
-        const char* itemMime = get_attr(dom, item, itemPrefix, "Mime");
-        if (!itemMime) {
-            SkCodecPrintf("Item is missing Mime.\n");
-            return false;
-        }
-
-        if (isFirstItem) {
-            isFirstItem = false;
-            // The first item must be Primary.
-            if (strcmp(itemSemantic, "Primary") != 0) {
-                SkCodecPrintf("First item is not Primary.\n");
-                return false;
-            }
-            // The first item has mime type image/jpeg (we are decoding a jpeg).
-            if (strcmp(itemMime, "image/jpeg") != 0) {
-                SkCodecPrintf("Primary does not report that it is image/jpeg.\n");
-                return false;
-            }
-            // The first media item can contain a Padding attribute, which specifies additional
-            // padding between the end of the encoded primary image and the beginning of the next
-            // media item. Only the first media item can contain a Padding attribute.
-            int32_t padding = 0;
-            if (get_attr_int32(dom, item, itemPrefix, "Padding", &padding)) {
-                if (padding < 0) {
-                    SkCodecPrintf("Item padding must be non-negative.");
-                    return false;
-                }
-                offset += padding;
-            }
-        } else {
-            // A Length is required for all non-Primary items.
-            int32_t length = 0;
-            if (!get_attr_int32(dom, item, itemPrefix, "Length", &length)) {
-                SkCodecPrintf("Item length is absent.");
-                return false;
-            }
-            if (length < 0) {
-                SkCodecPrintf("Item length must be non-negative.");
-                return false;
-            }
-            // If this is not the recovery map, then read past it.
-            if (strcmp(itemSemantic, "GainMap") != 0) {
-                offset += length;
-                continue;
-            }
-            // The recovery map must have mime type image/jpeg in this implementation.
-            if (strcmp(itemMime, "image/jpeg") != 0) {
-                SkCodecPrintf("GainMap does not report that it is image/jpeg.\n");
-                return false;
-            }
-
-            // Populate the location in the file at which to find the gainmap image.
-            *outOffset = offset;
-            *outSize = length;
-            return true;
-        }
-    }
-    return false;
-}
-
-// Return true if the specified XMP metadata identifies this image as an HDR gainmap.
-bool SkJpegXmp::getGainmapInfoHDRGainMap(SkGainmapInfo* info) const {
-    // Find a node that matches the requested namespaces and URIs.
-    const char* namespaces[2] = {nullptr, nullptr};
-    const char* uris[2] = {"http://ns.apple.com/pixeldatainfo/1.0/",
-                           "http://ns.apple.com/HDRGainMap/1.0/"};
-    const SkDOM* dom = nullptr;
-    const SkDOM::Node* node = nullptr;
-    if (!findUriNamespaces(2, uris, namespaces, &dom, &node)) {
-        return false;
-    }
-    const char* adpiPrefix = get_namespace_prefix(namespaces[0]);
-    const char* hdrGainMapPrefix = get_namespace_prefix(namespaces[1]);
-
-    const char* auxiliaryImageType = get_attr(dom, node, adpiPrefix, "AuxiliaryImageType");
-    if (!auxiliaryImageType) {
-        SkCodecPrintf("Did not find AuxiliaryImageType.\n");
-        return false;
-    }
-    if (strcmp(auxiliaryImageType, "urn:com:apple:photo:2020:aux:hdrgainmap") != 0) {
-        SkCodecPrintf("AuxiliaryImageType was not HDR gain map.\n");
-        return false;
-    }
-
-    int32_t version = 0;
-    if (!get_attr_int32(dom, node, hdrGainMapPrefix, "HDRGainMapVersion", &version)) {
-        SkCodecPrintf("Did not find HDRGainMapVersion.\n");
-        return false;
-    }
-    if (version != 65536) {
-        SkCodecPrintf("HDRGainMapVersion was not 65536.\n");
-        return false;
-    }
-
-    // This node will often have StoredFormat and NativeFormat children that have inner text that
-    // specifies the integer 'L008' (also known as kCVPixelFormatType_OneComponent8).
-    const float kRatioMax = sk_float_exp(1.f);
-    info->fGainmapRatioMin = {1.f, 1.f, 1.f, 1.f};
-    info->fGainmapRatioMax = {kRatioMax, kRatioMax, kRatioMax, 1.f};
-    info->fGainmapGamma = {1.f, 1.f, 1.f, 1.f};
-    info->fEpsilonSdr = {0.f, 0.f, 0.f, 1.f};
-    info->fEpsilonHdr = {0.f, 0.f, 0.f, 1.f};
-    info->fDisplayRatioSdr = 1.f;
-    info->fDisplayRatioHdr = kRatioMax;
-    info->fBaseImageType = SkGainmapInfo::BaseImageType::kSDR;
-    info->fType = SkGainmapInfo::Type::kMultiPicture;
-    return true;
-}
-
-bool SkJpegXmp::getGainmapInfoHDRGM(SkGainmapInfo* outGainmapInfo) const {
-    // Find a node that matches the requested namespace and URI.
-    const char* namespaces[1] = {nullptr};
-    const char* uris[1] = {"http://ns.adobe.com/hdr-gain-map/1.0/"};
-    const SkDOM* dom = nullptr;
-    const SkDOM::Node* node = nullptr;
-    if (!findUriNamespaces(1, uris, namespaces, &dom, &node)) {
-        return false;
-    }
-    const char* hdrgmPrefix = get_namespace_prefix(namespaces[0]);
-
-    // Require that hdrgm:Version="1.0" be present.
-    const char* version = get_attr(dom, node, hdrgmPrefix, "Version");
-    if (!version) {
-        SkCodecPrintf("Version attribute is absent.\n");
-        return false;
-    }
-    if (strcmp(version, "1.0") != 0) {
-        SkCodecPrintf("Version is \"%s\", not \"1.0\".\n", version);
-        return false;
-    }
-
-    // Initialize the parameters to their defaults.
-    bool baseRenditionIsHDR = false;
-    SkColor4f gainMapMin = {1.f, 1.f, 1.f, 1.f};
-    SkColor4f gainMapMax = {2.f, 2.f, 2.f, 1.f};
-    SkColor4f gamma = {1.f, 1.f, 1.f, 1.f};
-    SkColor4f offsetSdr = {1.f / 64.f, 1.f / 64.f, 1.f / 64.f, 0.f};
-    SkColor4f offsetHdr = {1.f / 64.f, 1.f / 64.f, 1.f / 64.f, 0.f};
-    SkScalar hdrCapacityMin = 1.f;
-    SkScalar hdrCapacityMax = 2.f;
-
-    // Read all parameters that are present.
-    get_attr_bool(dom, node, hdrgmPrefix, "BaseRenditionIsHDR", &baseRenditionIsHDR);
-    get_attr_float3(dom, node, hdrgmPrefix, "GainMapMin", &gainMapMin);
-    get_attr_float3(dom, node, hdrgmPrefix, "GainMapMax", &gainMapMax);
-    get_attr_float3(dom, node, hdrgmPrefix, "Gamma", &gamma);
-    get_attr_float3(dom, node, hdrgmPrefix, "OffsetSDR", &offsetSdr);
-    get_attr_float3(dom, node, hdrgmPrefix, "OffsetHDR", &offsetHdr);
-    get_attr_float(dom, node, hdrgmPrefix, "HDRCapacityMin", &hdrCapacityMin);
-    get_attr_float(dom, node, hdrgmPrefix, "HDRCapacityMax", &hdrCapacityMax);
-
-    // Translate all parameters to SkGainmapInfo's expected format.
-    const float kLog2 = sk_float_log(2.f);
-    outGainmapInfo->fGainmapRatioMin = {sk_float_exp(gainMapMin.fR * kLog2),
-                                        sk_float_exp(gainMapMin.fG * kLog2),
-                                        sk_float_exp(gainMapMin.fB * kLog2),
-                                        1.f};
-    outGainmapInfo->fGainmapRatioMax = {sk_float_exp(gainMapMax.fR * kLog2),
-                                        sk_float_exp(gainMapMax.fG * kLog2),
-                                        sk_float_exp(gainMapMax.fB * kLog2),
-                                        1.f};
-    outGainmapInfo->fGainmapGamma = {1.f / gamma.fR, 1.f / gamma.fG, 1.f / gamma.fB, 1.f};
-    outGainmapInfo->fEpsilonSdr = offsetSdr;
-    outGainmapInfo->fEpsilonHdr = offsetHdr;
-    outGainmapInfo->fDisplayRatioSdr = sk_float_exp(hdrCapacityMin * kLog2);
-    outGainmapInfo->fDisplayRatioHdr = sk_float_exp(hdrCapacityMax * kLog2);
-    if (baseRenditionIsHDR) {
-        outGainmapInfo->fBaseImageType = SkGainmapInfo::BaseImageType::kHDR;
-    } else {
-        outGainmapInfo->fBaseImageType = SkGainmapInfo::BaseImageType::kSDR;
-    }
-    outGainmapInfo->fType = SkGainmapInfo::Type::kHDRGM;
-    return true;
+    return SkXmp::Make(xmpStandard, xmpExtended);
 }
diff --git a/src/codec/SkJpegXmp.h b/src/codec/SkJpegXmp.h
index 5c2261a..c25441a 100644
--- a/src/codec/SkJpegXmp.h
+++ b/src/codec/SkJpegXmp.h
@@ -9,62 +9,14 @@
 #define SkJpegXmp_codec_DEFINED
 
 #include "include/core/SkRefCnt.h"
-#include "src/xml/SkDOM.h"
+#include "include/private/SkXmp.h"
 
 class SkData;
-struct SkGainmapInfo;
 
 #include <memory>
 #include <vector>
 
-/*
- * A structure to manage JPEG XMP metadata.
- */
-class SkJpegXmp {
-public:
-    // Find and parse all XMP metadata, given a list of all APP1 segment parameters.
-    static std::unique_ptr<SkJpegXmp> Make(const std::vector<sk_sp<SkData>>& decoderApp1Params);
-
-    // Extract HDRGM gainmap parameters.
-    bool getGainmapInfoHDRGM(SkGainmapInfo* info) const;
-
-    // Extract HDRGainMap gainmap parameters.
-    bool getGainmapInfoHDRGainMap(SkGainmapInfo* info) const;
-
-    // If this includes GContainer metadata and the GContainer contains an item with semantic
-    // GainMap and Mime of image/jpeg, then return true, and populate |offset| and |size| with
-    // that item's offset (from the end of the primary JPEG image's EndOfImage), and the size of
-    // the gainmap.
-    bool getContainerGainmapLocation(size_t* offset, size_t* size) const;
-
-private:
-    SkJpegXmp();
-
-    // Find an XMP node that assigns namespaces to the specified URIs. The XMP that this will search
-    // for is as follows. URIi is the input parameters in |uris|, and NAMESPACEi is the output
-    // written to |outNamespaces|. The output NAMESPACEi strings will always start with the prefix
-    // "xmlns:".
-    //
-    //   <x:xmpmeta ...>
-    //     <rdf:RDF ...>
-    //       <rdf:Description NAMESPACE0="URI0" NAMESPACE1="URI1" .../>
-    //     </rdf:RDF>
-    //   </x:xmpmeta>
-    //
-    // This function will sequentially search the standard XMP, followed by the extended XMP (which
-    // is not correct behavior -- it should merge the two XMP trees and search the merged tree).
-    bool findUriNamespaces(size_t count,
-                           const char* uris[],
-                           const char* outNamespaces[],
-                           const SkDOM** outDom,
-                           const SkDOM::Node** outNode) const;
-
-    // The DOM for the standard XMP.
-    SkDOM fStandardDOM;
-
-    // The DOM for the extended XMP. This may be invalid if there is no extended XMP, or the
-    // extended XMP failed to parse.
-    SkDOM fExtendedDOM;
-};
+// Find and parse all XMP metadata, given a list of all APP1 segment parameters.
+std::unique_ptr<SkXmp> SkJpegMakeXmp(const std::vector<sk_sp<SkData>>& decoderApp1Params);
 
 #endif
diff --git a/src/codec/SkXmp.cpp b/src/codec/SkXmp.cpp
new file mode 100644
index 0000000..d78f7b7
--- /dev/null
+++ b/src/codec/SkXmp.cpp
@@ -0,0 +1,664 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/private/SkXmp.h"
+
+#include "include/core/SkColor.h"
+#include "include/core/SkData.h"
+#include "include/core/SkScalar.h"
+#include "include/core/SkStream.h"
+#include "include/private/SkGainmapInfo.h"
+#include "include/private/base/SkFloatingPoint.h"
+#include "include/utils/SkParse.h"
+#include "src/codec/SkCodecPriv.h"
+#include "src/xml/SkDOM.h"
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <utility>
+#include <vector>
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// XMP parsing helper functions
+
+const char* kXmlnsPrefix = "xmlns:";
+const size_t kXmlnsPrefixLength = 6;
+
+static const char* get_namespace_prefix(const char* name) {
+    if (strlen(name) <= kXmlnsPrefixLength) {
+        return nullptr;
+    }
+    return name + kXmlnsPrefixLength;
+}
+
+/*
+ * Given a node, see if that node has only one child with the indicated name. If so, see if that
+ * child has only a single child of its own, and that child is text. If all of that is the case
+ * then return the text, otherwise return nullptr.
+ *
+ * In the following example, innerText will be returned.
+ *    <node><childName>innerText</childName></node>
+ *
+ * In the following examples, nullptr will be returned (because there are multiple children with
+ * childName in the first case, and because the child has children of its own in the second).
+ *    <node><childName>innerTextA</childName><childName>innerTextB</childName></node>
+ *    <node><childName>innerText<otherGrandChild/></childName></node>
+ */
+static const char* get_unique_child_text(const SkDOM& dom,
+                                         const SkDOM::Node* node,
+                                         const std::string& childName) {
+    // Fail if there are multiple children with childName.
+    if (dom.countChildren(node, childName.c_str()) != 1) {
+        return nullptr;
+    }
+    const auto* child = dom.getFirstChild(node, childName.c_str());
+    if (!child) {
+        return nullptr;
+    }
+    // Fail if the child has any children besides text.
+    if (dom.countChildren(child) != 1) {
+        return nullptr;
+    }
+    const auto* grandChild = dom.getFirstChild(child);
+    if (dom.getType(grandChild) != SkDOM::kText_Type) {
+        return nullptr;
+    }
+    // Return the text.
+    return dom.getName(grandChild);
+}
+
+/*
+ * Given a node, find a child node of the specified type.
+ *
+ * If there exists a child node with name |prefix| + ":" + |type|, then return that child.
+ *
+ * If there exists a child node with name "rdf:type" that has attribute "rdf:resource" with value
+ * of |type|, then if there also exists a child node with name "rdf:value" with attribute
+ * "rdf:parseType" of "Resource", then return that child node with name "rdf:value". See Example
+ * 3 in section 7.9.2.5: RDF Typed Nodes.
+ * TODO(ccameron): This should also accept a URI for the type.
+ */
+static const SkDOM::Node* get_typed_child(const SkDOM* dom,
+                                          const SkDOM::Node* node,
+                                          const std::string& prefix,
+                                          const std::string& type) {
+    const auto name = prefix + std::string(":") + type;
+    const SkDOM::Node* child = dom->getFirstChild(node, name.c_str());
+    if (child) {
+        return child;
+    }
+
+    const SkDOM::Node* typeChild = dom->getFirstChild(node, "rdf:type");
+    if (!typeChild) {
+        return nullptr;
+    }
+    const char* typeChildResource = dom->findAttr(typeChild, "rdf:resource");
+    if (!typeChildResource || typeChildResource != type) {
+        return nullptr;
+    }
+
+    const SkDOM::Node* valueChild = dom->getFirstChild(node, "rdf:value");
+    if (!valueChild) {
+        return nullptr;
+    }
+    const char* valueChildParseType = dom->findAttr(valueChild, "rdf:parseType");
+    if (!valueChildParseType || strcmp(valueChildParseType, "Resource") != 0) {
+        return nullptr;
+    }
+    return valueChild;
+}
+
+/*
+ * Given a node, return its value for the specified attribute.
+ *
+ * This will first look for an attribute with the name |prefix| + ":" + |key|, and return the value
+ * for that attribute.
+ *
+ * This will then look for a child node of name |prefix| + ":" + |key|, and return the field value
+ * for that child.
+ */
+static const char* get_attr(const SkDOM* dom,
+                            const SkDOM::Node* node,
+                            const std::string& prefix,
+                            const std::string& key) {
+    const auto name = prefix + ":" + key;
+    const char* attr = dom->findAttr(node, name.c_str());
+    if (attr) {
+        return attr;
+    }
+    return get_unique_child_text(*dom, node, name);
+}
+
+// Perform get_attr and parse the result as a bool.
+static bool get_attr_bool(const SkDOM* dom,
+                          const SkDOM::Node* node,
+                          const std::string& prefix,
+                          const std::string& key,
+                          bool* outValue) {
+    const char* attr = get_attr(dom, node, prefix, key);
+    if (!attr) {
+        return false;
+    }
+    switch (SkParse::FindList(attr, "False,True")) {
+        case 0:
+            *outValue = false;
+            return true;
+        case 1:
+            *outValue = true;
+            return true;
+        default:
+            break;
+    }
+    return false;
+}
+
+// Perform get_attr and parse the result as an int32_t.
+static bool get_attr_int32(const SkDOM* dom,
+                           const SkDOM::Node* node,
+                           const std::string& prefix,
+                           const std::string& key,
+                           int32_t* value) {
+    const char* attr = get_attr(dom, node, prefix, key);
+    if (!attr) {
+        return false;
+    }
+    if (!SkParse::FindS32(attr, value)) {
+        return false;
+    }
+    return true;
+}
+
+// Perform get_attr and parse the result as a float.
+static bool get_attr_float(const SkDOM* dom,
+                           const SkDOM::Node* node,
+                           const std::string& prefix,
+                           const std::string& key,
+                           float* outValue) {
+    const char* attr = get_attr(dom, node, prefix, key);
+    if (!attr) {
+        return false;
+    }
+    SkScalar value = 0.f;
+    if (SkParse::FindScalar(attr, &value)) {
+        *outValue = value;
+        return true;
+    }
+    return false;
+}
+
+// Perform get_attr and parse the result as three comma-separated floats. Return the result as an
+// SkColor4f with the alpha component set to 1.
+static bool get_attr_float3_as_list(const SkDOM* dom,
+                                    const SkDOM::Node* node,
+                                    const std::string& prefix,
+                                    const std::string& key,
+                                    SkColor4f* outValue) {
+    const auto name = prefix + ":" + key;
+
+    // Fail if there are multiple children with childName.
+    if (dom->countChildren(node, name.c_str()) != 1) {
+        return false;
+    }
+    // Find the child.
+    const auto* child = dom->getFirstChild(node, name.c_str());
+    if (!child) {
+        return false;
+    }
+
+    // Search for the rdf:Seq child.
+    const auto* seq = dom->getFirstChild(child, "rdf:Seq");
+    if (!seq) {
+        return false;
+    }
+
+    size_t count = 0;
+    SkScalar values[3] = {0.f, 0.f, 0.f};
+    for (const auto* liNode = dom->getFirstChild(seq, "rdf:li"); liNode;
+         liNode = dom->getNextSibling(liNode, "rdf:li")) {
+        if (count > 2) {
+            SkCodecPrintf("Too many items in list.\n");
+            return false;
+        }
+        if (dom->countChildren(liNode) != 1) {
+            SkCodecPrintf("Item can only have one child.\n");
+            return false;
+        }
+        const auto* liTextNode = dom->getFirstChild(liNode);
+        if (dom->getType(liTextNode) != SkDOM::kText_Type) {
+            SkCodecPrintf("Item's only child must be text.\n");
+            return false;
+        }
+        const char* liText = dom->getName(liTextNode);
+        if (!liText) {
+            SkCodecPrintf("Failed to get item's text.\n");
+            return false;
+        }
+        if (!SkParse::FindScalar(liText, values + count)) {
+            SkCodecPrintf("Failed to parse item's text to float.\n");
+            return false;
+        }
+        count += 1;
+    }
+    if (count < 3) {
+        SkCodecPrintf("List didn't have enough items.\n");
+        return false;
+    }
+    *outValue = {values[0], values[1], values[2], 1.f};
+    return true;
+}
+
+static bool get_attr_float3(const SkDOM* dom,
+                            const SkDOM::Node* node,
+                            const std::string& prefix,
+                            const std::string& key,
+                            SkColor4f* outValue) {
+    if (get_attr_float3_as_list(dom, node, prefix, key, outValue)) {
+        return true;
+    }
+    SkScalar value = -1.0;
+    if (get_attr_float(dom, node, prefix, key, &value)) {
+        *outValue = {value, value, value, 1.f};
+        return true;
+    }
+    return false;
+}
+
+static void find_uri_namespaces(const SkDOM& dom,
+                                const SkDOM::Node* node,
+                                size_t count,
+                                const char* uris[],
+                                const char* outNamespaces[]) {
+    // Search all attributes for xmlns:NAMESPACEi="URIi".
+    for (const auto* attr = dom.getFirstAttr(node); attr; attr = dom.getNextAttr(node, attr)) {
+        const char* attrName = dom.getAttrName(node, attr);
+        const char* attrValue = dom.getAttrValue(node, attr);
+        if (!attrName || !attrValue) {
+            continue;
+        }
+        // Make sure the name starts with "xmlns:".
+        if (strlen(attrName) <= kXmlnsPrefixLength) {
+            continue;
+        }
+        if (memcmp(attrName, kXmlnsPrefix, kXmlnsPrefixLength) != 0) {
+            continue;
+        }
+        // Search for a requested URI that matches.
+        for (size_t i = 0; i < count; ++i) {
+            if (strcmp(attrValue, uris[i]) != 0) {
+                continue;
+            }
+            outNamespaces[i] = attrName;
+        }
+    }
+}
+
+// See SkXmp::findUriNamespaces. This function has the same behavior, but only searches
+// a single SkDOM.
+static const SkDOM::Node* find_uri_namespaces(const SkDOM& dom,
+                                              size_t count,
+                                              const char* uris[],
+                                              const char* outNamespaces[]) {
+    const SkDOM::Node* root = dom.getRootNode();
+    if (!root) {
+        return nullptr;
+    }
+
+    // Ensure that the root node identifies itself as XMP metadata.
+    const char* rootName = dom.getName(root);
+    if (!rootName || strcmp(rootName, "x:xmpmeta") != 0) {
+        return nullptr;
+    }
+
+    //  Iterate the children with name rdf:RDF.
+    const char* kRdf = "rdf:RDF";
+    for (const auto* rdf = dom.getFirstChild(root, kRdf); rdf;
+         rdf = dom.getNextSibling(rdf, kRdf)) {
+        std::vector<const char*> rdfNamespaces(count, nullptr);
+        find_uri_namespaces(dom, rdf, count, uris, rdfNamespaces.data());
+
+        // Iterate the children with name rdf::Description.
+        const char* kDesc = "rdf:Description";
+        for (const auto* desc = dom.getFirstChild(rdf, kDesc); desc;
+             desc = dom.getNextSibling(desc, kDesc)) {
+            std::vector<const char*> descNamespaces = rdfNamespaces;
+            find_uri_namespaces(dom, desc, count, uris, descNamespaces.data());
+
+            // If we have a match for all the requested URIs, return.
+            bool foundAllUris = true;
+            for (size_t i = 0; i < count; ++i) {
+                if (!descNamespaces[i]) {
+                    foundAllUris = false;
+                    break;
+                }
+            }
+            if (foundAllUris) {
+                for (size_t i = 0; i < count; ++i) {
+                    outNamespaces[i] = descNamespaces[i];
+                }
+                return desc;
+            }
+        }
+    }
+    return nullptr;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// SkXmpImpl
+
+class SK_API SkXmpImpl final : public SkXmp {
+public:
+    SkXmpImpl() = default;
+
+    bool getGainmapInfoHDRGM(SkGainmapInfo* info) const override;
+    bool getGainmapInfoHDRGainMap(SkGainmapInfo* info) const override;
+    bool getContainerGainmapLocation(size_t* offset, size_t* size) const override;
+    const char* getExtendedXmpGuid() const override;
+    // Parse the given xmp data and store it into either the standard (main) DOM or the extended
+    // DOM. Returns true on successful parsing.
+    bool parseDom(sk_sp<SkData> xmpData, bool extended);
+
+private:
+    bool findUriNamespaces(size_t count,
+                           const char* uris[],
+                           const char* outNamespaces[],
+                           const SkDOM** outDom,
+                           const SkDOM::Node** outNode) const;
+
+    SkDOM fStandardDOM;
+    SkDOM fExtendedDOM;
+};
+
+const char* SkXmpImpl::getExtendedXmpGuid() const {
+    const char* namespaces[1] = {nullptr};
+    const char* uris[1] = {"http://ns.adobe.com/xmp/note/"};
+    const auto* extendedNode = find_uri_namespaces(fStandardDOM, 1, uris, namespaces);
+    if (!extendedNode) {
+        return nullptr;
+    }
+    const auto xmpNotePrefix = get_namespace_prefix(namespaces[0]);
+    // Extract the GUID (the MD5 hash) of the extended metadata.
+    return get_attr(&fStandardDOM, extendedNode, xmpNotePrefix, "HasExtendedXMP");
+}
+
+bool SkXmpImpl::findUriNamespaces(size_t count,
+                                  const char* uris[],
+                                  const char* outNamespaces[],
+                                  const SkDOM** outDom,
+                                  const SkDOM::Node** outNode) const {
+    // See XMP Specification Part 3: Storage in files, Section 1.1.3.1: Extended XMP in JPEG:
+    // A JPEG reader must recompose the StandardXMP and ExtendedXMP into a single data model tree
+    // containing all of the XMP for the JPEG file, and remove the xmpNote:HasExtendedXMP property.
+    // This code does not do that. Instead, it maintains the two separate trees and searches them
+    // sequentially.
+    *outNode = find_uri_namespaces(fStandardDOM, count, uris, outNamespaces);
+    if (*outNode) {
+        *outDom = &fStandardDOM;
+        return true;
+    }
+    *outNode = find_uri_namespaces(fExtendedDOM, count, uris, outNamespaces);
+    if (*outNode) {
+        *outDom = &fExtendedDOM;
+        return true;
+    }
+    *outDom = nullptr;
+    return false;
+}
+
+bool SkXmpImpl::getContainerGainmapLocation(size_t* outOffset, size_t* outSize) const {
+    // Find a node that matches the requested namespaces and URIs.
+    const char* namespaces[2] = {nullptr, nullptr};
+    const char* uris[2] = {"http://ns.google.com/photos/1.0/container/",
+                           "http://ns.google.com/photos/1.0/container/item/"};
+    const SkDOM* dom = nullptr;
+    const SkDOM::Node* node = nullptr;
+    if (!findUriNamespaces(2, uris, namespaces, &dom, &node)) {
+        return false;
+    }
+    const char* containerPrefix = get_namespace_prefix(namespaces[0]);
+    const char* itemPrefix = get_namespace_prefix(namespaces[1]);
+
+    // The node must have a Container:Directory child.
+    const auto* directory = get_typed_child(dom, node, containerPrefix, "Directory");
+    if (!directory) {
+        SkCodecPrintf("Missing Container Directory");
+        return false;
+    }
+
+    // That Container:Directory must have a sequence of  items.
+    const auto* seq = dom->getFirstChild(directory, "rdf:Seq");
+    if (!seq) {
+        SkCodecPrintf("Missing rdf:Seq");
+        return false;
+    }
+
+    // Iterate through the items in the Container:Directory's sequence. Keep a running sum of the
+    // Item:Length of all items that appear before the GainMap.
+    bool isFirstItem = true;
+    size_t offset = 0;
+    for (const auto* li = dom->getFirstChild(seq, "rdf:li"); li;
+         li = dom->getNextSibling(li, "rdf:li")) {
+        // Each list item must contain a Container:Item.
+        const auto* item = get_typed_child(dom, li, containerPrefix, "Item");
+        if (!item) {
+            SkCodecPrintf("List item does not have container Item.\n");
+            return false;
+        }
+        // A Semantic is required for every item.
+        const char* itemSemantic = get_attr(dom, item, itemPrefix, "Semantic");
+        if (!itemSemantic) {
+            SkCodecPrintf("Item is missing Semantic.\n");
+            return false;
+        }
+        // A Mime is required for every item.
+        const char* itemMime = get_attr(dom, item, itemPrefix, "Mime");
+        if (!itemMime) {
+            SkCodecPrintf("Item is missing Mime.\n");
+            return false;
+        }
+
+        if (isFirstItem) {
+            isFirstItem = false;
+            // The first item must be Primary.
+            if (strcmp(itemSemantic, "Primary") != 0) {
+                SkCodecPrintf("First item is not Primary.\n");
+                return false;
+            }
+            // The first item has mime type image/jpeg (we are decoding a jpeg).
+            if (strcmp(itemMime, "image/jpeg") != 0) {
+                SkCodecPrintf("Primary does not report that it is image/jpeg.\n");
+                return false;
+            }
+            // The first media item can contain a Padding attribute, which specifies additional
+            // padding between the end of the encoded primary image and the beginning of the next
+            // media item. Only the first media item can contain a Padding attribute.
+            int32_t padding = 0;
+            if (get_attr_int32(dom, item, itemPrefix, "Padding", &padding)) {
+                if (padding < 0) {
+                    SkCodecPrintf("Item padding must be non-negative.");
+                    return false;
+                }
+                offset += padding;
+            }
+        } else {
+            // A Length is required for all non-Primary items.
+            int32_t length = 0;
+            if (!get_attr_int32(dom, item, itemPrefix, "Length", &length)) {
+                SkCodecPrintf("Item length is absent.");
+                return false;
+            }
+            if (length < 0) {
+                SkCodecPrintf("Item length must be non-negative.");
+                return false;
+            }
+            // If this is not the recovery map, then read past it.
+            if (strcmp(itemSemantic, "GainMap") != 0) {
+                offset += length;
+                continue;
+            }
+            // The recovery map must have mime type image/jpeg in this implementation.
+            if (strcmp(itemMime, "image/jpeg") != 0) {
+                SkCodecPrintf("GainMap does not report that it is image/jpeg.\n");
+                return false;
+            }
+
+            // Populate the location in the file at which to find the gainmap image.
+            *outOffset = offset;
+            *outSize = length;
+            return true;
+        }
+    }
+    return false;
+}
+
+// Return true if the specified XMP metadata identifies this image as an HDR gainmap.
+bool SkXmpImpl::getGainmapInfoHDRGainMap(SkGainmapInfo* info) const {
+    // Find a node that matches the requested namespaces and URIs.
+    const char* namespaces[2] = {nullptr, nullptr};
+    const char* uris[2] = {"http://ns.apple.com/pixeldatainfo/1.0/",
+                           "http://ns.apple.com/HDRGainMap/1.0/"};
+    const SkDOM* dom = nullptr;
+    const SkDOM::Node* node = nullptr;
+    if (!findUriNamespaces(2, uris, namespaces, &dom, &node)) {
+        return false;
+    }
+    const char* adpiPrefix = get_namespace_prefix(namespaces[0]);
+    const char* hdrGainMapPrefix = get_namespace_prefix(namespaces[1]);
+
+    const char* auxiliaryImageType = get_attr(dom, node, adpiPrefix, "AuxiliaryImageType");
+    if (!auxiliaryImageType) {
+        SkCodecPrintf("Did not find AuxiliaryImageType.\n");
+        return false;
+    }
+    if (strcmp(auxiliaryImageType, "urn:com:apple:photo:2020:aux:hdrgainmap") != 0) {
+        SkCodecPrintf("AuxiliaryImageType was not HDR gain map.\n");
+        return false;
+    }
+
+    int32_t version = 0;
+    if (!get_attr_int32(dom, node, hdrGainMapPrefix, "HDRGainMapVersion", &version)) {
+        SkCodecPrintf("Did not find HDRGainMapVersion.\n");
+        return false;
+    }
+    if (version != 65536) {
+        SkCodecPrintf("HDRGainMapVersion was not 65536.\n");
+        return false;
+    }
+
+    // This node will often have StoredFormat and NativeFormat children that have inner text that
+    // specifies the integer 'L008' (also known as kCVPixelFormatType_OneComponent8).
+    const float kRatioMax = sk_float_exp(1.f);
+    info->fGainmapRatioMin = {1.f, 1.f, 1.f, 1.f};
+    info->fGainmapRatioMax = {kRatioMax, kRatioMax, kRatioMax, 1.f};
+    info->fGainmapGamma = {1.f, 1.f, 1.f, 1.f};
+    info->fEpsilonSdr = {0.f, 0.f, 0.f, 1.f};
+    info->fEpsilonHdr = {0.f, 0.f, 0.f, 1.f};
+    info->fDisplayRatioSdr = 1.f;
+    info->fDisplayRatioHdr = kRatioMax;
+    info->fBaseImageType = SkGainmapInfo::BaseImageType::kSDR;
+    info->fType = SkGainmapInfo::Type::kMultiPicture;
+    return true;
+}
+
+bool SkXmpImpl::getGainmapInfoHDRGM(SkGainmapInfo* outGainmapInfo) const {
+    // Find a node that matches the requested namespace and URI.
+    const char* namespaces[1] = {nullptr};
+    const char* uris[1] = {"http://ns.adobe.com/hdr-gain-map/1.0/"};
+    const SkDOM* dom = nullptr;
+    const SkDOM::Node* node = nullptr;
+    if (!findUriNamespaces(1, uris, namespaces, &dom, &node)) {
+        return false;
+    }
+    const char* hdrgmPrefix = get_namespace_prefix(namespaces[0]);
+
+    // Require that hdrgm:Version="1.0" be present.
+    const char* version = get_attr(dom, node, hdrgmPrefix, "Version");
+    if (!version) {
+        SkCodecPrintf("Version attribute is absent.\n");
+        return false;
+    }
+    if (strcmp(version, "1.0") != 0) {
+        SkCodecPrintf("Version is \"%s\", not \"1.0\".\n", version);
+        return false;
+    }
+
+    // Initialize the parameters to their defaults.
+    bool baseRenditionIsHDR = false;
+    SkColor4f gainMapMin = {1.f, 1.f, 1.f, 1.f};
+    SkColor4f gainMapMax = {2.f, 2.f, 2.f, 1.f};
+    SkColor4f gamma = {1.f, 1.f, 1.f, 1.f};
+    SkColor4f offsetSdr = {1.f / 64.f, 1.f / 64.f, 1.f / 64.f, 0.f};
+    SkColor4f offsetHdr = {1.f / 64.f, 1.f / 64.f, 1.f / 64.f, 0.f};
+    SkScalar hdrCapacityMin = 1.f;
+    SkScalar hdrCapacityMax = 2.f;
+
+    // Read all parameters that are present.
+    get_attr_bool(dom, node, hdrgmPrefix, "BaseRenditionIsHDR", &baseRenditionIsHDR);
+    get_attr_float3(dom, node, hdrgmPrefix, "GainMapMin", &gainMapMin);
+    get_attr_float3(dom, node, hdrgmPrefix, "GainMapMax", &gainMapMax);
+    get_attr_float3(dom, node, hdrgmPrefix, "Gamma", &gamma);
+    get_attr_float3(dom, node, hdrgmPrefix, "OffsetSDR", &offsetSdr);
+    get_attr_float3(dom, node, hdrgmPrefix, "OffsetHDR", &offsetHdr);
+    get_attr_float(dom, node, hdrgmPrefix, "HDRCapacityMin", &hdrCapacityMin);
+    get_attr_float(dom, node, hdrgmPrefix, "HDRCapacityMax", &hdrCapacityMax);
+
+    // Translate all parameters to SkGainmapInfo's expected format.
+    const float kLog2 = sk_float_log(2.f);
+    outGainmapInfo->fGainmapRatioMin = {sk_float_exp(gainMapMin.fR * kLog2),
+                                        sk_float_exp(gainMapMin.fG * kLog2),
+                                        sk_float_exp(gainMapMin.fB * kLog2),
+                                        1.f};
+    outGainmapInfo->fGainmapRatioMax = {sk_float_exp(gainMapMax.fR * kLog2),
+                                        sk_float_exp(gainMapMax.fG * kLog2),
+                                        sk_float_exp(gainMapMax.fB * kLog2),
+                                        1.f};
+    outGainmapInfo->fGainmapGamma = {1.f / gamma.fR, 1.f / gamma.fG, 1.f / gamma.fB, 1.f};
+    outGainmapInfo->fEpsilonSdr = offsetSdr;
+    outGainmapInfo->fEpsilonHdr = offsetHdr;
+    outGainmapInfo->fDisplayRatioSdr = sk_float_exp(hdrCapacityMin * kLog2);
+    outGainmapInfo->fDisplayRatioHdr = sk_float_exp(hdrCapacityMax * kLog2);
+    if (baseRenditionIsHDR) {
+        outGainmapInfo->fBaseImageType = SkGainmapInfo::BaseImageType::kHDR;
+    } else {
+        outGainmapInfo->fBaseImageType = SkGainmapInfo::BaseImageType::kSDR;
+    }
+    outGainmapInfo->fType = SkGainmapInfo::Type::kHDRGM;
+    return true;
+}
+
+bool SkXmpImpl::parseDom(sk_sp<SkData> xmpData, bool extended) {
+    SkDOM* dom = extended ? &fExtendedDOM : &fStandardDOM;
+    auto xmpdStream = SkMemoryStream::Make(xmpData);
+    if (!dom->build(*xmpdStream)) {
+        SkCodecPrintf("Failed to parse XMP %s metadata.\n", extended ? "extended" : "standard");
+        return false;
+    }
+    return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// SkXmp
+
+std::unique_ptr<SkXmp> SkXmp::Make(sk_sp<SkData> xmpData) {
+    std::unique_ptr<SkXmpImpl> xmp(new SkXmpImpl);
+    if (!xmp->parseDom(xmpData, /*extended=*/false)) {
+        return nullptr;
+    }
+    return std::move(xmp);
+}
+
+std::unique_ptr<SkXmp> SkXmp::Make(sk_sp<SkData> xmpStandard, sk_sp<SkData> xmpExtended) {
+    std::unique_ptr<SkXmpImpl> xmp(new SkXmpImpl);
+    if (!xmp->parseDom(xmpStandard, /*extended=*/false)) {
+        return nullptr;
+    }
+    if (!xmp->parseDom(xmpExtended, /*extended=*/true)) {
+        // Still return xmp with just standard metadata.
+        return std::move(xmp);
+    }
+    return std::move(xmp);
+}
diff --git a/tests/JpegGainmapTest.cpp b/tests/JpegGainmapTest.cpp
index 86fbce7..00676c1 100644
--- a/tests/JpegGainmapTest.cpp
+++ b/tests/JpegGainmapTest.cpp
@@ -23,7 +23,6 @@
 #include "src/codec/SkJpegMultiPicture.h"
 #include "src/codec/SkJpegSegmentScan.h"
 #include "src/codec/SkJpegSourceMgr.h"
-#include "src/codec/SkJpegXmp.h"
 #include "tests/Test.h"
 #include "tools/Resources.h"
 
@@ -389,7 +388,7 @@
 }
 
 // Render an applied gainmap.
-SkBitmap render_gainmap(const SkImageInfo& renderInfo,
+static SkBitmap render_gainmap(const SkImageInfo& renderInfo,
                         float renderHdrRatio,
                         const SkBitmap& baseBitmap,
                         const SkBitmap& gainmapBitmap,
@@ -432,234 +431,8 @@
     return result;
 }
 
-DEF_TEST(AndroidCodec_xmpHdrgmAsFieldValue, r) {
-    // Expose HDRM values as fields. Also place the HDRGM namespace in the rdf:RDF node.
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n"
-            "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
-            "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\">\n"
-            "      <rdf:Description rdf:about=\"\">\n"
-            "         <hdrgm:Version>1.0</hdrgm:Version>\n"
-            "         <hdrgm:GainMapMax>3</hdrgm:GainMapMax>\n"
-            "         <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>\n"
-            "      </rdf:Description>\n"
-            "   </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    SkGainmapInfo info;
-    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
-    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
-    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
-}
-
-DEF_TEST(AndroidCodec_xmpHdrgmRequiresVersion, r) {
-    // Same as the above, except with Version being absent.
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n"
-            "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
-            "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\">\n"
-            "      <rdf:Description rdf:about=\"\">\n"
-            "         <hdrgm:GainMapMax>3</hdrgm:GainMapMax>\n"
-            "         <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>\n"
-            "      </rdf:Description>\n"
-            "   </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    SkGainmapInfo info;
-    REPORTER_ASSERT(r, !xmp->getGainmapInfoHDRGM(&info));
-}
-
-DEF_TEST(AndroidCodec_xmpHdrgmAsDescriptionPropertyAttributes, r) {
-    // Expose HDRGM values as attributes on an rdf:Description node.
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n"
-            "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
-            "      <rdf:Description rdf:about=\"\"\n"
-            "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\"\n"
-            "         hdrgm:Version=\"1.0\"\n"
-            "         hdrgm:GainMapMax=\"3\"\n"
-            "         hdrgm:HDRCapacityMax=\"4\"/>\n"
-            "   </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    SkGainmapInfo info;
-    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
-    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
-    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
-}
-
-// Test mixed list and non-list entries.
-DEF_TEST(AndroidCodec_xmpHdrgmList, r) {
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n"
-            "   <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
-            "            xmlns:hdrgm=\"http://ns.adobe.com/hdr-gain-map/1.0/\">\n"
-            "      <rdf:Description rdf:about=\"\"\n"
-            "         hdrgm:Version=\"1.0\"\n"
-            "         hdrgm:GainMapMin=\"2.0\"\n"
-            "         hdrgm:OffsetSDR=\"0.1\">\n"
-            "         <hdrgm:GainMapMax>\n"
-            "           <rdf:Seq>\n"
-            "             <rdf:li>3</rdf:li>\n"
-            "             <rdf:li>4</rdf:li>\n"
-            "             <rdf:li>5</rdf:li>\n"
-            "           </rdf:Seq>\n"
-            "         </hdrgm:GainMapMax>\n"
-            "         <hdrgm:Gamma>\n"
-            "           1.2\n"
-            "         </hdrgm:Gamma>\n"
-            "         <hdrgm:OffsetHDR>\n"
-            "           <rdf:Seq>\n"
-            "             <rdf:li>\n"
-            "               0.2\n"
-            "             </rdf:li>\n"
-            "             <rdf:li>\n"
-            "               0.3\n"
-            "             </rdf:li>\n"
-            "             <rdf:li>\n"
-            "               0.4\n"
-            "             </rdf:li>\n"
-            "           </rdf:Seq>\n"
-            "         </hdrgm:OffsetHDR>\n"
-            "      </rdf:Description>\n"
-            "   </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    SkGainmapInfo info;
-    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
-    REPORTER_ASSERT(r, info.fGainmapRatioMin.fR == 4.f);
-    REPORTER_ASSERT(r, info.fGainmapRatioMin.fG == 4.f);
-    REPORTER_ASSERT(r, info.fGainmapRatioMin.fB == 4.f);
-    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
-    REPORTER_ASSERT(r, info.fGainmapRatioMax.fG == 16.f);
-    REPORTER_ASSERT(r, info.fGainmapRatioMax.fB == 32.f);
-
-    REPORTER_ASSERT(r, info.fGainmapGamma.fR == 1.f/1.2f);
-    REPORTER_ASSERT(r, info.fGainmapGamma.fG == 1.f/1.2f);
-    REPORTER_ASSERT(r, info.fGainmapGamma.fB == 1.f/1.2f);
-
-    REPORTER_ASSERT(r, info.fEpsilonSdr.fR == 0.1f);
-    REPORTER_ASSERT(r, info.fEpsilonSdr.fG == 0.1f);
-    REPORTER_ASSERT(r, info.fEpsilonSdr.fB == 0.1f);
-
-    REPORTER_ASSERT(r, info.fEpsilonHdr.fR == 0.2f);
-    REPORTER_ASSERT(r, info.fEpsilonHdr.fG == 0.3f);
-    REPORTER_ASSERT(r, info.fEpsilonHdr.fB == 0.4f);
-}
-
-DEF_TEST(AndroidCodec_xmpContainerTypedNode, r) {
-    // Container and Item using a node of type Container:Item.
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n"
-            " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
-            "  <rdf:Description rdf:about=\"\"\n"
-            "    xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"\n"
-            "    xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\">\n"
-            "   <Container:Directory>\n"
-            "    <rdf:Seq>\n"
-            "     <rdf:li rdf:parseType=\"Resource\">\n"
-            "      <Container:Item>\n"
-            "       <Item:Mime>image/jpeg</Item:Mime>\n"
-            "       <Item:Semantic>Primary</Item:Semantic>\n"
-            "      </Container:Item>\n"
-            "     </rdf:li>\n"
-            "     <rdf:li rdf:parseType=\"Resource\">\n"
-            "      <Container:Item\n"
-            "         Item:Semantic=\"GainMap\"\n"
-            "         Item:Mime=\"image/jpeg\"\n"
-            "         Item:Length=\"49035\"/>\n"
-            "     </rdf:li>\n"
-            "    </rdf:Seq>\n"
-            "   </Container:Directory>\n"
-            "  </rdf:Description>\n"
-            " </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    size_t offset = 999;
-    size_t size = 999;
-    REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size));
-    REPORTER_ASSERT(r, size == 49035);
-}
-
-DEF_TEST(AndroidCodec_xmpContainerTypedNodeRdfEquivalent, r) {
-    // Container and Item using rdf:value and rdf:type pairs.
-    const char xmpData[] =
-            "http://ns.adobe.com/xap/1.0/\0"
-            "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 5.5.0\">\n"
-            " <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n"
-            "  <rdf:Description rdf:about=\"\"\n"
-            "    xmlns:Container=\"http://ns.google.com/photos/1.0/container/\"\n"
-            "    xmlns:Item=\"http://ns.google.com/photos/1.0/container/item/\">\n"
-            "   <Container:Directory>\n"
-            "    <rdf:Seq>\n"
-            "     <rdf:li rdf:parseType=\"Resource\">\n"
-            "      <rdf:value rdf:parseType=\"Resource\">\n"
-            "       <Item:Mime>image/jpeg</Item:Mime>\n"
-            "       <Item:Semantic>Primary</Item:Semantic>\n"
-            "      </rdf:value>\n"
-            "      <rdf:type rdf:resource=\"Item\"/>\n"
-            "     </rdf:li>\n"
-            "     <rdf:li rdf:parseType=\"Resource\">\n"
-            "      <rdf:value rdf:parseType=\"Resource\">\n"
-            "       <Item:Semantic>GainMap</Item:Semantic>\n"
-            "       <Item:Mime>image/jpeg</Item:Mime>\n"
-            "       <Item:Length>49035</Item:Length>\n"
-            "      </rdf:value>\n"
-            "      <rdf:type rdf:resource=\"Item\"/>\n"
-            "     </rdf:li>\n"
-            "    </rdf:Seq>\n"
-            "   </Container:Directory>\n"
-            "  </rdf:Description>\n"
-            " </rdf:RDF>\n"
-            "</x:xmpmeta>\n";
-    std::vector<sk_sp<SkData>> app1Params;
-    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
-
-    auto xmp = SkJpegXmp::Make(app1Params);
-    REPORTER_ASSERT(r, xmp);
-
-    size_t offset = 999;
-    size_t size = 999;
-    REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size));
-    REPORTER_ASSERT(r, size == 49035);
-}
-
 // Render a single pixel of an applied gainmap and return it.
-SkColor4f render_gainmap_pixel(float renderHdrRatio,
+static SkColor4f render_gainmap_pixel(float renderHdrRatio,
                                const SkBitmap& baseBitmap,
                                const SkBitmap& gainmapBitmap,
                                const SkGainmapInfo& gainmapInfo,
diff --git a/tests/SkJpegXmpTest.cpp b/tests/SkJpegXmpTest.cpp
new file mode 100644
index 0000000..6c8bdbd
--- /dev/null
+++ b/tests/SkJpegXmpTest.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkData.h"
+#include "include/private/SkGainmapInfo.h"
+#include "src/codec/SkJpegXmp.h"
+#include "src/core/SkMD5.h"
+#include "tests/Test.h"
+
+#include <iomanip>
+#include <iostream>
+#include <regex>
+#include <sstream>
+
+DEF_TEST(SkJpegXmp_standardXmp, r) {
+    const char xmpData[] =
+            "http://ns.adobe.com/xap/1.0/\0"
+            R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                        xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/">
+                  <rdf:Description rdf:about="">
+                     <hdrgm:Version>1.0</hdrgm:Version>
+                     <hdrgm:GainMapMax>3</hdrgm:GainMapMax>
+                     <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>
+                  </rdf:Description>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    std::vector<sk_sp<SkData>> app1Params;
+    app1Params.push_back(SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1));
+
+    auto xmp = SkJpegMakeXmp(app1Params);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
+    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
+}
+
+static void append_uint32(uint32_t v, std::vector<char>* c) {
+    for (int i = 0; i < 4; ++i) {
+        c->push_back(static_cast<char>((v >> ((3 - i) * 8)) & 0xff));
+    }
+}
+
+static std::string digest_to_hex_string(const SkMD5::Digest& digest) {
+    std::stringstream ss;
+    for (int i = 0; i < 16; ++i) {
+        ss << std::uppercase << std::setfill('0') << std::setw(2) << std::right << std::hex
+           << (int)digest.data[i];
+    }
+    return ss.str();
+}
+
+static std::string standard_xmp_with_header(const SkMD5::Digest& digest, const std::string& data) {
+    const std::string sig = "http://ns.adobe.com/xap/1.0/";
+    std::vector<char> c(sig.begin(), sig.end());
+    c.push_back('\0');
+    const std::string guid = digest_to_hex_string(digest);
+    const std::string dataWithGuid = std::regex_replace(data, std::regex("\\$GUID"), guid);
+    c.insert(c.end(), dataWithGuid.begin(), dataWithGuid.end());
+    return std::string(c.data(), c.size());
+}
+
+static std::string extended_xmp_with_header(const SkMD5::Digest& digest,
+                                            uint32_t size,
+                                            uint32_t offset,
+                                            const std::string& data) {
+    const std::string sig = "http://ns.adobe.com/xmp/extension/";
+    std::vector<char> c(sig.begin(), sig.end());
+    c.push_back('\0');
+    const std::string guid = digest_to_hex_string(digest);
+    c.insert(c.end(), guid.begin(), guid.end());
+    append_uint32(size, &c);
+    append_uint32(offset, &c);
+    c.insert(c.end(), data.begin(), data.end());
+    return std::string(c.data(), c.size());
+}
+
+DEF_TEST(SkJpegXmp_readExtendedXmp, r) {
+    const std::string standardXmpData = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:xmpNote="http://ns.adobe.com/xmp/note/">
+                  <rdf:Description rdf:about="">
+                     <xmpNote:HasExtendedXMP>$GUID</xmpNote:HasExtendedXMP>
+                  </rdf:Description>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    const std::string extendedXmpData1 = R"(
+        <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+            <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                    xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/">
+                <rdf:Description rdf:about="">
+                     <hdrgm:Version>1.0</hdrgm:Version>
+                     <hdrgm:GainMapMax>3</hdrgm:GainMapMax>
+                    <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>)";
+    const std::string extendedXmpData2 = R"(
+                </rdf:Description>
+            </rdf:RDF>
+        </x:xmpmeta>)";
+
+    const uint32_t totalExtendedXmpSize = extendedXmpData1.size() + extendedXmpData2.size();
+    SkMD5 md5;
+    md5.write(extendedXmpData1.data(), extendedXmpData1.length());
+    md5.write(extendedXmpData2.data(), extendedXmpData2.length());
+    const SkMD5::Digest digest = md5.finish();
+
+    const std::string standardXmpDataWithHeader = standard_xmp_with_header(digest, standardXmpData);
+
+    const uint32_t offset1 = 0;
+    const std::string extendedXmpData1WithHeader =
+            extended_xmp_with_header(digest, totalExtendedXmpSize, offset1, extendedXmpData1);
+
+    const uint32_t offset2 = extendedXmpData1.size();
+    const std::string extendedXmpData2WithHeader =
+            extended_xmp_with_header(digest, totalExtendedXmpSize, offset2, extendedXmpData2);
+
+    std::vector<sk_sp<SkData>> app1Params;
+    app1Params.push_back(SkData::MakeWithoutCopy(standardXmpDataWithHeader.data(),
+                                                 standardXmpDataWithHeader.length()));
+    app1Params.push_back(SkData::MakeWithoutCopy(extendedXmpData1WithHeader.data(),
+                                                 extendedXmpData1WithHeader.length()));
+    app1Params.push_back(SkData::MakeWithoutCopy(extendedXmpData2WithHeader.data(),
+                                                 extendedXmpData2WithHeader.length()));
+
+    auto xmp = SkJpegMakeXmp(app1Params);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
+    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
+}
diff --git a/tests/SkXmpTest.cpp b/tests/SkXmpTest.cpp
new file mode 100644
index 0000000..78b893a
--- /dev/null
+++ b/tests/SkXmpTest.cpp
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkColor.h"
+#include "include/core/SkData.h"
+#include "include/core/SkRefCnt.h"
+#include "include/private/SkGainmapInfo.h"
+#include "include/private/SkXmp.h"
+#include "tests/Test.h"
+
+#include <cstddef>
+#include <memory>
+
+DEF_TEST(SkXmp_invalidXml, r) {
+    // Invalid truncated xml.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                        xmlns:)";
+
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp == nullptr);
+}
+
+DEF_TEST(SkXmp_xmpHdrgmAsFieldValue, r) {
+    // Expose HDRM values as fields. Also place the HDRGM namespace in the rdf:RDF node.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                        xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/">
+                  <rdf:Description rdf:about="">
+                     <hdrgm:Version>1.0</hdrgm:Version>
+                     <hdrgm:GainMapMax>3</hdrgm:GainMapMax>
+                     <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>
+                  </rdf:Description>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
+    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
+}
+
+DEF_TEST(SkXmp_xmpHdrgmRequiresVersion, r) {
+    // Same as the above, except with Version being absent.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                        xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/">
+                  <rdf:Description rdf:about="">
+                     <hdrgm:GainMapMax>3</hdrgm:GainMapMax>
+                     <hdrgm:HDRCapacityMax>4</hdrgm:HDRCapacityMax>
+                  </rdf:Description>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, !xmp->getGainmapInfoHDRGM(&info));
+}
+
+DEF_TEST(SkXmp_xmpHdrgmAsDescriptionPropertyAttributes, r) {
+    // Expose HDRGM values as attributes on an rdf:Description node.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+                  <rdf:Description rdf:about=""
+                        xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
+                     hdrgm:Version="1.0"
+                     hdrgm:GainMapMax="3"
+                     hdrgm:HDRCapacityMax="4"/>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
+    REPORTER_ASSERT(r, info.fDisplayRatioHdr == 16.f);
+}
+
+// Test mixed list and non-list entries.
+DEF_TEST(SkXmp_xmpHdrgmList, r) {
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0">
+               <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                        xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/">
+                  <rdf:Description rdf:about=""
+                     hdrgm:Version="1.0"
+                     hdrgm:GainMapMin="2.0"
+                     hdrgm:OffsetSDR="0.1">
+                     <hdrgm:GainMapMax>
+                       <rdf:Seq>
+                         <rdf:li>3</rdf:li>
+                         <rdf:li>4</rdf:li>
+                         <rdf:li>5</rdf:li>
+                       </rdf:Seq>
+                     </hdrgm:GainMapMax>
+                     <hdrgm:Gamma>
+                       1.2
+                     </hdrgm:Gamma>
+                     <hdrgm:OffsetHDR>
+                       <rdf:Seq>
+                         <rdf:li>
+                           0.2
+                         </rdf:li>
+                         <rdf:li>
+                           0.3
+                         </rdf:li>
+                         <rdf:li>
+                           0.4
+                         </rdf:li>
+                       </rdf:Seq>
+                     </hdrgm:OffsetHDR>
+                  </rdf:Description>
+               </rdf:RDF>
+            </x:xmpmeta>)";
+
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    SkGainmapInfo info;
+    REPORTER_ASSERT(r, xmp->getGainmapInfoHDRGM(&info));
+    REPORTER_ASSERT(r, info.fGainmapRatioMin.fR == 4.f);
+    REPORTER_ASSERT(r, info.fGainmapRatioMin.fG == 4.f);
+    REPORTER_ASSERT(r, info.fGainmapRatioMin.fB == 4.f);
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fR == 8.f);
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fG == 16.f);
+    REPORTER_ASSERT(r, info.fGainmapRatioMax.fB == 32.f);
+
+    REPORTER_ASSERT(r, info.fGainmapGamma.fR == 1.f/1.2f);
+    REPORTER_ASSERT(r, info.fGainmapGamma.fG == 1.f/1.2f);
+    REPORTER_ASSERT(r, info.fGainmapGamma.fB == 1.f/1.2f);
+
+    REPORTER_ASSERT(r, info.fEpsilonSdr.fR == 0.1f);
+    REPORTER_ASSERT(r, info.fEpsilonSdr.fG == 0.1f);
+    REPORTER_ASSERT(r, info.fEpsilonSdr.fB == 0.1f);
+
+    REPORTER_ASSERT(r, info.fEpsilonHdr.fR == 0.2f);
+    REPORTER_ASSERT(r, info.fEpsilonHdr.fG == 0.3f);
+    REPORTER_ASSERT(r, info.fEpsilonHdr.fB == 0.4f);
+}
+
+DEF_TEST(SkXmp_xmpContainerTypedNode, r) {
+    // Container and Item using a node of type Container:Item.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.5.0">
+             <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+              <rdf:Description rdf:about=""
+                xmlns:Container="http://ns.google.com/photos/1.0/container/"
+                xmlns:Item="http://ns.google.com/photos/1.0/container/item/">
+               <Container:Directory>
+                <rdf:Seq>
+                 <rdf:li rdf:parseType="Resource">
+                  <Container:Item>
+                   <Item:Mime>image/jpeg</Item:Mime>
+                   <Item:Semantic>Primary</Item:Semantic>
+                  </Container:Item>
+                 </rdf:li>
+                 <rdf:li rdf:parseType="Resource">
+                  <Container:Item
+                     Item:Semantic="GainMap"
+                     Item:Mime="image/jpeg"
+                     Item:Length="49035"/>
+                 </rdf:li>
+                </rdf:Seq>
+               </Container:Directory>
+              </rdf:Description>
+             </rdf:RDF>
+            </x:xmpmeta>)";
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    size_t offset = 999;
+    size_t size = 999;
+    REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size));
+    REPORTER_ASSERT(r, size == 49035);
+}
+
+DEF_TEST(SkXmp_xmpContainerTypedNodeRdfEquivalent, r) {
+    // Container and Item using rdf:value and rdf:type pairs.
+    const char xmpData[] = R"(
+            <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.5.0">
+             <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+              <rdf:Description rdf:about=""
+                xmlns:Container="http://ns.google.com/photos/1.0/container/"
+                xmlns:Item="http://ns.google.com/photos/1.0/container/item/">
+               <Container:Directory>
+                <rdf:Seq>
+                 <rdf:li rdf:parseType="Resource">
+                  <rdf:value rdf:parseType="Resource">
+                   <Item:Mime>image/jpeg</Item:Mime>
+                   <Item:Semantic>Primary</Item:Semantic>
+                  </rdf:value>
+                  <rdf:type rdf:resource="Item"/>
+                 </rdf:li>
+                 <rdf:li rdf:parseType="Resource">
+                  <rdf:value rdf:parseType="Resource">
+                   <Item:Semantic>GainMap</Item:Semantic>
+                   <Item:Mime>image/jpeg</Item:Mime>
+                   <Item:Length>49035</Item:Length>
+                  </rdf:value>
+                  <rdf:type rdf:resource="Item"/>
+                 </rdf:li>
+                </rdf:Seq>
+               </Container:Directory>
+              </rdf:Description>
+             </rdf:RDF>
+            </x:xmpmeta>)";
+    sk_sp<SkData> app1Param = SkData::MakeWithoutCopy(xmpData, sizeof(xmpData) - 1);
+
+    auto xmp = SkXmp::Make(app1Param);
+    REPORTER_ASSERT(r, xmp);
+
+    size_t offset = 999;
+    size_t size = 999;
+    REPORTER_ASSERT(r, xmp->getContainerGainmapLocation(&offset, &size));
+    REPORTER_ASSERT(r, size == 49035);
+}
diff --git a/tests/testgroups.bzl b/tests/testgroups.bzl
index f0188aa..1249688 100644
--- a/tests/testgroups.bzl
+++ b/tests/testgroups.bzl
@@ -169,6 +169,7 @@
     "SkSLDebugTraceTest.cpp",
     "SkVMTest.cpp",
     "SkVxTest.cpp",
+    "SkXmpTest.cpp",
     "Skbug6389.cpp",
     "SortTest.cpp",
     "SrcOverTest.cpp",