SkPDF: Fix skia:8397, reduce RAM use, stop copying ClipStacks.

In particular:
  - GraphicStateEntry no longer holds copy of SkClipStack.
  - ContentEntries are now usually concatinated together and include
    serialized GraphicStateEntry deltas.
  - One GraphicStackState is kept on the pdfdevice, for the currently
    active ContentEntry.

16% reduction in RAM use for running all GMs through SkPDF.

97% reduction in RAM use for a particular test case:
    SkRandom rand;
    SkPaint paint;
    for (int i = 400000; i-- > 0;) {
        SkPoint p0 = {0,   (float)rand.nextRangeU(0, 792)};
        SkPoint p1 = {612, (float)rand.nextRangeU(0, 792)};
        canvas->drawLine(p0, p1, paint);
    }

Bug: skia:8397
Change-Id: Ieb86c0eabac45b120a97fe5c749fdb26d8a85267
Reviewed-on: https://skia-review.googlesource.com/157340
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Hal Canary <halcanary@google.com>
diff --git a/src/pdf/SkPDFDevice.cpp b/src/pdf/SkPDFDevice.cpp
index 7179098..0026897 100644
--- a/src/pdf/SkPDFDevice.cpp
+++ b/src/pdf/SkPDFDevice.cpp
@@ -184,44 +184,27 @@
     }
 }
 
-class GraphicStackState {
-public:
-    GraphicStackState(SkWStream* s) : fContentStream(s) {}
 
-    void updateClip(const SkClipStack& clipStack, const SkIRect& bounds);
-    void updateMatrix(const SkMatrix& matrix);
-    void updateDrawingState(const SkPDFDevice::GraphicStateEntry& state);
-
-    void drainStack();
-
-private:
-    void push();
-    void pop();
-    SkPDFDevice::GraphicStateEntry* currentEntry() { return &fEntries[fStackDepth]; }
-
-    // Conservative limit on save depth, see impl. notes in PDF 1.4 spec.
-    static const int kMaxStackDepth = 12;
-    SkPDFDevice::GraphicStateEntry fEntries[kMaxStackDepth + 1];
-    int fStackDepth = 0;
-    SkWStream* fContentStream;
-};
-
-void GraphicStackState::drainStack() {
-    while (fStackDepth) {
-        pop();
+void SkPDFDevice::GraphicStackState::drainStack() {
+    if (fContentStream) {
+        while (fStackDepth) {
+            pop();
+        }
     }
+    SkASSERT(fStackDepth == 0);
 }
 
-void GraphicStackState::push() {
+void SkPDFDevice::GraphicStackState::push() {
     SkASSERT(fStackDepth < kMaxStackDepth);
     fContentStream->writeText("q\n");
     fStackDepth++;
     fEntries[fStackDepth] = fEntries[fStackDepth - 1];
 }
 
-void GraphicStackState::pop() {
+void SkPDFDevice::GraphicStackState::pop() {
     SkASSERT(fStackDepth > 0);
     fContentStream->writeText("Q\n");
+    fEntries[fStackDepth] = SkPDFDevice::GraphicStateEntry();
     fStackDepth--;
 }
 
@@ -336,22 +319,27 @@
 // TODO(vandebo): Take advantage of SkClipStack::getSaveCount(), the PDF
 // graphic state stack, and the fact that we can know all the clips used
 // on the page to optimize this.
-void GraphicStackState::updateClip(const SkClipStack& clipStack,
-                                   const SkIRect& bounds) {
-    if (clipStack == currentEntry()->fClipStack) {
+void SkPDFDevice::GraphicStackState::updateClip(const SkClipStack* clipStack,
+                                                const SkIRect& bounds) {
+    uint32_t clipStackGenID = clipStack ? clipStack->getTopmostGenID()
+                                        : SkClipStack::kWideOpenGenID;
+    if (clipStackGenID == currentEntry()->fClipStackGenID) {
         return;
     }
 
     while (fStackDepth > 0) {
         pop();
-        if (clipStack == currentEntry()->fClipStack) {
+        if (clipStackGenID == currentEntry()->fClipStackGenID) {
             return;
         }
     }
-    push();
+    if (clipStackGenID != SkClipStack::kWideOpenGenID) {
+        SkASSERT(clipStack);
+        push();
 
-    currentEntry()->fClipStack = clipStack;
-    append_clip(clipStack, bounds, fContentStream);
+        currentEntry()->fClipStackGenID = clipStackGenID;
+        append_clip(*clipStack, bounds, fContentStream);
+    }
 }
 
 static void append_transform(const SkMatrix& matrix, SkWStream* content) {
@@ -366,15 +354,15 @@
     content->writeText("cm\n");
 }
 
-void GraphicStackState::updateMatrix(const SkMatrix& matrix) {
+void SkPDFDevice::GraphicStackState::updateMatrix(const SkMatrix& matrix) {
     if (matrix == currentEntry()->fMatrix) {
         return;
     }
 
     if (currentEntry()->fMatrix.getType() != SkMatrix::kIdentity_Mask) {
         SkASSERT(fStackDepth > 0);
-        SkASSERT(fEntries[fStackDepth].fClipStack ==
-                 fEntries[fStackDepth -1].fClipStack);
+        SkASSERT(fEntries[fStackDepth].fClipStackGenID ==
+                 fEntries[fStackDepth -1].fClipStackGenID);
         pop();
 
         SkASSERT(currentEntry()->fMatrix.getType() == SkMatrix::kIdentity_Mask);
@@ -388,7 +376,7 @@
     currentEntry()->fMatrix = matrix;
 }
 
-void GraphicStackState::updateDrawingState(const SkPDFDevice::GraphicStateEntry& state) {
+void SkPDFDevice::GraphicStackState::updateDrawingState(const SkPDFDevice::GraphicStateEntry& state) {
     // PDF treats a shader as a color, so we only set one or the other.
     if (state.fShaderIndex >= 0) {
         if (state.fShaderIndex != currentEntry()->fShaderIndex) {
@@ -464,6 +452,7 @@
         , fContentEntry(nullptr)
         , fBlendMode(SkBlendMode::kSrcOver)
         , fDstFormXObject(nullptr)
+        , fClipStack(clipStack)
     {
         if (matrix.hasPerspective()) {
             NOT_IMPLEMENTED(!matrix.hasPerspective(), false);
@@ -482,12 +471,12 @@
             if (shape->isEmpty()) {
                 shape = nullptr;
             }
-            fDevice->finishContentEntry(fBlendMode, std::move(fDstFormXObject), shape);
+            fDevice->finishContentEntry(fClipStack, fBlendMode, std::move(fDstFormXObject), shape);
         }
     }
 
-    SkPDFDevice::ContentEntry* entry() { return fContentEntry; }
-    SkDynamicMemoryWStream* stream() { return &fContentEntry->fContent; }
+    SkDynamicMemoryWStream* entry() { return fContentEntry; }
+    SkDynamicMemoryWStream* stream() { return fContentEntry; }
 
     /* Returns true when we explicitly need the shape of the drawing. */
     bool needShape() {
@@ -525,10 +514,11 @@
 
 private:
     SkPDFDevice* fDevice;
-    SkPDFDevice::ContentEntry* fContentEntry;
+    SkDynamicMemoryWStream* fContentEntry;
     SkBlendMode fBlendMode;
     sk_sp<SkPDFObject> fDstFormXObject;
     SkPath fShape;
+    const SkClipStack* fClipStack;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -1417,21 +1407,26 @@
                                    std::move(fFontResources));
 }
 
-std::unique_ptr<SkStreamAsset> SkPDFDevice::content() const {
+std::unique_ptr<SkStreamAsset> SkPDFDevice::content() {
+    if (fActiveStackState.fContentStream) {
+        fActiveStackState.drainStack();
+        fActiveStackState = GraphicStackState();
+    }
+
     SkDynamicMemoryWStream buffer;
     if (fInitialTransform.getType() != SkMatrix::kIdentity_Mask) {
         append_transform(fInitialTransform, &buffer);
     }
-
-    GraphicStackState gsState(&buffer);
-    for (const auto& entry : fContentEntries) {
-        gsState.updateClip(entry.fState.fClipStack, this->bounds());
-        gsState.updateMatrix(entry.fState.fMatrix);
-        gsState.updateDrawingState(entry.fState);
-
-        entry.fContent.writeToStream(&buffer);
+    if (fContentEntries.back() && fContentEntries.back() == fContentEntries.front()) {
+        fContentEntries.front()->writeToAndReset(&buffer);
+    } else {
+        for (SkDynamicMemoryWStream& entry : fContentEntries) {
+            buffer.writeText("q\n");
+            entry.writeToAndReset(&buffer);
+            buffer.writeText("Q\n");
+        }
     }
-    gsState.drainStack();
+    fContentEntries.reset();
     if (buffer.bytesWritten() > 0) {
         return std::unique_ptr<SkStreamAsset>(buffer.detachAsStream());
     } else {
@@ -1580,25 +1575,27 @@
     this->clearMaskOnGraphicState(content.stream());
 }
 
-SkPDFDevice::ContentEntry* SkPDFDevice::setUpContentEntry(const SkClipStack* clipStack,
-                                                          const SkMatrix& matrix,
-                                                          const SkPaint& paint,
-                                                          bool hasText,
-                                                          sk_sp<SkPDFObject>* dst) {
+
+static bool treat_as_regular_pdf_blend_mode(SkBlendMode blendMode) {
+    return nullptr != SkPDFUtils::BlendModeName(blendMode);
+}
+
+SkDynamicMemoryWStream* SkPDFDevice::setUpContentEntry(const SkClipStack* clipStack,
+                                                       const SkMatrix& matrix,
+                                                       const SkPaint& paint,
+                                                       bool hasText,
+                                                       sk_sp<SkPDFObject>* dst) {
     *dst = nullptr;
     SkBlendMode blendMode = paint.getBlendMode();
 
+    // Dst xfer mode doesn't draw source at all.
+    if (blendMode == SkBlendMode::kDst) {
+        return nullptr;
+    }
+
     // For the following modes, we want to handle source and destination
     // separately, so make an object of what's already there.
-    if (blendMode == SkBlendMode::kClear       ||
-            blendMode == SkBlendMode::kSrc     ||
-            blendMode == SkBlendMode::kSrcIn   ||
-            blendMode == SkBlendMode::kDstIn   ||
-            blendMode == SkBlendMode::kSrcOut  ||
-            blendMode == SkBlendMode::kDstOut  ||
-            blendMode == SkBlendMode::kSrcATop ||
-            blendMode == SkBlendMode::kDstATop ||
-            blendMode == SkBlendMode::kModulate) {
+    if (!treat_as_regular_pdf_blend_mode(blendMode) && blendMode != SkBlendMode::kDstOver) {
         if (!isContentEmpty()) {
             *dst = this->makeFormXObjectFromDevice();
             SkASSERT(isContentEmpty());
@@ -1610,44 +1607,48 @@
         }
     }
     // TODO(vandebo): Figure out how/if we can handle the following modes:
-    // Xor, Plus.
+    // Xor, Plus.  For now, we treat them as SrcOver/Normal.
 
-    // Dst xfer mode doesn't draw source at all.
-    if (blendMode == SkBlendMode::kDst) {
-        return nullptr;
-    }
-
-    SkPDFDevice::ContentEntry* entry;
-    if (fContentEntries.back() && fContentEntries.back()->fContent.bytesWritten() == 0) {
-        entry = fContentEntries.back();
-    } else if (blendMode != SkBlendMode::kDstOver) {
-        entry = fContentEntries.emplace_back();
+    if (treat_as_regular_pdf_blend_mode(blendMode)) {
+        if (!fActiveStackState.fContentStream) {
+            fActiveStackState = GraphicStackState(fContentEntries.emplace_back());
+        }
     } else {
-        entry = fContentEntries.emplace_front();
+        fActiveStackState.drainStack();
+        if (blendMode != SkBlendMode::kDstOver) {
+             fActiveStackState = GraphicStackState(fContentEntries.emplace_back());
+        } else {
+             fActiveStackState = GraphicStackState(fContentEntries.emplace_front());
+        }
     }
-    this->populateGraphicStateEntryFromPaint(matrix, clipStack, paint, hasText, &entry->fState);
-    return entry;
+    SkASSERT(fActiveStackState.fContentStream);
+    GraphicStateEntry entry;
+    this->populateGraphicStateEntryFromPaint(matrix, clipStack, paint, hasText, &entry);
+    fActiveStackState.updateClip(clipStack, this->bounds());
+    fActiveStackState.updateMatrix(entry.fMatrix);
+    fActiveStackState.updateDrawingState(entry);
+
+    return fActiveStackState.fContentStream;
 }
 
-void SkPDFDevice::finishContentEntry(SkBlendMode blendMode,
+void SkPDFDevice::finishContentEntry(const SkClipStack* clipStack,
+                                     SkBlendMode blendMode,
                                      sk_sp<SkPDFObject> dst,
                                      SkPath* shape) {
-    if (blendMode != SkBlendMode::kClear       &&
-            blendMode != SkBlendMode::kSrc     &&
-            blendMode != SkBlendMode::kDstOver &&
-            blendMode != SkBlendMode::kSrcIn   &&
-            blendMode != SkBlendMode::kDstIn   &&
-            blendMode != SkBlendMode::kSrcOut  &&
-            blendMode != SkBlendMode::kDstOut  &&
-            blendMode != SkBlendMode::kSrcATop &&
-            blendMode != SkBlendMode::kDstATop &&
-            blendMode != SkBlendMode::kModulate) {
+    SkASSERT(blendMode != SkBlendMode::kDst);
+    if (treat_as_regular_pdf_blend_mode(blendMode)) {
         SkASSERT(!dst);
         return;
     }
+
+    SkASSERT(fActiveStackState.fContentStream);
+
+    fActiveStackState.drainStack();
+    fActiveStackState = GraphicStackState();
+
     if (blendMode == SkBlendMode::kDstOver) {
         SkASSERT(!dst);
-        if (fContentEntries.front()->fContent.bytesWritten() == 0) {
+        if (fContentEntries.front()->bytesWritten() == 0) {
             // For DstOver, an empty content entry was inserted before the rest
             // of the content entries. If nothing was drawn, it needs to be
             // removed.
@@ -1668,8 +1669,6 @@
     // if source has shape, we need to clip it too, so a copy of the clip is
     // saved.
 
-    SkClipStack clipStack = fContentEntries.front()->fState.fClipStack;
-
     SkPaint stockPaint;
 
     sk_sp<SkPDFObject> srcFormXObject;
@@ -1707,7 +1706,9 @@
             SkPaint filledPaint;
             filledPaint.setColor(SK_ColorBLACK);
             filledPaint.setStyle(SkPaint::kFill_Style);
-            this->internalDrawPath(clipStack, SkMatrix::I(), *shape, filledPaint, true);
+            SkClipStack empty;
+            this->internalDrawPath(clipStack ? *clipStack : empty,
+                                   SkMatrix::I(), *shape, filledPaint, true);
             this->drawFormXObjectWithMask(find_or_add(&fXObjectResources, dst),
                                           this->makeFormXObjectFromDevice(),
                                           SkBlendMode::kSrcOver, true);
@@ -1765,7 +1766,7 @@
 }
 
 bool SkPDFDevice::isContentEmpty() {
-    if (!fContentEntries.front() || fContentEntries.front()->fContent.bytesWritten() == 0) {
+    if (!fContentEntries.front() || fContentEntries.front()->bytesWritten() == 0) {
         SkASSERT(fContentEntries.count() <= 1);
         return true;
     }
@@ -1783,7 +1784,8 @@
     NOT_IMPLEMENTED(paint.getColorFilter() != nullptr, false);
 
     entry->fMatrix = matrix;
-    entry->fClipStack = clipStack ? *clipStack : SkClipStack();
+    entry->fClipStackGenID = clipStack ? clipStack->getTopmostGenID()
+                                       : SkClipStack::kWideOpenGenID;
     entry->fColor = SkColorSetA(paint.getColor(), 0xFF);
     entry->fShaderIndex = -1;
 
@@ -1810,7 +1812,8 @@
 
             // PDF doesn't support kClamp_TileMode, so we simulate it by making
             // a pattern the size of the current clip.
-            SkRect clipStackBounds = entry->fClipStack.bounds(this->bounds());
+            SkRect clipStackBounds = clipStack ? clipStack->bounds(this->bounds())
+                                               : SkRect::Make(this->bounds());
 
             // We need to apply the initial transform to bounds in order to get
             // bounds in a consistent coordinate system.
diff --git a/src/pdf/SkPDFDevice.h b/src/pdf/SkPDFDevice.h
index 5321a85..aabc001 100644
--- a/src/pdf/SkPDFDevice.h
+++ b/src/pdf/SkPDFDevice.h
@@ -112,7 +112,7 @@
 
     /** Returns a SkStream with the page contents.
      */
-    std::unique_ptr<SkStreamAsset> content() const;
+    std::unique_ptr<SkStreamAsset> content();
 
     SkPDFCanon* getCanon() const;
 
@@ -122,7 +122,7 @@
     // later being our representation of an object in the PDF file.
     struct GraphicStateEntry {
         SkMatrix fMatrix = SkMatrix::I();
-        SkClipStack fClipStack;
+        uint32_t fClipStackGenID = SkClipStack::kWideOpenGenID;
         SkColor fColor = SK_ColorBLACK;
         SkScalar fTextScaleX = 1;  // Zero means we don't care what the value is.
         SkPaint::Style fTextFill = SkPaint::kFill_Style;  // Only if TextScaleX is non-zero.
@@ -168,12 +168,23 @@
     std::vector<sk_sp<SkPDFObject>> fShaderResources;
     std::vector<sk_sp<SkPDFFont>> fFontResources;
 
-    struct ContentEntry {
-        GraphicStateEntry fState;
-        SkDynamicMemoryWStream fContent;
+    SkSinglyLinkedList<SkDynamicMemoryWStream> fContentEntries;
+    struct GraphicStackState {
+        GraphicStackState(SkDynamicMemoryWStream* s = nullptr) : fContentStream(s) {}
+        void updateClip(const SkClipStack* clipStack, const SkIRect& bounds);
+        void updateMatrix(const SkMatrix& matrix);
+        void updateDrawingState(const SkPDFDevice::GraphicStateEntry& state);
+        void push();
+        void pop();
+        void drainStack();
+        SkPDFDevice::GraphicStateEntry* currentEntry() { return &fEntries[fStackDepth]; }
+        // Conservative limit on save depth, see impl. notes in PDF 1.4 spec.
+        static const int kMaxStackDepth = 11;
+        SkPDFDevice::GraphicStateEntry fEntries[kMaxStackDepth + 1];
+        int fStackDepth = 0;
+        SkDynamicMemoryWStream* fContentStream;
     };
-    SkSinglyLinkedList<ContentEntry> fContentEntries;
-
+    GraphicStackState fActiveStackState;
     SkPDFDocument* fDocument;
 
     ////////////////////////////////////////////////////////////////////////////
@@ -192,12 +203,12 @@
     // returns nullptr and does not create a content entry.
     // setUpContentEntry and finishContentEntry can be used directly, but
     // the preferred method is to use the ScopedContentEntry helper class.
-    ContentEntry* setUpContentEntry(const SkClipStack* clipStack,
+    SkDynamicMemoryWStream* setUpContentEntry(const SkClipStack* clipStack,
                                     const SkMatrix& matrix,
                                     const SkPaint& paint,
                                     bool hasText,
                                     sk_sp<SkPDFObject>* dst);
-    void finishContentEntry(SkBlendMode, sk_sp<SkPDFObject> dst, SkPath* shape);
+    void finishContentEntry(const SkClipStack*, SkBlendMode, sk_sp<SkPDFObject> dst, SkPath* shape);
     bool isContentEmpty();
 
     void populateGraphicStateEntryFromPaint(const SkMatrix& matrix,
diff --git a/src/pdf/SkPDFGraphicState.cpp b/src/pdf/SkPDFGraphicState.cpp
index 65979b5..ade2b50 100644
--- a/src/pdf/SkPDFGraphicState.cpp
+++ b/src/pdf/SkPDFGraphicState.cpp
@@ -15,27 +15,9 @@
 #include "SkTo.h"
 
 static const char* as_pdf_blend_mode_name(SkBlendMode mode) {
-    // PDF32000.book section 11.3.5 "Blend Mode"
-    switch (mode) {
-        case SkBlendMode::kScreen:      return "Screen";
-        case SkBlendMode::kOverlay:     return "Overlay";
-        case SkBlendMode::kDarken:      return "Darken";
-        case SkBlendMode::kLighten:     return "Lighten";
-        case SkBlendMode::kColorDodge:  return "ColorDodge";
-        case SkBlendMode::kColorBurn:   return "ColorBurn";
-        case SkBlendMode::kHardLight:   return "HardLight";
-        case SkBlendMode::kSoftLight:   return "SoftLight";
-        case SkBlendMode::kDifference:  return "Difference";
-        case SkBlendMode::kExclusion:   return "Exclusion";
-        case SkBlendMode::kMultiply:    return "Multiply";
-        case SkBlendMode::kHue:         return "Hue";
-        case SkBlendMode::kSaturation:  return "Saturation";
-        case SkBlendMode::kColor:       return "Color";
-        case SkBlendMode::kLuminosity:  return "Luminosity";
-        // Other blendmodes are either unsupported or handled in
-        // SkPDFDevice::setUpContentEntry.
-        default:                        return "Normal";
-    }
+    const char* name = SkPDFUtils::BlendModeName(mode);
+    SkASSERT(name);
+    return name;
 }
 
 static int to_stroke_cap(uint8_t cap) {
@@ -61,27 +43,13 @@
 // If a SkXfermode is unsupported in PDF, this function returns
 // SrcOver, otherwise, it returns that Xfermode as a Mode.
 static uint8_t pdf_blend_mode(SkBlendMode mode) {
-    switch (mode) {
-        case SkBlendMode::kSrcOver:
-        case SkBlendMode::kMultiply:
-        case SkBlendMode::kScreen:
-        case SkBlendMode::kOverlay:
-        case SkBlendMode::kDarken:
-        case SkBlendMode::kLighten:
-        case SkBlendMode::kColorDodge:
-        case SkBlendMode::kColorBurn:
-        case SkBlendMode::kHardLight:
-        case SkBlendMode::kSoftLight:
-        case SkBlendMode::kDifference:
-        case SkBlendMode::kExclusion:
-        case SkBlendMode::kHue:
-        case SkBlendMode::kSaturation:
-        case SkBlendMode::kColor:
-        case SkBlendMode::kLuminosity:
-            return SkToU8((unsigned)mode);
-        default:
-            return SkToU8((unsigned)SkBlendMode::kSrcOver);
+    if (!SkPDFUtils::BlendModeName(mode)
+        || SkBlendMode::kXor  == mode
+        || SkBlendMode::kPlus == mode)
+    {
+        mode = SkBlendMode::kSrcOver;
     }
+    return SkToU8((unsigned)mode);
 }
 
 sk_sp<SkPDFDict> SkPDFGraphicState::GetGraphicStateForPaint(SkPDFCanon* canon,
diff --git a/src/pdf/SkPDFUtils.cpp b/src/pdf/SkPDFUtils.cpp
index a0dbe2a..b435170 100644
--- a/src/pdf/SkPDFUtils.cpp
+++ b/src/pdf/SkPDFUtils.cpp
@@ -18,6 +18,32 @@
 
 #include <cmath>
 
+const char* SkPDFUtils::BlendModeName(SkBlendMode mode) {
+    // PDF32000.book section 11.3.5 "Blend Mode"
+    switch (mode) {
+        case SkBlendMode::kSrcOver:     return "Normal";
+        case SkBlendMode::kXor:         return "Normal";  // (unsupported mode)
+        case SkBlendMode::kPlus:        return "Normal";  // (unsupported mode)
+        case SkBlendMode::kScreen:      return "Screen";
+        case SkBlendMode::kOverlay:     return "Overlay";
+        case SkBlendMode::kDarken:      return "Darken";
+        case SkBlendMode::kLighten:     return "Lighten";
+        case SkBlendMode::kColorDodge:  return "ColorDodge";
+        case SkBlendMode::kColorBurn:   return "ColorBurn";
+        case SkBlendMode::kHardLight:   return "HardLight";
+        case SkBlendMode::kSoftLight:   return "SoftLight";
+        case SkBlendMode::kDifference:  return "Difference";
+        case SkBlendMode::kExclusion:   return "Exclusion";
+        case SkBlendMode::kMultiply:    return "Multiply";
+        case SkBlendMode::kHue:         return "Hue";
+        case SkBlendMode::kSaturation:  return "Saturation";
+        case SkBlendMode::kColor:       return "Color";
+        case SkBlendMode::kLuminosity:  return "Luminosity";
+        // Other blendmodes are handled in SkPDFDevice::setUpContentEntry.
+        default:                        return nullptr;
+    }
+}
+
 sk_sp<SkPDFArray> SkPDFUtils::RectToArray(const SkRect& r) {
     return SkPDFMakeArray(r.left(), r.top(), r.right(), r.bottom());
 }
diff --git a/src/pdf/SkPDFUtils.h b/src/pdf/SkPDFUtils.h
index 511e8c6..209cb70 100644
--- a/src/pdf/SkPDFUtils.h
+++ b/src/pdf/SkPDFUtils.h
@@ -42,6 +42,8 @@
 
 namespace SkPDFUtils {
 
+const char* BlendModeName(SkBlendMode);
+
 sk_sp<SkPDFArray> RectToArray(const SkRect& rect);
 sk_sp<SkPDFArray> MatrixToArray(const SkMatrix& matrix);