[svg] Relative postioning support for text

Introduce support for relative position adjustments [1]:

  - plumb dx, dy attributes
  - extend ScopedPosResolver to also handle the new attributes
  - introduce ShapeBuffer to store both utf8 text and position
    adjustments for shaping (replaces prev 'filtered' array).
  - position adjustments are cumulative (relative adjustments affect
    all following characters)
  - utf8 encoding is variable length; for simplicity, ensure that the
    pos adjustment array and the utf8 array are always the same size by
    repeating the pos adjustment times number of utf8 bytes
  - introduce a temporary buffer for retrieving utf8 cluster information
    from SkShaper
  - post-shaping, use the utf8 cluster info to map back to character
    indices and apply the associated position adjutment

[1] https://www.w3.org/TR/SVG11/text.html#TSpanElementDXAttribute

Bug: skia:10840
Change-Id: Ia9f227f91723400711ff2b5d260976290da1e2e5
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/346636
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 8227cc1..d043f10 100644
--- a/modules/svg/src/SkSVGText.cpp
+++ b/modules/svg/src/SkSVGText.cpp
@@ -118,6 +118,8 @@
     , 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))
 {
     fTextContext->fPosResolver = this;
 }
@@ -139,7 +141,9 @@
         const auto localCharIndex = charIndex - fCharIndexOffset;
 
         const auto hasAllLocal = localCharIndex < fX.size() &&
-                                 localCharIndex < fY.size();
+                                 localCharIndex < fY.size() &&
+                                 localCharIndex < fDx.size() &&
+                                 localCharIndex < fDy.size();
         if (!hasAllLocal && fParent) {
             attrs = fParent->resolve(charIndex);
         }
@@ -150,6 +154,12 @@
         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];
+        }
 
         if (!attrs.hasAny()) {
             // Once we stop producing explicit position data, there is no reason to
@@ -161,6 +171,28 @@
     return attrs;
 }
 
+void SkSVGTextContext::ShapeBuffer::append(SkUnichar ch, SkVector pos) {
+    // relative pos adjustments are cumulative
+    if (!fUtf8PosAdjust.empty()) {
+        pos += fUtf8PosAdjust.back();
+    }
+
+    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 SkFont& font) {
+    // TODO: directionality hints?
+    const auto LTR  = true;
+
+    // Initiate shaping: this will generate a series of runs via callbacks.
+    fShaper->shape(fShapeBuffer.fUtf8.data(), fShapeBuffer.fUtf8.size(),
+                   font, LTR, SK_ScalarMax, this);
+    fShapeBuffer.reset();
+}
+
 SkSVGTextContext::SkSVGTextContext(const SkSVGPresentationContext& pctx, sk_sp<SkFontMgr> fmgr)
     : fShaper(SkShaper::Make(std::move(fmgr)))
     , fChunkPos{ 0, 0 }
@@ -206,17 +238,7 @@
     fCurrentStroke = ctx.strokePaint();
 
     const auto font = ResolveFont(ctx);
-
-    SkSTArray<128, char, true> filtered;
-    filtered.reserve_back(SkToInt(txt.size()));
-
-    auto shapePending = [&filtered, &font, this]() {
-        // TODO: directionality hints?
-        const auto LTR  = true;
-        // Initiate shaping: this will generate a series of runs via callbacks.
-        fShaper->shape(filtered.data(), filtered.size(), font, LTR, SK_ScalarMax, this);
-        filtered.reset();
-    };
+    fShapeBuffer.reserve(txt.size());
 
     const char* ch_ptr = txt.c_str();
     const char* ch_end = ch_ptr + txt.size();
@@ -238,7 +260,7 @@
         // 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)) {
-            shapePending();
+            this->shapePendingBuffer(font);
             this->flushChunk(ctx);
 
             // New chunk position.
@@ -250,15 +272,18 @@
             }
         }
 
-        char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence];
-        filtered.push_back_n(SkToInt(SkUTF::ToUTF8(ch, utf8_buf)), utf8_buf);
+        fShapeBuffer.append(ch, {
+            pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0,
+            pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0,
+        });
 
         fPrevCharSpace = (ch == ' ');
     }
 
-    // Note: at this point we have shaped and buffered the current fragment  The active
-    // text chunk continues until an explicit or implicit flush.
-    shapePending();
+    this->shapePendingBuffer(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.
 }
 
 void SkSVGTextContext::flushChunk(const SkSVGRenderContext& ctx) {
@@ -303,17 +328,28 @@
         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,
-        nullptr,
+        fShapeClusterBuffer.data(),
         fChunkAdvance,
     };
 }
 
 void SkSVGTextContext::commitRunBuffer(const RunInfo& ri) {
-    fChunkAdvance += ri.fAdvance;
+    // apply position adjustments
+    for (size_t i = 0; i < ri.glyphCount; ++i) {
+        const auto utf8_index = fShapeClusterBuffer[i];
+        fRuns.back().glyphPos[i] += fShapeBuffer.fUtf8PosAdjust[SkToInt(utf8_index)];
+    }
+
+    // Position adjustments are cumulative - we only need to advance the current chunk
+    // with the last value.
+    fChunkAdvance += ri.fAdvance + fShapeBuffer.fUtf8PosAdjust.back();
 }
 
 void SkSVGTextFragment::renderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
@@ -369,6 +405,8 @@
     return INHERITED::parseAndSetAttribute(name, value) ||
            this->setX(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("x", name, value)) ||
            this->setY(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("y", name, value)) ||
+           this->setDx(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("dx", name, value)) ||
+           this->setDy(SkSVGAttributeParser::parse<std::vector<SkSVGLength>>("dy", name, value)) ||
            this->setXmlSpace(SkSVGAttributeParser::parse<SkSVGXmlSpace>("xml:space", name, value));
 }
 
diff --git a/modules/svg/src/SkSVGTextPriv.h b/modules/svg/src/SkSVGTextPriv.h
index 9bdbac4..4842ccc 100644
--- a/modules/svg/src/SkSVGTextPriv.h
+++ b/modules/svg/src/SkSVGTextPriv.h
@@ -29,22 +29,26 @@
     // Helper for encoding optional positional attributes.
     class PosAttrs {
     public:
-        // TODO: dx, dy, rotate
+        // TODO: rotate
         enum Attr : size_t {
-            kX = 0,
-            kY = 1,
+            kX  = 0,
+            kY  = 1,
+            kDx = 2,
+            kDy = 3,
         };
 
         float  operator[](Attr a) const { return fStorage[a]; }
         float& operator[](Attr a)       { return fStorage[a]; }
 
         bool has(Attr a) const { return fStorage[a] != kNone; }
-        bool hasAny()    const { return this->has(kX) || this->has(kY); }
+        bool hasAny()    const {
+            return this->has(kX) || this->has(kY) || this->has(kDx) || this->has(kDy);
+        }
 
     private:
         static constexpr auto kNone = std::numeric_limits<float>::infinity();
 
-        float fStorage[2] = { kNone, kNone };
+        float fStorage[4] = { kNone, kNone, kNone, kNone };
     };
 
     // Helper for cascading position attribute resolution (x, y, dx, dy, rotate) [1]:
@@ -71,7 +75,9 @@
         const ScopedPosResolver* fParent;          // parent resolver (fallback)
         const size_t             fCharIndexOffset; // start index for the current resolver
         const std::vector<float> fX,
-                                 fY;
+                                 fY,
+                                 fDx,
+                                 fDy;
 
         // cache for the last known index with explicit positioning
         mutable size_t           fLastPosIndex = std::numeric_limits<size_t>::max();
@@ -87,6 +93,23 @@
     void flushChunk(const SkSVGRenderContext& ctx);
 
 private:
+    struct ShapeBuffer {
+        SkSTArray<128, char    , true> fUtf8;
+        SkSTArray<128, SkVector, true> fUtf8PosAdjust; // per-utf8-char cumulative pos adjustments
+
+        void reserve(size_t size) {
+            fUtf8.reserve_back(SkToInt(size));
+            fUtf8PosAdjust.reserve_back(SkToInt(size));
+        }
+
+        void reset() {
+            fUtf8.reset();
+            fUtf8PosAdjust.reset();
+        }
+
+        void append(SkUnichar, SkVector);
+    };
+
     struct RunRec {
         SkFont                       font;
         std::unique_ptr<SkPaint>     fillPaint,
@@ -97,6 +120,8 @@
         SkVector                     advance;
     };
 
+    void shapePendingBuffer(const SkFont&);
+
     // SkShaper callbacks
     void beginLine() override {}
     void runInfo(const RunInfo&) override {}
@@ -110,6 +135,11 @@
     std::vector<RunRec>             fRuns;
     const ScopedPosResolver*        fPosResolver = nullptr;
 
+    // shaper state
+    ShapeBuffer                     fShapeBuffer;
+    std::vector<uint32_t>           fShapeClusterBuffer;
+
+    // chunk state
     SkPoint                         fChunkPos;             // current text chunk position
     SkVector                        fChunkAdvance = {0,0}; // cumulative advance
     float                           fChunkAlignmentFactor; // current chunk alignment