[SVGDevice] Text whitespace unittest
Plumb SkDOM as needed to make it suitable for an SkXMLWriter backend.
Also fix a potential null typeface issue in
SkSVGDevice::AutoElement::addTextAttributes().
R=reed@google.com,mtklein@google.com
Review URL: https://codereview.chromium.org/940283002
diff --git a/gyp/tests.gypi b/gyp/tests.gypi
index 6b4a9e4..fdd892a 100644
--- a/gyp/tests.gypi
+++ b/gyp/tests.gypi
@@ -203,6 +203,7 @@
'../tests/StrokeTest.cpp',
'../tests/StrokerTest.cpp',
'../tests/SurfaceTest.cpp',
+ '../tests/SVGDeviceTest.cpp',
'../tests/TArrayTest.cpp',
'../tests/TDPQueueTest.cpp',
'../tests/Time.cpp',
diff --git a/include/xml/SkDOM.h b/include/xml/SkDOM.h
index e0bb744..df5766f 100644
--- a/include/xml/SkDOM.h
+++ b/include/xml/SkDOM.h
@@ -17,6 +17,9 @@
struct SkDOMNode;
struct SkDOMAttr;
+class SkDOMParser;
+class SkXMLParser;
+
class SkDOM {
public:
SkDOM();
@@ -32,6 +35,9 @@
const Node* getRootNode() const;
+ SkXMLParser* beginParsing();
+ const Node* finishParsing();
+
enum Type {
kElement_Type,
kText_Type
@@ -82,8 +88,10 @@
SkDEBUGCODE(static void UnitTest();)
private:
- SkChunkAlloc fAlloc;
- Node* fRoot;
+ SkChunkAlloc fAlloc;
+ Node* fRoot;
+ SkAutoTDelete<SkDOMParser> fParser;
+
friend class AttrIter;
friend class SkDOMParser;
};
diff --git a/src/svg/SkSVGDevice.cpp b/src/svg/SkSVGDevice.cpp
index da52fac..a56a631 100644
--- a/src/svg/SkSVGDevice.cpp
+++ b/src/svg/SkSVGDevice.cpp
@@ -523,14 +523,6 @@
void SkSVGDevice::AutoElement::addTextAttributes(const SkPaint& paint) {
this->addAttribute("font-size", paint.getTextSize());
- SkTypeface::Style style = paint.getTypeface()->style();
- if (style & SkTypeface::kItalic) {
- this->addAttribute("font-style", "italic");
- }
- if (style & SkTypeface::kBold) {
- this->addAttribute("font-weight", "bold");
- }
-
if (const char* textAlign = svg_text_align(paint.getTextAlign())) {
this->addAttribute("text-anchor", textAlign);
}
@@ -538,7 +530,17 @@
SkString familyName;
SkTHashSet<SkString, hash_family_string> familySet;
SkAutoTUnref<const SkTypeface> tface(paint.getTypeface() ?
- SkRef(paint.getTypeface()) : SkTypeface::RefDefault(style));
+ SkRef(paint.getTypeface()) : SkTypeface::RefDefault());
+
+ SkASSERT(tface);
+ SkTypeface::Style style = tface->style();
+ if (style & SkTypeface::kItalic) {
+ this->addAttribute("font-style", "italic");
+ }
+ if (style & SkTypeface::kBold) {
+ this->addAttribute("font-weight", "bold");
+ }
+
SkAutoTUnref<SkTypeface::LocalizedStrings> familyNameIter(tface->createFamilyNameIterator());
SkTypeface::LocalizedString familyString;
while (familyNameIter->next(&familyString)) {
diff --git a/src/xml/SkDOM.cpp b/src/xml/SkDOM.cpp
index 9854608..eb1bc09 100644
--- a/src/xml/SkDOM.cpp
+++ b/src/xml/SkDOM.cpp
@@ -8,11 +8,12 @@
#include "SkDOM.h"
+#include "SkStream.h"
+#include "SkXMLWriter.h"
/////////////////////////////////////////////////////////////////////////
#include "SkXMLParser.h"
-
bool SkXMLParser::parse(const SkDOM& dom, const SkDOMNode* node)
{
const char* elemName = dom.getName(node);
@@ -199,19 +200,22 @@
}
class SkDOMParser : public SkXMLParser {
- bool fNeedToFlush;
public:
SkDOMParser(SkChunkAlloc* chunk) : SkXMLParser(&fParserError), fAlloc(chunk)
{
+ fAlloc->reset();
fRoot = NULL;
fLevel = 0;
fNeedToFlush = true;
}
SkDOM::Node* getRoot() const { return fRoot; }
SkXMLParserError fParserError;
+
protected:
void flushAttributes()
{
+ SkASSERT(fLevel > 0);
+
int attrCount = fAttrs.count();
SkDOM::Node* node = (SkDOM::Node*)fAlloc->alloc(sizeof(SkDOM::Node) + attrCount * sizeof(SkDOM::Attr),
@@ -220,7 +224,7 @@
node->fName = fElemName;
node->fFirstChild = NULL;
node->fAttrCount = SkToU16(attrCount);
- node->fType = SkDOM::kElement_Type;
+ node->fType = fElemType;
if (fRoot == NULL)
{
@@ -240,24 +244,20 @@
fAttrs.reset();
}
- virtual bool onStartElement(const char elem[])
- {
- if (fLevel > 0 && fNeedToFlush)
- this->flushAttributes();
- fNeedToFlush = true;
- fElemName = dupstr(fAlloc, elem);
- ++fLevel;
+
+ bool onStartElement(const char elem[]) override {
+ this->startCommon(elem, SkDOM::kElement_Type);
return false;
}
- virtual bool onAddAttribute(const char name[], const char value[])
- {
+
+ bool onAddAttribute(const char name[], const char value[]) override {
SkDOM::Attr* attr = fAttrs.append();
attr->fName = dupstr(fAlloc, name);
attr->fValue = dupstr(fAlloc, value);
return false;
}
- virtual bool onEndElement(const char elem[])
- {
+
+ bool onEndElement(const char elem[]) override {
--fLevel;
if (fNeedToFlush)
this->flushAttributes();
@@ -279,20 +279,40 @@
parent->fFirstChild = prev;
return false;
}
+
+ bool onText(const char text[], int len) override {
+ SkString str(text, len);
+ this->startCommon(str.c_str(), SkDOM::kText_Type);
+ this->SkDOMParser::onEndElement(str.c_str());
+
+ return false;
+ }
+
private:
+ void startCommon(const char elem[], SkDOM::Type type) {
+ if (fLevel > 0 && fNeedToFlush)
+ this->flushAttributes();
+
+ fNeedToFlush = true;
+ fElemName = dupstr(fAlloc, elem);
+ fElemType = type;
+ ++fLevel;
+ }
+
SkTDArray<SkDOM::Node*> fParentStack;
- SkChunkAlloc* fAlloc;
- SkDOM::Node* fRoot;
+ SkChunkAlloc* fAlloc;
+ SkDOM::Node* fRoot;
+ bool fNeedToFlush;
// state needed for flushAttributes()
SkTDArray<SkDOM::Attr> fAttrs;
char* fElemName;
+ SkDOM::Type fElemType;
int fLevel;
};
const SkDOM::Node* SkDOM::build(const char doc[], size_t len)
{
- fAlloc.reset();
SkDOMParser parser(&fAlloc);
if (!parser.parse(doc, len))
{
@@ -310,6 +330,11 @@
static void walk_dom(const SkDOM& dom, const SkDOM::Node* node, SkXMLParser* parser)
{
const char* elem = dom.getName(node);
+ if (dom.getType(node) == SkDOM::kText_Type) {
+ SkASSERT(dom.countChildren(node) == 0);
+ parser->text(elem, SkToInt(strlen(elem)));
+ return;
+ }
parser->startElement(elem);
@@ -331,7 +356,6 @@
const SkDOM::Node* SkDOM::copy(const SkDOM& dom, const SkDOM::Node* node)
{
- fAlloc.reset();
SkDOMParser parser(&fAlloc);
walk_dom(dom, node, &parser);
@@ -340,6 +364,21 @@
return fRoot;
}
+SkXMLParser* SkDOM::beginParsing() {
+ SkASSERT(!fParser);
+ fParser.reset(SkNEW_ARGS(SkDOMParser, (&fAlloc)));
+
+ return fParser.get();
+}
+
+const SkDOM::Node* SkDOM::finishParsing() {
+ SkASSERT(fParser);
+ fRoot = fParser->getRoot();
+ fParser.free();
+
+ return fRoot;
+}
+
//////////////////////////////////////////////////////////////////////////
int SkDOM::countChildren(const Node* node, const char elem[]) const
@@ -427,41 +466,14 @@
#ifdef SK_DEBUG
-static void tab(int level)
-{
- while (--level >= 0)
- SkDebugf("\t");
-}
-
void SkDOM::dump(const Node* node, int level) const
{
if (node == NULL)
node = this->getRootNode();
- if (node)
- {
- tab(level);
- SkDebugf("<%s", this->getName(node));
- const Attr* attr = node->attrs();
- const Attr* stop = attr + node->fAttrCount;
- for (; attr < stop; attr++)
- SkDebugf(" %s=\"%s\"", attr->fName, attr->fValue);
-
- const Node* child = this->getFirstChild(node);
- if (child)
- {
- SkDebugf(">\n");
- while (child)
- {
- this->dump(child, level+1);
- child = this->getNextSibling(child);
- }
- tab(level);
- SkDebugf("</%s>\n", node->fName);
- }
- else
- SkDebugf("/>\n");
- }
+ SkDebugWStream debugStream;
+ SkXMLStreamWriter xmlWriter(&debugStream);
+ xmlWriter.writeDOM(*this, node, false);
}
void SkDOM::UnitTest()
diff --git a/src/xml/SkXMLWriter.cpp b/src/xml/SkXMLWriter.cpp
index 62e9668..7a1b042 100644
--- a/src/xml/SkXMLWriter.cpp
+++ b/src/xml/SkXMLWriter.cpp
@@ -169,7 +169,14 @@
{
if (!skipRoot)
{
- w->startElement(dom.getName(node));
+ const char* elem = dom.getName(node);
+ if (dom.getType(node) == SkDOM::kText_Type) {
+ SkASSERT(dom.countChildren(node) == 0);
+ w->addText(elem, strlen(elem));
+ return;
+ }
+
+ w->startElement(elem);
SkDOM::AttrIter iter(dom, node);
const char* name;
diff --git a/tests/SVGDeviceTest.cpp b/tests/SVGDeviceTest.cpp
new file mode 100644
index 0000000..c973f8b
--- /dev/null
+++ b/tests/SVGDeviceTest.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkCanvas.h"
+#include "SkData.h"
+#include "SkDOM.h"
+#include "SkParse.h"
+#include "SkStream.h"
+#include "SkSVGCanvas.h"
+#include "SkXMLWriter.h"
+#include "Test.h"
+
+#include <string.h>
+
+namespace {
+
+void check_text_node(skiatest::Reporter* reporter,
+ const SkDOM& dom,
+ const SkDOM::Node* root,
+ const SkPoint& offset,
+ unsigned scalarsPerPos,
+ const char* expected) {
+ if (root == NULL) {
+ ERRORF(reporter, "root element not found.");
+ return;
+ }
+
+ const SkDOM::Node* textElem = dom.getFirstChild(root, "text");
+ if (textElem == NULL) {
+ ERRORF(reporter, "<text> element not found.");
+ return;
+ }
+ REPORTER_ASSERT(reporter, dom.getType(textElem) == SkDOM::kElement_Type);
+
+ const SkDOM::Node* textNode= dom.getFirstChild(textElem);
+ REPORTER_ASSERT(reporter, textNode != NULL);
+ if (textNode != NULL) {
+ REPORTER_ASSERT(reporter, dom.getType(textNode) == SkDOM::kText_Type);
+ REPORTER_ASSERT(reporter, strcmp(expected, dom.getName(textNode)) == 0);
+ }
+
+ int textLen = SkToInt(strlen(expected));
+
+ const char* x = dom.findAttr(textElem, "x");
+ REPORTER_ASSERT(reporter, x != NULL);
+ if (x != NULL) {
+ int xposCount = (scalarsPerPos < 1) ? 1 : textLen;
+ REPORTER_ASSERT(reporter, SkParse::Count(x) == xposCount);
+
+ SkAutoTMalloc<SkScalar> xpos(xposCount);
+ SkParse::FindScalars(x, xpos.get(), xposCount);
+ if (scalarsPerPos < 1) {
+ REPORTER_ASSERT(reporter, xpos[0] == offset.x());
+ } else {
+ for (int i = 0; i < xposCount; ++i) {
+ REPORTER_ASSERT(reporter, xpos[i] == SkIntToScalar(expected[i]));
+ }
+ }
+ }
+
+ const char* y = dom.findAttr(textElem, "y");
+ REPORTER_ASSERT(reporter, y != NULL);
+ if (y != NULL) {
+ int yposCount = (scalarsPerPos < 2) ? 1 : textLen;
+ REPORTER_ASSERT(reporter, SkParse::Count(y) == yposCount);
+
+ SkAutoTMalloc<SkScalar> ypos(yposCount);
+ SkParse::FindScalars(y, ypos.get(), yposCount);
+ if (scalarsPerPos < 2) {
+ REPORTER_ASSERT(reporter, ypos[0] == offset.y());
+ } else {
+ for (int i = 0; i < yposCount; ++i) {
+ REPORTER_ASSERT(reporter, ypos[i] == -SkIntToScalar(expected[i]));
+ }
+ }
+ }
+}
+
+void test_whitespace_pos(skiatest::Reporter* reporter,
+ const char* txt,
+ const char* expected) {
+ size_t len = strlen(txt);
+
+ SkDOM dom;
+ SkPaint paint;
+ SkPoint offset = SkPoint::Make(10, 20);
+
+ {
+ SkXMLParserWriter writer(dom.beginParsing());
+ SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+ &writer));
+ svgCanvas->drawText(txt, len, offset.x(), offset.y(), paint);
+ }
+ check_text_node(reporter, dom, dom.finishParsing(), offset, 0, expected);
+
+ {
+ SkAutoTMalloc<SkScalar> xpos(len);
+ for (int i = 0; i < SkToInt(len); ++i) {
+ xpos[i] = SkIntToScalar(txt[i]);
+ }
+
+ SkXMLParserWriter writer(dom.beginParsing());
+ SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+ &writer));
+ svgCanvas->drawPosTextH(txt, len, xpos, offset.y(), paint);
+ }
+ check_text_node(reporter, dom, dom.finishParsing(), offset, 1, expected);
+
+ {
+ SkAutoTMalloc<SkPoint> pos(len);
+ for (int i = 0; i < SkToInt(len); ++i) {
+ pos[i] = SkPoint::Make(SkIntToScalar(txt[i]), -SkIntToScalar(txt[i]));
+ }
+
+ SkXMLParserWriter writer(dom.beginParsing());
+ SkAutoTUnref<SkCanvas> svgCanvas(SkSVGCanvas::Create(SkRect::MakeWH(100, 100),
+ &writer));
+ svgCanvas->drawPosText(txt, len, pos, paint);
+ }
+ check_text_node(reporter, dom, dom.finishParsing(), offset, 2, expected);
+}
+
+}
+
+DEF_TEST(SVGDevice_whitespace_pos, reporter) {
+ static const struct {
+ const char* tst_in;
+ const char* tst_out;
+ } tests[] = {
+ { "abcd" , "abcd" },
+ { "ab cd" , "ab cd" },
+ { "ab \t\t cd", "ab cd" },
+ { " abcd" , "abcd" },
+ { " abcd" , "abcd" },
+ { " \t\t abcd", "abcd" },
+ { "abcd " , "abcd " }, // we allow one trailing whitespace char
+ { "abcd " , "abcd " }, // because it makes no difference and
+ { "abcd\t " , "abcd\t" }, // simplifies the implementation
+ { "\t\t \t ab \t\t \t cd \t\t \t ", "ab cd " },
+ };
+
+ for (unsigned i = 0; i < SK_ARRAY_COUNT(tests); ++i) {
+ test_whitespace_pos(reporter, tests[i].tst_in, tests[i].tst_out);
+ }
+}