| /* | |
| * Copyright 2015 Google Inc. | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "src/svg/SkSVGDevice.h" | |
| #include "include/core/SkBitmap.h" | |
| #include "include/core/SkBlendMode.h" | |
| #include "include/core/SkClipOp.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkColorFilter.h" | |
| #include "include/core/SkData.h" | |
| #include "include/core/SkDataTable.h" | |
| #include "include/core/SkFont.h" | |
| #include "include/core/SkFontStyle.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkImageInfo.h" | |
| #include "include/core/SkMatrix.h" | |
| #include "include/core/SkPaint.h" | |
| #include "include/core/SkPath.h" | |
| #include "include/core/SkPathBuilder.h" | |
| #include "include/core/SkPathEffect.h" | |
| #include "include/core/SkPathTypes.h" | |
| #include "include/core/SkPathUtils.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkRRect.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/core/SkShader.h" | |
| #include "include/core/SkSize.h" | |
| #include "include/core/SkSpan.h" | |
| #include "include/core/SkStream.h" | |
| #include "include/core/SkString.h" | |
| #include "include/core/SkSurfaceProps.h" | |
| #include "include/core/SkTileMode.h" | |
| #include "include/core/SkTypeface.h" | |
| #include "include/private/SkDebug.h" | |
| #include "include/private/SkNoncopyable.h" | |
| #include "include/private/SkTPin.h" | |
| #include "include/private/SkTemplates.h" | |
| #include "include/private/SkTo.h" | |
| #include "include/svg/SkSVGCanvas.h" | |
| #include "src/core/SkAnnotationKeys.h" | |
| #include "src/core/SkBase64.h" | |
| #include "src/core/SkClipStack.h" | |
| #include "src/core/SkDevice.h" | |
| #include "src/core/SkFontPriv.h" | |
| #include "src/core/SkTHash.h" | |
| #include "src/core/SkTLazy.h" | |
| #include "src/image/SkImage_Base.h" | |
| #include "src/shaders/SkColorShader.h" | |
| #include "src/shaders/SkShaderBase.h" | |
| #include "src/text/GlyphRun.h" | |
| #include "src/xml/SkXMLWriter.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cstring> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| using namespace skia_private; | |
| class SkBlender; | |
| class SkMesh; | |
| class SkVertices; | |
| struct SkSamplingOptions; | |
| namespace { | |
| static SkString svg_color(SkColor color) { | |
| // https://www.w3.org/TR/css-color-3/#html4 | |
| auto named_color = [](SkColor c) -> const char* { | |
| switch (c & 0xffffff) { | |
| case 0x000000: return "black"; | |
| case 0x000080: return "navy"; | |
| case 0x0000ff: return "blue"; | |
| case 0x008000: return "green"; | |
| case 0x008080: return "teal"; | |
| case 0x00ff00: return "lime"; | |
| case 0x00ffff: return "aqua"; | |
| case 0x800000: return "maroon"; | |
| case 0x800080: return "purple"; | |
| case 0x808000: return "olive"; | |
| case 0x808080: return "gray"; | |
| case 0xc0c0c0: return "silver"; | |
| case 0xff0000: return "red"; | |
| case 0xff00ff: return "fuchsia"; | |
| case 0xffff00: return "yellow"; | |
| case 0xffffff: return "white"; | |
| default: break; | |
| } | |
| return nullptr; | |
| }; | |
| if (const auto* nc = named_color(color)) { | |
| return SkString(nc); | |
| } | |
| uint8_t r = SkColorGetR(color); | |
| uint8_t g = SkColorGetG(color); | |
| uint8_t b = SkColorGetB(color); | |
| // Some users care about every byte here, so we'll use hex colors with single-digit channels | |
| // when possible. | |
| uint8_t rh = r >> 4; | |
| uint8_t rl = r & 0xf; | |
| uint8_t gh = g >> 4; | |
| uint8_t gl = g & 0xf; | |
| uint8_t bh = b >> 4; | |
| uint8_t bl = b & 0xf; | |
| if ((rh == rl) && (gh == gl) && (bh == bl)) { | |
| return SkStringPrintf("#%1X%1X%1X", rh, gh, bh); | |
| } | |
| return SkStringPrintf("#%02X%02X%02X", r, g, b); | |
| } | |
| static SkScalar svg_opacity(SkColor color) { | |
| return SkIntToScalar(SkColorGetA(color)) / SK_AlphaOPAQUE; | |
| } | |
| // Keep in sync with SkPaint::Cap | |
| static constexpr auto cap_map = std::to_array<const char*>({ | |
| nullptr, // kButt_Cap (default) | |
| "round", // kRound_Cap | |
| "square", // kSquare_Cap | |
| }); | |
| static_assert(std::size(cap_map) == SkPaint::kCapCount, "missing_cap_map_entry"); | |
| static const char* svg_cap(SkPaint::Cap cap) { | |
| SkASSERT(static_cast<size_t>(cap) < std::size(cap_map)); | |
| return cap_map[cap]; | |
| } | |
| // Keep in sync with SkPaint::Join | |
| static constexpr auto join_map = std::to_array<const char*>({ | |
| nullptr, // kMiter_Join (default) | |
| "round", // kRound_Join | |
| "bevel", // kBevel_Join | |
| }); | |
| static_assert(std::size(join_map) == SkPaint::kJoinCount, "missing_join_map_entry"); | |
| static const char* svg_join(SkPaint::Join join) { | |
| SkASSERT(join < std::size(join_map)); | |
| return join_map[join]; | |
| } | |
| static SkString svg_transform(const SkMatrix& t) { | |
| SkASSERT(!t.isIdentity()); | |
| SkString tstr; | |
| switch (t.getType()) { | |
| case SkMatrix::kPerspective_Mask: | |
| // TODO: handle perspective matrices? | |
| break; | |
| case SkMatrix::kTranslate_Mask: | |
| tstr.printf("translate(%g %g)", t.getTranslateX(), t.getTranslateY()); | |
| break; | |
| case SkMatrix::kScale_Mask: | |
| tstr.printf("scale(%g %g)", t.getScaleX(), t.getScaleY()); | |
| break; | |
| default: | |
| // http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined | |
| // | a c e | | |
| // | b d f | | |
| // | 0 0 1 | | |
| tstr.printf("matrix(%g %g %g %g %g %g)", | |
| t.getScaleX(), t.getSkewY(), | |
| t.getSkewX(), t.getScaleY(), | |
| t.getTranslateX(), t.getTranslateY()); | |
| break; | |
| } | |
| return tstr; | |
| } | |
| struct Resources { | |
| Resources(const SkPaint& paint) | |
| : fPaintServer(svg_color(paint.getColor())) {} | |
| SkString fPaintServer; | |
| SkString fColorFilter; | |
| }; | |
| // 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; | |
| SkTileMode xy[2]; | |
| SkImage* image = shader->isAImage(nullptr, xy); | |
| if (!image) | |
| return false; | |
| for (int i = 0; i < 2; i++) { | |
| if (xy[i] == SkTileMode::kRepeat) | |
| return true; | |
| } | |
| return false; | |
| } | |
| void AddPath(const sktext::GlyphRun& glyphRun, const SkPoint& offset, SkPathBuilder* builder) { | |
| struct Rec { | |
| SkPathBuilder* fBuilder; | |
| const SkPoint fOffset; | |
| const SkPoint* fPos; | |
| } rec = { builder, offset, glyphRun.positions().data() }; | |
| glyphRun.font().getPaths(glyphRun.glyphsIDs(), | |
| [](const SkPath* path, const SkMatrix& mx, void* ctx) { | |
| Rec* rec = reinterpret_cast<Rec*>(ctx); | |
| if (path) { | |
| SkMatrix total = mx; | |
| total.postTranslate(rec->fPos->fX + rec->fOffset.fX, | |
| rec->fPos->fY + rec->fOffset.fY); | |
| rec->fBuilder->addPath(*path, total); | |
| } else { | |
| // TODO: this is going to drop color emojis. | |
| } | |
| rec->fPos += 1; // move to the next glyph's position | |
| }, &rec); | |
| } | |
| } // 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) | |
| , fPathCount(0) | |
| , fImageCount(0) | |
| , fPatternCount(0) | |
| , fColorFilterCount(0) {} | |
| SkString addGradient() { | |
| return SkStringPrintf("gradient_%u", fGradientCount++); | |
| } | |
| SkString addPath() { | |
| return SkStringPrintf("path_%u", fPathCount++); | |
| } | |
| SkString addImage() { | |
| return SkStringPrintf("img_%u", fImageCount++); | |
| } | |
| SkString addColorFilter() { return SkStringPrintf("cfilter_%u", fColorFilterCount++); } | |
| SkString addPattern() { | |
| return SkStringPrintf("pattern_%u", fPatternCount++); | |
| } | |
| private: | |
| uint32_t fGradientCount; | |
| uint32_t fPathCount; | |
| uint32_t fImageCount; | |
| uint32_t fPatternCount; | |
| uint32_t fColorFilterCount; | |
| }; | |
| struct SkSVGDevice::MxCp { | |
| const SkMatrix* fMatrix; | |
| const SkClipStack* fClipStack; | |
| MxCp(const SkMatrix* mx, const SkClipStack* cs) : fMatrix(mx), fClipStack(cs) {} | |
| MxCp(SkSVGDevice* device) : fMatrix(&device->localToDevice()), fClipStack(&device->cs()) {} | |
| }; | |
| class SkSVGDevice::AutoElement : ::SkNoncopyable { | |
| public: | |
| AutoElement(const char name[], const AutoElement& parent) | |
| : fWriter(parent.fWriter) | |
| , fPngEncoder(parent.fPngEncoder) | |
| , fResourceBucket(nullptr) { | |
| fWriter->startElement(name); | |
| } | |
| AutoElement(const char name[], SkSVGDevice* svgdev) | |
| : fWriter(svgdev->fWriter.get()) | |
| , fPngEncoder(svgdev->fOpts.pngEncoder) | |
| , fResourceBucket(nullptr) { | |
| fWriter->startElement(name); | |
| } | |
| AutoElement(const char name[], SkSVGDevice* svgdev, | |
| ResourceBucket* bucket, const MxCp& mc, const SkPaint& paint) | |
| : fWriter(svgdev->fWriter.get()) | |
| , fPngEncoder(svgdev->fOpts.pngEncoder) | |
| , fResourceBucket(bucket) { | |
| svgdev->syncClipStack(*mc.fClipStack); | |
| Resources res = this->addResources(mc, paint); | |
| fWriter->startElement(name); | |
| this->addPaint(paint, res); | |
| if (!mc.fMatrix->isIdentity()) { | |
| this->addAttribute("transform", svg_transform(*mc.fMatrix)); | |
| } | |
| } | |
| ~AutoElement() { | |
| fWriter->endElement(); | |
| } | |
| void addAttribute(const char name[], const char val[]) { | |
| fWriter->addAttribute(name, val); | |
| } | |
| void addAttribute(const char name[], const SkString& val) { | |
| fWriter->addAttribute(name, val.c_str()); | |
| } | |
| void addAttribute(const char name[], int32_t val) { | |
| fWriter->addS32Attribute(name, val); | |
| } | |
| void addAttribute(const char name[], SkScalar val) { | |
| fWriter->addScalarAttribute(name, val); | |
| } | |
| void addText(const SkString& text) { | |
| fWriter->addText(text.c_str(), text.size()); | |
| } | |
| void addRectAttributes(const SkRect&); | |
| void addPathAttributes(const SkPath&, SkParsePath::PathEncoding); | |
| void addTextAttributes(const SkFont&); | |
| private: | |
| Resources addResources(const MxCp&, const SkPaint& paint); | |
| void addShaderResources(const SkPaint& paint, Resources* resources); | |
| void addGradientShaderResources(const SkShader* shader, const SkPaint& paint, | |
| Resources* resources); | |
| void addColorFilterResources(const SkColorFilter& cf, 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 addGradientDef(SkShaderBase::GradientType, | |
| const SkShaderBase::GradientInfo& info, | |
| const SkShader* shader, | |
| const SkMatrix& localMatrix); | |
| SkXMLWriter* fWriter = nullptr; | |
| SkSVGCanvas::EncodePngCallback fPngEncoder = nullptr; | |
| ResourceBucket* fResourceBucket = nullptr; | |
| }; | |
| void SkSVGDevice::AutoElement::addPaint(const SkPaint& paint, const Resources& resources) { | |
| // Path effects are applied to all vector graphics (rects, rrects, ovals, | |
| // paths etc). This should only happen when a path effect is attached to | |
| // non-vector graphics (text, image) or a new vector graphics primitive is | |
| //added that is not handled by base drawPath() routine. | |
| if (paint.getPathEffect() != nullptr) { | |
| SkDebugf("Unsupported path effect in addPaint."); | |
| } | |
| SkPaint::Style style = paint.getStyle(); | |
| if (style == SkPaint::kFill_Style || style == SkPaint::kStrokeAndFill_Style) { | |
| static constexpr char kDefaultFill[] = "black"; | |
| if (!resources.fPaintServer.equals(kDefaultFill)) { | |
| this->addAttribute("fill", resources.fPaintServer); | |
| } | |
| if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) { | |
| this->addAttribute("fill-opacity", svg_opacity(paint.getColor())); | |
| } | |
| } else { | |
| SkASSERT(style == SkPaint::kStroke_Style); | |
| this->addAttribute("fill", "none"); | |
| } | |
| if (!resources.fColorFilter.isEmpty()) { | |
| this->addAttribute("filter", resources.fColorFilter.c_str()); | |
| } | |
| if (style == SkPaint::kStroke_Style || style == SkPaint::kStrokeAndFill_Style) { | |
| this->addAttribute("stroke", resources.fPaintServer); | |
| SkScalar strokeWidth = paint.getStrokeWidth(); | |
| if (strokeWidth == 0) { | |
| // Hairline stroke | |
| strokeWidth = 1; | |
| this->addAttribute("vector-effect", "non-scaling-stroke"); | |
| } | |
| this->addAttribute("stroke-width", strokeWidth); | |
| if (const char* cap = svg_cap(paint.getStrokeCap())) { | |
| this->addAttribute("stroke-linecap", cap); | |
| } | |
| if (const char* join = svg_join(paint.getStrokeJoin())) { | |
| this->addAttribute("stroke-linejoin", join); | |
| } | |
| if (paint.getStrokeJoin() == SkPaint::kMiter_Join) { | |
| this->addAttribute("stroke-miterlimit", paint.getStrokeMiter()); | |
| } | |
| if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) { | |
| this->addAttribute("stroke-opacity", svg_opacity(paint.getColor())); | |
| } | |
| } else { | |
| SkASSERT(style == SkPaint::kFill_Style); | |
| // SVG default stroke value is "none". | |
| } | |
| } | |
| Resources SkSVGDevice::AutoElement::addResources(const MxCp& mc, const SkPaint& paint) { | |
| Resources resources(paint); | |
| if (paint.getShader()) { | |
| AutoElement defs("defs", *this); | |
| this->addShaderResources(paint, &resources); | |
| } | |
| if (const SkColorFilter* cf = paint.getColorFilter()) { | |
| this->addColorFilterResources(*cf, &resources); | |
| } | |
| return resources; | |
| } | |
| void SkSVGDevice::AutoElement::addGradientShaderResources(const SkShader* shader, | |
| const SkPaint& paint, | |
| Resources* resources) { | |
| SkASSERT(shader); | |
| SkShaderBase::GradientInfo grInfo; | |
| const auto gradient_type = as_SB(shader)->asGradient(&grInfo); | |
| if (gradient_type != SkShaderBase::GradientType::kLinear && | |
| gradient_type != SkShaderBase::GradientType::kRadial && | |
| gradient_type != SkShaderBase::GradientType::kConical) { | |
| return; | |
| } | |
| AutoSTArray<16, SkColor4f> grColors(grInfo.fColorCount); | |
| AutoSTArray<16, SkScalar> grOffsets(grInfo.fColorCount); | |
| grInfo.fColors = grColors.get(); | |
| grInfo.fColorOffsets = grOffsets.get(); | |
| // One more call to get the actual colors/offsets and local matrix. | |
| SkMatrix localMatrix; | |
| as_SB(shader)->asGradient(&grInfo, &localMatrix); | |
| SkASSERT(grInfo.fColorCount <= grColors.count()); | |
| SkASSERT(grInfo.fColorCount <= grOffsets.count()); | |
| SkASSERT(grColors.size() > 0); | |
| resources->fPaintServer = SkStringPrintf("url(#%s)", | |
| addGradientDef(gradient_type, grInfo, shader, localMatrix).c_str()); | |
| } | |
| void SkSVGDevice::AutoElement::addColorFilterResources(const SkColorFilter& cf, | |
| Resources* resources) { | |
| SkColor filterColor; | |
| SkBlendMode bm; | |
| if (!cf.asAColorMode(&filterColor, &bm)) { | |
| // Only SkColorFilters::Blend is supported currently. | |
| return; | |
| } | |
| // Optional final filter effect stage in the chain, used to combine the result of feFlood | |
| // with the source. Different methods are used for different blend modes. | |
| enum CompositeMode { kNone, kComposite, kBlend }; | |
| struct BlendModeInfo { | |
| SkBlendMode bm; | |
| CompositeMode mode; | |
| const char* op = nullptr; // operator (feComposite) or mode (feBlend) attribute. | |
| bool swap_inputs = false; // whether the flood/sourcegraphic inputs are swapped | |
| std::optional<std::array<float, 4>> coeffs = std::nullopt; // arithmetic coefficients | |
| }; | |
| static constexpr BlendModeInfo gBlendModeInfoMap[] = { | |
| // kSrc does not have a feComposite/feBlend operator mapping, but we can achieve the same | |
| // result by skipping the final stage. | |
| { SkBlendMode::kSrc, kNone }, | |
| // Standard feComposite modes | |
| { SkBlendMode::kSrcOver, kComposite, "over" }, | |
| { SkBlendMode::kSrcIn , kComposite, "in" }, | |
| { SkBlendMode::kSrcOut , kComposite, "out" }, | |
| { SkBlendMode::kSrcATop, kComposite, "atop" }, | |
| { SkBlendMode::kDstOver, kComposite, "over", true }, | |
| { SkBlendMode::kDstIn , kComposite, "in" , true }, | |
| { SkBlendMode::kDstOut , kComposite, "out" , true }, | |
| { SkBlendMode::kDstATop, kComposite, "atop", true }, | |
| { SkBlendMode::kXor , kComposite, "xor" }, | |
| // Arithmetic feComposite modes | |
| // kPlus = s + d => K2 = 1 (in/flood), K3 = 1 (in2/SourceGraphic) | |
| { SkBlendMode::kPlus, kComposite, "arithmetic", false, {{0, 1, 1, 0}} }, | |
| // kModulate = s * d => K1 = 1 (in * in2) | |
| { SkBlendMode::kModulate, kComposite, "arithmetic", false, {{1, 0, 0, 0}} }, | |
| // Advanced feBlend modes (Correctly mapping kLighten and others!) | |
| { SkBlendMode::kMultiply, kBlend, "multiply" }, | |
| { SkBlendMode::kScreen, kBlend, "screen" }, | |
| { SkBlendMode::kDarken, kBlend, "darken" }, | |
| { SkBlendMode::kLighten, kBlend, "lighten" }, | |
| { SkBlendMode::kOverlay, kBlend, "overlay" }, | |
| { SkBlendMode::kColorDodge, kBlend, "color-dodge" }, | |
| { SkBlendMode::kColorBurn, kBlend, "color-burn" }, | |
| { SkBlendMode::kHardLight, kBlend, "hard-light" }, | |
| { SkBlendMode::kSoftLight, kBlend, "soft-light" }, | |
| { SkBlendMode::kDifference, kBlend, "difference" }, | |
| { SkBlendMode::kExclusion, kBlend, "exclusion" }, | |
| { SkBlendMode::kHue, kBlend, "hue" }, | |
| { SkBlendMode::kSaturation, kBlend, "saturation" }, | |
| { SkBlendMode::kColor, kBlend, "color" }, | |
| { SkBlendMode::kLuminosity, kBlend, "luminosity" }, | |
| }; | |
| const auto bm_info = std::ranges::find(gBlendModeInfoMap, bm, &BlendModeInfo::bm); | |
| if (bm_info == std::end(gBlendModeInfoMap)) { | |
| // Unsupported blend mode. | |
| return; | |
| } | |
| SkString colorfilterID = fResourceBucket->addColorFilter(); | |
| { | |
| AutoElement filterElement("filter", *this); | |
| filterElement.addAttribute("id", colorfilterID); | |
| filterElement.addAttribute("x", "0%"); | |
| filterElement.addAttribute("y", "0%"); | |
| filterElement.addAttribute("width", "100%"); | |
| filterElement.addAttribute("height", "100%"); | |
| // 1. flood to generate the filter color. | |
| { | |
| AutoElement floodElement("feFlood", *this); | |
| floodElement.addAttribute("flood-color", svg_color(filterColor)); | |
| floodElement.addAttribute("flood-opacity", svg_opacity(filterColor)); | |
| floodElement.addAttribute("result", "flood"); | |
| } | |
| // 2. optional compositing stage (feComposite or feBlend depending on blend mode). | |
| { | |
| std::optional<AutoElement> compositeElement; | |
| switch (bm_info->mode) { | |
| case kComposite: | |
| compositeElement.emplace("feComposite", *this); | |
| compositeElement->addAttribute("operator", bm_info->op); | |
| if (bm_info->coeffs) { | |
| compositeElement->addAttribute("k1", (*bm_info->coeffs)[0]); | |
| compositeElement->addAttribute("k2", (*bm_info->coeffs)[1]); | |
| compositeElement->addAttribute("k3", (*bm_info->coeffs)[2]); | |
| compositeElement->addAttribute("k4", (*bm_info->coeffs)[3]); | |
| } | |
| break; | |
| case kBlend: | |
| compositeElement.emplace("feBlend", *this); | |
| compositeElement->addAttribute("mode", bm_info->op); | |
| break; | |
| case kNone: | |
| break; | |
| } | |
| if (compositeElement) { | |
| // Normally we composite the flood result against source, but we swap the order | |
| // for inverted (kDst*) modes. | |
| compositeElement->addAttribute("in", bm_info->swap_inputs ? "SourceGraphic" | |
| : "flood"); | |
| compositeElement->addAttribute("in2", bm_info->swap_inputs ? "flood" | |
| : "SourceGraphic"); | |
| } | |
| } | |
| // 3. optional alpha mask stage to avoid flooding the whole filter area. | |
| SkBlendModeCoeff srcCoeff; | |
| const bool needs_alpha_mask = | |
| // All advanced/non-Porter-Duff modes can leak when dst is transparent. | |
| !SkBlendMode_AsCoeff(bm, &srcCoeff, nullptr) || | |
| // For Porter-Duff, dst stays transparent if the source term is | |
| // multiplied by destination alpha (kDA) or zero (kZero). | |
| (srcCoeff != SkBlendModeCoeff::kDA && srcCoeff != SkBlendModeCoeff::kZero); | |
| if (needs_alpha_mask) { | |
| AutoElement maskElement("feComposite", *this); | |
| // "in" defaults to the preceding stage, either feFlood or feComposite/feBlend. | |
| maskElement.addAttribute("in2", "SourceAlpha"); | |
| maskElement.addAttribute("operator", "in"); | |
| } | |
| } | |
| resources->fColorFilter.printf("url(#%s)", colorfilterID.c_str()); | |
| } | |
| static bool is_png(const void* bytes, size_t length) { | |
| static constexpr uint8_t pngSig[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; | |
| return length >= sizeof(pngSig) && !memcmp(bytes, pngSig, sizeof(pngSig)); | |
| } | |
| static bool is_jpeg(const void* bytes, size_t length) { | |
| static constexpr uint8_t jpegSig[] = {0xFF, 0xD8, 0xFF}; | |
| return length >= sizeof(jpegSig) && !memcmp(bytes, jpegSig, sizeof(jpegSig)); | |
| } | |
| static sk_sp<SkData> EncodePng(const SkPixmap& pixmap, SkSVGCanvas::EncodePngCallback pngEncoder) { | |
| SkDynamicMemoryWStream stream; | |
| if (!pngEncoder(&stream, pixmap)) { | |
| return nullptr; | |
| } | |
| return stream.detachAsData(); | |
| } | |
| static sk_sp<SkData> EncodePng(SkImage* image, SkSVGCanvas::EncodePngCallback pngEncoder) { | |
| // GrDirectContext is nullptr because we shouldn't have any texture-based images passed in. | |
| sk_sp<SkImage> rasterImage = image->makeRasterImage(nullptr); | |
| if (!rasterImage) { | |
| return nullptr; | |
| } | |
| SkPixmap pixmap; | |
| if (!rasterImage->peekPixels(&pixmap)) { | |
| return nullptr; | |
| } | |
| return EncodePng(pixmap, pngEncoder); | |
| } | |
| // Returns data uri from bytes. | |
| // it will use any cached data if available, otherwise will | |
| // encode as png. | |
| sk_sp<SkData> AsDataUri(SkImage* image, SkSVGCanvas::EncodePngCallback pngEncoder) { | |
| static constexpr char jpgDataPrefix[] = "data:image/jpeg;base64,"; | |
| static constexpr char pngDataPrefix[] = "data:image/png;base64,"; | |
| SkASSERT(!image->isTextureBacked()); | |
| const char* selectedPrefix = pngDataPrefix; | |
| size_t selectedPrefixLength = sizeof(pngDataPrefix); | |
| sk_sp<const SkData> imageData = image->refEncodedData(); | |
| if (imageData) { // Already encoded as something | |
| if (is_jpeg(imageData->data(), imageData->size())) { | |
| selectedPrefix = jpgDataPrefix; | |
| selectedPrefixLength = sizeof(jpgDataPrefix); | |
| } else if (!is_png(imageData->data(), imageData->size())) { | |
| // re-encode the image as a PNG. | |
| imageData = EncodePng(image, pngEncoder); | |
| if (!imageData) { | |
| return nullptr; | |
| } | |
| } | |
| // else, it's already encoded as a PNG - we don't need to do anything. | |
| } else { | |
| // It was not encoded as something, so we need to encode it as a PNG. | |
| imageData = EncodePng(image, pngEncoder); | |
| if (!imageData) { | |
| return nullptr; | |
| } | |
| } | |
| size_t b64Size = SkBase64::EncodedSize(imageData->size()); | |
| 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; | |
| SkTileMode xy[2]; | |
| SkImage* image = shader->isAImage(&outMatrix, xy); | |
| SkASSERT(image); | |
| std::array<SkString, 2> patternDims; // width, height | |
| sk_sp<SkData> dataUri = AsDataUri(image, fPngEncoder); | |
| if (!dataUri) { | |
| return; | |
| } | |
| SkIRect imageSize = image->bounds(); | |
| for (int i = 0; i < 2; i++) { | |
| int imageDimension = i == 0 ? imageSize.width() : imageSize.height(); | |
| switch (xy[i]) { | |
| case SkTileMode::kRepeat: | |
| patternDims[i].appendScalar(imageDimension); | |
| break; | |
| default: | |
| // TODO: other tile modes? | |
| patternDims[i] = "100%"; | |
| } | |
| } | |
| SkString patternID = fResourceBucket->addPattern(); | |
| { | |
| AutoElement pattern("pattern", *this); | |
| 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", *this); | |
| 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 (as_SB(shader)->type() == SkShaderBase::ShaderType::kColor) { | |
| auto colorShader = static_cast<const SkColorShader*>(shader); | |
| resources->fPaintServer = svg_color(colorShader->color().toSkColor()); | |
| } else | |
| // Checking the shader type is too restrictive, as it rejects local matrix wrappers. | |
| // Wrappers do forward asGradient() though, so we can reach nested gradients this way. | |
| if (as_SB(shader)->asGradient() != SkShaderBase::GradientType::kNone) { | |
| this->addGradientShaderResources(shader, paint, resources); | |
| } else | |
| if (shader->isAImage()) { | |
| this->addImageShaderResources(shader, paint, resources); | |
| } | |
| // TODO: other shader types? | |
| } | |
| SkString SkSVGDevice::AutoElement::addGradientDef(SkShaderBase::GradientType type, | |
| const SkShaderBase::GradientInfo& info, | |
| const SkShader* shader, | |
| const SkMatrix& localMatrix) { | |
| SkASSERT(fResourceBucket); | |
| SkString id = fResourceBucket->addGradient(); | |
| SkASSERT(type == SkShaderBase::GradientType::kLinear || | |
| type == SkShaderBase::GradientType::kRadial || | |
| type == SkShaderBase::GradientType::kConical); | |
| const char* elem_name = type == SkShaderBase::GradientType::kLinear ? "linearGradient" | |
| : "radialGradient"; | |
| { | |
| AutoElement gradient(elem_name, *this); | |
| gradient.addAttribute("id", id); | |
| gradient.addAttribute("gradientUnits", "userSpaceOnUse"); | |
| switch (type) { | |
| case SkShaderBase::GradientType::kLinear: | |
| gradient.addAttribute("x1", info.fPoint[0].x()); | |
| gradient.addAttribute("y1", info.fPoint[0].y()); | |
| gradient.addAttribute("x2", info.fPoint[1].x()); | |
| gradient.addAttribute("y2", info.fPoint[1].y()); | |
| break; | |
| case SkShaderBase::GradientType::kConical: | |
| gradient.addAttribute("fx", info.fPoint[1].x()); | |
| gradient.addAttribute("fy", info.fPoint[1].y()); | |
| gradient.addAttribute("fr", info.fRadius[1]); | |
| [[fallthrough]]; | |
| case SkShaderBase::GradientType::kRadial: | |
| gradient.addAttribute("cx", info.fPoint[0].x()); | |
| gradient.addAttribute("cy", info.fPoint[0].y()); | |
| gradient.addAttribute("r" , info.fRadius[0]); | |
| break; | |
| default: | |
| SkUNREACHABLE; | |
| } | |
| if (!localMatrix.isIdentity()) { | |
| this->addAttribute("gradientTransform", svg_transform(localMatrix)); | |
| } | |
| SkASSERT(info.fColorCount >= 2); | |
| for (int i = 0; i < info.fColorCount; ++i) { | |
| SkColor color = info.fColors[i].toSkColor(); | |
| SkString colorStr(svg_color(color)); | |
| { | |
| AutoElement stop("stop", *this); | |
| stop.addAttribute("offset", info.fColorOffsets[i]); | |
| stop.addAttribute("stop-color", colorStr.c_str()); | |
| if (SK_AlphaOPAQUE != SkColorGetA(color)) { | |
| stop.addAttribute("stop-opacity", svg_opacity(color)); | |
| } | |
| } | |
| } | |
| } | |
| return id; | |
| } | |
| void SkSVGDevice::AutoElement::addRectAttributes(const SkRect& rect) { | |
| // x, y default to 0 | |
| if (rect.x() != 0) { | |
| this->addAttribute("x", rect.x()); | |
| } | |
| if (rect.y() != 0) { | |
| this->addAttribute("y", rect.y()); | |
| } | |
| this->addAttribute("width", rect.width()); | |
| this->addAttribute("height", rect.height()); | |
| } | |
| void SkSVGDevice::AutoElement::addPathAttributes(const SkPath& path, | |
| SkParsePath::PathEncoding encoding) { | |
| this->addAttribute("d", SkParsePath::ToSVGString(path, encoding)); | |
| } | |
| void SkSVGDevice::AutoElement::addTextAttributes(const SkFont& font) { | |
| this->addAttribute("font-size", font.getSize()); | |
| SkString familyName; | |
| THashSet<SkString> familySet; | |
| sk_sp<SkTypeface> tface = font.refTypeface(); | |
| SkASSERT(tface); | |
| SkFontStyle style = tface->fontStyle(); | |
| if (style.slant() == SkFontStyle::kItalic_Slant) { | |
| this->addAttribute("font-style", "italic"); | |
| } else if (style.slant() == SkFontStyle::kOblique_Slant) { | |
| this->addAttribute("font-style", "oblique"); | |
| } | |
| int weightIndex = (SkTPin(style.weight(), 100, 900) - 50) / 100; | |
| if (weightIndex != 3) { | |
| static constexpr auto weights = std::to_array<const char*>({ | |
| "100", | |
| "200", | |
| "300", | |
| "normal", | |
| "400", | |
| "500", | |
| "600", | |
| "bold", | |
| "800", | |
| "900", | |
| }); | |
| this->addAttribute("font-weight", weights[weightIndex]); | |
| } | |
| int stretchIndex = style.width() - 1; | |
| if (stretchIndex != 4) { | |
| static constexpr auto stretches = std::to_array<const char*>({ | |
| "ultra-condensed", | |
| "extra-condensed", | |
| "condensed", | |
| "semi-condensed", | |
| "normal", | |
| "semi-expanded", | |
| "expanded", | |
| "extra-expanded", | |
| "ultra-expanded", | |
| }); | |
| this->addAttribute("font-stretch", stretches[stretchIndex]); | |
| } | |
| sk_sp<SkTypeface::LocalizedStrings> familyNameIter(tface->createFamilyNameIterator()); | |
| SkTypeface::LocalizedString familyString; | |
| if (familyNameIter) { | |
| while (familyNameIter->next(&familyString)) { | |
| if (familySet.contains(familyString.fString)) { | |
| continue; | |
| } | |
| familySet.add(familyString.fString); | |
| familyName.appendf((familyName.isEmpty() ? "%s" : ", %s"), | |
| familyString.fString.c_str()); | |
| } | |
| } | |
| if (!familyName.isEmpty()) { | |
| this->addAttribute("font-family", familyName); | |
| } | |
| } | |
| sk_sp<SkDevice> SkSVGDevice::Make(const SkISize& size, | |
| std::unique_ptr<SkXMLWriter> writer, | |
| SkSVGCanvas::Options opts) { | |
| return writer ? sk_sp<SkDevice>(new SkSVGDevice(size, std::move(writer), opts)) | |
| : nullptr; | |
| } | |
| SkSVGDevice::SkSVGDevice(const SkISize& size, | |
| std::unique_ptr<SkXMLWriter> writer, | |
| SkSVGCanvas::Options opts) | |
| : SkClipStackDevice( | |
| SkImageInfo::MakeUnknown(size.fWidth, size.fHeight), | |
| SkSurfaceProps()) | |
| , fWriter(std::move(writer)) | |
| , fResourceBucket(new ResourceBucket) | |
| , fOpts(opts) | |
| { | |
| SkASSERT(fWriter); | |
| fWriter->writeHeader(); | |
| // The root <svg> tag gets closed by the destructor. | |
| fRootElement = std::make_unique<AutoElement>("svg", this); | |
| fRootElement->addAttribute("xmlns", "http://www.w3.org/2000/svg"); | |
| fRootElement->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); | |
| fRootElement->addAttribute("width", size.width()); | |
| fRootElement->addAttribute("height", size.height()); | |
| } | |
| SkSVGDevice::~SkSVGDevice() { | |
| // Pop order is important. | |
| while (!fClipStack.empty()) { | |
| fClipStack.pop_back(); | |
| } | |
| } | |
| SkParsePath::PathEncoding SkSVGDevice::pathEncoding() const { | |
| return (fOpts.flags & SkSVGCanvas::kRelativePathEncoding_Flag) | |
| ? SkParsePath::PathEncoding::Relative | |
| : SkParsePath::PathEncoding::Absolute; | |
| } | |
| void SkSVGDevice::syncClipStack(const SkClipStack& cs) { | |
| SkClipStack::B2TIter iter(cs); | |
| const SkClipStack::Element* elem; | |
| int rec_idx = 0; | |
| // First, find/preserve the common bottom. | |
| while ((elem = iter.next()) && (rec_idx < fClipStack.size())) { | |
| if (fClipStack[SkToInt(rec_idx)].fGenID != elem->getGenID()) { | |
| break; | |
| } | |
| rec_idx++; | |
| } | |
| // Discard out-of-date stack top. | |
| while (fClipStack.size() > rec_idx) { | |
| fClipStack.pop_back(); | |
| } | |
| auto define_clip = [this](const SkClipStack::Element* e) { | |
| const auto cid = SkStringPrintf("cl_%x", e->getGenID()); | |
| AutoElement clip_path("clipPath", this); | |
| clip_path.addAttribute("id", cid); | |
| // TODO: handle non-intersect clips. | |
| switch (e->getDeviceSpaceType()) { | |
| case SkClipStack::Element::DeviceSpaceType::kEmpty: { | |
| // TODO: can we skip this? | |
| AutoElement rect("rect", this); | |
| } break; | |
| case SkClipStack::Element::DeviceSpaceType::kRect: { | |
| AutoElement rect("rect", this); | |
| rect.addRectAttributes(e->getDeviceSpaceRect()); | |
| } break; | |
| case SkClipStack::Element::DeviceSpaceType::kRRect: { | |
| // TODO: complex rrect handling? | |
| const auto& rr = e->getDeviceSpaceRRect(); | |
| const auto radii = rr.getSimpleRadii(); | |
| AutoElement rrect("rect", this); | |
| rrect.addRectAttributes(rr.rect()); | |
| rrect.addAttribute("rx", radii.x()); | |
| rrect.addAttribute("ry", radii.y()); | |
| } break; | |
| case SkClipStack::Element::DeviceSpaceType::kPath: { | |
| const auto& p = e->getDeviceSpacePath(); | |
| AutoElement path("path", this); | |
| path.addPathAttributes(p, this->pathEncoding()); | |
| if (p.getFillType() == SkPathFillType::kEvenOdd) { | |
| path.addAttribute("clip-rule", "evenodd"); | |
| } | |
| } break; | |
| case SkClipStack::Element::DeviceSpaceType::kShader: | |
| // TODO: handle shader clipping, perhaps rasterize and apply as a mask image? | |
| break; | |
| } | |
| return cid; | |
| }; | |
| // Rebuild the top. | |
| while (elem) { | |
| const auto cid = define_clip(elem); | |
| auto clip_grp = std::make_unique<AutoElement>("g", this); | |
| clip_grp->addAttribute("clip-path", SkStringPrintf("url(#%s)", cid.c_str())); | |
| fClipStack.push_back({ std::move(clip_grp), elem->getGenID() }); | |
| elem = iter.next(); | |
| } | |
| } | |
| void SkSVGDevice::drawPaint(const SkPaint& paint) { | |
| AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint); | |
| rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()), | |
| SkIntToScalar(this->height()))); | |
| } | |
| void SkSVGDevice::drawAnnotation(const SkRect& rect, const char key[], SkData* value) { | |
| if (!value) { | |
| return; | |
| } | |
| if (!strcmp(SkAnnotationKeys::URL_Key(), key) || | |
| !strcmp(SkAnnotationKeys::Link_Named_Dest_Key(), key)) { | |
| this->cs().save(); | |
| this->cs().clipRect(rect, this->localToDevice(), SkClipOp::kIntersect, true); | |
| SkRect transformedRect = this->cs().bounds(this->getGlobalBounds()); | |
| this->cs().restore(); | |
| if (transformedRect.isEmpty()) { | |
| return; | |
| } | |
| SkString url(static_cast<const char*>(value->data()), value->size() - 1); | |
| AutoElement a("a", this); | |
| a.addAttribute("xlink:href", url.c_str()); | |
| { | |
| AutoElement r("rect", this); | |
| r.addAttribute("fill-opacity", "0.0"); | |
| r.addRectAttributes(transformedRect); | |
| } | |
| } | |
| } | |
| void SkSVGDevice::drawPoints(SkCanvas::PointMode mode, SkSpan<const SkPoint> pts, | |
| const SkPaint& paint) { | |
| SkPathBuilder path; | |
| const size_t count = pts.size(); | |
| switch (mode) { | |
| // todo | |
| case SkCanvas::kPoints_PointMode: | |
| for (const auto& pt : pts) { | |
| path.moveTo(pt); | |
| path.lineTo(pt); | |
| } | |
| break; | |
| case SkCanvas::kLines_PointMode: | |
| for (size_t i = 1; i < count; i += 2) { | |
| path.moveTo(pts[i-1]); | |
| path.lineTo(pts[i]); | |
| } | |
| break; | |
| case SkCanvas::kPolygon_PointMode: | |
| if (count > 1) { | |
| path.addPolygon(pts, false); | |
| } | |
| break; | |
| } | |
| this->drawPath(path.detach(), paint); | |
| } | |
| void SkSVGDevice::drawRect(const SkRect& r, const SkPaint& paint) { | |
| if (paint.getPathEffect()) { | |
| this->drawPath(SkPath::Rect(r), paint); | |
| return; | |
| } | |
| std::unique_ptr<AutoElement> svg; | |
| if (RequiresViewportReset(paint)) { | |
| svg = std::make_unique<AutoElement>("svg", this, fResourceBucket.get(), MxCp(this), paint); | |
| svg->addRectAttributes(r); | |
| } | |
| AutoElement rect("rect", this, fResourceBucket.get(), MxCp(this), paint); | |
| 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) { | |
| if (paint.getPathEffect()) { | |
| this->drawPath(SkPath::Oval(oval), paint); | |
| return; | |
| } | |
| AutoElement ellipse("ellipse", this, fResourceBucket.get(), MxCp(this), paint); | |
| ellipse.addAttribute("cx", oval.centerX()); | |
| ellipse.addAttribute("cy", oval.centerY()); | |
| ellipse.addAttribute("rx", oval.width() / 2); | |
| ellipse.addAttribute("ry", oval.height() / 2); | |
| } | |
| void SkSVGDevice::drawRRect(const SkRRect& rr, const SkPaint& paint) { | |
| if (paint.getPathEffect()) { | |
| this->drawPath(SkPath::RRect(rr), paint); | |
| return; | |
| } | |
| AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), paint); | |
| elem.addPathAttributes(SkPath::RRect(rr), this->pathEncoding()); | |
| } | |
| void SkSVGDevice::drawPath(const SkPath& path, const SkPaint& paint) { | |
| if (path.isInverseFillType()) { | |
| SkDebugf("Inverse path fill type not yet implemented."); | |
| return; | |
| } | |
| SkPath pathStorage; | |
| const SkPath* pathPtr = const_cast<SkPath*>(&path); | |
| SkTCopyOnFirstWrite<SkPaint> path_paint(paint); | |
| // Apply path effect from paint to path. | |
| if (path_paint->getPathEffect()) { | |
| SkPathBuilder builder; | |
| bool fill = skpathutils::FillPathWithPaint(path, *path_paint, &builder); | |
| pathStorage = builder.detach(); | |
| pathPtr = &pathStorage; | |
| if (fill) { | |
| // Path should be filled. | |
| path_paint.writable()->setStyle(SkPaint::kFill_Style); | |
| } else { | |
| // Path should be drawn with a hairline (width == 0). | |
| path_paint.writable()->setStyle(SkPaint::kStroke_Style); | |
| path_paint.writable()->setStrokeWidth(0); | |
| } | |
| path_paint.writable()->setPathEffect(nullptr); // path effect processed | |
| } | |
| // Create path element. | |
| AutoElement elem("path", this, fResourceBucket.get(), MxCp(this), *path_paint); | |
| elem.addPathAttributes(*pathPtr, this->pathEncoding()); | |
| // TODO: inverse fill types? | |
| if (pathPtr->getFillType() == SkPathFillType::kEvenOdd) { | |
| elem.addAttribute("fill-rule", "evenodd"); | |
| } | |
| } | |
| void SkSVGDevice::drawBitmapCommon(const MxCp& mc, const SkBitmap& bm, const SkPaint& paint) { | |
| sk_sp<SkData> pngData = EncodePng(bm.pixmap(), fOpts.pngEncoder); | |
| if (!pngData) { | |
| return; | |
| } | |
| size_t b64Size = SkBase64::EncodedSize(pngData->size()); | |
| AutoTMalloc<char> b64Data(b64Size); | |
| SkBase64::Encode(pngData->data(), pngData->size(), b64Data.get()); | |
| SkString svgImageData("data:image/png;base64,"); | |
| svgImageData.append(b64Data.get(), b64Size); | |
| SkString imageID = fResourceBucket->addImage(); | |
| { | |
| AutoElement defs("defs", this); | |
| { | |
| AutoElement image("image", this); | |
| image.addAttribute("id", imageID); | |
| image.addAttribute("width", bm.width()); | |
| image.addAttribute("height", bm.height()); | |
| image.addAttribute("xlink:href", svgImageData); | |
| } | |
| } | |
| { | |
| AutoElement imageUse("use", this, fResourceBucket.get(), mc, paint); | |
| imageUse.addAttribute("xlink:href", SkStringPrintf("#%s", imageID.c_str())); | |
| } | |
| } | |
| void SkSVGDevice::drawImageRect(const SkImage* image, const SkRect* src, const SkRect& dst, | |
| const SkSamplingOptions& sampling, const SkPaint& paint, | |
| SkCanvas::SrcRectConstraint constraint) { | |
| SkBitmap bm; | |
| // TODO: support gpu images | |
| if (!as_IB(image)->getROPixels(nullptr, &bm)) { | |
| return; | |
| } | |
| SkClipStack* cs = &this->cs(); | |
| SkClipStack::AutoRestore ar(cs, false); | |
| if (src && *src != SkRect::Make(bm.bounds())) { | |
| cs->save(); | |
| cs->clipRect(dst, this->localToDevice(), SkClipOp::kIntersect, paint.isAntiAlias()); | |
| } | |
| SkMatrix adjustedMatrix = this->localToDevice() | |
| * SkMatrix::RectToRectOrIdentity(src ? *src : SkRect::Make(bm.bounds()), | |
| dst); | |
| drawBitmapCommon(MxCp(&adjustedMatrix, cs), bm, paint); | |
| } | |
| class SVGTextBuilder : SkNoncopyable { | |
| public: | |
| SVGTextBuilder(SkPoint origin, const sktext::GlyphRun& glyphRun) | |
| : fOrigin(origin) { | |
| auto runSize = glyphRun.runSize(); | |
| AutoSTArray<64, SkUnichar> unichars(runSize); | |
| SkFontPriv::GlyphsToUnichars(glyphRun.font(), glyphRun.glyphsIDs().data(), | |
| runSize, unichars.get()); | |
| auto positions = glyphRun.positions(); | |
| for (size_t i = 0; i < runSize; ++i) { | |
| this->appendUnichar(unichars[i], positions[i]); | |
| } | |
| } | |
| const SkString& text() const { return fText; } | |
| const SkString& posX() const { return fPosXStr; } | |
| const SkString& posY() const { return fHasConstY ? fConstYStr : fPosYStr; } | |
| private: | |
| void appendUnichar(SkUnichar c, SkPoint position) { | |
| bool discardPos = false; | |
| bool isWhitespace = false; | |
| switch(c) { | |
| case ' ': | |
| case '\t': | |
| // consolidate whitespace to match SVG's xml:space=default munging | |
| // (http://www.w3.org/TR/SVG/text.html#WhiteSpace) | |
| if (fLastCharWasWhitespace) { | |
| discardPos = true; | |
| } else { | |
| fText.appendUnichar(c); | |
| } | |
| isWhitespace = true; | |
| break; | |
| case '\0': | |
| // SkPaint::glyphsToUnichars() returns \0 for inconvertible glyphs, but these | |
| // are not legal XML characters (http://www.w3.org/TR/REC-xml/#charsets) | |
| discardPos = true; | |
| isWhitespace = fLastCharWasWhitespace; // preserve whitespace consolidation | |
| break; | |
| case '&': | |
| fText.append("&"); | |
| break; | |
| case '"': | |
| fText.append("""); | |
| break; | |
| case '\'': | |
| fText.append("'"); | |
| break; | |
| case '<': | |
| fText.append("<"); | |
| break; | |
| case '>': | |
| fText.append(">"); | |
| break; | |
| default: | |
| fText.appendUnichar(c); | |
| break; | |
| } | |
| fLastCharWasWhitespace = isWhitespace; | |
| if (discardPos) { | |
| return; | |
| } | |
| position += fOrigin; | |
| fPosXStr.appendf("%.8g, ", position.fX); | |
| fPosYStr.appendf("%.8g, ", position.fY); | |
| if (fConstYStr.isEmpty()) { | |
| fConstYStr = fPosYStr; | |
| fConstY = position.fY; | |
| } else { | |
| fHasConstY &= SkScalarNearlyEqual(fConstY, position.fY); | |
| } | |
| } | |
| const SkPoint fOrigin; | |
| SkString fText, | |
| fPosXStr, fPosYStr, | |
| fConstYStr; | |
| SkScalar fConstY; | |
| bool fLastCharWasWhitespace = true, // start off in whitespace mode to strip leading space | |
| fHasConstY = true; | |
| }; | |
| void SkSVGDevice::onDrawGlyphRunList(SkCanvas* canvas, | |
| const sktext::GlyphRunList& glyphRunList, | |
| const SkPaint& paint) { | |
| SkASSERT(!glyphRunList.hasRSXForm()); | |
| const auto draw_as_path = | |
| (fOpts.flags & SkSVGCanvas::kConvertTextToPaths_Flag) || paint.getPathEffect(); | |
| if (draw_as_path) { | |
| // Emit a single <path> element. | |
| SkPathBuilder builder; | |
| for (auto& glyphRun : glyphRunList) { | |
| AddPath(glyphRun, glyphRunList.origin(), &builder); | |
| } | |
| this->drawPath(builder.detach(), paint); | |
| return; | |
| } | |
| // Emit one <text> element for each run. | |
| for (auto& glyphRun : glyphRunList) { | |
| AutoElement elem("text", this, fResourceBucket.get(), MxCp(this), paint); | |
| elem.addTextAttributes(glyphRun.font()); | |
| SVGTextBuilder builder(glyphRunList.origin(), glyphRun); | |
| elem.addAttribute("x", builder.posX()); | |
| elem.addAttribute("y", builder.posY()); | |
| elem.addText(builder.text()); | |
| } | |
| } | |
| void SkSVGDevice::drawVertices(const SkVertices*, sk_sp<SkBlender>, const SkPaint&, bool) { | |
| // todo | |
| } | |
| void SkSVGDevice::drawMesh(const SkMesh&, sk_sp<SkBlender>, const SkPaint&) { | |
| // todo | |
| } |