1 /*
2 * Copyright (c) 2017 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 */
18
19 #include "KoSvgTextShapeMarkupConverter.h"
20
21 #include "klocalizedstring.h"
22 #include "kis_assert.h"
23 #include "kis_debug.h"
24
25 #include <QBuffer>
26 #include <QStringList>
27 #include <QXmlStreamReader>
28 #include <QXmlStreamWriter>
29
30 #include <QTextBlock>
31 #include <QTextLayout>
32 #include <QTextLine>
33 #include <QFont>
34
35 #include <QStack>
36
37 #include <KoSvgTextShape.h>
38 #include <KoXmlWriter.h>
39 #include <KoXmlReader.h>
40 #include <KoDocumentResourceManager.h>
41
42 #include <SvgParser.h>
43 #include <SvgWriter.h>
44 #include <SvgUtil.h>
45 #include <SvgSavingContext.h>
46 #include <SvgGraphicContext.h>
47
48 #include <html/HtmlSavingContext.h>
49 #include <html/HtmlWriter.h>
50
51 #include "kis_dom_utils.h"
52 #include <boost/optional.hpp>
53
54 #include <FlakeDebug.h>
55
56 struct KoSvgTextShapeMarkupConverter::Private {
PrivateKoSvgTextShapeMarkupConverter::Private57 Private(KoSvgTextShape *_shape) : shape(_shape) {}
58
59 KoSvgTextShape *shape;
60
61 QStringList errors;
62 QStringList warnings;
63
clearErrorsKoSvgTextShapeMarkupConverter::Private64 void clearErrors() {
65 errors.clear();
66 warnings.clear();
67 }
68 };
69
KoSvgTextShapeMarkupConverter(KoSvgTextShape * shape)70 KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape)
71 : d(new Private(shape))
72 {
73 }
74
~KoSvgTextShapeMarkupConverter()75 KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter()
76 {
77 }
78
convertToSvg(QString * svgText,QString * stylesText)79 bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText)
80 {
81 d->clearErrors();
82
83 QBuffer shapesBuffer;
84 QBuffer stylesBuffer;
85
86 shapesBuffer.open(QIODevice::WriteOnly);
87 stylesBuffer.open(QIODevice::WriteOnly);
88
89 {
90 SvgSavingContext savingContext(shapesBuffer, stylesBuffer);
91 savingContext.setStrippedTextMode(true);
92 SvgWriter writer({d->shape});
93 writer.saveDetached(savingContext);
94 }
95
96 shapesBuffer.close();
97 stylesBuffer.close();
98
99 *svgText = QString::fromUtf8(shapesBuffer.data());
100 *stylesText = QString::fromUtf8(stylesBuffer.data());
101
102 return true;
103 }
104
convertFromSvg(const QString & svgText,const QString & stylesText,const QRectF & boundsInPixels,qreal pixelsPerInch)105 bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText,
106 const QRectF &boundsInPixels, qreal pixelsPerInch)
107 {
108
109 debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch;
110
111 d->clearErrors();
112
113 QString errorMessage;
114 int errorLine = 0;
115 int errorColumn = 0;
116
117 const QString fullText = QString("<svg>\n%1\n%2\n</svg>\n").arg(stylesText).arg(svgText);
118
119 KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn);
120 if (doc.isNull()) {
121 d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage);
122 return false;
123 }
124
125 d->shape->resetTextShape();
126
127 KoDocumentResourceManager resourceManager;
128 SvgParser parser(&resourceManager);
129 parser.setResolution(boundsInPixels, pixelsPerInch);
130
131 KoXmlElement root = doc.documentElement();
132 KoXmlNode node = root.firstChild();
133
134 bool textNodeFound = false;
135
136 for (; !node.isNull(); node = node.nextSibling()) {
137 KoXmlElement el = node.toElement();
138 if (el.isNull()) continue;
139
140 if (el.tagName() == "defs") {
141 parser.parseDefsElement(el);
142 }
143 else if (el.tagName() == "text") {
144 if (textNodeFound) {
145 d->errors << i18n("More than one 'text' node found!");
146 return false;
147 }
148
149 KoShape *shape = parser.parseTextElement(el, d->shape);
150 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false);
151 textNodeFound = true;
152 break;
153 } else {
154 d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName());
155 return false;
156 }
157 }
158
159 if (!textNodeFound) {
160 d->errors << i18n("No \'text\' node found!");
161 return false;
162 }
163
164 return true;
165
166 }
167
convertToHtml(QString * htmlText)168 bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText)
169 {
170 d->clearErrors();
171
172 QBuffer shapesBuffer;
173 shapesBuffer.open(QIODevice::WriteOnly);
174 {
175 HtmlWriter writer({d->shape});
176 if (!writer.save(shapesBuffer)) {
177 d->errors = writer.errors();
178 d->warnings = writer.warnings();
179 return false;
180 }
181 }
182
183 shapesBuffer.close();
184
185 *htmlText = QString(shapesBuffer.data());
186
187 debugFlake << "\t\t" << *htmlText;
188
189 return true;
190 }
191
convertFromHtml(const QString & htmlText,QString * svgText,QString * styles)192 bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles)
193 {
194
195 debugFlake << ">>>>>>>>>>>" << htmlText;
196
197 QBuffer svgBuffer;
198 svgBuffer.open(QIODevice::WriteOnly);
199
200 QXmlStreamReader htmlReader(htmlText);
201 QXmlStreamWriter svgWriter(&svgBuffer);
202
203 svgWriter.setAutoFormatting(true);
204
205 QStringRef elementName;
206
207 bool newLine = false;
208 int lineCount = 0;
209 QString bodyEm = "1em";
210 QString em;
211 QString p("p");
212 //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags
213 QString previousStyleString;
214 QString appendStyle;
215
216 while (!htmlReader.atEnd()) {
217 QXmlStreamReader::TokenType token = htmlReader.readNext();
218 switch (token) {
219 case QXmlStreamReader::StartElement:
220 {
221 newLine = false;
222 if (htmlReader.name() == "br") {
223 debugFlake << "\tdoing br";
224 svgWriter.writeEndElement();
225 elementName = QStringRef(&p);
226 em = bodyEm;
227 appendStyle = previousStyleString;
228 }
229 else {
230 elementName = htmlReader.name();
231 em = "";
232 }
233
234 if (elementName == "body") {
235 debugFlake << "\tstart Element" << elementName;
236 svgWriter.writeStartElement("text");
237 appendStyle = QString();
238 }
239 else if (elementName == "p") {
240 // new line
241 debugFlake << "\t\tstart Element" << elementName;
242 svgWriter.writeStartElement("tspan");
243 newLine = true;
244 if (em.isEmpty()) {
245 em = bodyEm;
246 appendStyle = QString();
247 }
248 lineCount++;
249 }
250 else if (elementName == "span") {
251 debugFlake << "\tstart Element" << elementName;
252 svgWriter.writeStartElement("tspan");
253 appendStyle = QString();
254 }
255 else if (elementName == "b" || elementName == "strong") {
256 debugFlake << "\tstart Element" << elementName;
257 svgWriter.writeStartElement("tspan");
258 appendStyle = "font-weight:700;";
259 }
260 else if (elementName == "i" || elementName == "em") {
261 debugFlake << "\tstart Element" << elementName;
262 svgWriter.writeStartElement("tspan");
263 appendStyle = "font-style:italic;";
264 }
265 else if (elementName == "u") {
266 debugFlake << "\tstart Element" << elementName;
267 svgWriter.writeStartElement("tspan");
268 appendStyle = "text-decoration:underline";
269 }
270
271 QXmlStreamAttributes attributes = htmlReader.attributes();
272
273 QString textAlign;
274 if (attributes.hasAttribute("align")) {
275 textAlign = attributes.value("align").toString();
276 }
277
278 if (attributes.hasAttribute("style")) {
279 QString filteredStyles;
280 QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction letter-spacing").split(" ");
281 QStringList styles = attributes.value("style").toString().split(";");
282 for(int i=0; i<styles.size(); i++) {
283 QStringList style = QString(styles.at(i)).split(":");
284 debugFlake<<style.at(0);
285 if (svgStyles.contains(QString(style.at(0)).trimmed())) {
286 filteredStyles.append(styles.at(i)+";");
287 }
288
289 if (QString(style.at(0)).trimmed() == "color") {
290 filteredStyles.append(" fill:"+style.at(1)+";");
291 }
292
293 if (QString(style.at(0)).trimmed() == "text-align") {
294 textAlign = QString(style.at(1)).trimmed();
295 }
296
297 if (QString(style.at(0)).trimmed() == "line-height"){
298 if (style.at(1).contains("%")) {
299 double percentage = QString(style.at(1)).remove("%").toDouble();
300 em = QString::number(percentage/100.0)+"em";
301 } else if(style.at(1).contains("em")) {
302 em = style.at(1);
303 } else if(style.at(1).contains("px")) {
304 em = style.at(1);
305 }
306 if (elementName == "body") {
307 bodyEm = em;
308 }
309 }
310 }
311
312 if (textAlign == "center") {
313 filteredStyles.append(" text-anchor:middle;");
314 } else if (textAlign == "right") {
315 filteredStyles.append(" text-anchor:end;");
316 } else if (textAlign == "left"){
317 filteredStyles.append(" text-anchor:start;");
318 }
319
320 filteredStyles.append(appendStyle);
321
322 if (!filteredStyles.isEmpty()) {
323 svgWriter.writeAttribute("style", filteredStyles);
324 previousStyleString = filteredStyles;
325 }
326
327
328 }
329 if (newLine && lineCount > 1) {
330 debugFlake << "\t\tAdvancing to the next line";
331 svgWriter.writeAttribute("x", "0");
332 svgWriter.writeAttribute("dy", em);
333 }
334 break;
335 }
336 case QXmlStreamReader::EndElement:
337 {
338 if (htmlReader.name() == "br") break;
339 if (elementName == "p" || elementName == "span" || elementName == "body") {
340 debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")";
341 svgWriter.writeEndElement();
342 }
343 break;
344 }
345 case QXmlStreamReader::Characters:
346 {
347 if (elementName == "style") {
348 *styles = htmlReader.text().toString();
349 }
350 else {
351 if (!htmlReader.isWhitespace()) {
352 debugFlake << "\tCharacters:" << htmlReader.text();
353 svgWriter.writeCharacters(htmlReader.text().toString());
354 }
355 }
356 break;
357 }
358 default:
359 ;
360 }
361 }
362
363 if (htmlReader.hasError()) {
364 d->errors << htmlReader.errorString();
365 return false;
366 }
367 if (svgWriter.hasError()) {
368 d->errors << i18n("Unknown error writing SVG text element");
369 return false;
370 }
371
372 *svgText = QString::fromUtf8(svgBuffer.data());
373 return true;
374 }
375
postCorrectBlockHeight(QTextDocument * doc,qreal currLineAscent,qreal prevLineAscent,qreal prevLineDescent,int prevBlockCursorPosition,qreal currentBlockAbsoluteLineOffset)376 void postCorrectBlockHeight(QTextDocument *doc,
377 qreal currLineAscent,
378 qreal prevLineAscent,
379 qreal prevLineDescent,
380 int prevBlockCursorPosition,
381 qreal currentBlockAbsoluteLineOffset)
382 {
383 KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0);
384
385 QTextCursor postCorrectionCursor(doc);
386 postCorrectionCursor.setPosition(prevBlockCursorPosition);
387 if (!postCorrectionCursor.isNull()) {
388 const qreal relativeLineHeight =
389 ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) /
390 (prevLineAscent + prevLineDescent)) * 100.0;
391
392 QTextBlockFormat format = postCorrectionCursor.blockFormat();
393 format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight);
394 postCorrectionCursor.setBlockFormat(format);
395 postCorrectionCursor = QTextCursor();
396 }
397 }
398
findMostCommonFormat(const QList<QTextFormat> & allFormats)399 QTextFormat findMostCommonFormat(const QList<QTextFormat> &allFormats)
400 {
401 QTextCharFormat mostCommonFormat;
402
403 QSet<int> propertyIds;
404
405 /**
406 * Get all existing property ids
407 */
408 Q_FOREACH (const QTextFormat &format, allFormats) {
409 const QMap<int, QVariant> formatProperties = format.properties();
410 Q_FOREACH (int id, formatProperties.keys()) {
411 propertyIds.insert(id);
412 }
413 }
414
415 /**
416 * Filter out properties that do not exist in some formats. Otherwise, the
417 * global format may override the default value used in these formats
418 * (and yes, we do not have access to the default values to use them
419 * in difference calculation algorithm
420 */
421 Q_FOREACH (const QTextFormat &format, allFormats) {
422 for (auto it = propertyIds.begin(); it != propertyIds.end();) {
423 if (!format.hasProperty(*it)) {
424 it = propertyIds.erase(it);
425 } else {
426 ++it;
427 }
428 }
429 if (propertyIds.isEmpty()) break;
430 }
431
432 if (!propertyIds.isEmpty()) {
433 QMap<int, QMap<QVariant, int>> propertyFrequency;
434
435 /**
436 * Calculate the frequency of values used in *all* the formats
437 */
438 Q_FOREACH (const QTextFormat &format, allFormats) {
439 const QMap<int, QVariant> formatProperties = format.properties();
440
441 Q_FOREACH (int id, propertyIds) {
442 KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id));
443 propertyFrequency[id][formatProperties.value(id)]++;
444 }
445 }
446
447 /**
448 * Add the most popular property value to the set of most common properties
449 */
450 for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) {
451 const int id = it.key();
452 const QMap<QVariant, int> allValues = it.value();
453
454 int maxCount = 0;
455 QVariant maxValue;
456
457 for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) {
458 if (valIt.value() > maxCount) {
459 maxCount = valIt.value();
460 maxValue = valIt.key();
461 }
462 }
463
464 KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0);
465 mostCommonFormat.setProperty(id, maxValue);
466 }
467
468 }
469
470 return mostCommonFormat;
471 }
472
calcLineWidth(const QTextBlock & block)473 qreal calcLineWidth(const QTextBlock &block)
474 {
475 const QString blockText = block.text();
476
477 QTextLayout lineLayout;
478 lineLayout.setText(blockText);
479 lineLayout.setFont(block.charFormat().font());
480 lineLayout.setFormats(block.textFormats());
481 lineLayout.setTextOption(block.layout()->textOption());
482
483 lineLayout.beginLayout();
484 QTextLine fullLine = lineLayout.createLine();
485 if (!fullLine.isValid()) {
486 fullLine.setNumColumns(blockText.size());
487 }
488 lineLayout.endLayout();
489
490 return lineLayout.boundingRect().width();
491 }
492
convertDocumentToSvg(const QTextDocument * doc,QString * svgText)493 bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText)
494 {
495 QBuffer svgBuffer;
496 svgBuffer.open(QIODevice::WriteOnly);
497
498 QXmlStreamWriter svgWriter(&svgBuffer);
499
500 // disable auto-formatting to avoid axtra spaces appearing here and there
501 svgWriter.setAutoFormatting(false);
502
503
504 qreal maxParagraphWidth = 0.0;
505 QTextCharFormat mostCommonCharFormat;
506 QTextBlockFormat mostCommonBlockFormat;
507
508 struct LineInfo {
509 LineInfo() {}
510 LineInfo(QTextBlock _block, int _numSkippedLines)
511 : block(_block), numSkippedLines(_numSkippedLines)
512 {}
513
514 QTextBlock block;
515 int numSkippedLines = 0;
516 };
517
518
519 QVector<LineInfo> lineInfoList;
520
521 {
522 QTextBlock block = doc->begin();
523
524 QList<QTextFormat> allCharFormats;
525 QList<QTextFormat> allBlockFormats;
526
527 int numSequentialEmptyLines = 0;
528
529 while (block.isValid()) {
530 if (!block.text().trimmed().isEmpty()) {
531 lineInfoList.append(LineInfo(block, numSequentialEmptyLines));
532 numSequentialEmptyLines = 0;
533
534 maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block));
535
536 allBlockFormats.append(block.blockFormat());
537 Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) {
538 QTextFormat format = range.format;
539 allCharFormats.append(format);
540 }
541 } else {
542 numSequentialEmptyLines++;
543 }
544
545 block = block.next();
546 }
547
548 mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat();
549 mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat();
550 }
551
552 //Okay, now the actual writing.
553
554 QTextBlock block = doc->begin();
555 Q_UNUSED(block);
556
557 svgWriter.writeStartElement("text");
558
559 {
560 const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat);
561 if (!commonTextStyle.isEmpty()) {
562 svgWriter.writeAttribute("style", commonTextStyle);
563 }
564 }
565
566 int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight();
567 int prevBlockLineType = mostCommonBlockFormat.lineHeightType();
568 qreal prevBlockAscent = 0.0;
569 qreal prevBlockDescent= 0.0;
570
571 Q_FOREACH (const LineInfo &info, lineInfoList) {
572 QTextBlock block = info.block;
573
574 const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat();
575 QTextCharFormat blockCharFormatDiff = QTextCharFormat();
576 const QVector<QTextLayout::FormatRange> formats = block.textFormats();
577 if (formats.size()==1) {
578 blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat();
579 }
580
581 const QTextLayout *layout = block.layout();
582 const QTextLine line = layout->lineAt(0);
583
584 svgWriter.writeStartElement("tspan");
585
586 const QString text = block.text();
587
588 /**
589 * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text
590 * directionality, so the user expects his text being saved to SVG with the same
591 * directionality. Just emulate behavior of direction="auto", which is not supported by
592 * SVG 1.1
593 *
594 * BUG: 392064
595 */
596
597 bool isRightToLeft = false;
598 for (int i = 0; i < text.size(); i++) {
599 const QChar ch = text[i];
600 if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) {
601 isRightToLeft = true;
602 break;
603 } else if (ch.direction() == QChar::DirL) {
604 break;
605 }
606 }
607
608
609 if (isRightToLeft) {
610 svgWriter.writeAttribute("direction", "rtl");
611 svgWriter.writeAttribute("unicode-bidi", "embed");
612 }
613
614 {
615 const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff);
616 if (!blockStyleString.isEmpty()) {
617 svgWriter.writeAttribute("style", blockStyleString);
618 }
619 }
620
621 /**
622 * The alignment rule will be inverted while rendering the text in the text shape
623 * (according to the standard the alignment is defined not by "left" or "right",
624 * but by "start" and "end", which inverts for rtl text)
625 */
626 Qt::Alignment blockAlignment = block.blockFormat().alignment();
627 if (isRightToLeft) {
628 if (blockAlignment & Qt::AlignLeft) {
629 blockAlignment &= ~Qt::AlignLeft;
630 blockAlignment |= Qt::AlignRight;
631 } else if (blockAlignment & Qt::AlignRight) {
632 blockAlignment &= ~Qt::AlignRight;
633 blockAlignment |= Qt::AlignLeft;
634 }
635 }
636
637 if (blockAlignment & Qt::AlignHCenter) {
638 svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt");
639 } else if (blockAlignment & Qt::AlignRight) {
640 svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt");
641 } else {
642 svgWriter.writeAttribute("x", "0");
643 }
644
645 if (block.blockNumber() > 0) {
646 const qreal lineHeightPt =
647 line.ascent() - prevBlockAscent +
648 (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0;
649
650 const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt;
651 svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt");
652 }
653
654 prevBlockRelativeLineSpacing =
655 blockFormatDiff.hasProperty(QTextFormat::LineHeight) ?
656 blockFormatDiff.lineHeight() :
657 mostCommonBlockFormat.lineHeight();
658
659 prevBlockLineType =
660 blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ?
661 blockFormatDiff.lineHeightType() :
662 mostCommonBlockFormat.lineHeightType();
663
664 if (prevBlockLineType == QTextBlockFormat::SingleHeight) {
665 //single line will set lineHeight to 100%
666 prevBlockRelativeLineSpacing = 100;
667 }
668
669 prevBlockAscent = line.ascent();
670 prevBlockDescent = line.descent();
671
672
673 if (formats.size()>1) {
674 QStringList texts;
675 QVector<QTextCharFormat> charFormats;
676 for (int f=0; f<formats.size(); f++) {
677 QString chunk;
678 for (int c = 0; c<formats.at(f).length; c++) {
679 chunk.append(text.at(formats.at(f).start+c));
680 }
681 texts.append(chunk);
682 charFormats.append(formats.at(f).format);
683 }
684
685 for (int c = 0; c<texts.size(); c++) {
686 QTextCharFormat diff = formatDifference(charFormats.at(c), mostCommonCharFormat).toCharFormat();
687 const QString subStyle = style(diff, QTextBlockFormat(), mostCommonCharFormat);
688 if (!subStyle.isEmpty()) {
689 svgWriter.writeStartElement("tspan");
690 svgWriter.writeAttribute("style", subStyle);
691 svgWriter.writeCharacters(texts.at(c));
692 svgWriter.writeEndElement();
693 } else {
694 svgWriter.writeCharacters(texts.at(c));
695 }
696 }
697
698 } else {
699 svgWriter.writeCharacters(text);
700 //check format against
701 }
702 svgWriter.writeEndElement();
703 }
704 svgWriter.writeEndElement();//text root element.
705
706 if (svgWriter.hasError()) {
707 d->errors << i18n("Unknown error writing SVG text element");
708 return false;
709 }
710 *svgText = QString::fromUtf8(svgBuffer.data()).trimmed();
711 return true;
712 }
713
parseTextAttributes(const QXmlStreamAttributes & elementAttributes,QTextCharFormat & charFormat,QTextBlockFormat & blockFormat)714 void parseTextAttributes(const QXmlStreamAttributes &elementAttributes,
715 QTextCharFormat &charFormat,
716 QTextBlockFormat &blockFormat)
717 {
718 QString styleString;
719
720 // we convert all the presentation attributes into styles
721 QString presentationAttributes;
722 for (int a = 0; a < elementAttributes.size(); a++) {
723 if (elementAttributes.at(a).name() != "style") {
724 presentationAttributes
725 .append(elementAttributes.at(a).name().toString())
726 .append(":")
727 .append(elementAttributes.at(a).value().toString())
728 .append(";");
729 }
730 }
731
732 if (presentationAttributes.endsWith(";")) {
733 presentationAttributes.chop(1);
734 }
735
736 if (elementAttributes.hasAttribute("style")) {
737 styleString = elementAttributes.value("style").toString();
738 if (styleString.endsWith(";")) {
739 styleString.chop(1);
740 }
741 }
742
743 if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) {
744 //add attributes to parse them as part of the style.
745 styleString.append(";")
746 .append(presentationAttributes);
747 QStringList styles = styleString.split(";");
748 QVector<QTextFormat> formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat);
749
750 charFormat = formats.at(0).toCharFormat();
751 blockFormat = formats.at(1).toBlockFormat();
752 }
753 }
754
convertSvgToDocument(const QString & svgText,QTextDocument * doc)755 bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc)
756 {
757 QXmlStreamReader svgReader(svgText.trimmed());
758 doc->clear();
759 QTextCursor cursor(doc);
760
761 struct BlockFormatRecord {
762 BlockFormatRecord() {}
763 BlockFormatRecord(QTextBlockFormat _blockFormat,
764 QTextCharFormat _charFormat)
765 : blockFormat(_blockFormat),
766 charFormat(_charFormat)
767 {}
768
769 QTextBlockFormat blockFormat;
770 QTextCharFormat charFormat;
771 };
772
773 QStack<BlockFormatRecord> formatStack;
774 formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat()));
775
776 qreal currBlockAbsoluteLineOffset = 0.0;
777 int prevBlockCursorPosition = -1;
778 qreal prevLineDescent = 0.0;
779 qreal prevLineAscent = 0.0;
780 boost::optional<qreal> previousBlockAbsoluteXOffset = boost::none;
781
782 while (!svgReader.atEnd()) {
783 QXmlStreamReader::TokenType token = svgReader.readNext();
784 switch (token) {
785 case QXmlStreamReader::StartElement:
786 {
787 bool newBlock = false;
788 QTextBlockFormat newBlockFormat;
789 QTextCharFormat newCharFormat;
790 qreal absoluteLineOffset = 1.0;
791
792 // fetch format of the parent block and make it default
793 if (formatStack.size() >= 2) {
794 newBlockFormat = formatStack[formatStack.size() - 2].blockFormat;
795 newCharFormat = formatStack[formatStack.size() - 2].charFormat;
796 }
797
798 {
799 const QXmlStreamAttributes elementAttributes = svgReader.attributes();
800 parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat);
801
802 // mnemonic for a newline is (dy != 0 && x == 0)
803
804 boost::optional<qreal> blockAbsoluteXOffset = boost::none;
805
806 if (elementAttributes.hasAttribute("x")) {
807 QString xString = elementAttributes.value("x").toString();
808 if (xString.contains("pt")) {
809 xString = xString.remove("pt").trimmed();
810 }
811 blockAbsoluteXOffset = KisDomUtils::toDouble(xString);
812 }
813
814
815 if (previousBlockAbsoluteXOffset &&
816 blockAbsoluteXOffset &&
817 qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) &&
818 svgReader.name() != "text" &&
819 elementAttributes.hasAttribute("dy")) {
820
821 QString dyString = elementAttributes.value("dy").toString();
822 if (dyString.contains("pt")) {
823 dyString = dyString.remove("pt").trimmed();
824 }
825
826 KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text"));
827
828 absoluteLineOffset = KisDomUtils::toDouble(dyString);
829 newBlock = absoluteLineOffset > 0;
830 }
831
832 if (elementAttributes.hasAttribute("x")) {
833 previousBlockAbsoluteXOffset = blockAbsoluteXOffset;
834 }
835 }
836
837 //hack
838 doc->setTextWidth(100);
839 doc->setTextWidth(-1);
840
841 if (newBlock && absoluteLineOffset > 0) {
842 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false);
843 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() > 0, false);
844
845 QTextLine line = cursor.block().layout()->lineAt(0);
846
847 if (prevBlockCursorPosition >= 0) {
848 postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent,
849 prevBlockCursorPosition, currBlockAbsoluteLineOffset);
850 }
851
852 prevBlockCursorPosition = cursor.position();
853 prevLineAscent = line.ascent();
854 prevLineDescent = line.descent();
855 currBlockAbsoluteLineOffset = absoluteLineOffset;
856
857 cursor.insertBlock();
858 cursor.setCharFormat(formatStack.top().charFormat);
859 cursor.setBlockFormat(formatStack.top().blockFormat);
860 }
861
862
863
864 cursor.mergeCharFormat(newCharFormat);
865 cursor.mergeBlockFormat(newBlockFormat);
866
867 formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat()));
868
869 break;
870 }
871 case QXmlStreamReader::EndElement:
872 {
873 if (svgReader.name() != "text") {
874 formatStack.pop();
875 KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; }
876
877 cursor.setCharFormat(formatStack.top().charFormat);
878 cursor.setBlockFormat(formatStack.top().blockFormat);
879 }
880 break;
881 }
882 case QXmlStreamReader::Characters:
883 {
884 if (!svgReader.isWhitespace()) {
885 cursor.insertText(svgReader.text().toString());
886 }
887
888 break;
889 }
890 default:
891 break;
892 }
893 }
894
895 if (prevBlockCursorPosition >= 0) {
896 QTextLine line = cursor.block().layout()->lineAt(0);
897 postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent,
898 prevBlockCursorPosition, currBlockAbsoluteLineOffset);
899 }
900
901 if (svgReader.hasError()) {
902 d->errors << svgReader.errorString();
903 return false;
904 }
905 doc->setModified(false);
906 return true;
907 }
908
errors() const909 QStringList KoSvgTextShapeMarkupConverter::errors() const
910 {
911 return d->errors;
912 }
913
warnings() const914 QStringList KoSvgTextShapeMarkupConverter::warnings() const
915 {
916 return d->warnings;
917 }
918
compareFormatUnderlineWithMostCommon(QTextCharFormat format,QTextCharFormat mostCommon)919 bool compareFormatUnderlineWithMostCommon(QTextCharFormat format, QTextCharFormat mostCommon)
920 {
921 // color and style is not supported in rich text editor yet
922 // TODO: support color and style
923 return format.fontUnderline() == mostCommon.fontUnderline()
924 && format.fontOverline() == mostCommon.fontOverline()
925 && format.fontStrikeOut() == mostCommon.fontStrikeOut();
926 }
927
convertFormatUnderlineToSvg(QTextCharFormat format)928 QString convertFormatUnderlineToSvg(QTextCharFormat format)
929 {
930 // color and style is not supported in rich text editor yet
931 // and text-decoration-line and -style and -color are not supported in svg render either
932 // hence we just use text-decoration
933 // TODO: support color and style
934 QStringList line;
935
936 if (format.fontUnderline()) {
937 line.append("underline");
938 if (format.underlineStyle() != QTextCharFormat::SingleUnderline) {
939 warnFile << "Krita only supports solid underline style";
940 }
941 }
942
943 if (format.fontOverline()) {
944 line.append("overline");
945 }
946
947 if (format.fontStrikeOut()) {
948 line.append("line-through");
949 }
950
951 if (line.isEmpty())
952 {
953 line.append("none");
954 }
955
956 QString c = QString("text-decoration").append(":")
957 .append(line.join(" "));
958
959 return c;
960 }
961
style(QTextCharFormat format,QTextBlockFormat blockFormat,QTextCharFormat mostCommon)962 QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format,
963 QTextBlockFormat blockFormat,
964 QTextCharFormat mostCommon)
965 {
966 QStringList style;
967 for(int i=0; i<format.properties().size(); i++) {
968 QString c;
969 int propertyId = format.properties().keys().at(i);
970
971 if (propertyId == QTextCharFormat::FontFamily) {
972 c.append("font-family").append(":")
973 .append(format.properties()[propertyId].toString());
974 }
975 if (propertyId == QTextCharFormat::FontPointSize) {
976 c.append("font-size").append(":")
977 .append(format.properties()[propertyId].toString()+"pt");
978 style.append(c);
979 c.clear();
980 QFontMetricsF metrics(format.fontFamily());
981 qreal xRatio = metrics.xHeight()/metrics.height();
982 c.append("font-size-adjust").append(":").append(QString::number(xRatio));
983 }
984 if (propertyId == QTextCharFormat::FontPixelSize) {
985 c.append("font-size").append(":")
986 .append(format.properties()[propertyId].toString()+"px");
987 }
988 if (propertyId == QTextCharFormat::FontWeight) {
989 // Convert from QFont::Weight range to SVG range,
990 // as defined in qt's qfont.h
991 int convertedWeight = 400; // Defaulting to Weight::Normal in svg scale
992
993 switch (format.properties()[propertyId].toInt()) {
994 case QFont::Weight::Thin:
995 convertedWeight = 100;
996 break;
997 case QFont::Weight::ExtraLight:
998 convertedWeight = 200;
999 break;
1000 case QFont::Weight::Light:
1001 convertedWeight = 300;
1002 break;
1003 case QFont::Weight::Normal:
1004 convertedWeight = 400;
1005 break;
1006 case QFont::Weight::Medium:
1007 convertedWeight = 500;
1008 break;
1009 case QFont::Weight::DemiBold:
1010 convertedWeight = 600;
1011 break;
1012 case QFont::Weight::Bold:
1013 convertedWeight = 700;
1014 break;
1015 case QFont::Weight::ExtraBold:
1016 convertedWeight = 800;
1017 break;
1018 case QFont::Weight::Black:
1019 convertedWeight = 900;
1020 break;
1021 default:
1022 warnFile << "WARNING: Invalid QFont::Weight value supplied to KoSvgTextShapeMarkupConverter::style.";
1023 break;
1024 }
1025
1026 c.append("font-weight").append(":")
1027 .append(QString::number(convertedWeight));
1028 }
1029 if (propertyId == QTextCharFormat::FontItalic) {
1030 QString val = "italic";
1031 if (!format.fontItalic()) {
1032 val = "normal";
1033 }
1034 c.append("font-style").append(":")
1035 .append(val);
1036 }
1037
1038 if (propertyId == QTextCharFormat::FontCapitalization) {
1039 if (format.fontCapitalization() == QFont::SmallCaps){
1040 c.append("font-variant").append(":")
1041 .append("small-caps");
1042 } else if (format.fontCapitalization() == QFont::AllUppercase) {
1043 c.append("text-transform").append(":")
1044 .append("uppercase");
1045 } else if (format.fontCapitalization() == QFont::AllLowercase) {
1046 c.append("text-transform").append(":")
1047 .append("lowercase");
1048 } else if (format.fontCapitalization() == QFont::Capitalize) {
1049 c.append("text-transform").append(":")
1050 .append("capitalize");
1051 }
1052 }
1053
1054 if (propertyId == QTextCharFormat::FontStretch) {
1055 c.append("font-stretch").append(":")
1056 .append(format.properties()[propertyId].toString());
1057 }
1058 if (propertyId == QTextCharFormat::FontKerning) {
1059 QString val;
1060 if (format.fontKerning()) {
1061 val = "auto";
1062 } else {
1063 val = "0";
1064 }
1065 c.append("kerning").append(":")
1066 .append(val);
1067 }
1068 if (propertyId == QTextCharFormat::FontWordSpacing) {
1069 c.append("word-spacing").append(":")
1070 .append(QString::number(format.fontWordSpacing()));
1071 }
1072 if (propertyId == QTextCharFormat::FontLetterSpacing) {
1073 QString val;
1074 if (format.fontLetterSpacingType()==QFont::AbsoluteSpacing) {
1075 val = QString::number(format.fontLetterSpacing());
1076 } else {
1077 val = QString::number(((format.fontLetterSpacing()/100)*format.fontPointSize()));
1078 }
1079 c.append("letter-spacing").append(":")
1080 .append(val);
1081 }
1082 if (propertyId == QTextCharFormat::TextOutline) {
1083 if (format.textOutline().color() != mostCommon.textOutline().color()) {
1084 c.append("stroke").append(":")
1085 .append(format.textOutline().color().name());
1086 style.append(c);
1087 c.clear();
1088 }
1089 if (format.textOutline().width() != mostCommon.textOutline().width()) {
1090 c.append("stroke-width").append(":")
1091 .append(QString::number(format.textOutline().width()));
1092 }
1093 }
1094
1095
1096 if (propertyId == QTextCharFormat::TextVerticalAlignment) {
1097 QString val = "baseline";
1098 if (format.verticalAlignment() == QTextCharFormat::AlignSubScript) {
1099 val = QLatin1String("sub");
1100 }
1101 else if (format.verticalAlignment() == QTextCharFormat::AlignSuperScript) {
1102 val = QLatin1String("super");
1103 }
1104 c.append("baseline-shift").append(":").append(val);
1105 }
1106
1107 if (propertyId == QTextCharFormat::ForegroundBrush) {
1108 QColor::NameFormat colorFormat;
1109
1110 if (format.foreground().color().alphaF() < 1.0) {
1111 colorFormat = QColor::HexArgb;
1112 } else {
1113 colorFormat = QColor::HexRgb;
1114 }
1115
1116 c.append("fill").append(":")
1117 .append(format.foreground().color().name(colorFormat));
1118 }
1119
1120 if (!c.isEmpty()) {
1121 style.append(c);
1122 }
1123 }
1124
1125 if (!compareFormatUnderlineWithMostCommon(format, mostCommon)) {
1126
1127 QString c = convertFormatUnderlineToSvg(format);
1128 if (!c.isEmpty()) {
1129 style.append(c);
1130 }
1131 }
1132
1133 if (blockFormat.hasProperty(QTextBlockFormat::BlockAlignment)) {
1134 // TODO: Alignment works incorrectly! The offsets should be calculated
1135 // according to the shape width/height!
1136
1137 QString c;
1138 QString val;
1139 if (blockFormat.alignment()==Qt::AlignRight) {
1140 val = "end";
1141 } else if (blockFormat.alignment()==Qt::AlignCenter) {
1142 val = "middle";
1143 } else {
1144 val = "start";
1145 }
1146 c.append("text-anchor").append(":")
1147 .append(val);
1148 if (!c.isEmpty()) {
1149 style.append(c);
1150 }
1151 }
1152
1153 return style.join("; ");
1154 }
1155
stylesFromString(QStringList styles,QTextCharFormat currentCharFormat,QTextBlockFormat currentBlockFormat)1156 QVector<QTextFormat> KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat)
1157 {
1158 Q_UNUSED(currentBlockFormat);
1159
1160 QVector<QTextFormat> formats;
1161 QTextCharFormat charFormat;
1162 charFormat.setTextOutline(currentCharFormat.textOutline());
1163 QTextBlockFormat blockFormat;
1164 QScopedPointer<SvgGraphicsContext> context(new SvgGraphicsContext());
1165
1166 for (int i=0; i<styles.size(); i++) {
1167 if (!styles.at(i).isEmpty()){
1168 QStringList style = styles.at(i).split(":");
1169 // ignore the property instead of crashing,
1170 // if user forgets to separate property name and value with ':'.
1171 if (style.size() < 2) {
1172 continue;
1173 }
1174
1175 QString property = style.at(0).trimmed();
1176 QString value = style.at(1).trimmed();
1177
1178 if (property == "font-family") {
1179 charFormat.setFontFamily(value);
1180 }
1181
1182 if (property == "font-size") {
1183 qreal val = SvgUtil::parseUnitX(context.data(), value);
1184 charFormat.setFontPointSize(val);
1185 }
1186
1187 if (property == "font-variant") {
1188 if (value=="small-caps") {
1189 charFormat.setFontCapitalization(QFont::SmallCaps);
1190 } else {
1191 charFormat.setFontCapitalization(QFont::MixedCase);
1192 }
1193 }
1194
1195 if (property == "font-style") {
1196 if (value=="italic" || value=="oblique") {
1197 charFormat.setFontItalic(true);
1198 } else {
1199 charFormat.setFontItalic(false);
1200 }
1201 }
1202
1203 if (property == "font-stretch") {
1204 charFormat.setFontStretch(value.toInt());
1205 }
1206
1207 if (property == "font-weight") {
1208 // Convert from SVG range to QFont::Weight range,
1209 // as defined in qt's qfont.h
1210 int convertedWeight = QFont::Weight::Normal; // Defaulting to Weight::Normal
1211
1212 switch (value.toInt()) {
1213 case 100:
1214 convertedWeight = QFont::Weight::Thin;
1215 break;
1216 case 200:
1217 convertedWeight = QFont::Weight::ExtraLight;
1218 break;
1219 case 300:
1220 convertedWeight = QFont::Weight::Light;
1221 break;
1222 case 400:
1223 convertedWeight = QFont::Weight::Normal;
1224 break;
1225 case 500:
1226 convertedWeight = QFont::Weight::Medium;
1227 break;
1228 case 600:
1229 convertedWeight = QFont::Weight::DemiBold;
1230 break;
1231 case 700:
1232 convertedWeight = QFont::Weight::Bold;
1233 break;
1234 case 800:
1235 convertedWeight = QFont::Weight::ExtraBold;
1236 break;
1237 case 900:
1238 convertedWeight = QFont::Weight::Black;
1239 break;
1240 default:
1241 warnFile << "WARNING: Invalid weight value supplied to KoSvgTextShapeMarkupConverter::stylesFromString.";
1242 break;
1243 }
1244
1245 charFormat.setFontWeight(convertedWeight);
1246 }
1247
1248 if (property == "text-decoration") {
1249 charFormat.setFontUnderline(false);
1250 charFormat.setFontOverline(false);
1251 charFormat.setFontStrikeOut(false);
1252 QStringList values = value.split(" ");
1253 if (values.contains("line-through")) {
1254 charFormat.setFontStrikeOut(true);
1255 }
1256 if (values.contains("overline")) {
1257 charFormat.setFontOverline(true);
1258 }
1259 if(values.contains("underline")){
1260 charFormat.setFontUnderline(true);
1261 }
1262 }
1263
1264 if (property == "text-transform") {
1265 if (value == "uppercase") {
1266 charFormat.setFontCapitalization(QFont::AllUppercase);
1267 } else if (value == "lowercase") {
1268 charFormat.setFontCapitalization(QFont::AllLowercase);
1269 } else if (value == "capitalize") {
1270 charFormat.setFontCapitalization(QFont::Capitalize);
1271 } else{
1272 charFormat.setFontCapitalization(QFont::MixedCase);
1273 }
1274 }
1275
1276 if (property == "letter-spacing") {
1277 qreal val = SvgUtil::parseUnitX(context.data(), value);
1278 charFormat.setFontLetterSpacingType(QFont::AbsoluteSpacing);
1279 charFormat.setFontLetterSpacing(val);
1280 }
1281
1282 if (property == "word-spacing") {
1283 qreal val = SvgUtil::parseUnitX(context.data(), value);
1284 charFormat.setFontWordSpacing(val);
1285 }
1286
1287 if (property == "kerning") {
1288 if (value == "auto") {
1289 charFormat.setFontKerning(true);
1290 } else {
1291 qreal val = SvgUtil::parseUnitX(context.data(), value);
1292 charFormat.setFontKerning(false);
1293 charFormat.setFontLetterSpacingType(QFont::AbsoluteSpacing);
1294 charFormat.setFontLetterSpacing(charFormat.fontLetterSpacing() + val);
1295 }
1296 }
1297
1298 if (property == "stroke") {
1299 QPen pen = charFormat.textOutline();
1300 QColor color;
1301 color.setNamedColor(value);
1302 pen.setColor(color);
1303 charFormat.setTextOutline(pen);
1304 }
1305
1306 if (property == "stroke-width") {
1307 QPen pen = charFormat.textOutline();
1308 pen.setWidth(value.toInt());
1309 charFormat.setTextOutline(pen);
1310 }
1311
1312 if (property == "fill") {
1313 QColor color;
1314 color.setNamedColor(value);
1315
1316 // avoid assertion failure in `KoColor` later
1317 if (!color.isValid()) {
1318 continue;
1319 }
1320
1321 // default color is #ff000000, so default alpha will be 1.0
1322 qreal currentAlpha = charFormat.foreground().color().alphaF();
1323
1324 // if alpha was already defined by `fill-opacity` prop
1325 if (currentAlpha < 1.0) {
1326 // and `fill` doesn't have alpha component
1327 if (color.alphaF() < 1.0) {
1328 color.setAlphaF(currentAlpha);
1329 }
1330 }
1331
1332 charFormat.setForeground(color);
1333 }
1334
1335 if (property == "fill-opacity") {
1336 QColor color = charFormat.foreground().color();
1337 bool ok = true;
1338 qreal alpha = qBound(0.0, SvgUtil::fromPercentage(value, &ok), 1.0);
1339
1340 // if conversion fails due to non-numeric input,
1341 // it defaults to 0.0, default to current alpha instead
1342 if (!ok) {
1343 alpha = color.alphaF();
1344 }
1345 color.setAlphaF(alpha);
1346 charFormat.setForeground(color);
1347 }
1348
1349 if (property == "text-anchor") {
1350 if (value == "end") {
1351 blockFormat.setAlignment(Qt::AlignRight);
1352 } else if (value == "middle") {
1353 blockFormat.setAlignment(Qt::AlignCenter);
1354 } else {
1355 blockFormat.setAlignment(Qt::AlignLeft);
1356 }
1357 }
1358
1359 if (property == "baseline-shift") {
1360 if (value == "super") {
1361 charFormat.setVerticalAlignment(QTextCharFormat::AlignSuperScript);
1362 } else if (value == "sub") {
1363 charFormat.setVerticalAlignment(QTextCharFormat::AlignSubScript);
1364 } else {
1365 charFormat.setVerticalAlignment(QTextCharFormat::AlignNormal);
1366 }
1367 }
1368 }
1369 }
1370
1371 formats.append(charFormat);
1372 formats.append(blockFormat);
1373 return formats;
1374 }
1375
formatDifference(QTextFormat test,QTextFormat reference)1376 QTextFormat KoSvgTextShapeMarkupConverter::formatDifference(QTextFormat test, QTextFormat reference)
1377 {
1378 //copied from QTextDocument.cpp
1379 QTextFormat diff = test;
1380 //props should proly compare itself to the main text format...
1381 const QMap<int, QVariant> props = reference.properties();
1382 for (QMap<int, QVariant>::ConstIterator it = props.begin(), end = props.end();
1383 it != end; ++it)
1384 if (it.value() == test.property(it.key())) {
1385 // Some props must not be removed as default state gets in the way.
1386 if (it.key() == 0x2023) { // TextUnderlineStyle
1387 continue;
1388 } else if (it.key() == 0x2033) { // FontLetterSpacingType
1389 continue;
1390 }
1391 diff.clearProperty(it.key());
1392 }
1393 return diff;
1394 }
1395
1396