diff options
author | Alexander Midlash <amidlash@amazon.com> | 2018-03-06 17:21:28 -0800 |
---|---|---|
committer | Skia Commit-Bot <skia-commit-bot@chromium.org> | 2018-03-07 02:25:30 +0000 |
commit | 77e3afc908f5f1e79180beb558b163fcf1cdf8ac (patch) | |
tree | cee37923028b99c7a83c00a9eea9e7dda9d98b24 | |
parent | c5cf762177c5708a5efa39f95b086c751e03e2e4 (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.cpp | 168 | ||||
-rw-r--r-- | tests/SVGDeviceTest.cpp | 189 |
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 |