[svg] TextPath support

  - introduce SkSVGTextPath and update the text render logic to
    instantiate new SkSVGTextContexts in onRenderText() overrides
    instead of renderText() root -- this is to observe spec semantics
    [1] requiring <text> and <textPath> to always start a new chunk,
    regardless of their relative nesting.
  - expand SkSVGTextContext to also store PathData when used in the
    scope of a textPath
  - PathData caches SkContourMeasures, for path position lookup
  - update flushChunk() to apply path glyph adjustments [2]:

    * the horizontal glyph position (including relative offset dx),
      adjusted for the glyph center yields a path offset
    * if the offset is outside the path range, the glyph is skipped
    * otherwise the position is determined based on the path matrix
      at the computed offset

   - to support the logic above, the chunk starting position is no
     longer used as a global blob offset but instead is folded into
     individual glyph RSXforms (and the blob always draws at {0,0})

[1] https://www.w3.org/TR/SVG11/text.html#TextLayout
[2] https://www.w3.org/TR/SVG11/text.html#TextpathLayoutRules

Bug: skia:10840
Change-Id: I462eada7c086646afdc1bc84f08ec2368613f1c0
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/349397
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Tyler Denniston <tdenniston@google.com>
diff --git a/modules/svg/src/SkSVGText.cpp b/modules/svg/src/SkSVGText.cpp
index 65bbeb1..37f604f 100644
--- a/modules/svg/src/SkSVGText.cpp
+++ b/modules/svg/src/SkSVGText.cpp
@@ -10,6 +10,7 @@
 #include <limits>
 
 #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"
@@ -222,11 +223,37 @@
     fShapeBuffer.reset();
 }
 
-SkSVGTextContext::SkSVGTextContext(const SkSVGPresentationContext& pctx, sk_sp<SkFontMgr> fmgr)
-    : fShaper(SkShaper::Make(std::move(fmgr)))
-    , fChunkPos{ 0, 0 }
-    , fChunkAlignmentFactor(ComputeAlignmentFactor(pctx))
-{}
+SkSVGTextContext::SkSVGTextContext(const SkSVGRenderContext& ctx, const SkSVGTextPath* tpath)
+    : fRenderContext(ctx)
+    , fShaper(SkShaper::Make(ctx.fontMgr()))
+    , fChunkAlignmentFactor(ComputeAlignmentFactor(ctx.presentationContext()))
+{
+    if (tpath) {
+        fPathData = std::make_unique<PathData>(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 <length> 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::appendFragment(const SkString& txt, const SkSVGRenderContext& ctx,
                                       SkSVGXmlSpace xs) {
@@ -306,7 +333,7 @@
                 pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0,
                 pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0,
             },
-            pos.has(PosAttrs::kRotate) ? pos[PosAttrs::kRotate] : 0,
+            pos.has(PosAttrs::kRotate) ? SkDegreesToRadians(pos[PosAttrs::kRotate]) : 0,
         });
 
         fPrevCharSpace = (ch == ' ');
@@ -318,32 +345,86 @@
     // The active text chunk continues until an explicit or implicit flush.
 }
 
-void SkSVGTextContext::flushChunk(const SkSVGRenderContext& ctx) {
-    // The final rendering offset is determined by cumulative chunk advances and alignment.
-    const auto pos = fChunkPos + fChunkAdvance * fChunkAlignmentFactor;
+SkSVGTextContext::PathData::PathData(const SkSVGRenderContext& ctx, const SkSVGTextPath& tpath)
+{
+    const auto ref = ctx.findNodeById(tpath.getHref().fIRI);
+    if (!ref) {
+        return;
+    }
 
+    SkContourMeasureIter cmi(ref->asPath(ctx), false);
+    while (sk_sp<SkContourMeasure> 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<float>::infinity(),
+                               std::numeric_limits<float>::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) {
-            const auto& pos_adjust = run.glyhPosAdjust[i];
-
-            const auto pos = run.glyphPos[i] + pos_adjust.offset;
-            buf.xforms()[i] = SkRSXform::MakeFromRadians(/*scale=*/ 1,
-                                                         SkDegreesToRadians(pos_adjust.rotation),
-                                                         pos.fX, pos.fY, 0, 0);
+            buf.xforms()[i] = this->computeGlyphXform(run.glyphs[i],
+                                                      run.font,
+                                                      run.glyphPos[i],
+                                                      run.glyhPosAdjust[i]);
         }
 
         // Technically, blobs with compatible paints could be merged --
         // but likely not worth the effort.
         const auto blob = blobBuilder.make();
         if (run.fillPaint) {
-            ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.fillPaint);
+            ctx.canvas()->drawTextBlob(blob, 0, 0, *run.fillPaint);
         }
         if (run.strokePaint) {
-            ctx.canvas()->drawTextBlob(blob, pos.fX, pos.fY, *run.strokePaint);
+            ctx.canvas()->drawTextBlob(blob, 0, 0, *run.strokePaint);
         }
     }
 
@@ -413,6 +494,7 @@
     switch (child->tag()) {
     case SkSVGTag::kText:
     case SkSVGTag::kTextLiteral:
+    case SkSVGTag::kTextPath:
     case SkSVGTag::kTSpan:
         fChildren.push_back(
             sk_sp<SkSVGTextFragment>(static_cast<SkSVGTextFragment*>(child.release())));
@@ -456,12 +538,7 @@
 }
 
 void SkSVGTextContainer::onRender(const SkSVGRenderContext& ctx) const {
-    // Root text nodes establish a new text layout context.
-    SkSVGTextContext tctx(ctx.presentationContext(), ctx.fontMgr());
-
-    this->onRenderText(ctx, &tctx, this->getXmlSpace());
-
-    tctx.flushChunk(ctx);
+    this->onRenderText(ctx, nullptr, this->getXmlSpace());
 }
 
 void SkSVGTextLiteral::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
@@ -470,3 +547,25 @@
 
     tctx->appendFragment(this->getText(), ctx, xs);
 }
+
+void SkSVGText::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext*,
+                             SkSVGXmlSpace xs) const {
+    // Root text nodes establish a new text layout context.
+    SkSVGTextContext tctx(ctx);
+
+    this->INHERITED::onRenderText(ctx, &tctx, xs);
+}
+
+void SkSVGTextPath::onRenderText(const SkSVGRenderContext& ctx, SkSVGTextContext*,
+                                 SkSVGXmlSpace xs) const {
+    // Root text nodes establish a new text layout context.
+    SkSVGTextContext tctx(ctx, this);
+
+    this->INHERITED::onRenderText(ctx, &tctx, xs);
+}
+
+bool SkSVGTextPath::parseAndSetAttribute(const char* name, const char* value) {
+    return INHERITED::parseAndSetAttribute(name, value) ||
+        this->setHref(SkSVGAttributeParser::parse<SkSVGIRI>("xlink:href", name, value)) ||
+        this->setStartOffset(SkSVGAttributeParser::parse<SkSVGLength>("startOffset", name, value));
+}