diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index 68443c0e2c..b4443e012e 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1244 +1,1253 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn); if (doc.isNull()) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); debugFlake << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { debugFlake << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { debugFlake << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line debugFlake << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { debugFlake << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { debugFlake << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); // disable auto-formatting to avoid axtra spaces appearing here and there svgWriter.setAutoFormatting(false); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape * (according to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; boost::optional previousBlockAbsoluteXOffset = boost::none; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) boost::optional blockAbsoluteXOffset = boost::none; if (elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } blockAbsoluteXOffset = KisDomUtils::toDouble(xString); } if (previousBlockAbsoluteXOffset && blockAbsoluteXOffset && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) && svgReader.name() != "text" && elementAttributes.hasAttribute("dy")) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } if (elementAttributes.hasAttribute("x")) { previousBlockAbsoluteXOffset = blockAbsoluteXOffset; } } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() > 0, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; SvgGraphicsContext *context = new SvgGraphicsContext(); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) - if (it.value() == test.property(it.key())) + if (it.value() == test.property(it.key())) { + // Some props must not be removed as default state gets in the way. + if (it.key() == 0x2023) { // TextUnderlineStyle + continue; + } diff.clearProperty(it.key()); + } return diff; }