blob: 71f395eba9e33b1ce124f4aa0f7627dc9c90a77a [file] [log] [blame]
/*
* 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<SkPathEffect> 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<SkFontMgr>& fmgr,
const sk_sp<skresources::ResourceProvider>& rp,
const SkSVGIDMapper& mapper,
const SkSVGLengthContext& lctx,
const SkSVGPresentationContext& pctx,
const SkSVGNode* node)
: fFontMgr(fmgr)
, fResourceProvider(rp)
, fIDMapper(mapper)
, fLengthContext(lctx)
, fPresentationContext(pctx)
, fCanvas(canvas)
, fCanvasSaveCount(canvas->getSaveCount())
, fNode(node) {}
SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other)
: SkSVGRenderContext(other.fCanvas,
other.fFontMgr,
other.fResourceProvider,
other.fIDMapper,
*other.fLengthContext,
*other.fPresentationContext,
other.fNode) {}
SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other, SkCanvas* canvas)
: SkSVGRenderContext(canvas,
other.fFontMgr,
other.fResourceProvider,
other.fIDMapper,
*other.fLengthContext,
*other.fPresentationContext,
other.fNode) {}
SkSVGRenderContext::SkSVGRenderContext(const SkSVGRenderContext& other, const SkSVGNode* node)
: SkSVGRenderContext(other.fCanvas,
other.fFontMgr,
other.fResourceProvider,
other.fIDMapper,
*other.fLengthContext,
*other.fPresentationContext,
node) {}
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<const SkSVGFilter*>(node.get());
sk_sp<SkImageFilter> 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<const SkSVGClipPath*>(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<const SkSVGMask*>(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<SkPaint> SkSVGRenderContext::commonPaint(const SkSVGPaint& paint_selector,
float paint_opacity) const {
if (paint_selector.type() == SkSVGPaint::Type::kNone) {
return SkTLazy<SkPaint>();
}
SkTLazy<SkPaint> 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.
SkSVGPresentationContext pctx;
SkSVGRenderContext local_ctx(fCanvas,
fFontMgr,
fResourceProvider,
fIDMapper,
*fLengthContext,
pctx,
fNode);
const auto node = this->findNodeById(paint_selector.iri());
if (!node || !node->asPaint(local_ctx, p.get())) {
return SkTLazy<SkPaint>();
}
} 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<SkPaint> 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<SkPaint> 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;
}
SkRect SkSVGRenderContext::resolveOBBRect(const SkSVGLength& x, const SkSVGLength& y,
const SkSVGLength& w, const SkSVGLength& h,
SkSVGObjectBoundingBoxUnits obbu) const {
SkTCopyOnFirstWrite<SkSVGLengthContext> lctx(fLengthContext);
if (obbu.type() == SkSVGObjectBoundingBoxUnits::Type::kObjectBoundingBox) {
*lctx.writable() = SkSVGLengthContext({1,1});
}
auto r = lctx->resolveRect(x, y, w, h);
if (obbu.type() == SkSVGObjectBoundingBoxUnits::Type::kObjectBoundingBox) {
const auto obb = fNode->objectBoundingBox(*this);
r = SkRect::MakeXYWH(obb.x() + r.x() * obb.width(),
obb.y() + r.y() * obb.height(),
r.width() * obb.width(),
r.height() * obb.height());
}
return r;
}