[svg] xml:space support
Add xml:space attribute and implement related white space filters.
(https://www.w3.org/TR/SVG11/text.html#WhiteSpace)
Bug: skia:10840
Change-Id: I52fda50fae1cd7cf8b0dd7c1a2ee2e667ffa947b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/342299
Reviewed-by: Tyler Denniston <tdenniston@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Florin Malita <fmalita@google.com>
diff --git a/modules/svg/src/SkSVGText.cpp b/modules/svg/src/SkSVGText.cpp
index bf7cf0a..ee1705e 100644
--- a/modules/svg/src/SkSVGText.cpp
+++ b/modules/svg/src/SkSVGText.cpp
@@ -17,6 +17,7 @@
#include "modules/skshaper/include/SkShaper.h"
#include "modules/svg/include/SkSVGRenderContext.h"
#include "modules/svg/include/SkSVGValue.h"
+#include "src/utils/SkUTF.h"
namespace {
@@ -118,7 +119,63 @@
// Queues codepoints for rendering.
void appendFragment(const SkString& txt, const SkSVGRenderContext& ctx) {
- // TODO: xml::space filtering
+ // https://www.w3.org/TR/SVG11/text.html#WhiteSpace
+ // https://www.w3.org/TR/2008/REC-xml-20081126/#NT-S
+ auto filterWSDefault = [this](SkUnichar ch) -> SkUnichar {
+ // Remove all newline chars.
+ if (ch == '\n') {
+ return -1;
+ }
+
+ // Convert tab chars to space.
+ if (ch == '\t') {
+ ch = ' ';
+ }
+
+ // Consolidate contiguous space chars and strip leading spaces (fPrevCharSpace
+ // starts off as true).
+ if (fPrevCharSpace && ch == ' ') {
+ return -1;
+ }
+
+ // TODO: Strip trailing WS? Doing this across chunks would require another buffering
+ // layer. In general, trailing WS should have no rendering side effects. Skipping
+ // for now.
+ return ch;
+ };
+ auto filterWSPreserve = [](SkUnichar ch) -> SkUnichar {
+ // Convert newline and tab chars to space.
+ if (ch == '\n' || ch == '\t') {
+ ch = ' ';
+ }
+ return ch;
+ };
+
+ const auto xmlSpace = ctx.getXmlSpace();
+
+ SkSTArray<128, char, true> filtered;
+ filtered.reserve_back(SkToInt(txt.size()));
+
+ const char* ch_ptr = txt.c_str();
+ const char* ch_end = ch_ptr + txt.size();
+
+ while (ch_ptr < ch_end) {
+ auto ch = SkUTF::NextUTF8(&ch_ptr, ch_end);
+ ch = (xmlSpace == SkSVGXmlSpace::kDefault)
+ ? filterWSDefault(ch)
+ : filterWSPreserve(ch);
+
+ if (ch < 0) {
+ // invalid utf or char filtered out
+ continue;
+ }
+
+ char utf8_buf[SkUTF::kMaxBytesInUTF8Sequence];
+ filtered.push_back_n(SkToInt(SkUTF::ToUTF8(ch, utf8_buf)), utf8_buf);
+
+ fPrevCharSpace = (ch == ' ');
+ }
+
// TODO: absolute positioned chars => chunk breaks
// Stash paints for access from SkShaper callbacks.
@@ -129,7 +186,7 @@
const auto LTR = true;
// Initiate shaping: this will generate a series of runs via callbacks.
- fShaper->shape(txt.c_str(), txt.size(), ResolveFont(ctx), LTR, SK_ScalarMax, this);
+ fShaper->shape(filtered.data(), filtered.size(), ResolveFont(ctx), LTR, SK_ScalarMax, this);
}
// Perform actual rendering for queued codepoints.
@@ -214,6 +271,8 @@
// cached for access from SkShaper callbacks.
const SkPaint* fCurrentFill;
const SkPaint* fCurrentStroke;
+
+ bool fPrevCharSpace = true; // WS filter state
};
void SkSVGTextContainer::appendChild(sk_sp<SkSVGNode> child) {
@@ -229,10 +288,27 @@
}
}
+bool SkSVGTextContainer::onPrepareToRender(SkSVGRenderContext* ctx) const {
+ ctx->setXmlSpace(this->getXmlSpace());
+ return this->INHERITED::onPrepareToRender(ctx);
+}
+
+// https://www.w3.org/TR/SVG11/text.html#WhiteSpace
+template <>
+bool SkSVGAttributeParser::parse(SkSVGXmlSpace* xs) {
+ static constexpr std::tuple<const char*, SkSVGXmlSpace> gXmlSpaceMap[] = {
+ {"default" , SkSVGXmlSpace::kDefault },
+ {"preserve", SkSVGXmlSpace::kPreserve},
+ };
+
+ return this->parseEnumMap(gXmlSpaceMap, xs) && this->parseEOSToken();
+}
+
bool SkSVGTextContainer::parseAndSetAttribute(const char* name, const char* value) {
return INHERITED::parseAndSetAttribute(name, value) ||
this->setX(SkSVGAttributeParser::parse<SkSVGLength>("x", name, value)) ||
- this->setY(SkSVGAttributeParser::parse<SkSVGLength>("y", name, value));
+ this->setY(SkSVGAttributeParser::parse<SkSVGLength>("y", name, value)) ||
+ this->setXmlSpace(SkSVGAttributeParser::parse<SkSVGXmlSpace>("xml:space", name, value));
}
void SkSVGText::onRender(const SkSVGRenderContext& ctx) const {