diff --git a/libs/flake/text/KoSvgTextChunkShape.cpp b/libs/flake/text/KoSvgTextChunkShape.cpp index 15122653de..cf1ac88ba7 100644 --- a/libs/flake/text/KoSvgTextChunkShape.cpp +++ b/libs/flake/text/KoSvgTextChunkShape.cpp @@ -1,985 +1,985 @@ /* * 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 v * 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 "KoSvgTextChunkShape.h" #include "KoSvgTextChunkShape_p.h" #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { void appendLazy(QVector *list, boost::optional value, int iteration, bool hasDefault = true, qreal defaultValue = 0.0) { if (!value) return; if (value && *value == defaultValue && hasDefault == true && list->isEmpty()) return; while (list->size() < iteration) { list->append(defaultValue); } list->append(*value); } void fillTransforms(QVector *xPos, QVector *yPos, QVector *dxPos, QVector *dyPos, QVector *rotate, QVector localTransformations) { for (int i = 0; i < localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = localTransformations[i]; appendLazy(xPos, t.xPos, i, false); appendLazy(yPos, t.yPos, i, false); appendLazy(dxPos, t.dxPos, i); appendLazy(dyPos, t.dyPos, i); appendLazy(rotate, t.rotate, i); } } QVector parseListAttributeX(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitX(context.currentGC(), str); } return result; } QVector parseListAttributeY(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitY(context.currentGC(), str); } return result; } QVector parseListAttributeAngular(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitAngular(context.currentGC(), str); } return result; } QString convertListAttribute(const QVector &values) { QStringList stringValues; Q_FOREACH (qreal value, values) { stringValues.append(KisDomUtils::toString(value)); } return stringValues.join(','); } } struct KoSvgTextChunkShape::Private::LayoutInterface : public KoSvgTextChunkShapeLayoutInterface { LayoutInterface(KoSvgTextChunkShape *_q) : q(_q) {} KoSvgText::AutoValue textLength() const override { return q->d->textLength; } KoSvgText::LengthAdjust lengthAdjust() const override { return q->d->lengthAdjust; } int numChars() const override { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d->text.isEmpty(), 0); int result = 0; if (!q->shapeCount()) { result = q->d->text.size(); } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); result += chunkShape->layoutInterface()->numChars(); } } return result; } int relativeCharPos(KoSvgTextChunkShape *child, int pos) const override { QList childShapes = q->shapes(); int result = -1; int numCharsPassed = 0; Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); if (chunkShape == child) { result = pos + numCharsPassed; break; } else { numCharsPassed += chunkShape->layoutInterface()->numChars(); } } return result; } bool isTextNode() const override { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d->text.isEmpty(), false); return !q->shapeCount(); } QString nodeText() const override { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d->text.isEmpty(), 0); return !q->shapeCount() ? q->d->text : QString(); } QVector localCharTransformations() const override { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(isTextNode(), QVector()); const QVector t = q->d->localTransformations; return t.mid(0, qMin(t.size(), q->d->text.size())); } static QString getBidiOpening(KoSvgText::Direction direction, KoSvgText::UnicodeBidi bidi) { using namespace KoSvgText; QString result; if (bidi == BidiEmbed) { result = direction == DirectionLeftToRight ? "\u202a" : "\u202b"; } else if (bidi == BidiOverride) { result = direction == DirectionLeftToRight ? "\u202d" : "\u202e"; } return result; } QVector collectSubChunks() const override { QVector result; if (isTextNode()) { const QString text = q->d->text; const KoSvgText::KoSvgCharChunkFormat format = q->fetchCharFormat(); QVector transforms = q->d->localTransformations; /** * Sometimes SVG can contain the X,Y offsets for the pieces of text that * do not exist, just skip them. */ if (text.size() <= transforms.size()) { transforms.resize(text.size()); } KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(q->d->properties.propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt()); KoSvgText::Direction direction = KoSvgText::Direction(q->d->properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); const QString bidiOpening = getBidiOpening(direction, bidi); if (!bidiOpening.isEmpty()) { result << SubChunk(bidiOpening, format); } if (transforms.isEmpty()) { result << SubChunk(text, format); } else { for (int i = 0; i < transforms.size(); i++) { const KoSvgText::CharTransformation baseTransform = transforms[i]; int subChunkLength = 1; for (int j = i + 1; j < transforms.size(); j++) { if (transforms[j].isNull()) { subChunkLength++; } else { break; } } if (i + subChunkLength >= transforms.size()) { subChunkLength = text.size() - i; } result << SubChunk(text.mid(i, subChunkLength), format, baseTransform); i += subChunkLength - 1; } } if (!bidiOpening.isEmpty()) { result << SubChunk("\u202c", format); } } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result += chunkShape->layoutInterface()->collectSubChunks(); } } return result; } void addAssociatedOutline(const QRectF &rect) override { KIS_SAFE_ASSERT_RECOVER_RETURN(isTextNode()); QPainterPath path; path.addRect(rect); path |= q->d->associatedOutline; path.setFillRule(Qt::WindingFill); path = path.simplified(); q->d->associatedOutline = path; q->setSize(path.boundingRect().size()); q->notifyChanged(); q->shapeChangedPriv(KoShape::SizeChanged); } void clearAssociatedOutline() override { q->d->associatedOutline = QPainterPath(); q->setSize(QSizeF()); q->notifyChanged(); q->shapeChangedPriv(KoShape::SizeChanged); } private: KoSvgTextChunkShape *q; }; KoSvgTextChunkShape::KoSvgTextChunkShape() : KoShapeContainer() , d(new Private) { d->layoutInterface.reset(new KoSvgTextChunkShape::Private::LayoutInterface(this)); } KoSvgTextChunkShape::KoSvgTextChunkShape(const KoSvgTextChunkShape &rhs) : KoShapeContainer(rhs) , d(rhs.d) { if (rhs.model()) { SimpleShapeContainerModel *otherModel = dynamic_cast(rhs.model()); KIS_ASSERT_RECOVER_RETURN(otherModel); setModelInit(new SimpleShapeContainerModel(*otherModel)); } // XXX: this will immediately lead to a detach d->layoutInterface.reset(new KoSvgTextChunkShape::Private::LayoutInterface(this)); } KoSvgTextChunkShape::~KoSvgTextChunkShape() { } KoShape *KoSvgTextChunkShape::cloneShape() const { return new KoSvgTextChunkShape(*this); } QSizeF KoSvgTextChunkShape::size() const { return outlineRect().size(); } void KoSvgTextChunkShape::setSize(const QSizeF &size) { Q_UNUSED(size); // we do not support resizing! } QRectF KoSvgTextChunkShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoSvgTextChunkShape::outline() const { QPainterPath result; result.setFillRule(Qt::WindingFill); if (d->layoutInterface->isTextNode()) { result = d->associatedOutline; } else { Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result |= chunkShape->outline(); } } return result.simplified(); } void KoSvgTextChunkShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); } void KoSvgTextChunkShape::saveOdf(KoShapeSavingContext &context) const { Q_UNUSED(context); } bool KoSvgTextChunkShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_UNUSED(element); Q_UNUSED(context); return false; } bool KoSvgTextChunkShape::saveHtml(HtmlSavingContext &context) { // Should we add a newline? Check for vertical movement if we're using rtl or ltr text // XXX: if vertical text, check horizontal movement. QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); for (int i = 0; i < d->localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = d->localTransformations[i]; appendLazy(&xPos, t.xPos, i, false); appendLazy(&yPos, t.yPos, i, false); appendLazy(&dxPos, t.dxPos, i); appendLazy(&dyPos, t.dyPos, i); } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); // XXX: we don't save fill, stroke, text length, length adjust or spacing and glyphs. KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); if (isRootTextNode()) { context.shapeWriter().startElement("body", false); if (layoutInterface()->isTextNode()) { context.shapeWriter().startElement("p", false); } // XXX: Save the style? } else if (parent->isRootTextNode()) { context.shapeWriter().startElement("p", false); } else { context.shapeWriter().startElement("span", false); // XXX: Save the style? } QMap attributes = ownProperties.convertToSvgTextAttributes(); if (attributes.size() > 0) { QString styleString; for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { if (QString(it.key().toLatin1().data()).contains("text-anchor")) { QString val = it.value(); if (it.value()=="middle") { val = "center"; } else if (it.value()=="end") { val = "right"; } else { val = "left"; } styleString.append("text-align") .append(": ") .append(val) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("fill")){ styleString.append("color") .append(": ") .append(it.value()) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("font-size")){ QString val = it.value(); if (QRegExp ("\\d*").exactMatch(val)) { val.append("pt"); } styleString.append(it.key().toLatin1().data()) .append(": ") .append(val) .append(";" ); } else { styleString.append(it.key().toLatin1().data()) .append(": ") .append(it.value()) .append(";" ); } } context.shapeWriter().addAttribute("style", styleString); } if (layoutInterface()->isTextNode()) { debugFlake << "saveHTML" << this << d->text << xPos << yPos << dxPos << dyPos; // After adding all the styling to the

element, add the text context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveHtml(context); } } if (isRootTextNode() && layoutInterface()->isTextNode()) { context.shapeWriter().endElement(); // body } context.shapeWriter().endElement(); // p or span return true; } void writeTextListAttribute(const QString &attribute, const QVector &values, KoXmlWriter &writer) { const QString value = convertListAttribute(values); if (!value.isEmpty()) { writer.addAttribute(attribute.toLatin1().data(), value); } } bool KoSvgTextChunkShape::saveSvg(SvgSavingContext &context) { if (isRootTextNode()) { context.shapeWriter().startElement("text", false); if (!context.strippedTextMode()) { context.shapeWriter().addAttribute("id", context.getID(this)); context.shapeWriter().addAttribute("krita:useRichText", d->isRichTextPreferred ? "true" : "false"); SvgUtil::writeTransformAttributeLazy("transform", transformation(), context.shapeWriter()); SvgStyleWriter::saveSvgStyle(this, context); } else { SvgStyleWriter::saveSvgFill(this, context); SvgStyleWriter::saveSvgStroke(this, context); } } else { context.shapeWriter().startElement("tspan", false); if (!context.strippedTextMode()) { SvgStyleWriter::saveSvgBasicStyle(this, context); } } if (layoutInterface()->isTextNode()) { QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); writeTextListAttribute("x", xPos, context.shapeWriter()); writeTextListAttribute("y", yPos, context.shapeWriter()); writeTextListAttribute("dx", dxPos, context.shapeWriter()); writeTextListAttribute("dy", dyPos, context.shapeWriter()); writeTextListAttribute("rotate", rotate, context.shapeWriter()); } if (!d->textLength.isAuto) { context.shapeWriter().addAttribute("textLength", KisDomUtils::toString(d->textLength.customValue)); if (d->lengthAdjust == KoSvgText::LengthAdjustSpacingAndGlyphs) { context.shapeWriter().addAttribute("lengthAdjust", "spacingAndGlyphs"); } } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); // we write down stroke/fill iff they are different from the parent's value if (!isRootTextNode()) { if (ownProperties.hasProperty(KoSvgTextProperties::FillId)) { SvgStyleWriter::saveSvgFill(this, context); } if (ownProperties.hasProperty(KoSvgTextProperties::StrokeId)) { SvgStyleWriter::saveSvgStroke(this, context); } } QMap attributes = ownProperties.convertToSvgTextAttributes(); for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { context.shapeWriter().addAttribute(it.key().toLatin1().data(), it.value()); } if (layoutInterface()->isTextNode()) { context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveSvg(context); } } context.shapeWriter().endElement(); return true; } void KoSvgTextChunkShape::Private::loadContextBasedProperties(SvgGraphicsContext *gc) { properties = gc->textProperties; font = gc->font; fontFamiliesList = gc->fontFamiliesList; } void KoSvgTextChunkShape::resetTextShape() { using namespace KoSvgText; d->properties = KoSvgTextProperties(); d->font = QFont(); d->fontFamiliesList = QStringList(); d->textLength = AutoValue(); d->lengthAdjust = LengthAdjustSpacing; d->localTransformations.clear(); d->text.clear(); // all the subchunks are destroyed! // (first detach, then destroy) QList shapesToReset = shapes(); Q_FOREACH (KoShape *shape, shapesToReset) { shape->setParent(0); delete shape; } } bool KoSvgTextChunkShape::loadSvg(const KoXmlElement &e, SvgLoadingContext &context) { SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); d->textLength = KoSvgText::parseAutoValueXY(e.attribute("textLength", ""), context, ""); d->lengthAdjust = KoSvgText::parseLengthAdjust(e.attribute("lengthAdjust", "spacing")); QVector xPos = parseListAttributeX(e.attribute("x", ""), context); QVector yPos = parseListAttributeY(e.attribute("y", ""), context); QVector dxPos = parseListAttributeX(e.attribute("dx", ""), context); QVector dyPos = parseListAttributeY(e.attribute("dy", ""), context); QVector rotate = parseListAttributeAngular(e.attribute("rotate", ""), context); const int numLocalTransformations = std::max({xPos.size(), yPos.size(), dxPos.size(), dyPos.size(), rotate.size()}); d->localTransformations.resize(numLocalTransformations); for (int i = 0; i < numLocalTransformations; i++) { if (i < xPos.size()) { d->localTransformations[i].xPos = xPos[i]; } if (i < yPos.size()) { d->localTransformations[i].yPos = yPos[i]; } if (i < dxPos.size() && dxPos[i] != 0.0) { d->localTransformations[i].dxPos = dxPos[i]; } if (i < dyPos.size() && dyPos[i] != 0.0) { d->localTransformations[i].dyPos = dyPos[i]; } if (i < rotate.size()) { d->localTransformations[i].rotate = rotate[i]; } } return true; } namespace { QString cleanUpString(QString text) { - text.replace(QRegExp("[\\r\\n]"), ""); + text.replace(QRegExp("[\\r\\n\u2028]"), ""); text.replace(QRegExp(" {2,}"), " "); return text; } enum Result { FoundNothing, FoundText, FoundSpace }; Result hasPreviousSibling(KoXmlNode node) { while (!node.isNull()) { if (node.isElement()) { KoXmlElement element = node.toElement(); if (element.tagName() == "text") break; } while (!node.previousSibling().isNull()) { node = node.previousSibling(); while (!node.lastChild().isNull()) { node = node.lastChild(); } if (node.isText()) { KoXmlText textNode = node.toText(); const QString text = cleanUpString(textNode.data()); if (!text.isEmpty()) { // if we are the leading whitespace, we should report that // we are the last if (text == " ") { return hasPreviousSibling(node) == FoundNothing ? FoundNothing : FoundSpace; } return text[text.size() - 1] != ' ' ? FoundText : FoundSpace; } } } node = node.parentNode(); } return FoundNothing; } Result hasNextSibling(KoXmlNode node) { while (!node.isNull()) { while (!node.nextSibling().isNull()) { node = node.nextSibling(); while (!node.firstChild().isNull()) { node = node.firstChild(); } if (node.isText()) { KoXmlText textNode = node.toText(); const QString text = cleanUpString(textNode.data()); // if we are the trailing whitespace, we should report that // we are the last if (text == " ") { return hasNextSibling(node) == FoundNothing ? FoundNothing : FoundSpace; } if (!text.isEmpty()) { return text[0] != ' ' ? FoundText : FoundSpace; } } } node = node.parentNode(); } return FoundNothing; } } bool KoSvgTextChunkShape::loadSvgTextNode(const KoXmlText &text, SvgLoadingContext &context) { SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); QString data = cleanUpString(text.data()); const Result leftBorder = hasPreviousSibling(text); const Result rightBorder = hasNextSibling(text); if (data.startsWith(' ') && leftBorder == FoundNothing) { data.remove(0, 1); } if (data.endsWith(' ') && rightBorder != FoundText) { data.remove(data.size() - 1, 1); } if (data == " " && (leftBorder == FoundNothing || rightBorder == FoundNothing)) { data = ""; } //ENTER_FUNCTION() << text.data() << "-->" << data; d->text = data; return !data.isEmpty(); } void KoSvgTextChunkShape::normalizeCharTransformations() { applyParentCharTransformations(d->localTransformations); } void KoSvgTextChunkShape::simplifyFillStrokeInheritance() { if (!isRootTextNode()) { KoShape *parentShape = parent(); KIS_SAFE_ASSERT_RECOVER_RETURN(parentShape); QSharedPointer bg = background(); QSharedPointer parentBg = parentShape->background(); if (!inheritBackground() && ((!bg && !parentBg) || (bg && parentBg && bg->compareTo(parentShape->background().data())))) { setInheritBackground(true); } KoShapeStrokeModelSP stroke = this->stroke(); KoShapeStrokeModelSP parentStroke= parentShape->stroke(); if (!inheritStroke() && ((!stroke && !parentStroke) || (stroke && parentStroke && stroke->compareFillTo(parentShape->stroke().data()) && stroke->compareStyleTo(parentShape->stroke().data())))) { setInheritStroke(true); } } Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->simplifyFillStrokeInheritance(); } } KoSvgTextProperties KoSvgTextChunkShape::textProperties() const { KoSvgTextProperties properties = d->properties; properties.setProperty(KoSvgTextProperties::FillId, QVariant::fromValue(KoSvgText::BackgroundProperty(background()))); properties.setProperty(KoSvgTextProperties::StrokeId, QVariant::fromValue(KoSvgText::StrokeProperty(stroke()))); return properties; } bool KoSvgTextChunkShape::isTextNode() const { return d->layoutInterface->isTextNode(); } KoSvgTextChunkShapeLayoutInterface *KoSvgTextChunkShape::layoutInterface() { return d->layoutInterface.data(); } bool KoSvgTextChunkShape::isRichTextPreferred() const { return isRootTextNode() && d->isRichTextPreferred; } void KoSvgTextChunkShape::setRichTextPreferred(bool value) { KIS_SAFE_ASSERT_RECOVER_RETURN(isRootTextNode()); d->isRichTextPreferred = value; } bool KoSvgTextChunkShape::isRootTextNode() const { return false; } /**************************************************************************************************/ /* KoSvgTextChunkShape::Private */ /**************************************************************************************************/ #include "SimpleShapeContainerModel.h" KoSvgTextChunkShape::Private::Private() : QSharedData() { } KoSvgTextChunkShape::Private::Private(const Private &rhs) : QSharedData() , properties(rhs.properties) , font(rhs.font) , fontFamiliesList(rhs.fontFamiliesList) , localTransformations(rhs.localTransformations) , textLength(rhs.textLength) , lengthAdjust(rhs.lengthAdjust) , text(rhs.text) , isRichTextPreferred(rhs.isRichTextPreferred) { } KoSvgTextChunkShape::Private::~Private() { } #include #include #include KoSvgText::KoSvgCharChunkFormat KoSvgTextChunkShape::fetchCharFormat() const { KoSvgText::KoSvgCharChunkFormat format; format.setFont(d->font); format.setTextAnchor(KoSvgText::TextAnchor(d->properties.propertyOrDefault(KoSvgTextProperties::TextAnchorId).toInt())); KoSvgText::Direction direction = KoSvgText::Direction(d->properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); format.setLayoutDirection(direction == KoSvgText::DirectionLeftToRight ? Qt::LeftToRight : Qt::RightToLeft); KoSvgText::BaselineShiftMode shiftMode = KoSvgText::BaselineShiftMode(d->properties.propertyOrDefault(KoSvgTextProperties::BaselineShiftModeId).toInt()); // FIXME: we support only 'none', 'sub' and 'super' shifts at the moment. // Please implement 'percentage' as well! // WARNING!!! Qt's setVerticalAlignment() also changes the size of the font! And SVG does not(!) imply it! if (shiftMode == KoSvgText::ShiftSub) { format.setVerticalAlignment(QTextCharFormat::AlignSubScript); } else if (shiftMode == KoSvgText::ShiftSuper) { format.setVerticalAlignment(QTextCharFormat::AlignSuperScript); } KoSvgText::AutoValue letterSpacing = d->properties.propertyOrDefault(KoSvgTextProperties::LetterSpacingId).value(); if (!letterSpacing.isAuto) { format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(letterSpacing.customValue); } KoSvgText::AutoValue wordSpacing = d->properties.propertyOrDefault(KoSvgTextProperties::WordSpacingId).value(); if (!wordSpacing.isAuto) { format.setFontWordSpacing(wordSpacing.customValue); } KoSvgText::AutoValue kerning = d->properties.propertyOrDefault(KoSvgTextProperties::KerningId).value(); if (!kerning.isAuto) { format.setFontKerning(false); format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(format.fontLetterSpacing() + kerning.customValue); } QBrush textBrush = Qt::NoBrush; if (background()) { KoColorBackground *colorBackground = dynamic_cast(background().data()); if (!colorBackground) { qWarning() << "TODO: support gradient and pattern backgrounds for text"; textBrush = Qt::red; } if (colorBackground) { textBrush = colorBackground->brush(); } } format.setForeground(textBrush); QPen textPen = Qt::NoPen; if (stroke()) { KoShapeStroke *stroke = dynamic_cast(this->stroke().data()); if (stroke) { textPen = stroke->resultLinePen(); } } format.setTextOutline(textPen); // TODO: avoid const_cast somehow... format.setAssociatedShape(const_cast(this)); return format; } void KoSvgTextChunkShape::applyParentCharTransformations(const QVector transformations) { if (shapeCount()) { int numCharsPassed = 0; Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); const int numCharsInSubtree = chunkShape->layoutInterface()->numChars(); QVector t = transformations.mid(numCharsPassed, numCharsInSubtree); if (t.isEmpty()) break; chunkShape->applyParentCharTransformations(t); numCharsPassed += numCharsInSubtree; if (numCharsPassed >= transformations.size()) break; } } else { for (int i = 0; i < qMin(transformations.size(), d->text.size()); i++) { KIS_SAFE_ASSERT_RECOVER_RETURN(d->localTransformations.size() >= i); if (d->localTransformations.size() == i) { d->localTransformations.append(transformations[i]); } else { d->localTransformations[i].mergeInParentTransformation(transformations[i]); } } } } diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp index 5f52abd6e2..b6b086afa7 100644 --- a/libs/flake/text/KoSvgTextShape.cpp +++ b/libs/flake/text/KoSvgTextShape.cpp @@ -1,638 +1,649 @@ /* * 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 "KoSvgTextShape.h" #include #include #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KoSvgTextShape::Private : public QSharedData { public: Private() : QSharedData() { } Private(const Private &) : QSharedData() { } std::vector> cachedLayouts; std::vector cachedLayoutsOffsets; QThread *cachedLayoutsWorkingThread = 0; void clearAssociatedOutlines(KoShape *rootShape); }; KoSvgTextShape::KoSvgTextShape() : KoSvgTextChunkShape() , d(new Private) { setShapeId(KoSvgTextShape_SHAPEID); } KoSvgTextShape::KoSvgTextShape(const KoSvgTextShape &rhs) : KoSvgTextChunkShape(rhs) , d(rhs.d) { setShapeId(KoSvgTextShape_SHAPEID); // QTextLayout has no copy-ctor, so just relayout everything! relayout(); } KoSvgTextShape::~KoSvgTextShape() { } KoShape *KoSvgTextShape::cloneShape() const { return new KoSvgTextShape(*this); } void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape) { KoSvgTextChunkShape::shapeChanged(type, shape); if (type == StrokeChanged || type == BackgroundChanged || type == ContentChanged) { relayout(); } } void KoSvgTextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(paintContext); /** * HACK ALERT: * QTextLayout should only be accessed from the thread it has been created in. * If the cached layout has been created in a different thread, we should just * recreate the layouts in the current thread to be able to render them. */ if (QThread::currentThread() != d->cachedLayoutsWorkingThread) { relayout(); } applyConversion(painter, converter); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { d->cachedLayouts[i]->draw(&painter, d->cachedLayoutsOffsets[i]); } /** * HACK ALERT: * The layouts of non-gui threads must be destroyed in the same thread * they have been created. Because the thread might be restarted in the * meantime or just destroyed, meaning that the per-thread freetype data * will not be available. */ if (QThread::currentThread() != qApp->thread()) { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = 0; } } void KoSvgTextShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); // do nothing! everything is painted in paintComponent() } QPainterPath KoSvgTextShape::textOutline() { QPainterPath result; result.setFillRule(Qt::WindingFill); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; const QTextLayout *layout = d->cachedLayouts[i].get(); for (int j = 0; j < layout->lineCount(); j++) { QTextLine line = layout->lineAt(j); Q_FOREACH (const QGlyphRun &run, line.glyphRuns()) { const QVector indexes = run.glyphIndexes(); const QVector positions = run.positions(); const QRawFont font = run.rawFont(); KIS_SAFE_ASSERT_RECOVER(indexes.size() == positions.size()) { continue; } for (int k = 0; k < indexes.size(); k++) { QPainterPath glyph = font.pathForGlyph(indexes[k]); glyph.translate(positions[k] + layoutOffset); result += glyph; } const qreal thickness = font.lineThickness(); const QRectF runBounds = run.boundingRect(); if (run.overline()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y(); QRectF overlineBlob(runBounds.x(), y, runBounds.width(), thickness); overlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(overlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.strikeOut()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y() + 0.5 * line.height(); QRectF strikeThroughBlob(runBounds.x(), y, runBounds.width(), thickness); strikeThroughBlob.translate(layoutOffset); QPainterPath path; path.addRect(strikeThroughBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.underline()) { const qreal y = line.y() + line.ascent() + font.underlinePosition(); QRectF underlineBlob(runBounds.x(), y, runBounds.width(), thickness); underlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(underlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } } } } return result; } void KoSvgTextShape::resetTextShape() { KoSvgTextChunkShape::resetTextShape(); relayout(); } struct TextChunk { QString text; QVector formats; Qt::LayoutDirection direction = Qt::LeftToRight; Qt::Alignment alignment = Qt::AlignLeading; struct SubChunkOffset { QPointF offset; int start = 0; }; QVector offsets; boost::optional xStartPos; boost::optional yStartPos; QPointF applyStartPosOverride(const QPointF &pos) const { QPointF result = pos; if (xStartPos) { result.rx() = *xStartPos; } if (yStartPos) { result.ry() = *yStartPos; } return result; } }; QVector mergeIntoChunks(const QVector &subChunks) { QVector chunks; for (auto it = subChunks.begin(); it != subChunks.end(); ++it) { if (it->transformation.startsNewChunk() || it == subChunks.begin()) { TextChunk newChunk = TextChunk(); newChunk.direction = it->format.layoutDirection(); newChunk.alignment = it->format.calculateAlignment(); newChunk.xStartPos = it->transformation.xPos; newChunk.yStartPos = it->transformation.yPos; chunks.append(newChunk); } TextChunk ¤tChunk = chunks.last(); if (it->transformation.hasRelativeOffset()) { TextChunk::SubChunkOffset o; o.start = currentChunk.text.size(); o.offset = it->transformation.relativeOffset(); KIS_SAFE_ASSERT_RECOVER_NOOP(!o.offset.isNull()); currentChunk.offsets.append(o); } QTextLayout::FormatRange formatRange; formatRange.start = currentChunk.text.size(); formatRange.length = it->text.size(); formatRange.format = it->format; currentChunk.formats.append(formatRange); currentChunk.text += it->text; } return chunks; } /** * Qt's QTextLayout has a weird trait, it doesn't count space characters as * distinct characters in QTextLayout::setNumColumns(), that is, if we want to * position a block of text that starts with a space character in a specific * position, QTextLayout will drop this space and will move the text to the left. * * That is why we have a special wrapper object that ensures that no spaces are * dropped and their horizontal advance parameter is taken into account. */ struct LayoutChunkWrapper { LayoutChunkWrapper(QTextLayout *layout) : m_layout(layout) { } QPointF addTextChunk(int startPos, int length, const QPointF &textChunkStartPos) { QPointF currentTextPos = textChunkStartPos; const int lastPos = startPos + length - 1; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(startPos == m_addedChars, currentTextPos); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastPos < m_layout->text().size(), currentTextPos); +// qDebug() << m_layout->text(); + QTextLine line; std::swap(line, m_danglingLine); if (!line.isValid()) { line = m_layout->createLine(); } // skip all the space characters that were not included into the Qt's text line const int currentLineStart = line.isValid() ? line.textStart() : startPos + length; while (startPos < currentLineStart && startPos <= lastPos) { currentTextPos.rx() += skipSpaceCharacter(startPos); startPos++; } if (startPos <= lastPos) { // defines the number of columns to look for glyphs const int numChars = lastPos - startPos + 1; // Tabs break the normal column flow // grow to avoid missing glyphs int charOffset = 0; + int noChangeCount = 0; while (line.textLength() < numChars) { + int tl = line.textLength(); line.setNumColumns(numChars + charOffset); + if (tl == line.textLength()) { + noChangeCount++; + // 5 columns max are needed to discover tab char. Set to 10 to be safe. + if (noChangeCount > 10) break; + } else { + noChangeCount = 0; + } charOffset++; } line.setPosition(currentTextPos - QPointF(0, line.ascent())); currentTextPos.rx() += line.horizontalAdvance(); // skip all the space characters that were not included into the Qt's text line for (int i = line.textStart() + line.textLength(); i < lastPos; i++) { currentTextPos.rx() += skipSpaceCharacter(i); } } else { // keep the created but unused line for future use std::swap(line, m_danglingLine); } m_addedChars += length; return currentTextPos; } private: qreal skipSpaceCharacter(int pos) { const QTextCharFormat format = formatForPos(pos, m_layout->formats()); const QChar skippedChar = m_layout->text()[pos]; KIS_SAFE_ASSERT_RECOVER_NOOP(skippedChar.isSpace() || !skippedChar.isPrint()); QFontMetrics metrics(format.font()); #if QT_VERSION >= 0x051100 return metrics.horizontalAdvance(skippedChar); #else return metrics.width(skippedChar); #endif } static QTextCharFormat formatForPos(int pos, const QVector &formats) { Q_FOREACH (const QTextLayout::FormatRange &range, formats) { if (pos >= range.start && pos < range.start + range.length) { return range.format; } } KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "pos should be within the bounds of the layouted text"); return QTextCharFormat(); } private: int m_addedChars = 0; QTextLayout *m_layout; QTextLine m_danglingLine; }; void KoSvgTextShape::relayout() { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = QThread::currentThread(); QPointF currentTextPos; QVector textChunks = mergeIntoChunks(layoutInterface()->collectSubChunks()); Q_FOREACH (const TextChunk &chunk, textChunks) { std::unique_ptr layout(new QTextLayout()); QTextOption option; // WARNING: never activate this option! It breaks the RTL text layout! //option.setFlags(QTextOption::ShowTabsAndSpaces); option.setWrapMode(QTextOption::WrapAnywhere); option.setUseDesignMetrics(true); // TODO: investigate if it is needed? option.setTextDirection(chunk.direction); layout->setText(chunk.text); layout->setTextOption(option); layout->setFormats(chunk.formats); layout->setCacheEnabled(true); layout->beginLayout(); currentTextPos = chunk.applyStartPosOverride(currentTextPos); const QPointF anchorPointPos = currentTextPos; int lastSubChunkStart = 0; QPointF lastSubChunkOffset; LayoutChunkWrapper wrapper(layout.get()); for (int i = 0; i <= chunk.offsets.size(); i++) { const bool isFinalPass = i == chunk.offsets.size(); const int length = !isFinalPass ? chunk.offsets[i].start - lastSubChunkStart : chunk.text.size() - lastSubChunkStart; if (length > 0) { currentTextPos += lastSubChunkOffset; currentTextPos = wrapper.addTextChunk(lastSubChunkStart, length, currentTextPos); } if (!isFinalPass) { lastSubChunkOffset = chunk.offsets[i].offset; lastSubChunkStart = chunk.offsets[i].start; } } layout->endLayout(); QPointF diff; if (chunk.alignment & Qt::AlignTrailing || chunk.alignment & Qt::AlignHCenter) { if (chunk.alignment & Qt::AlignTrailing) { diff = currentTextPos - anchorPointPos; } else if (chunk.alignment & Qt::AlignHCenter) { diff = 0.5 * (currentTextPos - anchorPointPos); } // TODO: fix after t2b text implemented diff.ry() = 0; } d->cachedLayouts.push_back(std::move(layout)); d->cachedLayoutsOffsets.push_back(-diff); } d->clearAssociatedOutlines(this); for (int i = 0; i < int(d->cachedLayouts.size()); i++) { const QTextLayout &layout = *d->cachedLayouts[i]; const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; using namespace KoSvgText; Q_FOREACH (const QTextLayout::FormatRange &range, layout.formats()) { const KoSvgCharChunkFormat &format = static_cast(range.format); AssociatedShapeWrapper wrapper = format.associatedShapeWrapper(); const int rangeStart = range.start; const int safeRangeLength = range.length > 0 ? range.length : layout.text().size() - rangeStart; if (safeRangeLength <= 0) continue; const int rangeEnd = range.start + safeRangeLength - 1; const int firstLineIndex = layout.lineForTextPosition(rangeStart).lineNumber(); const int lastLineIndex = layout.lineForTextPosition(rangeEnd).lineNumber(); for (int i = firstLineIndex; i <= lastLineIndex; i++) { const QTextLine line = layout.lineAt(i); // It might happen that the range contains only one (or two) // symbol that is a whitespace symbol. In such a case we should // just skip this (invalid) line. if (!line.isValid()) continue; const int posStart = qMax(line.textStart(), rangeStart); const int posEnd = qMin(line.textStart() + line.textLength() - 1, rangeEnd); const QList glyphRuns = line.glyphRuns(posStart, posEnd - posStart + 1); Q_FOREACH (const QGlyphRun &run, glyphRuns) { const QPointF firstPosition = run.positions().first(); const quint32 firstGlyphIndex = run.glyphIndexes().first(); const QPointF lastPosition = run.positions().last(); const quint32 lastGlyphIndex = run.glyphIndexes().last(); const QRawFont rawFont = run.rawFont(); const QRectF firstGlyphRect = rawFont.boundingRect(firstGlyphIndex).translated(firstPosition); const QRectF lastGlyphRect = rawFont.boundingRect(lastGlyphIndex).translated(lastPosition); QRectF rect = run.boundingRect(); /** * HACK ALERT: there is a bug in a way how Qt calculates boundingRect() * of the glyph run. It doesn't care about left and right bearings * of the border chars in the run, therefore it becomes cropped. * * Here we just add a half-char width margin to both sides * of the glyph run to make sure the glyphs are fully painted. * * BUG: 389528 * BUG: 392068 */ rect.setLeft(qMin(rect.left(), lastGlyphRect.left()) - 0.5 * firstGlyphRect.width()); rect.setRight(qMax(rect.right(), lastGlyphRect.right()) + 0.5 * lastGlyphRect.width()); wrapper.addCharacterRect(rect.translated(layoutOffset)); } } } } } void KoSvgTextShape::Private::clearAssociatedOutlines(KoShape *rootShape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(rootShape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->layoutInterface()->clearAssociatedOutline(); Q_FOREACH (KoShape *child, chunkShape->shapes()) { clearAssociatedOutlines(child); } } bool KoSvgTextShape::isRootTextNode() const { return true; } KoSvgTextShapeFactory::KoSvgTextShapeFactory() : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18n("Text")) { setToolTip(i18n("SVG Text Shape")); setIconName(koIconNameCStr("x-shape-text")); setLoadingPriority(5); setXmlElementNames(KoXmlNS::svg, QStringList("text")); KoShapeTemplate t; t.name = i18n("SVG Text"); t.iconName = koIconName("x-shape-text"); t.toolTip = i18n("SVG Text Shape"); addTemplate(t); } KoShape *KoSvgTextShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const { debugFlake << "Create default svg text shape"; KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "", QRectF(0, 0, 200, 60), documentResources->documentResolution()); debugFlake << converter.errors() << converter.warnings(); return shape; } KoShape *KoSvgTextShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const { KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); QString svgText = params->stringProperty("svgText", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); QString defs = params->stringProperty("defs" , ""); QRectF shapeRect = QRectF(0, 0, 200, 60); QVariant rect = params->property("shapeRect"); if (rect.type()==QVariant::RectF) { shapeRect = rect.toRectF(); } KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg(svgText, defs, shapeRect, documentResources->documentResolution()); shape->setPosition(shapeRect.topLeft()); return shape; } bool KoSvgTextShapeFactory::supports(const KoXmlElement &/*e*/, KoShapeLoadingContext &/*context*/) const { return false; }