[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/include/SkSVGAttributeParser.h b/modules/svg/include/SkSVGAttributeParser.h
index 41f772f..ecaab8a 100644
--- a/modules/svg/include/SkSVGAttributeParser.h
+++ b/modules/svg/include/SkSVGAttributeParser.h
@@ -8,6 +8,8 @@
#ifndef SkSVGAttributeParser_DEFINED
#define SkSVGAttributeParser_DEFINED
+#include <vector>
+
#include "include/private/SkNoncopyable.h"
#include "modules/svg/include/SkSVGTypes.h"
#include "src/core/SkTLazy.h"
@@ -108,6 +110,9 @@
template <typename Func, typename T>
bool parseParenthesized(const char* prefix, Func, T* result);
+ template <typename T>
+ bool parseList(std::vector<T>*);
+
template <typename T, typename TArray>
bool parseEnumMap(const TArray& arr, T* result) {
for (size_t i = 0; i < SK_ARRAY_COUNT(arr); ++i) {
diff --git a/modules/svg/include/SkSVGText.h b/modules/svg/include/SkSVGText.h
index 1e2dd2a..4fbdc1c 100644
--- a/modules/svg/include/SkSVGText.h
+++ b/modules/svg/include/SkSVGText.h
@@ -39,6 +39,7 @@
SVG_ATTR(Y, std::vector<SkSVGLength>, {})
SVG_ATTR(Dx, std::vector<SkSVGLength>, {})
SVG_ATTR(Dy, std::vector<SkSVGLength>, {})
+ SVG_ATTR(Rotate, std::vector<SkSVGNumberType>, {})
SVG_ATTR(XmlSpace, SkSVGXmlSpace, SkSVGXmlSpace::kDefault)
diff --git a/modules/svg/src/SkSVGAttributeParser.cpp b/modules/svg/src/SkSVGAttributeParser.cpp
index 2a408e8..1f8cb66 100644
--- a/modules/svg/src/SkSVGAttributeParser.cpp
+++ b/modules/svg/src/SkSVGAttributeParser.cpp
@@ -5,8 +5,6 @@
* found in the LICENSE file.
*/
-#include <vector>
-
#include "include/private/SkTPin.h"
#include "include/utils/SkParse.h"
#include "modules/svg/include/SkSVGAttributeParser.h"
@@ -920,20 +918,30 @@
}
// https://www.w3.org/TR/SVG11/types.html#DataTypeCoordinates
-template <>
-bool SkSVGAttributeParser::parse(std::vector<SkSVGLength>* lengths) {
- SkASSERT(lengths->empty());
+template <typename T>
+bool SkSVGAttributeParser::parseList(std::vector<T>* vals) {
+ SkASSERT(vals->empty());
- SkSVGLength length;
+ T v;
for (;;) {
- if (!this->parse(&length)) {
+ if (!this->parse(&v)) {
break;
}
- lengths->push_back(length);
+ vals->push_back(v);
this->parseCommaWspToken();
}
- return !lengths->empty() && this->parseEOSToken();
+ return !vals->empty() && this->parseEOSToken();
+}
+
+template <>
+bool SkSVGAttributeParser::parse(std::vector<SkSVGLength>* lengths) {
+ return this->parseList(lengths);
+}
+
+template <>
+bool SkSVGAttributeParser::parse(std::vector<SkSVGNumberType>* numbers) {
+ return this->parseList(numbers);
}
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));
}
diff --git a/modules/svg/src/SkSVGTextPriv.h b/modules/svg/src/SkSVGTextPriv.h
index 4842ccc..2238b94 100644
--- a/modules/svg/src/SkSVGTextPriv.h
+++ b/modules/svg/src/SkSVGTextPriv.h
@@ -31,10 +31,11 @@
public:
// TODO: rotate
enum Attr : size_t {
- kX = 0,
- kY = 1,
- kDx = 2,
- kDy = 3,
+ kX = 0,
+ kY = 1,
+ kDx = 2,
+ kDy = 3,
+ kRotate = 4,
};
float operator[](Attr a) const { return fStorage[a]; }
@@ -42,13 +43,21 @@
bool has(Attr a) const { return fStorage[a] != kNone; }
bool hasAny() const {
- return this->has(kX) || this->has(kY) || this->has(kDx) || this->has(kDy);
+ return this->has(kX)
+ || this->has(kY)
+ || this->has(kDx)
+ || this->has(kDy)
+ || this->has(kRotate);
}
+ void setImplicitRotate(bool imp) { fImplicitRotate = imp; }
+ bool isImplicitRotate() const { return fImplicitRotate; }
+
private:
static constexpr auto kNone = std::numeric_limits<float>::infinity();
- float fStorage[4] = { kNone, kNone, kNone, kNone };
+ float fStorage[5] = { kNone, kNone, kNone, kNone, kNone };
+ bool fImplicitRotate = false;
};
// Helper for cascading position attribute resolution (x, y, dx, dy, rotate) [1]:
@@ -71,13 +80,14 @@
PosAttrs resolve(size_t charIndex) const;
private:
- SkSVGTextContext* fTextContext;
- const ScopedPosResolver* fParent; // parent resolver (fallback)
- const size_t fCharIndexOffset; // start index for the current resolver
- const std::vector<float> fX,
- fY,
- fDx,
- fDy;
+ SkSVGTextContext* fTextContext;
+ const ScopedPosResolver* fParent; // parent resolver (fallback)
+ const size_t fCharIndexOffset; // start index for the current resolver
+ const std::vector<float> fX,
+ fY,
+ fDx,
+ fDy;
+ const std::vector<float>& fRotate;
// cache for the last known index with explicit positioning
mutable size_t fLastPosIndex = std::numeric_limits<size_t>::max();
@@ -93,9 +103,15 @@
void flushChunk(const SkSVGRenderContext& ctx);
private:
+ struct PositionAdjustment {
+ SkVector offset;
+ float rotation;
+ };
+
struct ShapeBuffer {
- SkSTArray<128, char , true> fUtf8;
- SkSTArray<128, SkVector, true> fUtf8PosAdjust; // per-utf8-char cumulative pos adjustments
+ SkSTArray<128, char , true> fUtf8;
+ // per-utf8-char cumulative pos adjustments
+ SkSTArray<128, PositionAdjustment, true> fUtf8PosAdjust;
void reserve(size_t size) {
fUtf8.reserve_back(SkToInt(size));
@@ -107,7 +123,7 @@
fUtf8PosAdjust.reset();
}
- void append(SkUnichar, SkVector);
+ void append(SkUnichar, PositionAdjustment);
};
struct RunRec {
@@ -116,6 +132,7 @@
strokePaint;
std::unique_ptr<SkGlyphID[]> glyphs;
std::unique_ptr<SkPoint[]> glyphPos;
+ std::unique_ptr<float[]> glyphRot;
size_t glyphCount;
SkVector advance;
};