/* * 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 "SkSVGRenderContext.h" #include "SkCanvas.h" #include "SkDashPathEffect.h" #include "SkPath.h" #include "SkSVGAttribute.h" #include "SkSVGNode.h" #include "SkSVGTypes.h" #include "SkTo.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: return SkScalarSqrt(viewport.width() * viewport.height()); } 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; } // anonymous ns 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.type()) { case SkSVGLineCap::Type::kButt: return SkPaint::kButt_Cap; case SkSVGLineCap::Type::kRound: return SkPaint::kRound_Cap; case SkSVGLineCap::Type::kSquare: return SkPaint::kSquare_Cap; default: SkASSERT(false); return SkPaint::kButt_Cap; } } 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; } } void applySvgPaint(const SkSVGRenderContext& ctx, const SkSVGPaint& svgPaint, SkPaint* p) { switch (svgPaint.type()) { case SkSVGPaint::Type::kColor: p->setColor(SkColorSetA(svgPaint.color(), p->getAlpha())); break; case SkSVGPaint::Type::kIRI: { const auto* node = ctx.findNodeById(svgPaint.iri()); if (!node || !node->asPaint(ctx, p)) { p->setColor(SK_ColorTRANSPARENT); } break; } case SkSVGPaint::Type::kCurrentColor: SkDebugf("unimplemented 'currentColor' paint type"); // Fall through. case SkSVGPaint::Type::kNone: // Fall through. case SkSVGPaint::Type::kInherit: break; } } inline uint8_t opacity_to_alpha(SkScalar o) { return SkTo<uint8_t>(SkScalarRoundToInt(o * 255)); } // Commit the selected attribute to the paint cache. template <SkSVGAttribute> void commitToPaint(const SkSVGPresentationAttributes&, const SkSVGRenderContext&, SkSVGPresentationContext*); template <> void commitToPaint<SkSVGAttribute::kFill>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext& ctx, SkSVGPresentationContext* pctx) { applySvgPaint(ctx, *attrs.fFill.get(), &pctx->fFillPaint); } template <> void commitToPaint<SkSVGAttribute::kStroke>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext& ctx, SkSVGPresentationContext* pctx) { applySvgPaint(ctx, *attrs.fStroke.get(), &pctx->fStrokePaint); } template <> void commitToPaint<SkSVGAttribute::kFillOpacity>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext&, SkSVGPresentationContext* pctx) { pctx->fFillPaint.setAlpha(opacity_to_alpha(*attrs.fFillOpacity.get())); } template <> void commitToPaint<SkSVGAttribute::kStrokeDashArray>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext& ctx, SkSVGPresentationContext* pctx) { const auto& dashArray = attrs.fStrokeDashArray.get(); if (dashArray->type() != SkSVGDashArray::Type::kDashArray) { return; } const auto count = dashArray->dashArray().count(); SkSTArray<128, SkScalar, true> intervals(count); for (const auto& dash : dashArray->dashArray()) { intervals.push_back(ctx.lengthContext().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); } SkASSERT((intervals.count() & 1) == 0); const SkScalar phase = ctx.lengthContext().resolve(*pctx->fInherited.fStrokeDashOffset.get(), SkSVGLengthContext::LengthType::kOther); pctx->fStrokePaint.setPathEffect(SkDashPathEffect::Make(intervals.begin(), intervals.count(), phase)); } template <> void commitToPaint<SkSVGAttribute::kStrokeDashOffset>(const SkSVGPresentationAttributes&, const SkSVGRenderContext&, SkSVGPresentationContext*) { // Applied via kStrokeDashArray. } template <> void commitToPaint<SkSVGAttribute::kStrokeLineCap>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext&, SkSVGPresentationContext* pctx) { const auto& cap = *attrs.fStrokeLineCap.get(); if (cap.type() != SkSVGLineCap::Type::kInherit) { pctx->fStrokePaint.setStrokeCap(toSkCap(cap)); } } template <> void commitToPaint<SkSVGAttribute::kStrokeLineJoin>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext&, SkSVGPresentationContext* pctx) { const auto& join = *attrs.fStrokeLineJoin.get(); if (join.type() != SkSVGLineJoin::Type::kInherit) { pctx->fStrokePaint.setStrokeJoin(toSkJoin(join)); } } template <> void commitToPaint<SkSVGAttribute::kStrokeMiterLimit>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext&, SkSVGPresentationContext* pctx) { pctx->fStrokePaint.setStrokeMiter(*attrs.fStrokeMiterLimit.get()); } template <> void commitToPaint<SkSVGAttribute::kStrokeOpacity>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext&, SkSVGPresentationContext* pctx) { pctx->fStrokePaint.setAlpha(opacity_to_alpha(*attrs.fStrokeOpacity.get())); } template <> void commitToPaint<SkSVGAttribute::kStrokeWidth>(const SkSVGPresentationAttributes& attrs, const SkSVGRenderContext& ctx, SkSVGPresentationContext* pctx) { auto strokeWidth = ctx.lengthContext().resolve(*attrs.fStrokeWidth.get(), SkSVGLengthContext::LengthType::kOther); pctx->fStrokePaint.setStrokeWidth(strokeWidth); } template <> void commitToPaint<SkSVGAttribute::kFillRule>(const SkSVGPresentationAttributes&, const SkSVGRenderContext&, SkSVGPresentationContext*) { // Not part of the SkPaint state; applied to the path at render time. } template <> void commitToPaint<SkSVGAttribute::kClipRule>(const SkSVGPresentationAttributes&, const SkSVGRenderContext&, SkSVGPresentationContext*) { // Not part of the SkPaint state; applied to the path at clip time. } template <> void commitToPaint<SkSVGAttribute::kVisibility>(const SkSVGPresentationAttributes&, const SkSVGRenderContext&, SkSVGPresentationContext*) { // Not part of the SkPaint state; queried to veto rendering. } } // anonymous ns SkSVGPresentationContext::SkSVGPresentationContext() : fInherited(SkSVGPresentationAttributes::MakeInitial()) { fFillPaint.setStyle(SkPaint::kFill_Style); fStrokePaint.setStyle(SkPaint::kStroke_Style); // TODO: drive AA off presentation attrs also (shape-rendering?) fFillPaint.setAntiAlias(true); fStrokePaint.setAntiAlias(true); // Commit initial values to the paint cache. SkCanvas dummyCanvas(0, 0); SkSVGRenderContext dummy(&dummyCanvas, SkSVGIDMapper(), SkSVGLengthContext(SkSize::Make(0, 0)), *this); commitToPaint<SkSVGAttribute::kFill>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kFillOpacity>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStroke>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStrokeLineCap>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStrokeLineJoin>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStrokeMiterLimit>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStrokeOpacity>(fInherited, dummy, this); commitToPaint<SkSVGAttribute::kStrokeWidth>(fInherited, dummy, this); } SkSVGRenderContext::SkSVGRenderContext(SkCanvas* canvas, const SkSVGIDMapper& mapper, const SkSVGLengthContext& lctx, const SkSVGPresentationContext& pctx) : fIDMapper(mapper) , fLengthContext(lctx) , fPresentationContext(pctx) , fCanvas(canvas) , fCanvasSaveCount(canvas->getSaveCount()) {} SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other) : SkSVGRenderContext(other.fCanvas, other.fIDMapper, *other.fLengthContext, *other.fPresentationContext) {} SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other, SkCanvas* canvas) : SkSVGRenderContext(canvas, other.fIDMapper, *other.fLengthContext, *other.fPresentationContext) {} SkSVGRenderContext::~SkSVGRenderContext() { fCanvas->restoreToCount(fCanvasSaveCount); } const SkSVGNode* SkSVGRenderContext::findNodeById(const SkString& id) const { const auto* v = fIDMapper.find(id); return v ? v->get() : nullptr; } 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.isValid()); \ const auto* value = attrs.f ## ATTR.getMaybeNull(); \ if (value && *value != *fPresentationContext->fInherited.f ## ATTR.get()) { \ /* Update the local attribute value */ \ fPresentationContext.writable()->fInherited.f ## ATTR.set(*value); \ /* Update the cached paints */ \ commitToPaint<SkSVGAttribute::k ## ATTR>(attrs, *this, \ fPresentationContext.writable()); \ } \ } while (false) ApplyLazyInheritedAttribute(Fill); ApplyLazyInheritedAttribute(FillOpacity); ApplyLazyInheritedAttribute(FillRule); ApplyLazyInheritedAttribute(ClipRule); ApplyLazyInheritedAttribute(Stroke); ApplyLazyInheritedAttribute(StrokeDashOffset); ApplyLazyInheritedAttribute(StrokeDashArray); ApplyLazyInheritedAttribute(StrokeLineCap); ApplyLazyInheritedAttribute(StrokeLineJoin); ApplyLazyInheritedAttribute(StrokeMiterLimit); ApplyLazyInheritedAttribute(StrokeOpacity); ApplyLazyInheritedAttribute(StrokeWidth); ApplyLazyInheritedAttribute(Visibility); #undef ApplyLazyInheritedAttribute // Uninherited attributes. Only apply to the current context. if (auto* opacity = attrs.fOpacity.getMaybeNull()) { this->applyOpacity(opacity->value(), flags); } if (auto* clip = attrs.fClipPath.getMaybeNull()) { this->applyClip(*clip); } } void SkSVGRenderContext::applyOpacity(SkScalar opacity, uint32_t flags) { if (opacity >= 1) { return; } const bool hasFill = SkToBool(this->fillPaint()); const bool hasStroke = SkToBool(this->strokePaint()); // We can apply the opacity as paint alpha iif it only affects one atomic draw. // For now, this means a) the target node doesn't have any descendants, and // b) it only has a stroke or a fill (but not both). Going forward, we may need // to refine this heuristic (e.g. to accommodate markers). if ((flags & kLeaf) && (hasFill ^ hasStroke)) { auto* pctx = fPresentationContext.writable(); if (hasFill) { pctx->fFillPaint.setAlpha( SkScalarRoundToInt(opacity * pctx->fFillPaint.getAlpha())); } else { pctx->fStrokePaint.setAlpha( SkScalarRoundToInt(opacity * pctx->fStrokePaint.getAlpha())); } } else { // Expensive, layer-based fall back. SkPaint opacityPaint; opacityPaint.setAlpha(opacity_to_alpha(opacity)); // Balanced in the destructor, via restoreToCount(). fCanvas->saveLayer(nullptr, &opacityPaint); } } 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 SkSVGClip& clip) { if (clip.type() != SkSVGClip::Type::kIRI) { return; } const SkSVGNode* clipNode = this->findNodeById(clip.iri()); if (!clipNode || clipNode->tag() != SkSVGTag::kClipPath) { return; } const SkPath clipPath = clipNode->asPath(*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); } const SkPaint* SkSVGRenderContext::fillPaint() const { const SkSVGPaint::Type paintType = fPresentationContext->fInherited.fFill.get()->type(); return paintType != SkSVGPaint::Type::kNone ? &fPresentationContext->fFillPaint : nullptr; } const SkPaint* SkSVGRenderContext::strokePaint() const { const SkSVGPaint::Type paintType = fPresentationContext->fInherited.fStroke.get()->type(); return paintType != SkSVGPaint::Type::kNone ? &fPresentationContext->fStrokePaint : nullptr; }