Krita Source Code Documentation
Loading...
Searching...
No Matches
KoSvgTextShapeMarkupConverter.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2017 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include "klocalizedstring.h"
10#include "kis_assert.h"
11#include "kis_debug.h"
12
13#include <ft2build.h>
14#include FT_FREETYPE_H
15#include FT_TRUETYPE_TABLES_H
16
17#include <QXmlStreamReader>
18#include <QXmlStreamWriter>
19#include <QBuffer>
20#include <QTextCodec>
21#include <QtMath>
22
23#include <QTextBlock>
24#include <QTextLayout>
25#include <QTextLine>
26
27#include <QFont>
28
29#include <QStack>
30
31#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
32#include <QStringRef>
33#else
34#include <QStringView>
35#endif
36
37#include <KoSvgTextShape.h>
38#include <KoXmlWriter.h>
40
41#include <KoColor.h>
42
43#include <SvgParser.h>
44#include <SvgWriter.h>
45#include <SvgUtil.h>
46#include <SvgSavingContext.h>
47#include <SvgGraphicContext.h>
48
50#include <html/HtmlWriter.h>
51
52#include "kis_dom_utils.h"
53#include <boost/optional.hpp>
54
55#include <FlakeDebug.h>
56
70
75
79
84
85bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText)
86{
87 d->clearErrors();
88
89 QBuffer shapesBuffer;
90 QBuffer stylesBuffer;
91
92 shapesBuffer.open(QIODevice::WriteOnly);
93 stylesBuffer.open(QIODevice::WriteOnly);
94
95 {
96 SvgSavingContext savingContext(shapesBuffer, stylesBuffer);
97 savingContext.setStrippedTextMode(true);
98 SvgWriter writer({d->shape});
99 writer.saveDetached(savingContext);
100 }
101
102 shapesBuffer.close();
103 stylesBuffer.close();
104
105 *svgText = QString::fromUtf8(shapesBuffer.data());
106 *stylesText = QString::fromUtf8(stylesBuffer.data());
107
108 return true;
109}
110
111bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText,
112 const QRectF &boundsInPixels, qreal pixelsPerInch)
113{
114
115 debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch;
116
117 d->clearErrors();
118
119 QString errorMessage;
120 int errorLine = 0;
121 int errorColumn = 0;
122
123 const QString fullText = QString("<svg>\n%1\n%2\n</svg>\n").arg(stylesText).arg(svgText);
124
125 QDomDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn);
126 if (doc.isNull()) {
127 d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage);
128 return false;
129 }
130
131 KoDocumentResourceManager resourceManager;
132 SvgParser parser(&resourceManager);
133 parser.setResolution(boundsInPixels, pixelsPerInch);
134
135 QDomElement root = doc.documentElement();
136 QDomNode node = root.firstChild();
137
138 bool textNodeFound = false;
139
140 for (; !node.isNull(); node = node.nextSibling()) {
141 QDomElement el = node.toElement();
142 if (el.isNull()) continue;
143
144 if (el.tagName() == "defs") {
145 parser.parseDefsElement(el);
146 }
147 else if (el.tagName() == "text") {
148 if (textNodeFound) {
149 d->errors << i18n("More than one 'text' node found!");
150 return false;
151 }
152
153 KoShape *shape = parser.parseTextElement(el, d->shape);
154 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false);
155 textNodeFound = true;
156 break;
157 } else {
158 d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName());
159 return false;
160 }
161 }
162
163 if (!textNodeFound) {
164 d->errors << i18n("No \'text\' node found!");
165 return false;
166 }
167
168 return true;
169
170}
171
173{
174 d->clearErrors();
175
176 QBuffer shapesBuffer;
177 shapesBuffer.open(QIODevice::WriteOnly);
178 {
179 HtmlWriter writer({d->shape});
180 if (!writer.save(shapesBuffer)) {
181 d->errors = writer.errors();
182 d->warnings = writer.warnings();
183 return false;
184 }
185 }
186
187 shapesBuffer.close();
188
189 *htmlText = QString(shapesBuffer.data());
190
191 debugFlake << "\t\t" << *htmlText;
192
193 return true;
194}
195
196bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles)
197{
198
199 debugFlake << ">>>>>>>>>>>" << htmlText;
200
201 QBuffer svgBuffer;
202 svgBuffer.open(QIODevice::WriteOnly);
203
204 QXmlStreamReader htmlReader(htmlText);
205 QXmlStreamWriter svgWriter(&svgBuffer);
206
207 svgWriter.setAutoFormatting(false);
208#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
209 QStringRef elementName;
210#else
211 QStringView elementName;
212#endif
213
214 bool newLine = false;
215 int lineCount = 0;
216 QString bodyEm = "1em";
217 QString em;
218 QString p("p");
219 //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags
220 QString previousStyleString;
221 QString appendStyle;
222
223 const QStringList spanLikes = {
224 "span", "font", "b", "strong", "em", "i", "pre", "u"
225 };
226 bool firstElement = true;
227
228 while (!htmlReader.atEnd()) {
229 QXmlStreamReader::TokenType token = htmlReader.readNext();
230 QLatin1String elName = firstElement? QLatin1String("text"): QLatin1String("tspan");
231 switch (token) {
232 case QXmlStreamReader::StartElement:
233 {
234 newLine = false;
235 if (htmlReader.name() == "br") {
236 debugFlake << "\tdoing br";
237 svgWriter.writeEndElement();
238#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
239 elementName = QStringRef(&p);
240#else
241 elementName = QStringView(p);
242#endif
243 em = bodyEm;
244 appendStyle = previousStyleString;
245 }
246 else {
247 elementName = htmlReader.name();
248 em = "";
249 }
250
251 if (elementName == "body") {
252 debugFlake << "\tstart Element" << elementName;
253 svgWriter.writeStartElement(elName);
254 firstElement = false;
255 appendStyle = QString();
256 }
257 else if (elementName == "p") {
258 // new line
259 debugFlake << "\t\tstart Element" << elementName;
260 svgWriter.writeStartElement(elName);
261 firstElement = false;
262 newLine = true;
263 if (em.isEmpty()) {
264 em = bodyEm;
265 appendStyle = QString();
266 }
267 lineCount++;
268 }
269 else if (elementName == "span") {
270 debugFlake << "\tstart Element" << elementName;
271 svgWriter.writeStartElement(elName);
272 firstElement = false;
273 appendStyle = QString();
274 }
275 else if (elementName == "b" || elementName == "strong") {
276 debugFlake << "\tstart Element" << elementName;
277 svgWriter.writeStartElement(elName);
278 firstElement = false;
279 appendStyle = "font-weight:700;";
280 }
281 else if (elementName == "i" || elementName == "em") {
282 debugFlake << "\tstart Element" << elementName;
283 svgWriter.writeStartElement(elName);
284 firstElement = false;
285 appendStyle = "font-style:italic;";
286 }
287 else if (elementName == "u") {
288 debugFlake << "\tstart Element" << elementName;
289 svgWriter.writeStartElement(elName);
290 firstElement = false;
291 appendStyle = "text-decoration:underline";
292 }
293 else if (elementName == "font") {
294 debugFlake << "\tstart Element" << elementName;
295 svgWriter.writeStartElement(elName);
296 firstElement = false;
297 appendStyle = QString();
298 if (htmlReader.attributes().hasAttribute("color")) {
299 svgWriter.writeAttribute("fill", htmlReader.attributes().value("color").toString());
300 }
301 }
302 else if (elementName == "pre") {
303 debugFlake << "\tstart Element" << elementName;
304 svgWriter.writeStartElement(elName);
305 firstElement = false;
306 appendStyle = "white-space:pre";
307 }
308
309 QXmlStreamAttributes attributes = htmlReader.attributes();
310
311 QString textAlign;
312 if (attributes.hasAttribute("align")) {
313 textAlign = attributes.value("align").toString();
314 }
315
316 if (attributes.hasAttribute("style") || !appendStyle.isEmpty()) {
317 QString filteredStyles;
318 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(" ");
319 QStringList styles = attributes.value("style").toString().split(";");
320 for(int i=0; i<styles.size(); i++) {
321 QStringList style = QString(styles.at(i)).split(":");
322 debugFlake<<style.at(0);
323 if (svgStyles.contains(QString(style.at(0)).trimmed())) {
324 filteredStyles.append(styles.at(i)+";");
325 }
326
327 if (QString(style.at(0)).trimmed() == "color") {
328 filteredStyles.append(" fill:"+style.at(1)+";");
329 }
330
331 if (QString(style.at(0)).trimmed() == "text-align") {
332 textAlign = QString(style.at(1)).trimmed();
333 }
334
335 if (QString(style.at(0)).trimmed() == "line-height"){
336 if (style.at(1).contains("%")) {
337 double percentage = QString(style.at(1)).remove("%").toDouble();
338 em = QString::number(percentage/100.0)+"em";
339 } else if(style.at(1).contains("em")) {
340 em = style.at(1);
341 } else if(style.at(1).contains("px")) {
342 em = style.at(1);
343 }
344 if (elementName == "body") {
345 bodyEm = em;
346 }
347 }
348 }
349
350 if (textAlign == "center") {
351 filteredStyles.append(" text-anchor:middle;");
352 } else if (textAlign == "right") {
353 filteredStyles.append(" text-anchor:end;");
354 } else if (textAlign == "left"){
355 filteredStyles.append(" text-anchor:start;");
356 }
357
358 filteredStyles.append(appendStyle);
359
360 if (!filteredStyles.isEmpty()) {
361 svgWriter.writeAttribute("style", filteredStyles);
362 previousStyleString = filteredStyles;
363 }
364
365
366 }
367 if (newLine && lineCount > 1) {
368 debugFlake << "\t\tAdvancing to the next line";
369 svgWriter.writeAttribute("x", "0");
370 svgWriter.writeAttribute("dy", em);
371 }
372 break;
373 }
374 case QXmlStreamReader::EndElement:
375 {
376 if (htmlReader.name() == "br") break;
377 if (elementName == "p" || spanLikes.contains(elementName) || elementName == "body") {
378 debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")";
379 svgWriter.writeEndElement();
380 }
381 break;
382 }
383 case QXmlStreamReader::Characters:
384 {
385 if (elementName == "style") {
386 *styles = htmlReader.text().toString();
387 }
388 else {
389 //TODO: Think up what to do with mix of pretty-print and <BR> (what libreoffice uses).
390 //if (!htmlReader.isWhitespace()) {
391 debugFlake << "\tCharacters:" << htmlReader.text();
392 svgWriter.writeCharacters(htmlReader.text().toString());
393 //}
394 }
395 break;
396 }
397 default:
398 ;
399 }
400 }
401
402 if (htmlReader.hasError()) {
403 d->errors << htmlReader.errorString();
404 return false;
405 }
406 if (svgWriter.hasError()) {
407 d->errors << i18n("Unknown error writing SVG text element");
408 return false;
409 }
410
411 *svgText = QString::fromUtf8(svgBuffer.data());
412 return true;
413}
414
415void postCorrectBlockHeight(QTextDocument *doc,
416 qreal currLineAscent,
417 qreal prevLineAscent,
418 qreal prevLineDescent,
419 int prevBlockCursorPosition,
420 qreal currentBlockAbsoluteLineOffset)
421{
422 KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0);
423
424 QTextCursor postCorrectionCursor(doc);
425 postCorrectionCursor.setPosition(prevBlockCursorPosition);
426 if (!postCorrectionCursor.isNull()) {
427 const qreal relativeLineHeight =
428 ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) /
429 (prevLineAscent + prevLineDescent)) * 100.0;
430
431 QTextBlockFormat format = postCorrectionCursor.blockFormat();
432 format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight);
433 postCorrectionCursor.setBlockFormat(format);
434 postCorrectionCursor = QTextCursor();
435 }
436}
437
438QTextFormat findMostCommonFormat(const QList<QTextFormat> &allFormats)
439{
440 QTextCharFormat mostCommonFormat;
441
442 QSet<int> propertyIds;
443
447 Q_FOREACH (const QTextFormat &format, allFormats) {
448 const QMap<int, QVariant> formatProperties = format.properties();
449 Q_FOREACH (int id, formatProperties.keys()) {
450 propertyIds.insert(id);
451 }
452 }
453
460 Q_FOREACH (const QTextFormat &format, allFormats) {
461 for (auto it = propertyIds.begin(); it != propertyIds.end();) {
462 if (!format.hasProperty(*it)) {
463 it = propertyIds.erase(it);
464 } else {
465 ++it;
466 }
467 }
468 if (propertyIds.isEmpty()) break;
469 }
470
471 if (!propertyIds.isEmpty()) {
472 QMap<int, QList<std::pair<QVariant, int>>> propertyFrequency;
473
477 Q_FOREACH (const QTextFormat &format, allFormats) {
478 const QMap<int, QVariant> formatProperties = format.properties();
479
480 Q_FOREACH (int id, propertyIds) {
481 KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id));
482 QList<std::pair<QVariant, int>> &valueFrequencies = propertyFrequency[id];
483 const QVariant formatPropValue = formatProperties.value(id);
484
485 // Find the value in frequency table
486 auto it = std::find_if(valueFrequencies.begin(), valueFrequencies.end(),
487 [formatPropValue](const std::pair<QVariant, int> &element) { return element.first == formatPropValue; });
488
489 if (it != valueFrequencies.end()) {
490 // Increase frequency by 1, if already met
491 it->second += 1;
492 } else {
493 // Add with initial frequency of 1 if met for the first time
494 valueFrequencies.push_back({formatPropValue, 1});
495 }
496 }
497 }
498
502 for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) {
503 const int id = it.key();
504 const QList<std::pair<QVariant, int>>& allValues = it.value();
505
506 int maxCount = 0;
507 QVariant maxValue;
508
509 for (const auto& [propValue, valFrequency] : allValues) {
510 if (valFrequency > maxCount) {
511 maxCount = valFrequency;
512 maxValue = propValue;
513 }
514 }
515
516 KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0);
517 mostCommonFormat.setProperty(id, maxValue);
518 }
519
520 }
521
522 return mostCommonFormat;
523}
524
525Q_GUI_EXPORT int qt_defaultDpi();
526
527qreal fixFromQtDpi(qreal value)
528{
529 // HACK ALERT: see a comment in convertDocumentToSvg!
530 return value * 72.0 / qt_defaultDpi();
531}
532
533qreal fixToQtDpi(qreal value)
534{
535 // HACK ALERT: see a comment in convertDocumentToSvg!
536 return value * qt_defaultDpi() / 72.0;
537}
538
539qreal calcLineWidth(const QTextBlock &block)
540{
541 const QString blockText = block.text();
542
543 QTextLayout lineLayout;
544 lineLayout.setText(blockText);
545 lineLayout.setFont(block.charFormat().font());
546 lineLayout.setFormats(block.textFormats());
547 lineLayout.setTextOption(block.layout()->textOption());
548
549 lineLayout.beginLayout();
550 QTextLine fullLine = lineLayout.createLine();
551 if (!fullLine.isValid()) {
552 fullLine.setNumColumns(blockText.size());
553 }
554 lineLayout.endLayout();
555
556 return fixFromQtDpi(lineLayout.boundingRect().width());
557}
558
567static bool guessIsRightToLeft(QStringView text) {
568 // Is this just a worse version of QString::isRightToLeft??
569 for (int i = 0; i < text.size(); i++) {
570 const QChar ch = text[i];
571 if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) {
572 return true;
573 } else if (ch.direction() == QChar::DirL) {
574 return false;
575 }
576 }
577 return false;
578}
579
580bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText)
581{
582 QBuffer svgBuffer;
583 svgBuffer.open(QIODevice::WriteOnly);
584
585 QXmlStreamWriter svgWriter(&svgBuffer);
586
587 // disable auto-formatting to avoid extra spaces appearing here and there
588 svgWriter.setAutoFormatting(false);
589
590
591 qreal maxParagraphWidth = 0.0;
592 QTextCharFormat mostCommonCharFormat;
593 QTextBlockFormat mostCommonBlockFormat;
594
595 struct LineInfo {
596 LineInfo() {}
597 LineInfo(QTextBlock _block, int _numSkippedLines)
598 : block(_block), numSkippedLines(_numSkippedLines)
599 {}
600
601 QTextBlock block;
602 int numSkippedLines = 0;
603 };
604
605 const WrappingMode wrappingMode = getWrappingMode(doc->rootFrame()->frameFormat());
606
623 QVector<LineInfo> lineInfoList;
624
625 {
626 QTextBlock block = doc->begin();
627
628 QList<QTextFormat> allCharFormats;
629 QList<QTextFormat> allBlockFormats;
630
631 int numSequentialEmptyLines = 0;
632
633 bool hasExplicitTextWidth = false;
634 if (wrappingMode == WrappingMode::WhiteSpacePreWrap) {
635 // If the doc is pre-wrap, we expect the inline-size to be set.
636 if (std::optional<double> inlineSize = getInlineSize(doc->rootFrame()->frameFormat())) {
637 if (*inlineSize > 0.0) {
638 hasExplicitTextWidth = true;
639 maxParagraphWidth = *inlineSize;
640 }
641 }
642 }
643
644 while (block.isValid()) {
645 if (wrappingMode != WrappingMode::QtLegacy || !block.text().trimmed().isEmpty()) {
646 lineInfoList.append(LineInfo(block, numSequentialEmptyLines));
647 numSequentialEmptyLines = 0;
648
649 if (!hasExplicitTextWidth) {
650 maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block));
651 }
652
653 allBlockFormats.append(block.blockFormat());
654 Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) {
655 QTextFormat format = range.format;
656 allCharFormats.append(format);
657 }
658 } else {
659 numSequentialEmptyLines++;
660 }
661
662 block = block.next();
663 }
664
665 mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat();
666 mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat();
667 }
668
669 //Okay, now the actual writing.
670
671 QTextBlock block = doc->begin();
672
673 svgWriter.writeStartElement("text");
674
675 if (wrappingMode == WrappingMode::WhiteSpacePreWrap) {
676 // There can only be one text direction for pre-wrap, so take that of
677 // the first block.
678 if (block.textDirection() == Qt::RightToLeft) {
679 svgWriter.writeAttribute("direction", "rtl");
680 }
681 }
682
683 {
684 QString commonTextStyle = style(mostCommonCharFormat,
685 mostCommonBlockFormat,
686 {},
687 /*includeLineHeight=*/wrappingMode != WrappingMode::QtLegacy);
688 if (wrappingMode != WrappingMode::QtLegacy) {
689 if (!commonTextStyle.isEmpty()) {
690 commonTextStyle += "; ";
691 }
692 commonTextStyle += "white-space: pre";
693 if (wrappingMode == WrappingMode::WhiteSpacePreWrap) {
694 commonTextStyle += "-wrap;inline-size:";
695 commonTextStyle += QString::number(maxParagraphWidth);
696 }
697 }
698 if (!commonTextStyle.isEmpty()) {
699 svgWriter.writeAttribute("style", commonTextStyle);
700 }
701 }
702
703 // TODO: check if we should change into to float
704 int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight();
705 int prevBlockLineType = mostCommonBlockFormat.lineHeightType();
706 qreal prevBlockAscent = 0.0;
707 qreal prevBlockDescent= 0.0;
708
709 Q_FOREACH (const LineInfo &info, lineInfoList) {
710 QTextBlock block = info.block;
711
712 const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat();
713 QTextCharFormat blockCharFormatDiff = QTextCharFormat();
714 const QVector<QTextLayout::FormatRange> formats = block.textFormats();
715 if (formats.size()==1) {
716 blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat();
717 if (wrappingMode == WrappingMode::WhiteSpacePreWrap) {
718 // For pre-wrap, be extra sure we are not writing text-anchor
719 // to the `tspan`s because they don't do anything.
720 blockCharFormatDiff.clearProperty(QTextBlockFormat::BlockAlignment);
721 }
722 }
723
724 const QTextLayout *layout = block.layout();
725 const QTextLine line = layout->lineAt(0);
726 if (!line.isValid()) {
727 // This layout probably has no lines at all. This can happen when
728 // wrappingMode != QtLegacy and the text doc is completely empty.
729 // It is safe to just skip the line. Trying to get its metrics will
730 // crash.
731 continue;
732 }
733
734 svgWriter.writeStartElement("tspan");
735
736 const QString text = block.text();
737
738 bool isRightToLeft;
739 switch (block.textDirection()) {
740 case Qt::LeftToRight:
741 isRightToLeft = false;
742 break;
743 case Qt::RightToLeft:
744 isRightToLeft = true;
745 break;
746 case Qt::LayoutDirectionAuto:
747 default:
748 // QTextBlock::textDirection() is not supposed to return these,
749 // but just in case...
750 isRightToLeft = guessIsRightToLeft(text);;
751 break;
752 }
753
754 if (isRightToLeft && wrappingMode != WrappingMode::WhiteSpacePreWrap) {
755 svgWriter.writeAttribute("direction", "rtl");
756 svgWriter.writeAttribute("unicode-bidi", "embed");
757 }
758
759 {
760 const QString blockStyleString = style(blockCharFormatDiff,
761 blockFormatDiff,
762 {},
763 /*includeLineHeight=*/wrappingMode != WrappingMode::QtLegacy);
764 if (!blockStyleString.isEmpty()) {
765 svgWriter.writeAttribute("style", blockStyleString);
766 }
767 }
768
769 if (wrappingMode != WrappingMode::WhiteSpacePreWrap) {
775 Qt::Alignment blockAlignment = block.blockFormat().alignment();
776 if (isRightToLeft) {
777 if (blockAlignment & Qt::AlignLeft) {
778 blockAlignment &= ~Qt::AlignLeft;
779 blockAlignment |= Qt::AlignRight;
780 } else if (blockAlignment & Qt::AlignRight) {
781 blockAlignment &= ~Qt::AlignRight;
782 blockAlignment |= Qt::AlignLeft;
783 }
784 }
785
786 if (blockAlignment & Qt::AlignHCenter) {
787 svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt");
788 } else if (blockAlignment & Qt::AlignRight) {
789 svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt");
790 } else {
791 svgWriter.writeAttribute("x", "0");
792 }
793 }
794
795 if (wrappingMode == WrappingMode::QtLegacy && block.blockNumber() > 0) {
796 qreal lineHeightPt =
797 fixFromQtDpi(line.ascent()) - prevBlockAscent +
798 (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0;
799
800 const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt;
801 svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt");
802 }
803
804 prevBlockRelativeLineSpacing =
805 blockFormatDiff.hasProperty(QTextFormat::LineHeight) ?
806 blockFormatDiff.lineHeight() :
807 mostCommonBlockFormat.lineHeight();
808
809 prevBlockLineType =
810 blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ?
811 blockFormatDiff.lineHeightType() :
812 mostCommonBlockFormat.lineHeightType();
813
814 if (prevBlockLineType == QTextBlockFormat::SingleHeight) {
815 //single line will set lineHeight to 100%
816 prevBlockRelativeLineSpacing = 100;
817 }
818
819 prevBlockAscent = fixFromQtDpi(line.ascent());
820 prevBlockDescent = fixFromQtDpi(line.descent());
821
822
823 if (formats.size()>1) {
824 QStringList texts;
825 QVector<QTextCharFormat> charFormats;
826 for (int f=0; f<formats.size(); f++) {
827 QString chunk;
828 for (int c = 0; c<formats.at(f).length; c++) {
829 chunk.append(text.at(formats.at(f).start+c));
830 }
831 texts.append(chunk);
832 charFormats.append(formats.at(f).format);
833 }
834
835 for (int c = 0; c<texts.size(); c++) {
836 QTextCharFormat diff = formatDifference(charFormats.at(c), mostCommonCharFormat).toCharFormat();
837 const QString subStyle = style(diff, QTextBlockFormat(), mostCommonCharFormat);
838 if (!subStyle.isEmpty()) {
839 svgWriter.writeStartElement("tspan");
840 svgWriter.writeAttribute("style", subStyle);
841 svgWriter.writeCharacters(texts.at(c));
842 svgWriter.writeEndElement();
843 } else {
844 svgWriter.writeCharacters(texts.at(c));
845 }
846 }
847
848 } else {
849 svgWriter.writeCharacters(text);
850 //check format against
851 }
852
853 // Add line-breaks for `pre` modes, but not for the final line.
854 if (wrappingMode != WrappingMode::QtLegacy && &info != &lineInfoList.constLast()) {
855 svgWriter.writeCharacters(QLatin1String("\n"));
856 }
857 svgWriter.writeEndElement();
858 }
859 svgWriter.writeEndElement();//text root element.
860
861 if (svgWriter.hasError()) {
862 d->errors << i18n("Unknown error writing SVG text element");
863 return false;
864 }
865 *svgText = QString::fromUtf8(svgBuffer.data()).trimmed();
866 return true;
867}
868
869void parseTextAttributes(const QXmlStreamAttributes &elementAttributes,
870 QTextCharFormat &charFormat,
871 QTextBlockFormat &blockFormat,
873{
874 QString styleString;
875
876 // we convert all the presentation attributes into styles
877 QString presentationAttributes;
878 for (int a = 0; a < elementAttributes.size(); a++) {
879 if (elementAttributes.at(a).name() != "style") {
880 presentationAttributes
881 .append(elementAttributes.at(a).name().toString())
882 .append(":")
883 .append(elementAttributes.at(a).value().toString())
884 .append(";");
885 }
886 }
887
888 if (presentationAttributes.endsWith(";")) {
889 presentationAttributes.chop(1);
890 }
891
892 if (elementAttributes.hasAttribute("style")) {
893 styleString = elementAttributes.value("style").toString();
894 if (styleString.endsWith(";")) {
895 styleString.chop(1);
896 }
897 }
898
899 if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) {
900 //add attributes to parse them as part of the style.
901 styleString.append(";")
902 .append(presentationAttributes);
903 QStringList styles = styleString.split(";");
904 QVector<QTextFormat> formats =
905 KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat, extraStyles);
906
907 charFormat = formats.at(0).toCharFormat();
908 blockFormat = formats.at(1).toBlockFormat();
909 }
910}
911
912bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc)
913{
914 QXmlStreamReader svgReader(svgText.trimmed());
915 doc->clear();
916 QTextCursor cursor(doc);
917
918 struct BlockFormatRecord {
919 BlockFormatRecord() {}
920 BlockFormatRecord(QTextBlockFormat _blockFormat,
921 QTextCharFormat _charFormat)
922 : blockFormat(_blockFormat),
923 charFormat(_charFormat)
924 {}
925
926 QTextBlockFormat blockFormat;
927 QTextCharFormat charFormat;
928 };
929
930 QStack<BlockFormatRecord> formatStack;
931 formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat()));
932 cursor.setCharFormat(formatStack.top().charFormat);
933 cursor.setBlockFormat(formatStack.top().blockFormat);
934
935 qreal currBlockAbsoluteLineOffset = 0.0;
936 int prevBlockCursorPosition = -1;
937 Qt::Alignment prevBlockAlignment = Qt::AlignLeft;
938 bool prevTspanHasTrailingLF = false;
939 qreal prevLineDescent = 0.0;
940 qreal prevLineAscent = 0.0;
941 // work around uninitialized memory warning, therefore, no boost::none
942 boost::optional<qreal> previousBlockAbsoluteXOffset =
943 boost::optional<qreal>(false, qreal());
944
945 std::optional<ExtraStyles> docExtraStyles;
946
947 while (!svgReader.atEnd()) {
948 QXmlStreamReader::TokenType token = svgReader.readNext();
949 switch (token) {
950 case QXmlStreamReader::StartElement:
951 {
952 prevTspanHasTrailingLF = false;
953
954 bool newBlock = false;
955 QTextBlockFormat newBlockFormat;
956 QTextCharFormat newCharFormat;
957 qreal absoluteLineOffset = 1.0;
958
959 // fetch format of the parent block and make it default
960 if (!formatStack.empty()) {
961 newBlockFormat = formatStack.top().blockFormat;
962 newCharFormat = formatStack.top().charFormat;
963 }
964
965 {
966 ExtraStyles extraStyles{};
967 const QXmlStreamAttributes elementAttributes = svgReader.attributes();
968 parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat, extraStyles);
969
970 if (!docExtraStyles && svgReader.name() == QLatin1String("text")) {
971 if (extraStyles.inlineSize > 0.0) {
972 // There is a valid inline-size, forcing pre-wrap mode.
973 extraStyles.wrappingMode = WrappingMode::WhiteSpacePreWrap;
974 } else if (extraStyles.wrappingMode == WrappingMode::WhiteSpacePreWrap && extraStyles.inlineSize <= 0.0) {
975 // Without a valid inline-size, there is no point using
976 // pre-wrap, so change to pre.
977 extraStyles.wrappingMode = WrappingMode::WhiteSpacePre;
978 }
979 docExtraStyles = extraStyles;
980 }
981
982 // For WrappingMode::QtLegacy,
983 // mnemonic for a newline is (dy != 0 && (x == prevX || alignmentChanged))
984
985 // work around uninitialized memory warning, therefore, no
986 // boost::none
987 boost::optional<qreal> blockAbsoluteXOffset =
988 boost::make_optional(false, qreal());
989
990 if (elementAttributes.hasAttribute("x")) {
991 QString xString = elementAttributes.value("x").toString();
992 if (xString.contains("pt")) {
993 xString = xString.remove("pt").trimmed();
994 }
995 blockAbsoluteXOffset = fixToQtDpi(KisDomUtils::toDouble(xString));
996 }
997
998 // Get current text alignment: If current block has alignment,
999 // use it. Otherwise, try to inherit from parent block.
1000 Qt::Alignment thisBlockAlignment = Qt::AlignLeft;
1001 if (newBlockFormat.hasProperty(QTextBlockFormat::BlockAlignment)) {
1002 thisBlockAlignment = newBlockFormat.alignment();
1003 } else if (!formatStack.empty()) {
1004 thisBlockAlignment = formatStack.top().blockFormat.alignment();
1005 }
1006
1007 const auto isSameXOffset = [&]() {
1008 return previousBlockAbsoluteXOffset && blockAbsoluteXOffset
1009 && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset);
1010 };
1011 if ((isSameXOffset() || thisBlockAlignment != prevBlockAlignment) && svgReader.name() != "text"
1012 && elementAttributes.hasAttribute("dy")) {
1013
1014 QString dyString = elementAttributes.value("dy").toString();
1015 if (dyString.contains("pt")) {
1016 dyString = dyString.remove("pt").trimmed();
1017 }
1018
1019 KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text"));
1020
1021 absoluteLineOffset = fixToQtDpi(KisDomUtils::toDouble(dyString));
1022 newBlock = absoluteLineOffset > 0;
1023 }
1024
1025 if (elementAttributes.hasAttribute("x")) {
1026 previousBlockAbsoluteXOffset = blockAbsoluteXOffset;
1027 }
1028 prevBlockAlignment = thisBlockAlignment;
1029 }
1030
1031 //hack
1032 doc->setTextWidth(100);
1033 doc->setTextWidth(-1);
1034
1035 if (newBlock && absoluteLineOffset > 0) {
1036 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false);
1037 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() > 0, false);
1038
1039 QTextLine line = cursor.block().layout()->lineAt(0);
1040
1041 if (prevBlockCursorPosition >= 0) {
1042 postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent,
1043 prevBlockCursorPosition, currBlockAbsoluteLineOffset);
1044 }
1045
1046 prevBlockCursorPosition = cursor.position();
1047 prevLineAscent = line.ascent();
1048 prevLineDescent = line.descent();
1049 currBlockAbsoluteLineOffset = absoluteLineOffset;
1050
1051 cursor.insertBlock();
1052 cursor.setCharFormat(formatStack.top().charFormat);
1053 cursor.setBlockFormat(formatStack.top().blockFormat);
1054 }
1055
1056 cursor.mergeCharFormat(newCharFormat);
1057 cursor.mergeBlockFormat(newBlockFormat);
1058
1059 formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat()));
1060
1061 break;
1062 }
1063 case QXmlStreamReader::EndElement:
1064 {
1065 if (svgReader.name() != "text") {
1066 formatStack.pop();
1067 KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; }
1068
1069 cursor.setCharFormat(formatStack.top().charFormat);
1070 // For legacy wrapping mode, don't reset block format here
1071 // because this will break the block formats of the current
1072 // (last) block. The latest block format will be applied when
1073 // creating a new block.
1074 // However, resetting the block format is required for pre/pre-
1075 // wrap modes because they do not trigger the same code that
1076 // creates the new block; these modes rely on a trailing `\n`
1077 // at the end of a tspan to start a new block. At this point, if
1078 // there was indeed a trailing `\n`, this means we are already
1079 // in a new block.
1080 if (docExtraStyles && docExtraStyles->wrappingMode != WrappingMode::QtLegacy
1081 && prevTspanHasTrailingLF) {
1082 cursor.setBlockFormat(formatStack.top().blockFormat);
1083 }
1084 prevTspanHasTrailingLF = false;
1085 }
1086 break;
1087 }
1088 case QXmlStreamReader::Characters:
1089 {
1090 cursor.insertText(svgReader.text().toString());
1091 prevTspanHasTrailingLF = svgReader.text().endsWith('\n');
1092 break;
1093 }
1094 default:
1095 break;
1096 }
1097 }
1098
1099 if (prevBlockCursorPosition >= 0) {
1100 QTextLine line = cursor.block().layout()->lineAt(0);
1101 postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent,
1102 prevBlockCursorPosition, currBlockAbsoluteLineOffset);
1103 }
1104
1105 {
1106 if (!docExtraStyles) {
1107 docExtraStyles = ExtraStyles{};
1108 }
1109 QTextFrameFormat f = doc->rootFrame()->frameFormat();
1110 setWrappingMode(&f, docExtraStyles->wrappingMode);
1111 setInlineSize(&f, docExtraStyles->inlineSize);
1112 doc->rootFrame()->setFrameFormat(f);
1113 }
1114
1115 if (svgReader.hasError()) {
1116 d->errors << svgReader.errorString();
1117 return false;
1118 }
1119 doc->setModified(false);
1120 return true;
1121}
1122
1123
1124
1126{
1127 return d->errors;
1128}
1129
1131{
1132 return d->warnings;
1133}
1134
1135bool compareFormatUnderlineWithMostCommon(QTextCharFormat format, QTextCharFormat mostCommon)
1136{
1137 // color and style is not supported in rich text editor yet
1138 // TODO: support color and style
1139 return format.fontUnderline() == mostCommon.fontUnderline()
1140 && format.fontOverline() == mostCommon.fontOverline()
1141 && format.fontStrikeOut() == mostCommon.fontStrikeOut();
1142}
1143
1144QString convertFormatUnderlineToSvg(QTextCharFormat format)
1145{
1146 // color and style is not supported in rich text editor yet
1147 // and text-decoration-line and -style and -color are not supported in svg render either
1148 // hence we just use text-decoration
1149 // TODO: support color and style
1150 QStringList line;
1151
1152 if (format.fontUnderline()) {
1153 line.append("underline");
1154 if (format.underlineStyle() != QTextCharFormat::SingleUnderline) {
1155 warnFile << "Krita only supports solid underline style";
1156 }
1157 }
1158
1159 if (format.fontOverline()) {
1160 line.append("overline");
1161 }
1162
1163 if (format.fontStrikeOut()) {
1164 line.append("line-through");
1165 }
1166
1167 if (line.isEmpty())
1168 {
1169 line.append("none");
1170 }
1171
1172 QString c = QString("text-decoration").append(":")
1173 .append(line.join(" "));
1174
1175 return c;
1176}
1177
1178QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format,
1179 QTextBlockFormat blockFormat,
1180 QTextCharFormat mostCommon,
1181 const bool includeLineHeight)
1182{
1184 for(int i=0; i<format.properties().size(); i++) {
1185 QString c;
1186 int propertyId = format.properties().keys().at(i);
1187
1188 if (propertyId == QTextCharFormat::FontFamily) {
1189 const QString fontFamily = format.properties()[propertyId].toString();
1190 c.append("font-family").append(":").append(fontFamily);
1191 }
1192 if (propertyId == QTextCharFormat::FontPointSize ||
1193 propertyId == QTextCharFormat::FontPixelSize) {
1194
1195 // in Krita we unify point size and pixel size of the font
1196
1197 c.append("font-size").append(":")
1198 .append(format.properties()[propertyId].toString());
1199 }
1200 if (propertyId == QTextCharFormat::FontWeight) {
1201 // Convert from QFont::Weight range to SVG range,
1202 // as defined in qt's qfont.h
1203 int convertedWeight = 400; // Defaulting to Weight::Normal in svg scale
1204
1205 switch (format.properties()[propertyId].toInt()) {
1206 case QFont::Weight::Thin:
1207 convertedWeight = 100;
1208 break;
1209 case QFont::Weight::ExtraLight:
1210 convertedWeight = 200;
1211 break;
1212 case QFont::Weight::Light:
1213 convertedWeight = 300;
1214 break;
1215 case QFont::Weight::Normal:
1216 convertedWeight = 400;
1217 break;
1218 case QFont::Weight::Medium:
1219 convertedWeight = 500;
1220 break;
1221 case QFont::Weight::DemiBold:
1222 convertedWeight = 600;
1223 break;
1224 case QFont::Weight::Bold:
1225 convertedWeight = 700;
1226 break;
1227 case QFont::Weight::ExtraBold:
1228 convertedWeight = 800;
1229 break;
1230 case QFont::Weight::Black:
1231 convertedWeight = 900;
1232 break;
1233 default:
1234 warnFile << "WARNING: Invalid QFont::Weight value supplied to KoSvgTextShapeMarkupConverter::style.";
1235 break;
1236 }
1237
1238 c.append("font-weight").append(":")
1239 .append(QString::number(convertedWeight));
1240 }
1241 if (propertyId == QTextCharFormat::FontItalic) {
1242 QString val = "italic";
1243 if (!format.fontItalic()) {
1244 val = "normal";
1245 }
1246 c.append("font-style").append(":")
1247 .append(val);
1248 }
1249
1250 if (propertyId == QTextCharFormat::FontCapitalization) {
1251 if (format.fontCapitalization() == QFont::SmallCaps){
1252 c.append("font-variant").append(":")
1253 .append("small-caps");
1254 } else if (format.fontCapitalization() == QFont::AllUppercase) {
1255 c.append("text-transform").append(":")
1256 .append("uppercase");
1257 } else if (format.fontCapitalization() == QFont::AllLowercase) {
1258 c.append("text-transform").append(":")
1259 .append("lowercase");
1260 } else if (format.fontCapitalization() == QFont::Capitalize) {
1261 c.append("text-transform").append(":")
1262 .append("capitalize");
1263 }
1264 }
1265
1266 if (propertyId == QTextCharFormat::FontStretch) {
1267 QString valueString = QString::number(format.fontStretch(), 10);
1268 if (format.fontStretch() == QFont::ExtraCondensed) {
1269 valueString = "extra-condensed";
1270 } else if (format.fontStretch() == QFont::SemiCondensed) {
1271 valueString = "semi-condensed";
1272 } else if (format.fontStretch() == QFont::Condensed) {
1273 valueString = "condensed";
1274 } else if (format.fontStretch() == QFont::AnyStretch) {
1275 valueString = "normal";
1276 } else if (format.fontStretch() == QFont::Expanded) {
1277 valueString = "expanded";
1278 } else if (format.fontStretch() == QFont::SemiExpanded) {
1279 valueString = "semi-expanded";
1280 } else if (format.fontStretch() == QFont::ExtraExpanded) {
1281 valueString = "extra-expanded";
1282 } else if (format.fontStretch() == QFont::UltraExpanded) {
1283 valueString = "ultra-expanded";
1284 }
1285 c.append("font-stretch").append(":")
1286 .append(valueString);
1287 }
1288 if (propertyId == QTextCharFormat::FontKerning) {
1289 QString val;
1290 if (format.fontKerning()) {
1291 val = "auto";
1292 } else {
1293 val = "0";
1294 }
1295 c.append("kerning").append(":")
1296 .append(val);
1297 }
1298 if (propertyId == QTextCharFormat::FontWordSpacing) {
1299 c.append("word-spacing").append(":")
1300 .append(QString::number(format.fontWordSpacing()));
1301 }
1302 if (propertyId == QTextCharFormat::FontLetterSpacing) {
1303 QString val;
1304 if (format.fontLetterSpacingType()==QFont::AbsoluteSpacing) {
1305 val = QString::number(format.fontLetterSpacing());
1306 } else {
1307 val = QString::number(((format.fontLetterSpacing()/100)*format.fontPointSize()));
1308 }
1309 c.append("letter-spacing").append(":")
1310 .append(val);
1311 }
1312 if (propertyId == QTextCharFormat::TextOutline) {
1313 if (format.textOutline().color() != mostCommon.textOutline().color()) {
1314 c.append("stroke").append(":")
1315 .append(format.textOutline().color().name());
1316 style.append(c);
1317 c.clear();
1318 }
1319 if (format.textOutline().width() != mostCommon.textOutline().width()) {
1320 c.append("stroke-width").append(":")
1321 .append(QString::number(format.textOutline().width()));
1322 }
1323 }
1324
1325
1326 if (propertyId == QTextCharFormat::TextVerticalAlignment) {
1327 QString val = "baseline";
1328 if (format.verticalAlignment() == QTextCharFormat::AlignSubScript) {
1329 val = QLatin1String("sub");
1330 }
1331 else if (format.verticalAlignment() == QTextCharFormat::AlignSuperScript) {
1332 val = QLatin1String("super");
1333 }
1334 c.append("baseline-shift").append(":").append(val);
1335 }
1336
1337 if (propertyId == QTextCharFormat::ForegroundBrush) {
1338 QColor::NameFormat colorFormat;
1339
1340 if (format.foreground().color().alphaF() < 1.0) {
1341 colorFormat = QColor::HexArgb;
1342 } else {
1343 colorFormat = QColor::HexRgb;
1344 }
1345
1346 c.append("fill").append(":")
1347 .append(format.foreground().color().name(colorFormat));
1348 }
1349
1350 if (!c.isEmpty()) {
1351 style.append(c);
1352 }
1353 }
1354
1355 if (!compareFormatUnderlineWithMostCommon(format, mostCommon)) {
1356
1357 QString c = convertFormatUnderlineToSvg(format);
1358 if (!c.isEmpty()) {
1359 style.append(c);
1360 }
1361 }
1362
1363 if (blockFormat.hasProperty(QTextBlockFormat::BlockAlignment)) {
1364 // TODO: Alignment works incorrectly! The offsets should be calculated
1365 // according to the shape width/height!
1366
1367 QString c;
1368 QString val;
1369 if (blockFormat.alignment()==Qt::AlignRight) {
1370 val = "end";
1371 } else if (blockFormat.alignment()==Qt::AlignCenter) {
1372 val = "middle";
1373 } else {
1374 val = "start";
1375 }
1376 c.append("text-anchor").append(":")
1377 .append(val);
1378 if (!c.isEmpty()) {
1379 style.append(c);
1380 }
1381 }
1382
1383 if (includeLineHeight && blockFormat.hasProperty(QTextBlockFormat::LineHeight)) {
1384 double h = 0;
1385 if (blockFormat.lineHeightType() == QTextBlockFormat::ProportionalHeight) {
1386 h = blockFormat.lineHeight() / 100.0;
1387 } else if (blockFormat.lineHeightType() == QTextBlockFormat::SingleHeight) {
1388 h = -1.0;
1389 }
1390 QString c = "line-height:";
1391 if (h >= 0) {
1392 c += QString::number(blockFormat.lineHeight() / 100.0);
1393 } else {
1394 c += "normal";
1395 }
1396 style.append(c);
1397 }
1398
1399 return style.join("; ");
1400}
1401
1403 QTextCharFormat currentCharFormat,
1404 QTextBlockFormat currentBlockFormat,
1405 ExtraStyles &extraStyles)
1406{
1407 Q_UNUSED(currentBlockFormat);
1408
1409 QVector<QTextFormat> formats;
1410 QTextCharFormat charFormat;
1411 charFormat.setTextOutline(currentCharFormat.textOutline());
1412 QTextBlockFormat blockFormat;
1413 QScopedPointer<SvgGraphicsContext> context(new SvgGraphicsContext());
1415
1416 for (int i=0; i<styles.size(); i++) {
1417 if (!styles.at(i).isEmpty()){
1418 QStringList style = styles.at(i).split(":");
1419 // ignore the property instead of crashing,
1420 // if user forgets to separate property name and value with ':'.
1421 if (style.size() < 2) {
1422 continue;
1423 }
1424
1425 QString property = style.at(0).trimmed();
1426 QString value = style.at(1).trimmed();
1427
1428 if (property == "font-family") {
1429 charFormat.setFontFamily(value);
1430 }
1431
1432 if (property == "font-size") {
1433 qreal val = SvgUtil::parseUnitX(context.data(), resolved, value);
1434 charFormat.setFontPointSize(val);
1435 }
1436
1437 if (property == "font-variant") {
1438 if (value=="small-caps") {
1439 charFormat.setFontCapitalization(QFont::SmallCaps);
1440 } else {
1441 charFormat.setFontCapitalization(QFont::MixedCase);
1442 }
1443 }
1444
1445 if (property == "font-style") {
1446 if (value=="italic" || value=="oblique") {
1447 charFormat.setFontItalic(true);
1448 } else {
1449 charFormat.setFontItalic(false);
1450 }
1451 }
1452
1453 if (property == "font-stretch") {
1454 if (value == "ultra-condensed") {
1455 charFormat.setFontStretch(QFont::UltraCondensed);
1456 } else if (value == "condensed") {
1457 charFormat.setFontStretch(QFont::Condensed);
1458 } else if (value == "semi-condensed") {
1459 charFormat.setFontStretch(QFont::SemiCondensed);
1460 } else if (value == "normal") {
1461 charFormat.setFontStretch(100);
1462 } else if (value == "semi-expanded") {
1463 charFormat.setFontStretch(QFont::SemiExpanded);
1464 } else if (value == "expanded") {
1465 charFormat.setFontStretch(QFont::Expanded);
1466 } else if (value == "extra-expanded") {
1467 charFormat.setFontStretch(QFont::ExtraExpanded);
1468 } else if (value == "ultra-expanded") {
1469 charFormat.setFontStretch(QFont::UltraExpanded);
1470 } else { // "normal"
1471 charFormat.setFontStretch(value.toInt());
1472 }
1473 }
1474
1475 if (property == "font-weight") {
1476 // Convert from SVG range to QFont::Weight range,
1477 // as defined in qt's qfont.h
1478 int convertedWeight = QFont::Weight::Normal; // Defaulting to Weight::Normal
1479
1480 switch (value.toInt()) {
1481 case 100:
1482 convertedWeight = QFont::Weight::Thin;
1483 break;
1484 case 200:
1485 convertedWeight = QFont::Weight::ExtraLight;
1486 break;
1487 case 300:
1488 convertedWeight = QFont::Weight::Light;
1489 break;
1490 case 400:
1491 convertedWeight = QFont::Weight::Normal;
1492 break;
1493 case 500:
1494 convertedWeight = QFont::Weight::Medium;
1495 break;
1496 case 600:
1497 convertedWeight = QFont::Weight::DemiBold;
1498 break;
1499 case 700:
1500 convertedWeight = QFont::Weight::Bold;
1501 break;
1502 case 800:
1503 convertedWeight = QFont::Weight::ExtraBold;
1504 break;
1505 case 900:
1506 convertedWeight = QFont::Weight::Black;
1507 break;
1508 default:
1509 warnFile << "WARNING: Invalid weight value supplied to KoSvgTextShapeMarkupConverter::stylesFromString.";
1510 break;
1511 }
1512
1513 charFormat.setFontWeight(convertedWeight);
1514 }
1515
1516 if (property == "text-decoration") {
1517 charFormat.setFontUnderline(false);
1518 charFormat.setFontOverline(false);
1519 charFormat.setFontStrikeOut(false);
1520 QStringList values = value.split(" ");
1521 if (values.contains("line-through")) {
1522 charFormat.setFontStrikeOut(true);
1523 }
1524 if (values.contains("overline")) {
1525 charFormat.setFontOverline(true);
1526 }
1527 if(values.contains("underline")){
1528 charFormat.setFontUnderline(true);
1529 }
1530 }
1531
1532 if (property == "text-transform") {
1533 if (value == "uppercase") {
1534 charFormat.setFontCapitalization(QFont::AllUppercase);
1535 } else if (value == "lowercase") {
1536 charFormat.setFontCapitalization(QFont::AllLowercase);
1537 } else if (value == "capitalize") {
1538 charFormat.setFontCapitalization(QFont::Capitalize);
1539 } else{
1540 charFormat.setFontCapitalization(QFont::MixedCase);
1541 }
1542 }
1543
1544 if (property == "letter-spacing") {
1545 qreal val = SvgUtil::parseUnitX(context.data(), resolved, value);
1546 charFormat.setFontLetterSpacingType(QFont::AbsoluteSpacing);
1547 charFormat.setFontLetterSpacing(val);
1548 }
1549
1550 if (property == "word-spacing") {
1551 qreal val = SvgUtil::parseUnitX(context.data(), resolved, value);
1552 charFormat.setFontWordSpacing(val);
1553 }
1554
1555 if (property == "kerning") {
1556 if (value == "auto") {
1557 charFormat.setFontKerning(true);
1558 } else {
1559 qreal val = SvgUtil::parseUnitX(context.data(), resolved, value);
1560 charFormat.setFontKerning(false);
1561 charFormat.setFontLetterSpacingType(QFont::AbsoluteSpacing);
1562 charFormat.setFontLetterSpacing(charFormat.fontLetterSpacing() + val);
1563 }
1564 }
1565
1566 if (property == "stroke") {
1567 QPen pen = charFormat.textOutline();
1568 QColor color;
1569 color.setNamedColor(value);
1570 pen.setColor(color);
1571 charFormat.setTextOutline(pen);
1572 }
1573
1574 if (property == "stroke-width") {
1575 QPen pen = charFormat.textOutline();
1576 pen.setWidth(value.toInt());
1577 charFormat.setTextOutline(pen);
1578 }
1579
1580 if (property == "fill") {
1581 QColor color;
1582 color.setNamedColor(value);
1583
1584 // avoid assertion failure in `KoColor` later
1585 if (!color.isValid()) {
1586 continue;
1587 }
1588
1589 // default color is #ff000000, so default alpha will be 1.0
1590 qreal currentAlpha = charFormat.foreground().color().alphaF();
1591
1592 // if alpha was already defined by `fill-opacity` prop
1593 if (currentAlpha < 1.0) {
1594 // and `fill` doesn't have alpha component
1595 if (color.alphaF() < 1.0) {
1596 color.setAlphaF(currentAlpha);
1597 }
1598 }
1599
1600 charFormat.setForeground(color);
1601 }
1602
1603 if (property == "fill-opacity") {
1604 QColor color = charFormat.foreground().color();
1605 bool ok = true;
1606 qreal alpha = qBound(0.0, SvgUtil::fromPercentage(value, &ok), 1.0);
1607
1608 // if conversion fails due to non-numeric input,
1609 // it defaults to 0.0, default to current alpha instead
1610 if (!ok) {
1611 alpha = color.alphaF();
1612 }
1613 color.setAlphaF(alpha);
1614 charFormat.setForeground(color);
1615 }
1616
1617 if (property == "text-anchor") {
1618 if (value == "end") {
1619 blockFormat.setAlignment(Qt::AlignRight);
1620 } else if (value == "middle") {
1621 blockFormat.setAlignment(Qt::AlignCenter);
1622 } else {
1623 blockFormat.setAlignment(Qt::AlignLeft);
1624 }
1625 }
1626
1627 if (property == "baseline-shift") {
1628 if (value == "super") {
1629 charFormat.setVerticalAlignment(QTextCharFormat::AlignSuperScript);
1630 } else if (value == "sub") {
1631 charFormat.setVerticalAlignment(QTextCharFormat::AlignSubScript);
1632 } else {
1633 charFormat.setVerticalAlignment(QTextCharFormat::AlignNormal);
1634 }
1635 }
1636
1637 if (property == "line-height") {
1638 double lineHeightPercent = -1.0;
1639 bool ok = false;
1640 if (value.endsWith('%')) {
1641 // Note: Percentage line-height behaves differently than
1642 // unitless number in case of nested descendant elements,
1643 // but here we pretend they are the same.
1644 lineHeightPercent = value.left(value.length() - 1).toDouble(&ok);
1645 if (!ok) {
1646 lineHeightPercent = -1.0;
1647 }
1648 } else if(const double unitless = value.toDouble(&ok); ok) {
1649 lineHeightPercent = unitless * 100.0;
1650 } else if (value == QLatin1String("normal")) {
1651 lineHeightPercent = -1.0;
1652 blockFormat.setLineHeight(1, QTextBlockFormat::SingleHeight);
1653 }
1654 if (lineHeightPercent >= 0) {
1655 blockFormat.setLineHeight(lineHeightPercent, QTextBlockFormat::ProportionalHeight);
1656 }
1657 }
1658
1659 if (property == "inline-size") {
1660 const qreal val = SvgUtil::parseUnitX(context.data(), resolved, value);
1661 if (val > 0.0) {
1662 extraStyles.inlineSize = val;
1663 }
1664 }
1665
1666 if (property == "white-space") {
1667 if (value == QLatin1String("pre")) {
1669 } else if (value == QLatin1String("pre-wrap")) {
1671 } else {
1673 }
1674 }
1675 }
1676 }
1677
1678 formats.append(charFormat);
1679 formats.append(blockFormat);
1680 return formats;
1681}
1682
1683QTextFormat KoSvgTextShapeMarkupConverter::formatDifference(QTextFormat test, QTextFormat reference)
1684{
1685 //copied from QTextDocument.cpp
1686 QTextFormat diff = test;
1687 //props should proly compare itself to the main text format...
1688 const QMap<int, QVariant> props = reference.properties();
1689 for (QMap<int, QVariant>::ConstIterator it = props.begin(), end = props.end();
1690 it != end; ++it)
1691 if (it.value() == test.property(it.key())) {
1692 // Some props must not be removed as default state gets in the way.
1693 switch (it.key()) {
1694 case QTextFormat::TextUnderlineStyle: // 0x2023
1695 case QTextFormat::FontLetterSpacingType: // 0x2033 in Qt5, but is 0x1FE9 in Qt6
1696 case QTextFormat::LineHeightType:
1697 continue;
1698 }
1699 diff.clearProperty(it.key());
1700 }
1701 return diff;
1702}
1703
1705KoSvgTextShapeMarkupConverter::getWrappingMode(const QTextFrameFormat &frameFormat)
1706{
1707 const QVariant wrappingMode = frameFormat.property(WrappingModeProperty);
1708 if (wrappingMode.userType() != QMetaType::Int) {
1710 }
1711 return static_cast<WrappingMode>(wrappingMode.toInt());
1712}
1713
1714void KoSvgTextShapeMarkupConverter::setWrappingMode(QTextFrameFormat *frameFormat, WrappingMode wrappingMode)
1715{
1716 frameFormat->setProperty(WrappingModeProperty, static_cast<int>(wrappingMode));
1717}
1718
1719std::optional<double> KoSvgTextShapeMarkupConverter::getInlineSize(const QTextFrameFormat &frameFormat)
1720{
1721 const QVariant inlineSize = frameFormat.property(InlineSizeProperty);
1722 if (inlineSize.userType() != QMetaType::Double) {
1723 return {};
1724 }
1725 const double val = inlineSize.toDouble();
1726 if (val > 0.0) {
1727 return {val};
1728 }
1729 return {};
1730}
1731
1732void KoSvgTextShapeMarkupConverter::setInlineSize(QTextFrameFormat *frameFormat, double inlineSize)
1733{
1734 if (inlineSize >= 0.0) {
1735 frameFormat->setProperty(InlineSizeProperty, inlineSize);
1736 } else {
1737 frameFormat->clearProperty(InlineSizeProperty);
1738 }
1739}
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
#define debugFlake
Definition FlakeDebug.h:15
float value(const T *src, size_t ch)
const Params2D p
void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat, KoSvgTextShapeMarkupConverter::ExtraStyles &extraStyles)
QTextFormat findMostCommonFormat(const QList< QTextFormat > &allFormats)
bool compareFormatUnderlineWithMostCommon(QTextCharFormat format, QTextCharFormat mostCommon)
Q_GUI_EXPORT int qt_defaultDpi()
qreal calcLineWidth(const QTextBlock &block)
qreal fixToQtDpi(qreal value)
qreal fixFromQtDpi(qreal value)
QString convertFormatUnderlineToSvg(QTextCharFormat format)
void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset)
static bool guessIsRightToLeft(QStringView text)
QStringList errors() const
static const KoSvgTextProperties & defaultProperties()
bool convertToSvg(QString *svgText, QString *stylesText)
bool convertToHtml(QString *htmlText)
convertToHtml convert the text in the text shape to html
QString style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon=QTextCharFormat(), bool includeLineHeight=false)
style creates a style string based on the blockformat and the format.
static void setWrappingMode(QTextFrameFormat *frameFormat, WrappingMode wrappingMode)
Set the Wrapping Mode on a frame format to be applied to the rootFrame of a QTextDocument.
bool convertFromHtml(const QString &htmlText, QString *svgText, QString *styles)
convertFromHtml converted Qt rich text html (and no other: https://doc.qt.io/qt-5/richtext-html-subse...
static std::optional< double > getInlineSize(const QTextFrameFormat &frameFormat)
Get the inline-size from the frameFormat of the rootFrame of a QTextDocument.
bool convertDocumentToSvg(const QTextDocument *doc, QString *svgText)
convertDocumentToSvg
QTextFormat formatDifference(QTextFormat test, QTextFormat reference)
formatDifference A class to get the difference between two text-char formats.
static QVector< QTextFormat > stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat, ExtraStyles &extraStyles)
stylesFromString returns a qvector with two textformats: at 0 is the QTextCharFormat at 1 is the QTex...
bool convertSvgToDocument(const QString &svgText, QTextDocument *doc)
convertSvgToDocument
static WrappingMode getWrappingMode(const QTextFrameFormat &frameFormat)
Get the Wrapping Mode from the frameFormat of the rootFrame of a QTextDocument.
bool convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch)
upload the svg representation of text into the shape
static constexpr QTextFormat::Property WrappingModeProperty
static constexpr QTextFormat::Property InlineSizeProperty
static void setInlineSize(QTextFrameFormat *frameFormat, double inlineSize)
Set or unset the inline-size on a frameFormat to be applied to the rootFrame of a QTextDocument.
static QDomDocument createDocumentFromSvg(QIODevice *device, QString *errorMsg=0, int *errorLine=0, int *errorColumn=0)
KoShape * parseTextElement(const QDomElement &e, KoSvgTextShape *mergeIntoShape=0)
void setResolution(const QRectF boundsInPixels, qreal pixelsPerInch)
void parseDefsElement(const QDomElement &e)
Context for saving svg files.
void setStrippedTextMode(bool value)
static qreal parseUnitX(SvgGraphicsContext *gc, const KoSvgTextProperties &resolved, const QString &unit)
parses a length attribute in x-direction
Definition SvgUtil.cpp:304
static double fromPercentage(QString s, bool *ok=nullptr)
Definition SvgUtil.cpp:64
Implements exporting shapes to SVG.
Definition SvgWriter.h:33
bool saveDetached(QIODevice &outputDevice)
static bool qFuzzyCompare(half p1, half p2)
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_BREAK(cond)
Definition kis_assert.h:127
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define warnFile
Definition kis_debug.h:95
double toDouble(const QString &str, bool *ok=nullptr)
QString toString(const QString &value)