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