[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>
This commit is contained in:
Florin Malita 2020-12-22 11:23:32 -05:00 committed by Skia Commit-Bot
parent f4a5e5d180
commit 735ac97eb4
3 changed files with 96 additions and 26 deletions

View File

@ -37,6 +37,8 @@ class SkSVGTextContainer : public SkSVGTextFragment {
public:
SVG_ATTR(X, std::vector<SkSVGLength>, {})
SVG_ATTR(Y, std::vector<SkSVGLength>, {})
SVG_ATTR(Dx, std::vector<SkSVGLength>, {})
SVG_ATTR(Dy, std::vector<SkSVGLength>, {})
SVG_ATTR(XmlSpace, SkSVGXmlSpace, SkSVGXmlSpace::kDefault)

View File

@ -118,6 +118,8 @@ SkSVGTextContext::ScopedPosResolver::ScopedPosResolver(const SkSVGTextContainer&
, 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 @@ SkSVGTextContext::PosAttrs SkSVGTextContext::ScopedPosResolver::resolve(size_t c
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 @@ SkSVGTextContext::PosAttrs SkSVGTextContext::ScopedPosResolver::resolve(size_t c
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 @@ SkSVGTextContext::PosAttrs SkSVGTextContext::ScopedPosResolver::resolve(size_t c
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 @@ void SkSVGTextContext::appendFragment(const SkString& txt, const SkSVGRenderCont
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 @@ void SkSVGTextContext::appendFragment(const SkString& txt, const SkSVGRenderCont
// 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 @@ void SkSVGTextContext::appendFragment(const SkString& txt, const SkSVGRenderCont
}
}
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 @@ SkShaper::RunHandler::Buffer SkSVGTextContext::runBuffer(const RunInfo& ri) {
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 @@ bool SkSVGTextContainer::parseAndSetAttribute(const char* name, const char* valu
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));
}

View File

@ -29,22 +29,26 @@ public:
// 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 @@ public:
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 @@ public:
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 @@ private:
SkVector advance;
};
void shapePendingBuffer(const SkFont&);
// SkShaper callbacks
void beginLine() override {}
void runInfo(const RunInfo&) override {}
@ -110,6 +135,11 @@ private:
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