[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:
parent
f4a5e5d180
commit
735ac97eb4
@ -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)
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user