[svg] Text rotate support
Implement support for text 'rotate' attribute:
https://www.w3.org/TR/SVG11/text.html#TSpanElementRotateAttribute.
Unlike other character-positioning attributes (x/y/dx/dy), rotate
- is not cumulative
- only applies to its respective node scope (does not affect other
fragments in the current chunk)
- has different padding semantics: if there are fewer rotate
values than characters, the remaining characters use the last
specified value from the closest ancestor
To the last point, we now have to discriminate three states:
- unspecified (default -> 0)
- explicit value for the given character index
- implicit value (last value in the closest ancestor)
Local implicit values override implicit ancestor values -- but not
explicit ancestor values.
High level changes:
- plumb 'rotate' attribute
- expand per-character position info (ShapeBuffer) to include rotation
- expand per-glyph position info (RunRec) to include rotation
- expand PosAttrs to include rotation and add specific inheritance
rules (see above)
- pass computed rotation values to RSX blob buffers
Bug: skia:10840
Change-Id: Ia19ec5e8bb6fea06d49a9bd72ace575c2ffd100e
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/348877
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 f96ba3f..79ac161 100644
--- a/modules/svg/src/SkSVGText.cpp
+++ b/modules/svg/src/SkSVGText.cpp
@@ -121,6 +121,7 @@
, fY(ResolveLengths(lctx, txt.getY(), SkSVGLengthContext::LengthType::kVertical))
, fDx(ResolveLengths(lctx, txt.getDx(), SkSVGLengthContext::LengthType::kHorizontal))
, fDy(ResolveLengths(lctx, txt.getDy(), SkSVGLengthContext::LengthType::kVertical))
+ , fRotate(txt.getRotate())
{
fTextContext->fPosResolver = this;
}
@@ -144,7 +145,8 @@
const auto hasAllLocal = localCharIndex < fX.size() &&
localCharIndex < fY.size() &&
localCharIndex < fDx.size() &&
- localCharIndex < fDy.size();
+ localCharIndex < fDy.size() &&
+ localCharIndex < fRotate.size();
if (!hasAllLocal && fParent) {
attrs = fParent->resolve(charIndex);
}
@@ -162,6 +164,32 @@
attrs[PosAttrs::kDy] = fDy[localCharIndex];
}
+ // Rotation semantics are interestingly different [1]:
+ //
+ // - values are not cumulative
+ // - if explicit values are present at any level in the ancestor chain, those take
+ // precedence (closest ancestor)
+ // - last specified value applies to all remaining chars (closest ancestor)
+ // - these rules apply at node scope (not chunk scope)
+ //
+ // This means we need to discriminate between explicit rotation (rotate value provided for
+ // current char) and implicit rotation (ancestor has some values - but not for the requested
+ // char - we use the last specified value).
+ //
+ // [1] https://www.w3.org/TR/SVG11/text.html#TSpanElementRotateAttribute
+ if (!fRotate.empty()) {
+ if (localCharIndex < fRotate.size()) {
+ // Explicit rotation value overrides anything in the ancestor chain.
+ attrs[PosAttrs::kRotate] = fRotate[localCharIndex];
+ attrs.setImplicitRotate(false);
+ } else if (!attrs.has(PosAttrs::kRotate) || attrs.isImplicitRotate()){
+ // Local implicit rotation (last specified value) overrides ancestor implicit
+ // rotation.
+ attrs[PosAttrs::kRotate] = fRotate.back();
+ attrs.setImplicitRotate(true);
+ }
+ }
+
if (!attrs.hasAny()) {
// Once we stop producing explicit position data, there is no reason to
// continue trying for higher indices. We can suppress future lookups.
@@ -172,10 +200,10 @@
return attrs;
}
-void SkSVGTextContext::ShapeBuffer::append(SkUnichar ch, SkVector pos) {
+void SkSVGTextContext::ShapeBuffer::append(SkUnichar ch, PositionAdjustment pos) {
// relative pos adjustments are cumulative
if (!fUtf8PosAdjust.empty()) {
- pos += fUtf8PosAdjust.back();
+ pos.offset += fUtf8PosAdjust.back().offset;
}
char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence];
@@ -274,8 +302,11 @@
}
fShapeBuffer.append(ch, {
- pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0,
- pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0,
+ {
+ pos.has(PosAttrs::kDx) ? pos[PosAttrs::kDx] : 0,
+ pos.has(PosAttrs::kDy) ? pos[PosAttrs::kDy] : 0,
+ },
+ pos.has(PosAttrs::kRotate) ? pos[PosAttrs::kRotate] : 0,
});
fPrevCharSpace = (ch == ' ');
@@ -298,9 +329,8 @@
std::copy(run.glyphs.get(), run.glyphs.get() + run.glyphCount, buf.glyphs);
for (size_t i = 0; i < run.glyphCount; ++i) {
const auto& pos = run.glyphPos[i];
- const auto rotation = 0.0f; // TODO
- buf.xforms()[i] = SkRSXform::MakeFromRadians(/*scale=*/1,
- rotation,
+ buf.xforms()[i] = SkRSXform::MakeFromRadians(/*scale=*/ 1,
+ SkDegreesToRadians(run.glyphRot[i]),
pos.fX, pos.fY, 0, 0);
}
@@ -331,6 +361,7 @@
fCurrentStroke ? std::make_unique<SkPaint>(*fCurrentStroke) : nullptr,
std::make_unique<SkGlyphID[]>(ri.glyphCount),
std::make_unique<SkPoint[] >(ri.glyphCount),
+ std::make_unique<float[] >(ri.glyphCount),
ri.glyphCount,
ri.fAdvance,
});
@@ -348,15 +379,20 @@
}
void SkSVGTextContext::commitRunBuffer(const RunInfo& ri) {
+ const auto& current_run = fRuns.back();
+
// 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)];
+ const auto& pos = fShapeBuffer.fUtf8PosAdjust[SkToInt(utf8_index)];
+
+ current_run.glyphPos[i] += pos.offset;
+ current_run.glyphRot[i] = pos.rotation;
}
// Position adjustments are cumulative - we only need to advance the current chunk
// with the last value.
- fChunkAdvance += ri.fAdvance + fShapeBuffer.fUtf8PosAdjust.back();
+ fChunkAdvance += ri.fAdvance + fShapeBuffer.fUtf8PosAdjust.back().offset;
}
void SkSVGTextFragment::renderText(const SkSVGRenderContext& ctx, SkSVGTextContext* tctx,
@@ -414,6 +450,9 @@
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->setRotate(SkSVGAttributeParser::parse<std::vector<SkSVGNumberType>>("rotate",
+ name,
+ value)) ||
this->setXmlSpace(SkSVGAttributeParser::parse<SkSVGXmlSpace>("xml:space", name, value));
}