/* * Copyright 2019 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/SkSVGText.h" #include "include/core/SkCanvas.h" #include "include/core/SkContourMeasure.h" #include "include/core/SkFont.h" #include "include/core/SkFontMgr.h" #include "include/core/SkFontStyle.h" #include "include/core/SkFontTypes.h" #include "include/core/SkMatrix.h" #include "include/core/SkPaint.h" #include "include/core/SkPathBuilder.h" #include "include/core/SkPoint.h" #include "include/core/SkRSXform.h" #include "include/core/SkScalar.h" #include "include/core/SkString.h" #include "include/core/SkTextBlob.h" #include "include/core/SkTypeface.h" #include "include/core/SkTypes.h" #include "include/private/base/SkTArray.h" #include "include/private/base/SkTemplates.h" #include "include/private/base/SkTo.h" #include "modules/skshaper/include/SkShaper.h" #include "modules/svg/include/SkSVGAttribute.h" #include "modules/svg/include/SkSVGAttributeParser.h" #include "modules/svg/include/SkSVGRenderContext.h" #include "modules/svg/src/SkSVGTextPriv.h" #include "src/base/SkTLazy.h" #include "src/base/SkUTF.h" #include "src/core/SkTextBlobPriv.h" #include #include #include #include #include #include #include #include using namespace skia_private; namespace { static SkFont ResolveFont(const SkSVGRenderContext& ctx) { auto weight = [](const SkSVGFontWeight& w) { switch (w.type()) { case SkSVGFontWeight::Type::k100: return SkFontStyle::kThin_Weight; case SkSVGFontWeight::Type::k200: return SkFontStyle::kExtraLight_Weight; case SkSVGFontWeight::Type::k300: return SkFontStyle::kLight_Weight; case SkSVGFontWeight::Type::k400: return SkFontStyle::kNormal_Weight; case SkSVGFontWeight::Type::k500: return SkFontStyle::kMedium_Weight; case SkSVGFontWeight::Type::k600: return SkFontStyle::kSemiBold_Weight; case SkSVGFontWeight::Type::k700: return SkFontStyle::kBold_Weight; case SkSVGFontWeight::Type::k800: return SkFontStyle::kExtraBold_Weight; case SkSVGFontWeight::Type::k900: return SkFontStyle::kBlack_Weight; case SkSVGFontWeight::Type::kNormal: return SkFontStyle::kNormal_Weight; case SkSVGFontWeight::Type::kBold: return SkFontStyle::kBold_Weight; case SkSVGFontWeight::Type::kBolder: return SkFontStyle::kExtraBold_Weight; case SkSVGFontWeight::Type::kLighter: return SkFontStyle::kLight_Weight; case SkSVGFontWeight::Type::kInherit: { SkASSERT(false); return SkFontStyle::kNormal_Weight; } } SkUNREACHABLE; }; auto slant = [](const SkSVGFontStyle& s) { switch (s.type()) { case SkSVGFontStyle::Type::kNormal: return SkFontStyle::kUpright_Slant; case SkSVGFontStyle::Type::kItalic: return SkFontStyle::kItalic_Slant; case SkSVGFontStyle::Type::kOblique: return SkFontStyle::kOblique_Slant; case SkSVGFontStyle::Type::kInherit: { SkASSERT(false); return SkFontStyle::kUpright_Slant; } } SkUNREACHABLE; }; const auto& family = ctx.presentationContext().fInherited.fFontFamily->family(); const SkFontStyle style(weight(*ctx.presentationContext().fInherited.fFontWeight), SkFontStyle::kNormal_Width, slant(*ctx.presentationContext().fInherited.fFontStyle)); const auto size = ctx.lengthContext().resolve(ctx.presentationContext().fInherited.fFontSize->size(), SkSVGLengthContext::LengthType::kVertical); // TODO: we likely want matchFamilyStyle here, but switching away from legacyMakeTypeface // changes all the results when using the default fontmgr. auto tf = ctx.fontMgr()->legacyMakeTypeface(family.c_str(), style); if (!tf) { tf = ctx.fontMgr()->legacyMakeTypeface(nullptr, style); } SkASSERT(tf); SkFont font(std::move(tf), size); font.setHinting(SkFontHinting::kNone); font.setSubpixel(true); font.setLinearMetrics(true); font.setBaselineSnap(false); font.setEdging(SkFont::Edging::kAntiAlias); return font; } static std::vector ResolveLengths(const SkSVGLengthContext& lctx, const std::vector& lengths, SkSVGLengthContext::LengthType lt) { std::vector resolved; resolved.reserve(lengths.size()); for (const auto& l : lengths) { resolved.push_back(lctx.resolve(l, lt)); } return resolved; } static float ComputeAlignmentFactor(const SkSVGPresentationContext& pctx) { switch (pctx.fInherited.fTextAnchor->type()) { case SkSVGTextAnchor::Type::kStart : return 0.0f; case SkSVGTextAnchor::Type::kMiddle: return -0.5f; case SkSVGTextAnchor::Type::kEnd : return -1.0f; case SkSVGTextAnchor::Type::kInherit: SkASSERT(false); return 0.0f; } SkUNREACHABLE; } } // namespace SkSVGTextContext::ScopedPosResolver::ScopedPosResolver(const SkSVGTextContainer& txt, const SkSVGLengthContext& lctx, SkSVGTextContext* tctx, size_t charIndexOffset) : fTextContext(tctx) , fParent(tctx->fPosResolver) , fCharIndexOffset(charIndexOffset) , fX(ResolveLengths(lctx, txt.getX(), SkSVGLengthContext::LengthType::kHorizontal)) , fY(ResolveLengths(lctx, txt.getY(), SkSVGLengthContext::LengthType::kVertical)) , fDx(ResolveLengths(lctx, txt.getDx(), SkSVGLengthContext::LengthType::kHorizontal)) , fDy(ResolveLengths(lctx, txt.getDy(), SkSVGLengthContext::LengthType::kVertical)) , fRotate(txt.getRotate()) { fTextContext->fPosResolver = this; } SkSVGTextContext::ScopedPosResolver::ScopedPosResolver(const SkSVGTextContainer& txt, const SkSVGLengthContext& lctx, SkSVGTextContext* tctx) : ScopedPosResolver(txt, lctx, tctx, tctx->fCurrentCharIndex) {} SkSVGTextContext::ScopedPosResolver::~ScopedPosResolver() { fTextContext->fPosResolver = fParent; } SkSVGTextContext::PosAttrs SkSVGTextContext::ScopedPosResolver::resolve(size_t charIndex) const { PosAttrs attrs; if (charIndex < fLastPosIndex) { SkASSERT(charIndex >= fCharIndexOffset); const auto localCharIndex = charIndex - fCharIndexOffset; const auto hasAllLocal = localCharIndex < fX.size() && localCharIndex < fY.size() && localCharIndex < fDx.size() && localCharIndex < fDy.size() && localCharIndex < fRotate.size(); if (!hasAllLocal && fParent) { attrs = fParent->resolve(charIndex); } if (localCharIndex < fX.size()) { attrs[PosAttrs::kX] = fX[localCharIndex]; } if (localCharIndex < fY.size()) { attrs[PosAttrs::kY] = fY[localCharIndex]; } if (localCharIndex < fDx.size()) { attrs[PosAttrs::kDx] = fDx[localCharIndex]; } if (localCharIndex < fDy.size()) { attrs[PosAttrs::kDy] = fDy[localCharIndex]; } // Rotation semantics are interestingly different [1]: // // - values are not cumulative // - if explicit values are present at any level in the ancestor chain, those take // precedence (closest ancestor) // - last specified value applies to all remaining chars (closest ancestor) // - these rules apply at node scope (not chunk scope) // // This means we need to discriminate between explicit rotation (rotate value provided for // current char) and implicit rotation (ancestor has some values - but not for the requested // char - we use the last specified value). // // [1] https://www.w3.org/TR/SVG11/text.html#TSpanElementRotateAttribute if (!fRotate.empty()) { if (localCharIndex < fRotate.size()) { // Explicit rotation value overrides anything in the ancestor chain. attrs[PosAttrs::kRotate] = fRotate[localCharIndex]; attrs.setImplicitRotate(false); } else if (!attrs.has(PosAttrs::kRotate) || attrs.isImplicitRotate()){ // Local implicit rotation (last specified value) overrides ancestor implicit // rotation. attrs[PosAttrs::kRotate] = fRotate.back(); attrs.setImplicitRotate(true); } } if (!attrs.hasAny()) { // Once we stop producing explicit position data, there is no reason to // continue trying for higher indices. We can suppress future lookups. fLastPosIndex = charIndex; } } return attrs; } void SkSVGTextContext::ShapeBuffer::append(SkUnichar ch, PositionAdjustment pos) { // relative pos adjustments are cumulative if (!fUtf8PosAdjust.empty()) { pos.offset += fUtf8PosAdjust.back().offset; } char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence]; const auto utf8_len = SkToInt(SkUTF::ToUTF8(ch, utf8_buf)); fUtf8 .push_back_n(utf8_len, utf8_buf); fUtf8PosAdjust.push_back_n(utf8_len, pos); } void SkSVGTextContext::shapePendingBuffer(const SkSVGRenderContext& ctx, const SkFont& font) { const char* utf8 = fShapeBuffer.fUtf8.data(); size_t utf8Bytes = fShapeBuffer.fUtf8.size(); std::unique_ptr font_runs = SkShaper::MakeFontMgrRunIterator(utf8, utf8Bytes, font, ctx.fontMgr()); if (!font_runs) { return; } if (!fForcePrimitiveShaping) { // Try to use the passed in shaping callbacks to shape, for example, using harfbuzz and ICU. const uint8_t defaultLTR = 0; std::unique_ptr bidi = ctx.makeBidiRunIterator(utf8, utf8Bytes, defaultLTR); std::unique_ptr language = SkShaper::MakeStdLanguageRunIterator(utf8, utf8Bytes); std::unique_ptr script = ctx.makeScriptRunIterator(utf8, utf8Bytes); if (bidi && script && language) { fShaper->shape(utf8, utf8Bytes, *font_runs, *bidi, *script, *language, nullptr, 0, SK_ScalarMax, this); fShapeBuffer.reset(); return; } // If any of the callbacks fail, we'll fallback to the primitive shaping. } // bidi, script, and lang are all unused so we can construct them with empty data. SkShaper::TrivialBiDiRunIterator trivial_bidi{0, 0}; SkShaper::TrivialScriptRunIterator trivial_script{0, 0}; SkShaper::TrivialLanguageRunIterator trivial_lang{nullptr, 0}; fShaper->shape(utf8, utf8Bytes, *font_runs, trivial_bidi, trivial_script, trivial_lang, nullptr, 0, SK_ScalarMax, this); fShapeBuffer.reset(); } SkSVGTextContext::SkSVGTextContext(const SkSVGRenderContext& ctx, const ShapedTextCallback& cb, const SkSVGTextPath* tpath) : fRenderContext(ctx) , fCallback(cb) , fShaper(ctx.makeShaper()) , fChunkAlignmentFactor(ComputeAlignmentFactor(ctx.presentationContext())) { // If the shaper callback returns null, fallback to the primitive shaper and // signal that we should not use the other callbacks in shapePendingBuffer if (!fShaper) { fShaper = SkShapers::Primitive::PrimitiveText(); fForcePrimitiveShaping = true; } if (tpath) { fPathData = std::make_unique(ctx, *tpath); // https://www.w3.org/TR/SVG11/text.html#TextPathElementStartOffsetAttribute auto resolve_offset = [this](const SkSVGLength& offset) { if (offset.unit() != SkSVGLength::Unit::kPercentage) { // "If a other than a percentage is given, then the ‘startOffset’ // represents a distance along the path measured in the current user coordinate // system." return fRenderContext.lengthContext() .resolve(offset, SkSVGLengthContext::LengthType::kHorizontal); } // "If a percentage is given, then the ‘startOffset’ represents a percentage distance // along the entire path." return offset.value() * fPathData->length() / 100; }; // startOffset acts as an initial absolute position fChunkPos.fX = resolve_offset(tpath->getStartOffset()); } } SkSVGTextContext::~SkSVGTextContext() { this->flushChunk(fRenderContext); } void SkSVGTextContext::shapeFragment(const SkString& txt, const SkSVGRenderContext& ctx, SkSVGXmlSpace xs) { // https://www.w3.org/TR/SVG11/text.html#WhiteSpace // https://www.w3.org/TR/2008/REC-xml-20081126/#NT-S auto filterWSDefault = [this](SkUnichar ch) -> SkUnichar { // Remove all newline chars. if (ch == '\n') { return -1; } // Convert tab chars to space. if (ch == '\t') { ch = ' '; } // Consolidate contiguous space chars and strip leading spaces (fPrevCharSpace // starts off as true). if (fPrevCharSpace && ch == ' ') { return -1; } // TODO: Strip trailing WS? Doing this across chunks would require another buffering // layer. In general, trailing WS should have no rendering side effects. Skipping // for now. return ch; }; auto filterWSPreserve = [](SkUnichar ch) -> SkUnichar { // Convert newline and tab chars to space. if (ch == '\n' || ch == '\t') { ch = ' '; } return ch; }; // Stash paints for access from SkShaper callbacks. fCurrentFill = ctx.fillPaint(); fCurrentStroke = ctx.strokePaint(); const auto font = ResolveFont(ctx); fShapeBuffer.reserve(txt.size()); const char* ch_ptr = txt.c_str(); const char* ch_end = ch_ptr + txt.size(); while (ch_ptr < ch_end) { auto ch = SkUTF::NextUTF8(&ch_ptr, ch_end); ch = (xs == SkSVGXmlSpace::kDefault) ? filterWSDefault(ch) : filterWSPreserve(ch); if (ch < 0) { // invalid utf or char filtered out continue; } SkASSERT(fPosResolver); const auto pos = fPosResolver->resolve(fCurrentCharIndex++); // Absolute position adjustments define a new chunk. // (https://www.w3.org/TR/SVG11/text.html#TextLayoutIntroduction) if (pos.has(PosAttrs::kX) || pos.has(PosAttrs::kY)) { this->shapePendingBuffer(ctx, font); this->flushChunk(ctx); // New chunk position. if (pos.has(PosAttrs::kX)) { fChunkPos.fX = pos[PosAttrs::kX]; } if (pos.has(PosAttrs::kY)) { fChunkPos.fY = pos[PosAttrs::kY]; } } fShapeBuffer.append(ch, { { pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0, pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0, }, pos.has(PosAttrs::kRotate) ? SkDegreesToRadians(pos[PosAttrs::kRotate]) : 0, }); fPrevCharSpace = (ch == ' '); } this->shapePendingBuffer(ctx, font); // Note: at this point we have shaped and buffered RunRecs for the current fragment. // The active text chunk continues until an explicit or implicit flush. } SkSVGTextContext::PathData::PathData(const SkSVGRenderContext& ctx, const SkSVGTextPath& tpath) { const auto ref = ctx.findNodeById(tpath.getHref()); if (!ref) { return; } SkContourMeasureIter cmi(ref->asPath(ctx), false); while (sk_sp contour = cmi.next()) { fLength += contour->length(); fContours.push_back(std::move(contour)); } } SkMatrix SkSVGTextContext::PathData::getMatrixAt(float offset) const { if (offset >= 0) { for (const auto& contour : fContours) { const auto contour_len = contour->length(); if (offset < contour_len) { SkMatrix m; return contour->getMatrix(offset, &m) ? m : SkMatrix::I(); } offset -= contour_len; } } // Quick & dirty way to "skip" rendering of glyphs off path. return SkMatrix::Translate(std::numeric_limits::infinity(), std::numeric_limits::infinity()); } SkRSXform SkSVGTextContext::computeGlyphXform(SkGlyphID glyph, const SkFont& font, const SkPoint& glyph_pos, const PositionAdjustment& pos_adjust) const { SkPoint pos = fChunkPos + glyph_pos + pos_adjust.offset + fChunkAdvance * fChunkAlignmentFactor; if (!fPathData) { return SkRSXform::MakeFromRadians(/*scale=*/ 1, pos_adjust.rotation, pos.fX, pos.fY, 0, 0); } // We're in a textPath scope, reposition the glyph on path. // (https://www.w3.org/TR/SVG11/text.html#TextpathLayoutRules) // Path positioning is based on the glyph center (horizontal component). float glyph_width; font.getWidths(&glyph, 1, &glyph_width); auto path_offset = pos.fX + glyph_width * .5f; // In addition to the path matrix, the final glyph matrix also includes: // // -- vertical position adjustment "dy" ("dx" is factored into path_offset) // -- glyph origin adjustment (undoing the glyph center offset above) // -- explicit rotation adjustment (composing with the path glyph rotation) const auto m = fPathData->getMatrixAt(path_offset) * SkMatrix::Translate(-glyph_width * .5f, pos_adjust.offset.fY) * SkMatrix::RotateRad(pos_adjust.rotation); return SkRSXform::Make(m.getScaleX(), m.getSkewY(), m.getTranslateX(), m.getTranslateY()); } void SkSVGTextContext::flushChunk(const SkSVGRenderContext& ctx) { SkTextBlobBuilder blobBuilder; for (const auto& run : fRuns) { const auto& buf = blobBuilder.allocRunRSXform(run.font, SkToInt(run.glyphCount)); std::copy(run.glyphs.get(), run.glyphs.get() + run.glyphCount, buf.glyphs); for (size_t i = 0; i < run.glyphCount; ++i) { buf.xforms()[i] = this->computeGlyphXform(run.glyphs[i], run.font, run.glyphPos[i], run.glyhPosAdjust[i]); } fCallback(ctx, blobBuilder.make(), run.fillPaint.get(), run.strokePaint.get()); } fChunkPos += fChunkAdvance; fChunkAdvance = {0,0}; fChunkAlignmentFactor = ComputeAlignmentFactor(ctx.presentationContext()); fRuns.clear(); } SkShaper::RunHandler::Buffer SkSVGTextContext::runBuffer(const RunInfo& ri) { SkASSERT(ri.glyphCount); fRuns.push_back({ ri.fFont, fCurrentFill.isValid() ? std::make_unique(*fCurrentFill) : nullptr, fCurrentStroke.isValid() ? std::make_unique(*fCurrentStroke) : nullptr, std::make_unique(ri.glyphCount), std::make_unique(ri.glyphCount), std::make_unique(ri.glyphCount), ri.glyphCount, ri.fAdvance, }); // Ensure sufficient space to temporarily fetch cluster information. fShapeClusterBuffer.resize(std::max(fShapeClusterBuffer.size(), ri.glyphCount)); return { fRuns.back().glyphs.get(), fRuns.back().glyphPos.get(), nullptr, fShapeClusterBuffer.data(), fChunkAdvance, }; } void SkSVGTextContext::commitRunBuffer(const RunInfo& ri) { const auto& current_run = fRuns.back(); // stash position adjustments for (size_t i = 0; i < ri.glyphCount; ++i) { const auto utf8_index = fShapeClusterBuffer[i]; current_run.glyhPosAdjust[i] = fShapeBuffer.fUtf8PosAdjust[SkToInt(utf8_index)]; } fChunkAdvance += ri.fAdvance; } void SkSVGTextContext::commitLine() { if (!fShapeBuffer.fUtf8PosAdjust.empty()) { // Offset adjustments are cumulative - only advance the current chunk with the last value. fChunkAdvance += fShapeBuffer.fUtf8PosAdjust.back().offset; } } void SkSVGTextFragment::renderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx, SkSVGXmlSpace xs) const { // N.B.: unlike regular elements, text fragments do not establish a new OBB scope -- they // always defer to the root element for OBB resolution. SkSVGRenderContext localContext(ctx); if (this->onPrepareToRender(&localContext)) { this->onShapeText(localContext, tctx, xs); } } SkPath SkSVGTextFragment::onAsPath(const SkSVGRenderContext&) const { // TODO return SkPath(); } void SkSVGTextContainer::appendChild(sk_sp child) { // Only allow text content child nodes. switch (child->tag()) { case SkSVGTag::kTextLiteral: case SkSVGTag::kTextPath: case SkSVGTag::kTSpan: fChildren.push_back( sk_sp(static_cast(child.release()))); break; default: break; } } void SkSVGTextContainer::onShapeText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx, SkSVGXmlSpace) const { SkASSERT(tctx); const SkSVGTextContext::ScopedPosResolver resolver(*this, ctx.lengthContext(), tctx); for (const auto& frag : fChildren) { // Containers always override xml:space with the local value. frag->renderText(ctx, tctx, this->getXmlSpace()); } } // https://www.w3.org/TR/SVG11/text.html#WhiteSpace template <> bool SkSVGAttributeParser::parse(SkSVGXmlSpace* xs) { static constexpr std::tuple gXmlSpaceMap[] = { {"default" , SkSVGXmlSpace::kDefault }, {"preserve", SkSVGXmlSpace::kPreserve}, }; return this->parseEnumMap(gXmlSpaceMap, xs) && this->parseEOSToken(); } bool SkSVGTextContainer::parseAndSetAttribute(const char* name, const char* value) { return INHERITED::parseAndSetAttribute(name, value) || this->setX(SkSVGAttributeParser::parse>("x", name, value)) || this->setY(SkSVGAttributeParser::parse>("y", name, value)) || this->setDx(SkSVGAttributeParser::parse>("dx", name, value)) || this->setDy(SkSVGAttributeParser::parse>("dy", name, value)) || this->setRotate(SkSVGAttributeParser::parse>("rotate", name, value)) || this->setXmlSpace(SkSVGAttributeParser::parse("xml:space", name, value)); } void SkSVGTextLiteral::onShapeText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx, SkSVGXmlSpace xs) const { SkASSERT(tctx); tctx->shapeFragment(this->getText(), ctx, xs); } void SkSVGText::onRender(const SkSVGRenderContext& ctx) const { const SkSVGTextContext::ShapedTextCallback render_text = [](const SkSVGRenderContext& ctx, const sk_sp& blob, const SkPaint* fill, const SkPaint* stroke) { if (fill) { ctx.canvas()->drawTextBlob(blob, 0, 0, *fill); } if (stroke) { ctx.canvas()->drawTextBlob(blob, 0, 0, *stroke); } }; // Root nodes establish a text layout context. SkSVGTextContext tctx(ctx, render_text); this->onShapeText(ctx, &tctx, this->getXmlSpace()); } SkRect SkSVGText::onObjectBoundingBox(const SkSVGRenderContext& ctx) const { SkRect bounds = SkRect::MakeEmpty(); const SkSVGTextContext::ShapedTextCallback compute_bounds = [&bounds](const SkSVGRenderContext& ctx, const sk_sp& blob, const SkPaint*, const SkPaint*) { if (!blob) { return; } AutoSTArray<64, SkRect> glyphBounds; for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) { glyphBounds.reset(SkToInt(it.glyphCount())); it.font().getBounds(it.glyphs(), it.glyphCount(), glyphBounds.get(), nullptr); SkASSERT(it.positioning() == SkTextBlobRunIterator::kRSXform_Positioning); SkMatrix m; for (uint32_t i = 0; i < it.glyphCount(); ++i) { m.setRSXform(it.xforms()[i]); bounds.join(m.mapRect(glyphBounds[i])); } } }; { SkSVGTextContext tctx(ctx, compute_bounds); this->onShapeText(ctx, &tctx, this->getXmlSpace()); } return bounds; } SkPath SkSVGText::onAsPath(const SkSVGRenderContext& ctx) const { SkPathBuilder builder; const SkSVGTextContext::ShapedTextCallback as_path = [&builder](const SkSVGRenderContext& ctx, const sk_sp& blob, const SkPaint*, const SkPaint*) { if (!blob) { return; } for (SkTextBlobRunIterator it(blob.get()); !it.done(); it.next()) { struct GetPathsCtx { SkPathBuilder& builder; const SkRSXform* xform; } get_paths_ctx {builder, it.xforms()}; it.font().getPaths(it.glyphs(), it.glyphCount(), [](const SkPath* path, const SkMatrix& matrix, void* raw_ctx) { auto* get_paths_ctx = static_cast(raw_ctx); const auto& glyph_rsx = *get_paths_ctx->xform++; if (!path) { return; } SkMatrix glyph_matrix; glyph_matrix.setRSXform(glyph_rsx); glyph_matrix.preConcat(matrix); get_paths_ctx->builder.addPath(path->makeTransform(glyph_matrix)); }, &get_paths_ctx); } }; { SkSVGTextContext tctx(ctx, as_path); this->onShapeText(ctx, &tctx, this->getXmlSpace()); } auto path = builder.detach(); this->mapToParent(&path); return path; } void SkSVGTextPath::onShapeText(const SkSVGRenderContext& ctx, SkSVGTextContext* parent_tctx, SkSVGXmlSpace xs) const { SkASSERT(parent_tctx); // textPath nodes establish a new text layout context. SkSVGTextContext tctx(ctx, parent_tctx->getCallback(), this); this->INHERITED::onShapeText(ctx, &tctx, xs); } bool SkSVGTextPath::parseAndSetAttribute(const char* name, const char* value) { return INHERITED::parseAndSetAttribute(name, value) || this->setHref(SkSVGAttributeParser::parse("xlink:href", name, value)) || this->setStartOffset(SkSVGAttributeParser::parse("startOffset", name, value)); }