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);