[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