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 }