Reland "Deserialize MultiPictureDocument based SKP files (with image sharing proc) in wasm debugger."

This is a reland of 7635013ad16a60cfa5ff93af28c5fab8927c92ce

Original change's description:
> Deserialize MultiPictureDocument based SKP files (with image sharing proc) in wasm debugger.
> 
> Change-Id: I73affae3cd05a2aa6ac1c75c8e049d352bbf3a85
> Bug: 9176
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/217135
> Commit-Queue: Nathaniel Nifong <nifong@google.com>
> Reviewed-by: Derek Sollenberger <djsollen@google.com>
> Reviewed-by: Kevin Lubick <kjlubick@google.com>

Bug: 9176
Change-Id: Ifef1ff45ac0013ba3015f88c7ecd75527b28b604
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/222505
Commit-Queue: Nathaniel Nifong <nifong@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 2897286..14b9cf6 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1057,6 +1057,7 @@
     public_configs = [ ":skia_public" ]
 
     sources = [
+      "tools/SkSharingProc.cpp",
       "tools/UrlDataManager.cpp",
       "tools/debugger/DebugCanvas.cpp",
       "tools/debugger/DrawCommand.cpp",
@@ -1597,6 +1598,7 @@
       "tools/LsanSuppressions.cpp",
       "tools/ProcStats.cpp",
       "tools/Resources.cpp",
+      "tools/SkSharingProc.cpp",
       "tools/ToolUtils.cpp",
       "tools/UrlDataManager.cpp",
       "tools/debugger/DebugCanvas.cpp",
diff --git a/experimental/wasm-skp-debugger/debugger_bindings.cpp b/experimental/wasm-skp-debugger/debugger_bindings.cpp
index b4fb35d..19084c4 100644
--- a/experimental/wasm-skp-debugger/debugger_bindings.cpp
+++ b/experimental/wasm-skp-debugger/debugger_bindings.cpp
@@ -8,6 +8,8 @@
 #include "include/core/SkPicture.h"
 #include "include/core/SkSurface.h"
 #include "src/utils/SkJSONWriter.h"
+#include "src/utils/SkMultiPictureDocument.h"
+#include "tools/SkSharingProc.h"
 #include "tools/UrlDataManager.h"
 #include "tools/debugger/DebugCanvas.h"
 
@@ -26,6 +28,10 @@
 
 using JSColor = int32_t;
 
+// file signature for SkMultiPictureDocument
+// TODO(nifong): make public and include from SkMultiPictureDocument.h
+static constexpr char kMultiMagic[] = "Skia Multi-Picture Doc\n\n";
+
 struct SimpleImageInfo {
   int width;
   int height;
@@ -52,45 +58,61 @@
      * pointer we're expecting.
      */
     void loadSkp(uintptr_t cptr, int length) {
-      const auto* data = reinterpret_cast<const uint8_t*>(cptr);
-      // note overloaded = operator that actually does a move
-      fPicture = SkPicture::MakeFromData(data, length);
-      if (!fPicture) {
-        SkDebugf("Unable to parse SKP file.\n");
-        return;
+      const uint8_t* data = reinterpret_cast<const uint8_t*>(cptr);
+      char magic[8];
+      // Both traditional and multi-frame skp files have a magic word
+      SkMemoryStream stream(data, length);
+      SkDebugf("make stream at %p, with %d bytes\n",data, length);
+      // Why -1? I think it's got to do with using a constexpr, just a guess.
+      const size_t magicsize = sizeof(kMultiMagic) - 1;
+      if (memcmp(data, kMultiMagic, magicsize) == 0) {
+        SkDebugf("Try reading as a multi-frame skp\n");
+        loadMultiFrame(&stream);
+      } else {
+        SkDebugf("Try reading as single-frame skp\n");
+        frames.push_back(loadSingleFrame(&stream));
       }
-      SkDebugf("Parsed SKP file.\n");
-      // Make debug canvas using bounds from SkPicture
-      fBounds = fPicture->cullRect().roundOut();
-      fDebugCanvas.reset(new DebugCanvas(fBounds));
-      SkDebugf("DebugCanvas created.\n");
-
-      // Only draw picture to the debug canvas once.
-      fDebugCanvas->drawPicture(fPicture);
-      SkDebugf("Added picture with %d commands.\n", fDebugCanvas->getSize());
     }
 
     /* drawTo asks the debug canvas to draw from the beginning of the picture
      * to the given command and flush the canvas.
      */
     void drawTo(SkSurface* surface, int32_t index) {
-      fDebugCanvas->drawTo(surface->getCanvas(), index);
+      int cmdlen = frames[fp]->getSize();
+      if (cmdlen == 0) {
+        SkDebugf("Zero commands to execute");
+        return;
+      }
+      if (index >= cmdlen) {
+        SkDebugf("Constrained command index (%d) within this frame's length (%d)\n", index, cmdlen);
+        index = cmdlen-1;
+      }
+      frames[fp]->drawTo(surface->getCanvas(), index);
       surface->getCanvas()->flush();
     }
 
     const SkIRect& getBounds() { return fBounds; }
 
-    void setOverdrawVis(bool on) { fDebugCanvas->setOverdrawViz(on); }
+    void setOverdrawVis(bool on) {
+      frames[fp]->setOverdrawViz(on);
+    }
     void setGpuOpBounds(bool on) {
-      fDebugCanvas->setDrawGpuOpBounds(on);
+      frames[fp]->setDrawGpuOpBounds(on);
     }
     void setClipVizColor(JSColor color) {
-      fDebugCanvas->setClipVizColor(SkColor(color));
+      frames[fp]->setClipVizColor(SkColor(color));
     }
-    int getSize() const { return fDebugCanvas->getSize(); }
-    void deleteCommand(int index) { fDebugCanvas->deleteDrawCommandAt(index); }
+    void deleteCommand(int index) {
+      frames[fp]->deleteDrawCommandAt(index);
+    }
     void setCommandVisibility(int index, bool visible) {
-      fDebugCanvas->toggleCommand(index, visible);
+      frames[fp]->toggleCommand(index, visible);
+    }
+    int getSize() const {
+      return frames[fp]->getSize();
+    }
+    int getFrameCount() const {
+      return frames.size();
     }
 
     // Return the command list in JSON representation as a string
@@ -101,7 +123,7 @@
       // this will be prepended to any links that are created in the json command list.
       UrlDataManager udm(SkString("/"));
       writer.beginObject(); // root
-      fDebugCanvas->toJSON(writer, udm, getSize(), surface->getCanvas());
+      frames[fp]->toJSON(writer, udm, getSize(), surface->getCanvas());
       writer.endObject(); // root
       writer.flush();
       auto skdata = stream.detachAsData();
@@ -113,8 +135,8 @@
 
     // Gets the clip and matrix of the last command drawn
     std::string lastCommandInfo() {
-      SkMatrix vm = fDebugCanvas->getCurrentMatrix();
-      SkIRect clip = fDebugCanvas->getCurrentClip();
+      SkMatrix vm = frames[fp]->getCurrentMatrix();
+      SkIRect clip = frames[fp]->getCurrentClip();
 
       SkDynamicMemoryWStream stream;
       SkJSONWriter writer(&stream, SkJSONWriter::Mode::kFast);
@@ -135,10 +157,81 @@
       return std::string(data_view);
     }
 
+    void changeFrame(int index) {
+      fp = index;
+    }
+
   private:
-      std::unique_ptr<DebugCanvas> fDebugCanvas;
-      sk_sp<SkPicture>             fPicture;
-      SkIRect                      fBounds;
+
+      // Loads a single frame (traditional) skp file from the provided data stream and returns
+      // a newly allocated DebugCanvas initialized with the SkPicture that was in the file.
+      std::unique_ptr<DebugCanvas> loadSingleFrame(SkMemoryStream* stream) {
+        // note overloaded = operator that actually does a move
+        sk_sp<SkPicture> picture = SkPicture::MakeFromStream(stream);
+        if (!picture) {
+          SkDebugf("Unable to deserialze frame.\n");
+          return nullptr;
+        }
+        SkDebugf("Parsed SKP file.\n");
+        // Make debug canvas using bounds from SkPicture
+        fBounds = picture->cullRect().roundOut();
+        std::unique_ptr<DebugCanvas> debugDanvas = std::make_unique<DebugCanvas>(fBounds);
+        SkDebugf("DebugCanvas created.\n");
+
+        // Only draw picture to the debug canvas once.
+        debugDanvas->drawPicture(picture);
+        SkDebugf("Added picture with %d commands.\n", debugDanvas->getSize());
+        return debugDanvas;
+      }
+
+      void loadMultiFrame(SkMemoryStream* stream) {
+
+          // Attempt to deserialize with an image sharing serial proc.
+          auto deserialContext = std::make_unique<SkSharingDeserialContext>();
+          SkDeserialProcs procs;
+          procs.fImageProc = SkSharingDeserialContext::deserializeImage;
+          procs.fImageCtx = deserialContext.get();
+
+          int page_count = SkMultiPictureDocumentReadPageCount(stream);
+          if (!page_count) {
+            SkDebugf("Not a MultiPictureDocument");
+            return;
+          }
+          SkDebugf("Expecting %d frames\n", page_count);
+
+          std::vector<SkDocumentPage> pages(page_count);
+          if (!SkMultiPictureDocumentRead(stream, pages.data(), page_count, &procs)) {
+            SkDebugf("Reading frames from MultiPictureDocument failed");
+            return;
+          }
+
+          for (const auto& page : pages) {
+            // Make debug canvas using bounds from SkPicture
+            fBounds = page.fPicture->cullRect().roundOut();
+            std::unique_ptr<DebugCanvas> debugDanvas = std::make_unique<DebugCanvas>(fBounds);
+            // Only draw picture to the debug canvas once.
+            debugDanvas->drawPicture(page.fPicture);
+            SkDebugf("Added picture with %d commands.\n", debugDanvas->getSize());
+
+            if (debugDanvas->getSize() <=0 ){
+              SkDebugf("Skipped corrupted frame, had %d commands \n", debugDanvas->getSize());
+              continue;
+            }
+            debugDanvas->setOverdrawViz(false);
+            debugDanvas->setDrawGpuOpBounds(false);
+            debugDanvas->setClipVizColor(SK_ColorTRANSPARENT);
+            frames.push_back(std::move(debugDanvas));
+          }
+      }
+
+      // A vector of DebugCanvas, each one initialized to a frame of the animation.
+      std::vector<std::unique_ptr<DebugCanvas>> frames;
+      // The index of the current frame (into the vector above)
+      int fp = 0;
+      // The width and height of the animation. (in practice the bounds of the last loaded frame)
+      SkIRect fBounds;
+      // SKP version of loaded file.
+      uint32_t fFileVersion;
 };
 
 #if SK_SUPPORT_GPU
@@ -218,7 +311,9 @@
     .function("setCommandVisibility", &SkpDebugPlayer::setCommandVisibility)
     .function("setGpuOpBounds",       &SkpDebugPlayer::setGpuOpBounds)
     .function("jsonCommandList",      &SkpDebugPlayer::jsonCommandList, allow_raw_pointers())
-    .function("lastCommandInfo",      &SkpDebugPlayer::lastCommandInfo);
+    .function("lastCommandInfo",      &SkpDebugPlayer::lastCommandInfo)
+    .function("changeFrame",          &SkpDebugPlayer::changeFrame)
+    .function("getFrameCount",        &SkpDebugPlayer::getFrameCount);
 
   // Structs used as arguments or returns to the functions above
   value_object<SkIRect>("SkIRect")
diff --git a/gn/tests.gni b/gn/tests.gni
index 734c1e4..ac67998 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -152,6 +152,7 @@
   "$_tests/MessageBusTest.cpp",
   "$_tests/MetaDataTest.cpp",
   "$_tests/MipMapTest.cpp",
+  "$_tests/MultiSkpTest.cpp",
   "$_tests/NonlinearBlendingTest.cpp",
   "$_tests/OctoBoundsTest.cpp",
   "$_tests/OSPathTest.cpp",
diff --git a/public.bzl b/public.bzl
index f3f56af..3df6fc9 100644
--- a/public.bzl
+++ b/public.bzl
@@ -453,6 +453,7 @@
         "experimental/svg/model/*.h",
         "gm/*.cpp",
         "gm/*.h",
+        "src/utils/SkMultiPictureDocument.cpp",
         "src/xml/*.cpp",
         "tests/*.cpp",
         "tests/*.h",
@@ -499,6 +500,7 @@
         "tools/gpu/**/*.h",
         "tools/random_parse_path.cpp",
         "tools/random_parse_path.h",
+        "tools/SkSharingProc.cpp",
         "tools/ToolUtils.cpp",
         "tools/ToolUtils.h",
         "tools/timer/*.cpp",
diff --git a/src/utils/SkMultiPictureDocument.cpp b/src/utils/SkMultiPictureDocument.cpp
index b87f967..25be716 100644
--- a/src/utils/SkMultiPictureDocument.cpp
+++ b/src/utils/SkMultiPictureDocument.cpp
@@ -80,7 +80,8 @@
         SkCanvas* c = fPictureRecorder.beginRecording(SkRect::MakeSize(bigsize));
         for (const sk_sp<SkPicture>& page : fPages) {
             c->drawPicture(page);
-            c->drawAnnotation(SkRect::MakeEmpty(), kEndPage, nullptr);
+            // Annotations must include some data.
+            c->drawAnnotation(SkRect::MakeEmpty(), kEndPage, SkData::MakeWithCString("X"));
         }
         sk_sp<SkPicture> p = fPictureRecorder.finishRecordingAsPicture();
         p->serialize(wStream, &fProcs);
@@ -195,7 +196,8 @@
     // PagerCanvas::onDrawAnnotation().
     picture->playback(&canvas);
     if (canvas.fIndex != dstArrayCount) {
-        SkDEBUGF("Malformed SkMultiPictureDocument\n");
+        SkDEBUGF("Malformed SkMultiPictureDocument: canvas.fIndex=%d dstArrayCount=%d\n",
+            canvas.fIndex, dstArrayCount);
     }
     return true;
 }
diff --git a/tests/MultiSkpTest.cpp b/tests/MultiSkpTest.cpp
new file mode 100644
index 0000000..8e3ea3b
--- /dev/null
+++ b/tests/MultiSkpTest.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * This test confirms that a multi skp can be serialize and deserailzied without error.
+ */
+
+#include "include/core/SkDocument.h"
+#include "include/core/SkPicture.h"
+#include "include/core/SkSurface.h"
+#include "include/core/SkString.h"
+#include "include/core/SkTextBlob.h"
+#include "include/core/SkFont.h"
+#include "src/core/SkRecord.h"
+#include "src/core/SkRecorder.h"
+#include "src/utils/SkMultiPictureDocument.h"
+#include "tools/SkSharingProc.h"
+#include "tests/Test.h"
+
+namespace {
+
+class RecordVisitor {
+// An SkRecord visitor that remembers the name of the last visited command.
+public:
+    SkString name;
+
+    explicit RecordVisitor() {}
+
+    template <typename T>
+    void operator()(const T& command) {
+        name = SkString(NameOf(command));
+    }
+
+    SkString lastCommandName() {
+        return name;
+    }
+private:
+    template <typename T>
+    static const char* NameOf(const T&) {
+        #define CASE(U) case SkRecords::U##_Type: return #U;
+        switch (T::kType) { SK_RECORD_TYPES(CASE) }
+        #undef CASE
+        return "Unknown T";
+    }
+};
+} // namespace
+
+// Compare record tested with record expected. Assert op sequence is the same (comparing types)
+// frame_num is only used for error message.
+static void compareRecords(const SkRecord& tested, const SkRecord& expected,
+    int frame_num, skiatest::Reporter* reporter) {
+    REPORTER_ASSERT(reporter, tested.count() == expected.count(),
+        "Found %d commands in frame %d, expected %d", tested.count(), frame_num, expected.count());
+
+    RecordVisitor rv;
+    for (int i = 0; i < tested.count(); i++) {
+        tested.visit(i, rv);
+        const SkString testCommandName = rv.lastCommandName();
+        expected.visit(i, rv);
+        const SkString expectedCommandName = rv.lastCommandName();
+        REPORTER_ASSERT(reporter, testCommandName == expectedCommandName,
+            "Unexpected command type '%s' in frame %d, op %d. Expected '%s'",
+            testCommandName.c_str(), frame_num, i, expectedCommandName.c_str());
+    }
+}
+
+static void draw_something(SkCanvas* canvas, int seed, sk_sp<SkImage> image) {
+    canvas->drawColor(SK_ColorWHITE);
+
+    SkPaint paint;
+    paint.setStyle(SkPaint::kStroke_Style);
+    paint.setStrokeWidth(seed);
+    paint.setColor(SK_ColorRED);
+
+    SkRect rect = SkRect::MakeXYWH(50+seed, 50+seed, 4*seed, 60);
+    canvas->drawRect(rect, paint);
+
+    SkRRect oval;
+    oval.setOval(rect);
+    oval.offset(40, 60+seed);
+    paint.setColor(SK_ColorBLUE);
+    canvas->drawRRect(oval, paint);
+
+    paint.setColor(SK_ColorCYAN);
+    canvas->drawCircle(180, 50, 5*seed, paint);
+
+    rect.offset(80, 0);
+    paint.setColor(SK_ColorYELLOW);
+    canvas->drawRoundRect(rect, 10, 10, paint);
+
+    SkPath path;
+    path.cubicTo(768, 0, -512, 256, 256, 256);
+    paint.setColor(SK_ColorGREEN);
+    canvas->drawPath(path, paint);
+
+    canvas->drawImage(image, 128-seed, 128, &paint);
+
+    if (seed % 2 == 0) {
+        SkRect rect2 = SkRect::MakeXYWH(0, 0, 40, 60);
+        canvas->drawImageRect(image, rect2, &paint);
+    }
+
+    SkPaint paint2;
+    auto text = SkTextBlob::MakeFromString(
+        SkStringPrintf("Frame %d", seed).c_str(), SkFont(nullptr, 2+seed));
+    canvas->drawTextBlob(text.get(), 50, 25, paint2);
+}
+
+// Test serialization and deserialization of multi skp.
+DEF_TEST(Serialize_and_deserialize_multi_skp, reporter) {
+    // Create the stream we will serialize into.
+    SkDynamicMemoryWStream stream;
+
+    // Create the image sharing proc.
+    SkSharingSerialContext ctx;
+    SkSerialProcs procs;
+    procs.fImageProc = SkSharingSerialContext::serializeImage;
+    procs.fImageCtx = &ctx;
+
+    // Create the mulit picture document used for recording frames.
+    sk_sp<SkDocument> multipic = SkMakeMultiPictureDocument(&stream, &procs);
+
+    static const int NUM_FRAMES = 12;
+    static const int WIDTH = 256;
+    static const int HEIGHT = 256;
+
+    // Make an image to be used in a later step.
+    auto surface(SkSurface::MakeRasterN32Premul(100, 100));
+    surface->getCanvas()->clear(SK_ColorGREEN);
+    sk_sp<SkImage> image(surface->makeImageSnapshot());
+    REPORTER_ASSERT(reporter, image);
+
+    // Create frames, recording them to multipic.
+    SkRecord expectedRecords[NUM_FRAMES];
+    for (int i=0; i<NUM_FRAMES; i++) {
+        SkCanvas* pictureCanvas = multipic->beginPage(WIDTH, HEIGHT);
+        draw_something(pictureCanvas, i, image);
+        multipic->endPage();
+        // Also record the same commands to separate SkRecords for later comparison
+        SkRecorder canvas(&expectedRecords[i], WIDTH, HEIGHT);
+        draw_something(&canvas, i, image);
+    }
+    // Finalize
+    multipic->close();
+
+    // Confirm written data is at least as large as the magic word
+    std::unique_ptr<SkStreamAsset> writtenStream = stream.detachAsStream();
+    REPORTER_ASSERT(reporter, writtenStream->getLength() > 24,
+        "Written data length too short (%d)", writtenStream->getLength());
+    SkDebugf("Multi Frame file size = %d\n", writtenStream->getLength());
+
+    // Set up deserialization
+    SkSharingDeserialContext deserialContext;
+    SkDeserialProcs dprocs;
+    dprocs.fImageProc = SkSharingDeserialContext::deserializeImage;
+    dprocs.fImageCtx = &deserialContext;
+
+    // Confirm data is a MultiPictureDocument
+    int frame_count = SkMultiPictureDocumentReadPageCount(writtenStream.get());
+    REPORTER_ASSERT(reporter, frame_count == NUM_FRAMES,
+        "Expected %d frames, got %d. \n 0 frames may indicate the written file was not a "
+        "MultiPictureDocument.", NUM_FRAMES, frame_count);
+
+    // Deserailize
+    std::vector<SkDocumentPage> frames(frame_count);
+    REPORTER_ASSERT(reporter,
+        SkMultiPictureDocumentRead(writtenStream.get(), frames.data(), frame_count, &dprocs),
+        "Failed while reading MultiPictureDocument");
+
+    // Examine each frame.
+    SkRecorder resultRecorder(nullptr, 1, 1);
+    int i=0;
+    for (const auto& frame : frames) {
+        SkRect bounds = frame.fPicture->cullRect();
+        REPORTER_ASSERT(reporter, bounds.width() == WIDTH,
+            "Page width: expected (%d) got (%d)", WIDTH, bounds.width());
+        REPORTER_ASSERT(reporter, bounds.height() == HEIGHT,
+            "Page height: expected (%d) got (%d)", HEIGHT, bounds.height());
+        // confirm contents of picture match what we drew.
+        // There are several ways of doing this, an ideal comparison would not break in the same
+        // way at the same time as the code under test (no serialization), and would involve only
+        // minimal transformation of frame.fPicture, minimizing the chance that a detected fault lies
+        // in the test itself. The comparions also would not be an overly sensitive change detector,
+        // so that it doesn't break every time someone submits code (no golden file)
+
+        // Extract the SkRecord from the deserialized picture using playback (instead of a mess of
+        // friend classes to grab the private record inside frame.fPicture
+        SkRecord record;
+        // This picture mode is necessary so that we record the command contents of frame.fPicture
+        // not just a 'DrawPicture' command.
+        resultRecorder.reset(&record, bounds, SkRecorder::Playback_DrawPictureMode, nullptr);
+        frame.fPicture->playback(&resultRecorder);
+        // Compare the record to the expected one
+        compareRecords(record, expectedRecords[i], i, reporter);
+        i++;
+    }
+}
diff --git a/tools/SkSharingProc.cpp b/tools/SkSharingProc.cpp
new file mode 100644
index 0000000..803f766
--- /dev/null
+++ b/tools/SkSharingProc.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "tools/SkSharingProc.h"
+
+#include "include/core/SkData.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkSerialProcs.h"
+
+sk_sp<SkData> SkSharingSerialContext::serializeImage(SkImage* img, void* ctx) {
+    SkSharingSerialContext* context = reinterpret_cast<SkSharingSerialContext*>(ctx);
+    uint32_t id = img->uniqueID(); // get this process's id for the image. these are not hashes.
+    // find out if we have already serialized this, and if so, what its in-file id is.
+    auto iter = context->fImageMap.find(id);
+    if (iter == context->fImageMap.end()) {
+        // When not present, add its id to the map and return its usual serialized form.
+        context->fImageMap[id] = context->fImageMap.size();
+        return img->encodeToData();
+    }
+    uint32_t fid = context->fImageMap[id];
+    // if present, return only the in-file id we registered the first time we serialized it.
+    return SkData::MakeWithCopy(&fid, sizeof(fid));
+}
+
+sk_sp<SkImage> SkSharingDeserialContext::deserializeImage(
+  const void* data, size_t length, void* ctx) {
+    SkSharingDeserialContext* context = reinterpret_cast<SkSharingDeserialContext*>(ctx);
+    uint32_t fid;
+    // If the data is an image fid, look up an already deserialized image from our map
+    if (length == sizeof(fid)) {
+        memcpy(&fid, data, sizeof(fid));
+        if (fid >= context->fImages.size()) {
+            SkDebugf("We do not have the data for image %d.\n", fid);
+            return nullptr;
+        }
+        return context->fImages[fid];
+    }
+    // Otherwise, the data is an image, deserialise it, store it in our map at its fid.
+    // TODO(nifong): make DeserialProcs accept sk_sp<SkData> so we don't have to copy this.
+    sk_sp<SkData> dataView = SkData::MakeWithCopy(data, length);
+    const sk_sp<SkImage> image = SkImage::MakeFromEncoded(std::move(dataView));
+    context->fImages.push_back(image);
+    return image;
+}
diff --git a/tools/SkSharingProc.h b/tools/SkSharingProc.h
new file mode 100644
index 0000000..12a7b80
--- /dev/null
+++ b/tools/SkSharingProc.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSharingProc_DEFINED
+#define SkSharingProc_DEFINED
+
+#include <unordered_map>
+#include <vector>
+
+#include "include/core/SkData.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkSerialProcs.h"
+
+struct SkSharingSerialContext {
+    // A map from the ids from SkImage::uniqueID() to ids used within the file
+    std::unordered_map<uint32_t, uint32_t> fImageMap;
+
+    // A serial proc that shares images between subpictures
+    // To use this, create an instance of SkSerialProcs and populate it this way.
+    // The client must retain ownership of the context.
+    // auto ctx = std::make_unique<SkSharingSerialContext>()
+    // SkSerialProcs procs;
+    // procs.fImageProc = SkSharingSerialContext::serializeImage;
+    // procs.fImageCtx = ctx.get();
+    static sk_sp<SkData> serializeImage(SkImage* img, void* ctx);
+};
+
+struct SkSharingDeserialContext {
+    // a list of unique images in the order they were encountered in the file
+    // Subsequent occurrences of an image refer to it by it's index in this list.
+    std::vector<sk_sp<SkImage>> fImages;
+
+    // A deserial proc that can interpret id's in place of images as references to previous images.
+    // Can also deserialize a SKP where all images are inlined (it's backwards compatible)
+    static sk_sp<SkImage> deserializeImage(const void* data, size_t length, void* ctx);
+};
+
+#endif