File indexing completed on 2024-12-22 04:09:08

0001 /*
0002  *  SPDX-FileCopyrightText: 2024 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "KoSvgTextContentElement.h"
0008 
0009 #include "KoCssTextUtils.h"
0010 #include <kis_dom_utils.h>
0011 #include "SvgUtil.h"
0012 #include "KoXmlWriter.h"
0013 #include "SvgStyleWriter.h"
0014 #include <kis_global.h>
0015 
0016 #include "SvgGraphicContext.h"
0017 
0018 #include <QRegularExpression>
0019 
0020 KoSvgTextContentElement::KoSvgTextContentElement()
0021 {
0022 
0023 }
0024 
0025 namespace {
0026 void appendLazy(QVector<qreal> *list, boost::optional<qreal> value, int iteration, bool hasDefault = true, qreal defaultValue = 0.0)
0027 {
0028     if (!value) return;
0029     if (value && *value == defaultValue && hasDefault == true && list->isEmpty()) return;
0030 
0031     while (list->size() < iteration) {
0032         list->append(defaultValue);
0033     }
0034 
0035     list->append(*value);
0036 }
0037 
0038 void fillTransforms(QVector<qreal> *xPos, QVector<qreal> *yPos, QVector<qreal> *dxPos, QVector<qreal> *dyPos, QVector<qreal> *rotate,
0039                     QVector<KoSvgText::CharTransformation> localTransformations)
0040 {
0041     for (int i = 0; i < localTransformations.size(); i++) {
0042         const KoSvgText::CharTransformation &t = localTransformations[i];
0043         appendLazy(xPos, t.xPos, i, false);
0044         appendLazy(yPos, t.yPos, i, false);
0045         appendLazy(dxPos, t.dxPos, i);
0046         appendLazy(dyPos, t.dyPos, i);
0047         appendLazy(rotate, t.rotate, i);
0048     }
0049 }
0050 
0051 
0052 
0053 QVector<qreal> parseListAttributeX(const QString &value, SvgLoadingContext &context)
0054 {
0055     QVector<qreal> result;
0056 
0057     QStringList list = SvgUtil::simplifyList(value);
0058     Q_FOREACH (const QString &str, list) {
0059         result << SvgUtil::parseUnitX(context.currentGC(), str);
0060     }
0061 
0062     return result;
0063 }
0064 
0065 QVector<qreal> parseListAttributeY(const QString &value, SvgLoadingContext &context)
0066 {
0067     QVector<qreal> result;
0068 
0069     QStringList list = SvgUtil::simplifyList(value);
0070     Q_FOREACH (const QString &str, list) {
0071         result << SvgUtil::parseUnitY(context.currentGC(), str);
0072     }
0073 
0074     return result;
0075 }
0076 
0077 QVector<qreal> parseListAttributeAngular(const QString &value, SvgLoadingContext &context)
0078 {
0079     QVector<qreal> result;
0080 
0081     QStringList list = SvgUtil::simplifyList(value);
0082     Q_FOREACH (const QString &str, list) {
0083         result << SvgUtil::parseUnitAngular(context.currentGC(), str);
0084     }
0085 
0086     return result;
0087 }
0088 
0089 QString convertListAttribute(const QVector<qreal> &values) {
0090     QStringList stringValues;
0091 
0092     Q_FOREACH (qreal value, values) {
0093         stringValues.append(KisDomUtils::toString(value));
0094     }
0095 
0096     return stringValues.join(',');
0097 }
0098 
0099 void writeTextListAttribute(const QString &attribute, const QVector<qreal> &values, KoXmlWriter &writer)
0100 {
0101     const QString value = convertListAttribute(values);
0102     if (!value.isEmpty()) {
0103         writer.addAttribute(attribute.toLatin1().data(), value);
0104     }
0105 }
0106 }
0107 
0108 #include <ksharedconfig.h>
0109 #include <kconfiggroup.h>
0110 
0111 /**
0112  * HACK ALERT: this is a function from a private Qt's header qfont_p.h,
0113  * we don't include the whole header, because it is painful in the
0114  * environments we don't fully control, e.g. in distribution packages.
0115  */
0116 Q_GUI_EXPORT int qt_defaultDpi();
0117 
0118 namespace {
0119 int forcedDpiForQtFontBugWorkaround() {
0120     KConfigGroup cfg(KSharedConfig::openConfig(), "");
0121     int value = cfg.readEntry("forcedDpiForQtFontBugWorkaround", qt_defaultDpi());
0122 
0123     if (value < 0) {
0124         value = qt_defaultDpi();
0125     }
0126 
0127     return value;
0128 }
0129 
0130 
0131 KoSvgTextProperties adjustPropertiesForFontSizeWorkaround(const KoSvgTextProperties &properties)
0132 {
0133     if (!properties.hasProperty(KoSvgTextProperties::FontSizeId) || !properties.hasProperty(KoSvgTextProperties::FontSizeAdjustId))
0134         return properties;
0135 
0136     KoSvgTextProperties result = properties;
0137 
0138     const int forcedFontDPI = forcedDpiForQtFontBugWorkaround();
0139 
0140     if (result.hasProperty(KoSvgTextProperties::KraTextVersionId) &&
0141         result.property(KoSvgTextProperties::KraTextVersionId).toInt() < 2 &&
0142         forcedFontDPI > 0) {
0143 
0144         qreal fontSize = result.property(KoSvgTextProperties::FontSizeId).toReal();
0145         fontSize *= qreal(forcedFontDPI) / 72.0;
0146         result.setProperty(KoSvgTextProperties::FontSizeId, fontSize);
0147     }
0148     if (result.hasProperty(KoSvgTextProperties::KraTextVersionId) && result.property(KoSvgTextProperties::KraTextVersionId).toInt() < 3
0149         && result.hasProperty(KoSvgTextProperties::FontSizeAdjustId)) {
0150         result.setProperty(KoSvgTextProperties::FontSizeAdjustId, KoSvgText::fromAutoValue(KoSvgText::AutoValue()));
0151     }
0152 
0153     result.setProperty(KoSvgTextProperties::KraTextVersionId, 3);
0154 
0155     return result;
0156 }
0157 
0158 }
0159 
0160 bool KoSvgTextContentElement::loadSvg(const QDomElement &e, SvgLoadingContext &context)
0161 {
0162     SvgGraphicsContext *gc = context.currentGC();
0163     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false);
0164 
0165     KoSvgTextProperties props = gc->textProperties;
0166     QVector<KoSvgTextProperties::PropertyId> generic = {KoSvgTextProperties::FillId,
0167                                                         KoSvgTextProperties::StrokeId,
0168                                                         KoSvgTextProperties::PaintOrder,
0169                                                         KoSvgTextProperties::Opacity,
0170                                                         KoSvgTextProperties::Visiblity};
0171     for (auto it = generic.begin(); it != generic.end(); it++) {
0172         if (properties.hasProperty(*it.i)) {
0173             props.setProperty(*it.i, properties.property(*it.i));
0174         }
0175     }
0176     properties = gc->textProperties;
0177 
0178     textLength = KoSvgText::parseAutoValueXY(e.attribute("textLength", ""), context, "");
0179     lengthAdjust = KoSvgText::parseLengthAdjust(e.attribute("lengthAdjust", "spacing"));
0180 
0181     QVector<qreal> xPos = parseListAttributeX(e.attribute("x", ""), context);
0182     QVector<qreal> yPos = parseListAttributeY(e.attribute("y", ""), context);
0183     QVector<qreal> dxPos = parseListAttributeX(e.attribute("dx", ""), context);
0184     QVector<qreal> dyPos = parseListAttributeY(e.attribute("dy", ""), context);
0185     QVector<qreal> rotate = parseListAttributeAngular(e.attribute("rotate", ""), context);
0186 
0187     const int numLocalTransformations =
0188         std::max({xPos.size(), yPos.size(),
0189                   dxPos.size(), dyPos.size(),
0190                   rotate.size()});
0191 
0192     localTransformations.resize(numLocalTransformations);
0193     for (int i = 0; i < numLocalTransformations; i++) {
0194         if (i < xPos.size()) {
0195             localTransformations[i].xPos = xPos[i];
0196         }
0197         if (i < yPos.size()) {
0198             localTransformations[i].yPos = yPos[i];
0199         }
0200         if (i < dxPos.size() && dxPos[i] != 0.0) {
0201             localTransformations[i].dxPos = dxPos[i];
0202         }
0203         if (i < dyPos.size() && dyPos[i] != 0.0) {
0204             localTransformations[i].dyPos = dyPos[i];
0205         }
0206         if (i < rotate.size()) {
0207             localTransformations[i].rotate = rotate[i];
0208         }
0209     }
0210 
0211     if (e.tagName() == "textPath") {
0212         // we'll read the value 'path' later.
0213 
0214         textPathInfo.side = KoSvgText::parseTextPathSide(e.attribute("side", "left"));
0215         textPathInfo.method = KoSvgText::parseTextPathMethod(e.attribute("method", "align"));
0216         textPathInfo.spacing = KoSvgText::parseTextPathSpacing(e.attribute("spacing", "auto"));
0217         // This depends on pathLength;
0218         if (e.hasAttribute("startOffset")) {
0219             QString offset = e.attribute("startOffset", "0");
0220             if (offset.endsWith("%")) {
0221                 textPathInfo.startOffset = SvgUtil::parseNumber(offset.left(offset.size() - 1));
0222                 textPathInfo.startOffsetIsPercentage = true;
0223             } else {
0224                 textPathInfo.startOffset = SvgUtil::parseUnit(context.currentGC(), offset);
0225             }
0226         }
0227     }
0228 
0229     return true;
0230 }
0231 
0232 bool KoSvgTextContentElement::loadSvgTextNode(const QDomText &text, SvgLoadingContext &context)
0233 {
0234     SvgGraphicsContext *gc = context.currentGC();
0235     KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false);
0236 
0237     // In theory, the XML spec requires XML parsers to normalize line endings to
0238     // LF. However, QXmlInputSource + QXmlSimpleReader do not do this, so we can
0239     // end up with CR in the text. The SVG spec explicitly calls out that all
0240     // newlines in SVG are to be represented by a single LF (U+000A) character,
0241     // so we can replace all CRLF and CR into LF here for simplicity.
0242     static const QRegularExpression s_regexCrlf(R"==((?:\r\n|\r(?!\n)))==");
0243     QString content = text.data();
0244     content.replace(s_regexCrlf, QStringLiteral("\n"));
0245 
0246     this->text = std::move(content);
0247 
0248     return true;
0249 }
0250 
0251 bool KoSvgTextContentElement::saveSvg(SvgSavingContext &context,
0252                                       KoSvgTextProperties parentProperties,
0253                                       bool rootText,
0254                                       bool saveText,
0255                                       QMap<QString, QString> shapeSpecificAttributes)
0256 {
0257     if (textPath) {
0258         if (textPath) {
0259             // we'll always save as an embedded shape as "path" is an svg 2.0
0260             // feature.
0261             QString id = SvgStyleWriter::embedShape(textPath.data(), context);
0262             // inkscape can only read 'xlink:href'
0263             if (!id.isEmpty()) {
0264                 context.shapeWriter().addAttribute("xlink:href", "#" + id);
0265             }
0266         }
0267         if (textPathInfo.startOffset != 0) {
0268             QString offset = KisDomUtils::toString(textPathInfo.startOffset);
0269             if (textPathInfo.startOffsetIsPercentage) {
0270                 offset += "%";
0271             }
0272             context.shapeWriter().addAttribute("startOffset", offset);
0273         }
0274         if (textPathInfo.method != KoSvgText::TextPathAlign) {
0275             context.shapeWriter().addAttribute("method", KoSvgText::writeTextPathMethod(textPathInfo.method));
0276         }
0277         if (textPathInfo.side != KoSvgText::TextPathSideLeft) {
0278             context.shapeWriter().addAttribute("side", KoSvgText::writeTextPathSide(textPathInfo.side));
0279         }
0280         if (textPathInfo.spacing != KoSvgText::TextPathAuto) {
0281             context.shapeWriter().addAttribute("spacing", KoSvgText::writeTextPathSpacing(textPathInfo.spacing));
0282         }
0283     }
0284 
0285     if (!localTransformations.isEmpty()) {
0286 
0287         QVector<qreal> xPos;
0288         QVector<qreal> yPos;
0289         QVector<qreal> dxPos;
0290         QVector<qreal> dyPos;
0291         QVector<qreal> rotate;
0292 
0293         fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, localTransformations);
0294 
0295         for (int i = 0; i < rotate.size(); i++) {
0296             rotate[i] = kisRadiansToDegrees(rotate[i]);
0297         }
0298 
0299         writeTextListAttribute("x", xPos, context.shapeWriter());
0300         writeTextListAttribute("y", yPos, context.shapeWriter());
0301         writeTextListAttribute("dx", dxPos, context.shapeWriter());
0302         writeTextListAttribute("dy", dyPos, context.shapeWriter());
0303         writeTextListAttribute("rotate", rotate, context.shapeWriter());
0304     }
0305 
0306     if (!textLength.isAuto) {
0307         context.shapeWriter().addAttribute("textLength", KisDomUtils::toString(textLength.customValue));
0308 
0309         if (lengthAdjust == KoSvgText::LengthAdjustSpacingAndGlyphs) {
0310             context.shapeWriter().addAttribute("lengthAdjust", "spacingAndGlyphs");
0311         }
0312     }
0313 
0314     KoSvgTextProperties ownProperties = properties.ownProperties(parentProperties, rootText);
0315 
0316     ownProperties = adjustPropertiesForFontSizeWorkaround(ownProperties);
0317 
0318     // we write down stroke/fill if they are different from the parent's value
0319     if (!rootText) {
0320         if (ownProperties.hasProperty(KoSvgTextProperties::FillId)) {
0321             SvgStyleWriter::saveSvgFill(properties.background(),
0322                                         false,
0323                                         this->associatedOutline.boundingRect(),
0324                                         associatedOutline.boundingRect().size(),
0325                                         QTransform(),
0326                                         context);
0327         }
0328 
0329         if (ownProperties.hasProperty(KoSvgTextProperties::StrokeId)) {
0330             SvgStyleWriter::saveSvgStroke(properties.stroke(), context);
0331         }
0332     }
0333 
0334     QMap<QString, QString> attributes = ownProperties.convertToSvgTextAttributes();
0335     QStringList allowedAttributes = properties.supportedXmlAttributes();
0336     QString styleString;
0337 
0338     for (auto it = shapeSpecificAttributes.constBegin(); it != shapeSpecificAttributes.constEnd(); ++it) {
0339         styleString.append(it.key().toLatin1().data()).append(": ").append(it.value()).append(";");
0340     }
0341     for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
0342         if (allowedAttributes.contains(it.key())) {
0343             context.shapeWriter().addAttribute(it.key().toLatin1().data(), it.value());
0344         } else {
0345             styleString.append(it.key().toLatin1().data()).append(": ").append(it.value()).append(";");
0346         }
0347     }
0348     if (!styleString.isEmpty()) {
0349         context.shapeWriter().addAttribute("style", styleString);
0350     }
0351 
0352     if (saveText) {
0353         context.shapeWriter().addTextNode(text);
0354     }
0355     return true;
0356 }
0357 
0358 static QString transformText(QString text, KoSvgText::TextTransformInfo textTransformInfo, const QString &lang, QVector<QPair<int, int>> &positions)
0359 {
0360     if (textTransformInfo.capitals == KoSvgText::TextTransformCapitalize) {
0361         text = KoCssTextUtils::transformTextCapitalize(text, lang, positions);
0362     } else if (textTransformInfo.capitals == KoSvgText::TextTransformUppercase) {
0363         text = KoCssTextUtils::transformTextToUpperCase(text, lang, positions);
0364     } else if (textTransformInfo.capitals == KoSvgText::TextTransformLowercase) {
0365         text = KoCssTextUtils::transformTextToLowerCase(text, lang, positions);
0366     } else {
0367         positions.clear();
0368         for (int i = 0; i < text.size(); i++) {
0369             positions.append(QPair<int, int>(i, i));
0370         }
0371     }
0372 
0373     if (textTransformInfo.fullWidth) {
0374         text = KoCssTextUtils::transformTextFullWidth(text);
0375     }
0376     if (textTransformInfo.fullSizeKana) {
0377         text = KoCssTextUtils::transformTextFullSizeKana(text);
0378     }
0379     return text;
0380 }
0381 
0382 int KoSvgTextContentElement::numChars(bool withControls) const
0383 {
0384     int result = 0;
0385     if (withControls) {
0386         KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(properties.propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt());
0387         KoSvgText::Direction direction = KoSvgText::Direction(properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt());
0388         KoSvgText::TextTransformInfo textTransformInfo =
0389             properties.propertyOrDefault(KoSvgTextProperties::TextTransformId).value<KoSvgText::TextTransformInfo>();
0390         QString lang = properties.property(KoSvgTextProperties::TextLanguage).toString().toUtf8();
0391         QVector<QPair<int, int>> positions;
0392 
0393         result = KoCssTextUtils::getBidiOpening(direction == KoSvgText::DirectionLeftToRight, bidi).size();
0394         result += transformText(text, textTransformInfo, lang, positions).size();
0395         result += KoCssTextUtils::getBidiClosing(bidi).size();
0396     } else {
0397         result = text.size();
0398     }
0399     return result;
0400 }
0401 
0402 void KoSvgTextContentElement::insertText(int start, QString insertText)
0403 {
0404     if (start >= text.size()) {
0405         text.append(insertText);
0406     } else {
0407         text.insert(start, insertText);
0408     }
0409 }
0410 
0411 
0412 QString KoSvgTextContentElement::getTransformedString(QVector<QPair<int, int> > &positions) const
0413 {
0414     KoSvgText::TextTransformInfo textTransformInfo =
0415         properties.propertyOrDefault(KoSvgTextProperties::TextTransformId).value<KoSvgText::TextTransformInfo>();
0416     QString lang = properties.property(KoSvgTextProperties::TextLanguage).toString().toUtf8();
0417     return transformText(text, textTransformInfo, lang, positions);
0418 }
0419 
0420 void KoSvgTextContentElement::removeText(int &start, int length)
0421 {
0422     KoCssTextUtils::removeText(text, start, length);
0423 }