/* * Copyright 2016 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "modules/svg/include/SkSVGRenderContext.h" #include "include/core/SkCanvas.h" #include "include/core/SkImageFilter.h" #include "include/core/SkPath.h" #include "include/effects/SkDashPathEffect.h" #include "include/private/SkTo.h" #include "modules/svg/include/SkSVGAttribute.h" #include "modules/svg/include/SkSVGClipPath.h" #include "modules/svg/include/SkSVGFilter.h" #include "modules/svg/include/SkSVGMask.h" #include "modules/svg/include/SkSVGNode.h" #include "modules/svg/include/SkSVGTypes.h" namespace { SkScalar length_size_for_type(const SkSize& viewport, SkSVGLengthContext::LengthType t) { switch (t) { case SkSVGLengthContext::LengthType::kHorizontal: return viewport.width(); case SkSVGLengthContext::LengthType::kVertical: return viewport.height(); case SkSVGLengthContext::LengthType::kOther: { // https://www.w3.org/TR/SVG11/coords.html#Units_viewport_percentage constexpr SkScalar rsqrt2 = 1.0f / SK_ScalarSqrt2; const SkScalar w = viewport.width(), h = viewport.height(); return rsqrt2 * SkScalarSqrt(w * w + h * h); } } SkASSERT(false); // Not reached. return 0; } // Multipliers for DPI-relative units. constexpr SkScalar kINMultiplier = 1.00f; constexpr SkScalar kPTMultiplier = kINMultiplier / 72.272f; constexpr SkScalar kPCMultiplier = kPTMultiplier * 12; constexpr SkScalar kMMMultiplier = kINMultiplier / 25.4f; constexpr SkScalar kCMMultiplier = kMMMultiplier * 10; } // namespace SkScalar SkSVGLengthContext::resolve(const SkSVGLength& l, LengthType t) const { switch (l.unit()) { case SkSVGLength::Unit::kNumber: // Fall through. case SkSVGLength::Unit::kPX: return l.value(); case SkSVGLength::Unit::kPercentage: return l.value() * length_size_for_type(fViewport, t) / 100; case SkSVGLength::Unit::kCM: return l.value() * fDPI * kCMMultiplier; case SkSVGLength::Unit::kMM: return l.value() * fDPI * kMMMultiplier; case SkSVGLength::Unit::kIN: return l.value() * fDPI * kINMultiplier; case SkSVGLength::Unit::kPT: return l.value() * fDPI * kPTMultiplier; case SkSVGLength::Unit::kPC: return l.value() * fDPI * kPCMultiplier; default: SkDebugf("unsupported unit type: <%d>\n", l.unit()); return 0; } } SkRect SkSVGLengthContext::resolveRect(const SkSVGLength& x, const SkSVGLength& y, const SkSVGLength& w, const SkSVGLength& h) const { return SkRect::MakeXYWH( this->resolve(x, SkSVGLengthContext::LengthType::kHorizontal), this->resolve(y, SkSVGLengthContext::LengthType::kVertical), this->resolve(w, SkSVGLengthContext::LengthType::kHorizontal), this->resolve(h, SkSVGLengthContext::LengthType::kVertical)); } namespace { SkPaint::Cap toSkCap(const SkSVGLineCap& cap) { switch (cap) { case SkSVGLineCap::kButt: return SkPaint::kButt_Cap; case SkSVGLineCap::kRound: return SkPaint::kRound_Cap; case SkSVGLineCap::kSquare: return SkPaint::kSquare_Cap; } SkUNREACHABLE; } SkPaint::Join toSkJoin(const SkSVGLineJoin& join) { switch (join.type()) { case SkSVGLineJoin::Type::kMiter: return SkPaint::kMiter_Join; case SkSVGLineJoin::Type::kRound: return SkPaint::kRound_Join; case SkSVGLineJoin::Type::kBevel: return SkPaint::kBevel_Join; default: SkASSERT(false); return SkPaint::kMiter_Join; } } static sk_sp dash_effect(const SkSVGPresentationAttributes& props, const SkSVGLengthContext& lctx) { if (props.fStrokeDashArray->type() != SkSVGDashArray::Type::kDashArray) { return nullptr; } const auto& da = *props.fStrokeDashArray; const auto count = da.dashArray().count(); SkSTArray<128, SkScalar, true> intervals(count); for (const auto& dash : da.dashArray()) { intervals.push_back(lctx.resolve(dash, SkSVGLengthContext::LengthType::kOther)); } if (count & 1) { // If an odd number of values is provided, then the list of values // is repeated to yield an even number of values. intervals.push_back_n(count); memcpy(intervals.begin() + count, intervals.begin(), count * sizeof(SkScalar)); } SkASSERT((intervals.count() & 1) == 0); const auto phase = lctx.resolve(*props.fStrokeDashOffset, SkSVGLengthContext::LengthType::kOther); return SkDashPathEffect::Make(intervals.begin(), intervals.count(), phase); } } // namespace SkSVGPresentationContext::SkSVGPresentationContext() : fInherited(SkSVGPresentationAttributes::MakeInitial()) {} SkSVGRenderContext::SkSVGRenderContext(SkCanvas* canvas, const sk_sp& fmgr, const sk_sp& rp, const SkSVGIDMapper& mapper, const SkSVGLengthContext& lctx, const SkSVGPresentationContext& pctx, const OBBScope& obbs) : fFontMgr(fmgr) , fResourceProvider(rp) , fIDMapper(mapper) , fLengthContext(lctx) , fPresentationContext(pctx) , fCanvas(canvas) , fCanvasSaveCount(canvas->getSaveCount()) , fOBBScope(obbs) {} SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other) : SkSVGRenderContext(other.fCanvas, other.fFontMgr, other.fResourceProvider, other.fIDMapper, *other.fLengthContext, *other.fPresentationContext, other.fOBBScope) {} SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other, SkCanvas* canvas) : SkSVGRenderContext(canvas, other.fFontMgr, other.fResourceProvider, other.fIDMapper, *other.fLengthContext, *other.fPresentationContext, other.fOBBScope) {} SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other, const SkSVGNode* node) : SkSVGRenderContext(other.fCanvas, other.fFontMgr, other.fResourceProvider, other.fIDMapper, *other.fLengthContext, *other.fPresentationContext, OBBScope{node, this}) {} SkSVGRenderContext::~SkSVGRenderContext() { fCanvas->restoreToCount(fCanvasSaveCount); } SkSVGRenderContext::BorrowedNode SkSVGRenderContext::findNodeById(const SkSVGIRI& iri) const { if (iri.type() != SkSVGIRI::Type::kLocal) { SkDebugf("non-local iri references not currently supported"); return BorrowedNode(nullptr); } return BorrowedNode(fIDMapper.find(iri.iri())); } void SkSVGRenderContext::applyPresentationAttributes(const SkSVGPresentationAttributes& attrs, uint32_t flags) { #define ApplyLazyInheritedAttribute(ATTR) \ do { \ /* All attributes should be defined on the inherited context. */ \ SkASSERT(fPresentationContext->fInherited.f ## ATTR.isValue()); \ const auto& attr = attrs.f ## ATTR; \ if (attr.isValue() && *attr != *fPresentationContext->fInherited.f ## ATTR) { \ /* Update the local attribute value */ \ fPresentationContext.writable()->fInherited.f ## ATTR.set(*attr); \ } \ } while (false) ApplyLazyInheritedAttribute(Fill); ApplyLazyInheritedAttribute(FillOpacity); ApplyLazyInheritedAttribute(FillRule); ApplyLazyInheritedAttribute(FontFamily); ApplyLazyInheritedAttribute(FontSize); ApplyLazyInheritedAttribute(FontStyle); ApplyLazyInheritedAttribute(FontWeight); ApplyLazyInheritedAttribute(ClipRule); ApplyLazyInheritedAttribute(Stroke); ApplyLazyInheritedAttribute(StrokeDashOffset); ApplyLazyInheritedAttribute(StrokeDashArray); ApplyLazyInheritedAttribute(StrokeLineCap); ApplyLazyInheritedAttribute(StrokeLineJoin); ApplyLazyInheritedAttribute(StrokeMiterLimit); ApplyLazyInheritedAttribute(StrokeOpacity); ApplyLazyInheritedAttribute(StrokeWidth); ApplyLazyInheritedAttribute(TextAnchor); ApplyLazyInheritedAttribute(Visibility); ApplyLazyInheritedAttribute(Color); ApplyLazyInheritedAttribute(ColorInterpolation); ApplyLazyInheritedAttribute(ColorInterpolationFilters); #undef ApplyLazyInheritedAttribute // Uninherited attributes. Only apply to the current context. const bool hasFilter = attrs.fFilter.isValue(); if (attrs.fOpacity.isValue()) { this->applyOpacity(*attrs.fOpacity, flags, hasFilter); } if (attrs.fClipPath.isValue()) { this->applyClip(*attrs.fClipPath); } if (attrs.fMask.isValue()) { this->applyMask(*attrs.fMask); } // TODO: when both a filter and opacity are present, we can apply both with a single layer if (hasFilter) { this->applyFilter(*attrs.fFilter); } // Remaining uninherited presentation attributes are accessed as SkSVGNode fields, not via // the render context. // TODO: resolve these in a pre-render styling pass and assert here that they are values. // - stop-color // - stop-opacity // - flood-color // - flood-opacity // - lighting-color } void SkSVGRenderContext::applyOpacity(SkScalar opacity, uint32_t flags, bool hasFilter) { if (opacity >= 1) { return; } const auto& props = fPresentationContext->fInherited; const bool hasFill = props.fFill ->type() != SkSVGPaint::Type::kNone, hasStroke = props.fStroke->type() != SkSVGPaint::Type::kNone; // We can apply the opacity as paint alpha if it only affects one atomic draw. // For now, this means all of the following must be true: // - the target node doesn't have any descendants; // - it only has a stroke or a fill (but not both); // - it does not have a filter. // Going forward, we may needto refine this heuristic (e.g. to accommodate markers). if ((flags & kLeaf) && (hasFill ^ hasStroke) && !hasFilter) { fDeferredPaintOpacity *= opacity; } else { // Expensive, layer-based fall back. SkPaint opacityPaint; opacityPaint.setAlphaf(SkTPin(opacity, 0.0f, 1.0f)); // Balanced in the destructor, via restoreToCount(). fCanvas->saveLayer(nullptr, &opacityPaint); } } void SkSVGRenderContext::applyFilter(const SkSVGFuncIRI& filter) { if (filter.type() != SkSVGFuncIRI::Type::kIRI) { return; } const auto node = this->findNodeById(filter.iri()); if (!node || node->tag() != SkSVGTag::kFilter) { return; } const SkSVGFilter* filterNode = reinterpret_cast(node.get()); sk_sp imageFilter = filterNode->buildFilterDAG(*this); if (imageFilter) { SkPaint filterPaint; filterPaint.setImageFilter(imageFilter); // Balanced in the destructor, via restoreToCount(). fCanvas->saveLayer(nullptr, &filterPaint); } } void SkSVGRenderContext::saveOnce() { // The canvas only needs to be saved once, per local SkSVGRenderContext. if (fCanvas->getSaveCount() == fCanvasSaveCount) { fCanvas->save(); } SkASSERT(fCanvas->getSaveCount() > fCanvasSaveCount); } void SkSVGRenderContext::applyClip(const SkSVGFuncIRI& clip) { if (clip.type() != SkSVGFuncIRI::Type::kIRI) { return; } const auto clipNode = this->findNodeById(clip.iri()); if (!clipNode || clipNode->tag() != SkSVGTag::kClipPath) { return; } const SkPath clipPath = static_cast(clipNode.get())->resolveClip(*this); // We use the computed clip path in two ways: // // - apply to the current canvas, for drawing // - track in the presentation context, for asPath() composition // // TODO: the two uses are exclusive, avoid canvas churn when non needed. this->saveOnce(); fCanvas->clipPath(clipPath, true); fClipPath.set(clipPath); } void SkSVGRenderContext::applyMask(const SkSVGFuncIRI& mask) { if (mask.type() != SkSVGFuncIRI::Type::kIRI) { return; } const auto node = this->findNodeById(mask.iri()); if (!node || node->tag() != SkSVGTag::kMask) { return; } const auto* mask_node = static_cast(node.get()); const auto mask_bounds = mask_node->bounds(*this); // Isolation/mask layer. fCanvas->saveLayer(mask_bounds, nullptr); // Render and filter mask content. mask_node->renderMask(*this); // Content layer SkPaint masking_paint; masking_paint.setBlendMode(SkBlendMode::kSrcIn); fCanvas->saveLayer(mask_bounds, &masking_paint); // Content is also clipped to the specified mask bounds. fCanvas->clipRect(mask_bounds, true); // At this point we're set up for content rendering. // The pending layers are restored in the destructor (render context scope exit). // Restoring triggers srcIn-compositing the content against the mask. } SkTLazy SkSVGRenderContext::commonPaint(const SkSVGPaint& paint_selector, float paint_opacity) const { if (paint_selector.type() == SkSVGPaint::Type::kNone) { return SkTLazy(); } SkTLazy p; p.init(); switch (paint_selector.type()) { case SkSVGPaint::Type::kColor: p->setColor(this->resolveSvgColor(paint_selector.color())); break; case SkSVGPaint::Type::kIRI: { // Our property inheritance is borked as it follows the render path and not the tree // hierarchy. To avoid gross transgressions like leaf node presentation attributes // leaking into the paint server context, use a pristine presentation context when // following hrefs. // // Preserve the OBB scope because some paints use object bounding box coords // (e.g. gradient control points), which requires access to the render context // and node being rendered. SkSVGPresentationContext pctx; SkSVGRenderContext local_ctx(fCanvas, fFontMgr, fResourceProvider, fIDMapper, *fLengthContext, pctx, fOBBScope); const auto node = this->findNodeById(paint_selector.iri()); if (!node || !node->asPaint(local_ctx, p.get())) { // Use the fallback color. p->setColor(this->resolveSvgColor(paint_selector.color())); } } break; default: SkUNREACHABLE; } p->setAntiAlias(true); // TODO: shape-rendering support // We observe 3 opacity components: // - initial paint server opacity (e.g. color stop opacity) // - paint-specific opacity (e.g. 'fill-opacity', 'stroke-opacity') // - deferred opacity override (optimization for leaf nodes 'opacity') p->setAlphaf(SkTPin(p->getAlphaf() * paint_opacity * fDeferredPaintOpacity, 0.0f, 1.0f)); return p; } SkTLazy SkSVGRenderContext::fillPaint() const { const auto& props = fPresentationContext->fInherited; auto p = this->commonPaint(*props.fFill, *props.fFillOpacity); if (p.isValid()) { p->setStyle(SkPaint::kFill_Style); } return p; } SkTLazy SkSVGRenderContext::strokePaint() const { const auto& props = fPresentationContext->fInherited; auto p = this->commonPaint(*props.fStroke, *props.fStrokeOpacity); if (p.isValid()) { p->setStyle(SkPaint::kStroke_Style); p->setStrokeWidth(fLengthContext->resolve(*props.fStrokeWidth, SkSVGLengthContext::LengthType::kOther)); p->setStrokeCap(toSkCap(*props.fStrokeLineCap)); p->setStrokeJoin(toSkJoin(*props.fStrokeLineJoin)); p->setStrokeMiter(*props.fStrokeMiterLimit); p->setPathEffect(dash_effect(props, *fLengthContext)); } return p; } SkSVGColorType SkSVGRenderContext::resolveSvgColor(const SkSVGColor& color) const { switch (color.type()) { case SkSVGColor::Type::kColor: return color.color(); case SkSVGColor::Type::kCurrentColor: return *fPresentationContext->fInherited.fColor; case SkSVGColor::Type::kICCColor: SkDebugf("ICC color unimplemented"); return SK_ColorBLACK; } SkUNREACHABLE; } SkSVGRenderContext::OBBTransform SkSVGRenderContext::transformForCurrentOBB(SkSVGObjectBoundingBoxUnits u) const { if (!fOBBScope.fNode || u.type() == SkSVGObjectBoundingBoxUnits::Type::kUserSpaceOnUse) { return {{0,0},{1,1}}; } SkASSERT(fOBBScope.fCtx); const auto obb = fOBBScope.fNode->objectBoundingBox(*fOBBScope.fCtx); return {{obb.x(), obb.y()}, {obb.width(), obb.height()}}; } SkRect SkSVGRenderContext::resolveOBBRect(const SkSVGLength& x, const SkSVGLength& y, const SkSVGLength& w, const SkSVGLength& h, SkSVGObjectBoundingBoxUnits obbu) const { SkTCopyOnFirstWrite lctx(fLengthContext); if (obbu.type() == SkSVGObjectBoundingBoxUnits::Type::kObjectBoundingBox) { *lctx.writable() = SkSVGLengthContext({1,1}); } auto r = lctx->resolveRect(x, y, w, h); const auto obbt = this->transformForCurrentOBB(obbu); return SkRect::MakeXYWH(obbt.scale.x * r.x() + obbt.offset.x, obbt.scale.y * r.y() + obbt.offset.y, obbt.scale.x * r.width(), obbt.scale.y * r.height()); }