aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Alexander Midlash <amidlash@amazon.com>2018-03-06 17:21:28 -0800
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2018-03-07 02:25:30 +0000
commit77e3afc908f5f1e79180beb558b163fcf1cdf8ac (patch)
treecee37923028b99c7a83c00a9eea9e7dda9d98b24
parentc5cf762177c5708a5efa39f95b086c751e03e2e4 (diff)
[SkSVGDevice] Add support for image shaders.
Below is an example of the generated svg for an image shader that repeats in the x direction only: <svg stroke="none" x="9" y="153" width="50" height="30"> <defs> <pattern id="pattern_1_19" patternUnits="userSpaceOnUse" patternContentUnits="userSpaceOnUse" width="31" height="100%" x="0" y="0"> <image id="img_2_19" x="0" y="0" width="31" height="30" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,LONG_B64_STRING_HERE"></image> </pattern> </defs> <rect fill="url(#pattern_1_19)" stroke="none" x="0" y="0" width="100%" height="100%"></rect> </svg> Matching the height of the pattern with the height of the container prevents it from repeating in the y direction. R=fmalita@chromium.org Bug: skia::7681 Change-Id: I43e4f19acda4bd40c7a8b5259d67c26a108d6f67 Reviewed-on: https://skia-review.googlesource.com/111420 Commit-Queue: Florin Malita <fmalita@chromium.org> Reviewed-by: Florin Malita <fmalita@chromium.org>
-rw-r--r--src/svg/SkSVGDevice.cpp168
-rw-r--r--tests/SVGDeviceTest.cpp189
2 files changed, 346 insertions, 11 deletions
diff --git a/src/svg/SkSVGDevice.cpp b/src/svg/SkSVGDevice.cpp
index 7206709e5c..e3f93cb022 100644
--- a/src/svg/SkSVGDevice.cpp
+++ b/src/svg/SkSVGDevice.cpp
@@ -15,10 +15,13 @@
#include "SkClipStack.h"
#include "SkData.h"
#include "SkDraw.h"
+#include "SkImage.h"
#include "SkImageEncoder.h"
+#include "SkJpegCodec.h"
#include "SkPaint.h"
#include "SkPaintPriv.h"
#include "SkParsePath.h"
+#include "SkPngCodec.h"
#include "SkShader.h"
#include "SkStream.h"
#include "SkTHash.h"
@@ -245,13 +248,35 @@ private:
bool fLastCharWasWhitespace;
};
+// Determine if the paint requires us to reset the viewport.
+// Currently, we do this whenever the paint shader calls
+// for a repeating image.
+bool RequiresViewportReset(const SkPaint& paint) {
+ SkShader* shader = paint.getShader();
+ if (!shader)
+ return false;
+
+ SkShader::TileMode xy[2];
+ SkImage* image = shader->isAImage(nullptr, xy);
+
+ if (!image)
+ return false;
+
+ for (int i = 0; i < 2; i++) {
+ if (xy[i] == SkShader::kRepeat_TileMode)
+ return true;
+ }
+ return false;
}
+} // namespace
+
// For now all this does is serve unique serial IDs, but it will eventually evolve to track
// and deduplicate resources.
class SkSVGDevice::ResourceBucket : ::SkNoncopyable {
public:
- ResourceBucket() : fGradientCount(0), fClipCount(0), fPathCount(0), fImageCount(0) {}
+ ResourceBucket()
+ : fGradientCount(0), fClipCount(0), fPathCount(0), fImageCount(0), fPatternCount(0) {}
SkString addLinearGradient() {
return SkStringPrintf("gradient_%d", fGradientCount++);
@@ -269,11 +294,16 @@ public:
return SkStringPrintf("img_%d", fImageCount++);
}
+ SkString addPattern() {
+ return SkStringPrintf("pattern_%d", fPatternCount++);
+ }
+
private:
uint32_t fGradientCount;
uint32_t fClipCount;
uint32_t fPathCount;
uint32_t fImageCount;
+ uint32_t fPatternCount;
};
struct SkSVGDevice::MxCp {
@@ -298,6 +328,7 @@ public:
, fResourceBucket(bucket) {
Resources res = this->addResources(mc, paint);
+
if (!res.fClip.isEmpty()) {
// The clip is in device space. Apply it via a <g> wrapper to avoid local transform
// interference.
@@ -346,9 +377,16 @@ private:
Resources addResources(const MxCp&, const SkPaint& paint);
void addClipResources(const MxCp&, Resources* resources);
void addShaderResources(const SkPaint& paint, Resources* resources);
+ void addGradientShaderResources(const SkShader* shader, const SkPaint& paint,
+ Resources* resources);
+ void addImageShaderResources(const SkShader* shader, const SkPaint& paint,
+ Resources* resources);
+
+ void addPatternDef(const SkBitmap& bm);
void addPaint(const SkPaint& paint, const Resources& resources);
+
SkString addLinearGradientDef(const SkShader::GradientInfo& info, const SkShader* shader);
SkXMLWriter* fWriter;
@@ -423,10 +461,9 @@ Resources SkSVGDevice::AutoElement::addResources(const MxCp& mc, const SkPaint&
return resources;
}
-void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resources* resources) {
- const SkShader* shader = paint.getShader();
- SkASSERT(SkToBool(shader));
-
+void SkSVGDevice::AutoElement::addGradientShaderResources(const SkShader* shader,
+ const SkPaint& paint,
+ Resources* resources) {
SkShader::GradientInfo grInfo;
grInfo.fColorCount = 0;
if (SkShader::kLinear_GradientType != shader->asAGradient(&grInfo)) {
@@ -448,6 +485,111 @@ void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resource
resources->fPaintServer.printf("url(#%s)", addLinearGradientDef(grInfo, shader).c_str());
}
+// Returns data uri from bytes.
+// it will use any cached data if available, otherwise will
+// encode as png.
+sk_sp<SkData> AsDataUri(SkImage* image) {
+ sk_sp<SkData> imageData = image->encodeToData();
+ if (!imageData) {
+ SkDebugf("Failed to encode image shader's contents.");
+ return nullptr;
+ }
+
+ const char* src = (char*)imageData->data();
+ const char* selectedPrefix = nullptr;
+ size_t selectedPrefixLength = 0;
+
+ const static char pngDataPrefix[] = "data:image/png;base64,";
+ const static char jpgDataPrefix[] = "data:image/jpeg;base64,";
+
+ if (SkJpegCodec::IsJpeg(src, imageData->size())) {
+ selectedPrefix = jpgDataPrefix;
+ selectedPrefixLength = sizeof(jpgDataPrefix);
+ } else {
+ if (!SkPngCodec::IsPng(src, imageData->size())) {
+ SkDebugf("Cached image is stored as unsupported type: %d . re-encoding",
+ SkCodec::MakeFromData(imageData)->getEncodedFormat());
+ imageData = image->encodeToData(SkEncodedImageFormat::kPNG, 100);
+ }
+ selectedPrefix = pngDataPrefix;
+ selectedPrefixLength = sizeof(pngDataPrefix);
+ }
+
+ size_t b64Size = SkBase64::Encode(imageData->data(), imageData->size(), nullptr);
+ sk_sp<SkData> dataUri = SkData::MakeUninitialized(selectedPrefixLength + b64Size);
+ char* dest = (char*)dataUri->writable_data();
+ memcpy(dest, selectedPrefix, selectedPrefixLength);
+ SkBase64::Encode(imageData->data(), imageData->size(), dest + selectedPrefixLength - 1);
+ dest[dataUri->size() - 1] = 0;
+ return dataUri;
+}
+
+void SkSVGDevice::AutoElement::addImageShaderResources(const SkShader* shader, const SkPaint& paint,
+ Resources* resources) {
+ SkMatrix outMatrix;
+
+ SkShader::TileMode xy[2];
+ SkImage* image = shader->isAImage(&outMatrix, xy);
+ SkASSERT(image);
+
+ SkString patternDims[2]; // width, height
+
+ sk_sp<SkData> dataUri = AsDataUri(image);
+ if (!dataUri) {
+ SkDebugf("Failed to encode data as data URI.");
+ return;
+ }
+ SkIRect imageSize = image->bounds();
+ for (int i = 0; i < 2; i++) {
+ int imageDimension = i == 0 ? imageSize.width() : imageSize.height();
+ switch (xy[i]) {
+ case SkShader::kRepeat_TileMode:
+ patternDims[i].appendScalar(imageDimension);
+ break;
+ default:
+ patternDims[i] = "100%";
+ SkDebugf("unhandled tilemode for %d th dim : %d", i, xy[i]);
+ }
+ }
+
+ SkString patternID = fResourceBucket->addPattern();
+ {
+ AutoElement pattern("pattern", fWriter);
+ pattern.addAttribute("id", patternID);
+ pattern.addAttribute("patternUnits", "userSpaceOnUse");
+ pattern.addAttribute("patternContentUnits", "userSpaceOnUse");
+ pattern.addAttribute("width", patternDims[0]);
+ pattern.addAttribute("height", patternDims[1]);
+ pattern.addAttribute("x", 0);
+ pattern.addAttribute("y", 0);
+
+ {
+ SkString imageID = fResourceBucket->addImage();
+ AutoElement imageTag("image", fWriter);
+ imageTag.addAttribute("id", imageID);
+ imageTag.addAttribute("x", 0);
+ imageTag.addAttribute("y", 0);
+ imageTag.addAttribute("width", image->width());
+ imageTag.addAttribute("height", image->height());
+ imageTag.addAttribute("xlink:href", static_cast<const char*>(dataUri->data()));
+ }
+ }
+ resources->fPaintServer.printf("url(#%s)", patternID.c_str());
+}
+
+void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resources* resources) {
+ const SkShader* shader = paint.getShader();
+ SkASSERT(shader);
+
+ if (shader->asAGradient(nullptr) != SkShader::kNone_GradientType) {
+ addGradientShaderResources(shader, paint, resources);
+ } else if (shader->isAImage()) {
+ addImageShaderResources(shader, paint, resources);
+ } else {
+ SkDebugf("unsupported shader type\n");
+ }
+}
+
void SkSVGDevice::AutoElement::addClipResources(const MxCp& mc, Resources* resources) {
SkASSERT(!mc.fClipStack->isWideOpen());
@@ -680,8 +822,22 @@ void SkSVGDevice::drawPoints(SkCanvas::PointMode mode, size_t count,
}
void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) {
+ std::unique_ptr<AutoElement> svg;
+ if (RequiresViewportReset(paint)) {
+ svg.reset(new AutoElement("svg", fWriter, fResourceBucket.get(), MxCp(this), paint));
+ svg->addRectAttributes(r);
+ }
+
AutoElement rect("rect", fWriter, fResourceBucket.get(), MxCp(this), paint);
- rect.addRectAttributes(r);
+
+ if (svg) {
+ rect.addAttribute("x", 0);
+ rect.addAttribute("y", 0);
+ rect.addAttribute("width", "100%");
+ rect.addAttribute("height", "100%");
+ } else {
+ rect.addRectAttributes(r);
+ }
}
void SkSVGDevice::drawOval(const SkRect& oval, const SkPaint& paint) {
diff --git a/tests/SVGDeviceTest.cpp b/tests/SVGDeviceTest.cpp
index 7044559d83..bdd832c43b 100644
--- a/tests/SVGDeviceTest.cpp
+++ b/tests/SVGDeviceTest.cpp
@@ -5,23 +5,27 @@
* found in the LICENSE file.
*/
-#include "SkTypes.h"
-
-#ifdef SK_XML
+#include "SkBitmap.h"
#include "SkCanvas.h"
-#include "SkData.h"
#include "SkDOM.h"
+#include "SkData.h"
+#include "SkImage.h"
+#include "SkImageShader.h"
#include "SkParse.h"
-#include "SkStream.h"
#include "SkSVGCanvas.h"
+#include "SkShader.h"
+#include "SkStream.h"
#include "SkXMLWriter.h"
#include "Test.h"
#include <string.h>
+#ifdef SK_XML
+
namespace {
+
void check_text_node(skiatest::Reporter* reporter,
const SkDOM& dom,
const SkDOM::Node* root,
@@ -149,4 +153,179 @@ DEF_TEST(SVGDevice_whitespace_pos, reporter) {
}
}
+
+void SetImageShader(SkPaint* paint, int imageWidth, int imageHeight, SkShader::TileMode xTile,
+ SkShader::TileMode yTile) {
+ auto surface = SkSurface::MakeRasterN32Premul(imageWidth, imageHeight);
+ paint->setShader(SkImageShader::Make(surface->makeImageSnapshot(), xTile, yTile, nullptr));
+}
+
+// Attempt to find the three nodes on which we have expectations:
+// the pattern node, the image within that pattern, and the rect which
+// uses the pattern as a fill.
+// returns false if not all nodes are found.
+bool FindImageShaderNodes(skiatest::Reporter* reporter, const SkDOM* dom, const SkDOM::Node* root,
+ const SkDOM::Node** patternOut, const SkDOM::Node** imageOut,
+ const SkDOM::Node** rectOut) {
+ if (root == nullptr || dom == nullptr) {
+ ERRORF(reporter, "root element not found");
+ return false;
+ }
+
+
+ const SkDOM::Node* rect = dom->getFirstChild(root, "rect");
+ if (rect == nullptr) {
+ ERRORF(reporter, "rect not found");
+ return false;
+ }
+ *rectOut = rect;
+
+ const SkDOM::Node* defs = dom->getFirstChild(root, "defs");
+ if (defs == nullptr) {
+ ERRORF(reporter, "defs not found");
+ return false;
+ }
+
+ const SkDOM::Node* pattern = dom->getFirstChild(defs, "pattern");
+ if (pattern == nullptr) {
+ ERRORF(reporter, "pattern not found");
+ return false;
+ }
+ *patternOut = pattern;
+
+ const SkDOM::Node* image = dom->getFirstChild(pattern, "image");
+ if (image == nullptr) {
+ ERRORF(reporter, "image not found");
+ return false;
+ }
+ *imageOut = image;
+
+ return true;
+}
+
+void ImageShaderTestSetup(SkDOM* dom, SkPaint* paint, int imageWidth, int imageHeight,
+ int rectWidth, int rectHeight, SkShader::TileMode xTile,
+ SkShader::TileMode yTile) {
+ SetImageShader(paint, imageWidth, imageHeight, xTile, yTile);
+ SkXMLParserWriter writer(dom->beginParsing());
+ std::unique_ptr<SkCanvas> svgCanvas = SkSVGCanvas::Make(SkRect::MakeWH(100, 100), &writer);
+
+ SkRect bounds{0, 0, SkIntToScalar(rectWidth), SkIntToScalar(rectHeight)};
+ svgCanvas->drawRect(bounds, *paint);
+}
+
+
+DEF_TEST(SVGDevice_image_shader_norepeat, reporter) {
+ SkDOM dom;
+ SkPaint paint;
+ int imageWidth = 3, imageHeight = 3;
+ int rectWidth = 10, rectHeight = 10;
+ ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
+ SkShader::kClamp_TileMode, SkShader::kClamp_TileMode);
+
+ const SkDOM::Node* root = dom.finishParsing();
+
+ const SkDOM::Node *patternNode, *imageNode, *rectNode;
+ bool structureAppropriate =
+ FindImageShaderNodes(reporter, &dom, root, &patternNode, &imageNode, &rectNode);
+ REPORTER_ASSERT(reporter, structureAppropriate);
+
+ // the image should always maintain its size.
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
+
+ // making the pattern as large as the container prevents
+ // it from repeating.
+ REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "width"), "100%") == 0);
+ REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "height"), "100%") == 0);
+}
+
+DEF_TEST(SVGDevice_image_shader_tilex, reporter) {
+ SkDOM dom;
+ SkPaint paint;
+ int imageWidth = 3, imageHeight = 3;
+ int rectWidth = 10, rectHeight = 10;
+ ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
+ SkShader::kRepeat_TileMode, SkShader::kClamp_TileMode);
+
+ const SkDOM::Node* root = dom.finishParsing();
+ const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
+ if (innerSvg == nullptr) {
+ ERRORF(reporter, "inner svg element not found");
+ return;
+ }
+
+ const SkDOM::Node *patternNode, *imageNode, *rectNode;
+ bool structureAppropriate =
+ FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
+ REPORTER_ASSERT(reporter, structureAppropriate);
+
+ // the imageNode should always maintain its size.
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
+
+ // if the patternNode width matches the imageNode width,
+ // it will repeat in along the x axis.
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "width")) == imageWidth);
+ REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "height"), "100%") == 0);
+}
+
+DEF_TEST(SVGDevice_image_shader_tiley, reporter) {
+ SkDOM dom;
+ SkPaint paint;
+ int imageNodeWidth = 3, imageNodeHeight = 3;
+ int rectNodeWidth = 10, rectNodeHeight = 10;
+ ImageShaderTestSetup(&dom, &paint, imageNodeWidth, imageNodeHeight, rectNodeWidth,
+ rectNodeHeight, SkShader::kClamp_TileMode, SkShader::kRepeat_TileMode);
+
+ const SkDOM::Node* root = dom.finishParsing();
+ const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
+ if (innerSvg == nullptr) {
+ ERRORF(reporter, "inner svg element not found");
+ return;
+ }
+
+ const SkDOM::Node *patternNode, *imageNode, *rectNode;
+ bool structureAppropriate =
+ FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
+ REPORTER_ASSERT(reporter, structureAppropriate);
+
+ // the imageNode should always maintain its size.
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageNodeWidth);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageNodeHeight);
+
+ // making the patternNode as large as the container prevents
+ // it from repeating.
+ REPORTER_ASSERT(reporter, strcmp(dom.findAttr(patternNode, "width"), "100%") == 0);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "height")) == imageNodeHeight);
+}
+
+DEF_TEST(SVGDevice_image_shader_tileboth, reporter) {
+ SkDOM dom;
+ SkPaint paint;
+ int imageWidth = 3, imageHeight = 3;
+ int rectWidth = 10, rectHeight = 10;
+ ImageShaderTestSetup(&dom, &paint, imageWidth, imageHeight, rectWidth, rectHeight,
+ SkShader::kRepeat_TileMode, SkShader::kRepeat_TileMode);
+
+ const SkDOM::Node* root = dom.finishParsing();
+
+ const SkDOM::Node *patternNode, *imageNode, *rectNode;
+ const SkDOM::Node* innerSvg = dom.getFirstChild(root, "svg");
+ if (innerSvg == nullptr) {
+ ERRORF(reporter, "inner svg element not found");
+ return;
+ }
+ bool structureAppropriate =
+ FindImageShaderNodes(reporter, &dom, innerSvg, &patternNode, &imageNode, &rectNode);
+ REPORTER_ASSERT(reporter, structureAppropriate);
+
+ // the imageNode should always maintain its size.
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "width")) == imageWidth);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(imageNode, "height")) == imageHeight);
+
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "width")) == imageWidth);
+ REPORTER_ASSERT(reporter, atoi(dom.findAttr(patternNode, "height")) == imageHeight);
+}
+
#endif