File indexing completed on 2024-05-12 04:06:10
0001 /* 0002 SPDX-FileCopyrightText: 2007 Mark A. Taff <kde@marktaff.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-only 0005 */ 0006 0007 #include "kgamesvgdocument.h" 0008 #include "kgamesvgdocument_p.h" 0009 0010 // own 0011 #include <kdegamesprivate_logging.h> 0012 // KF 0013 #include <KCompressionDevice> 0014 // Qt 0015 #include <QBuffer> 0016 #include <QDomElement> 0017 #include <QDomNode> 0018 #include <QFile> 0019 #include <QRegularExpression> 0020 #include <QString> 0021 // Std 0022 #include <cmath> 0023 0024 // 0025 // Public 0026 // 0027 0028 /** 0029 * @brief A class holding private members for KGameSvgDocument 0030 * 0031 * @see KGameSvgDocument 0032 * @author Mark A. Taff \<kde@marktaff.com\> 0033 * @version 0.1 0034 */ 0035 class KGameSvgDocumentPrivate 0036 { 0037 public: 0038 /** 0039 * @brief Instantiates a KGameSvgDocumentPrivate object 0040 */ 0041 KGameSvgDocumentPrivate() 0042 { 0043 } 0044 0045 ~KGameSvgDocumentPrivate() 0046 { 0047 } 0048 0049 /** 0050 * @brief Performs a preorder traversal of the DOM tree to find element matching @c attributeName & @c attributeValue 0051 * 0052 * @param attributeName The name of the attribute to find 0053 * @param attributeValue The value of the @p attributeName attribute to find 0054 * @param node The node to start the traversal from. 0055 * @returns the node with id of @c elementId. If no node has that id, returns a null node. 0056 */ 0057 QDomNode findElementById(const QString &attributeName, const QString &attributeValue, const QDomNode &node); 0058 0059 /** 0060 * @brief Returns the current element 0061 * @returns The current element 0062 */ 0063 QDomElement currentElement() const; 0064 0065 /** 0066 * @brief Sets the current element 0067 * 0068 * @returns nothing 0069 */ 0070 void setCurrentElement(); 0071 0072 /** 0073 * @brief Returns whether the original style attribute has a trailing semicolon 0074 * @returns whether the original style attribute has a trailing semicolon 0075 */ 0076 bool styleHasTrailingSemicolon() const; 0077 0078 /** 0079 * @brief Sets whether the original style attribute has a trailing semicolon 0080 * 0081 * @param hasSemicolon whether the original style attribute has a trailing semicolon 0082 * @returns nothing 0083 */ 0084 void setStyleHasTrailingSemicolon(bool hasSemicolon); 0085 0086 /** 0087 * @brief The last node found by elementById, or a null node if not found. 0088 */ 0089 QDomNode m_currentNode; 0090 0091 /** 0092 * @brief The current node turned into an element. 0093 */ 0094 QDomElement m_currentElement; 0095 0096 /** 0097 * @brief The order Inkscape write properties in the style attribute of an element. 0098 * 0099 * Inkscape order is defined as: 0100 * "fill", "fill-opacity", "fill-rule", "stroke", "stroke-width", "stroke-linecap", 0101 * "stroke-linejoin", "stroke-miterlimit", "stroke-dasharray", "stroke-opacity" 0102 */ 0103 QStringList m_inkscapeOrder; 0104 0105 /** 0106 * @brief The xml that must be prepended to a node to make it a valid svg document 0107 * 0108 * Defined as: <?xml version="1.0" encoding="UTF-8" standalone="no"?>\<svg\> 0109 */ 0110 static const QString SVG_XML_PREPEND; 0111 0112 /** 0113 * @brief The xml that must be appended to a node to make it a valid svg document 0114 * 0115 * Defined as: \</svg\> 0116 */ 0117 static const QString SVG_XML_APPEND; 0118 0119 /** 0120 * @brief The filename of the SVG file to open. 0121 */ 0122 QString m_svgFilename; 0123 0124 /** 0125 * @brief Whether the style attribute has a trailing semicolon 0126 */ 0127 bool m_hasSemicolon; 0128 }; 0129 0130 const QString KGameSvgDocumentPrivate::SVG_XML_PREPEND = QStringLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><svg>"); 0131 const QString KGameSvgDocumentPrivate::SVG_XML_APPEND = QStringLiteral("</svg>"); 0132 0133 KGameSvgDocument::KGameSvgDocument() 0134 : QDomDocument() 0135 , d(new KGameSvgDocumentPrivate) 0136 { 0137 } 0138 0139 KGameSvgDocument::KGameSvgDocument(const KGameSvgDocument &doc) 0140 : QDomDocument() 0141 , d(new KGameSvgDocumentPrivate(*doc.d)) 0142 { 0143 } 0144 0145 KGameSvgDocument::~KGameSvgDocument() = default; 0146 0147 KGameSvgDocument &KGameSvgDocument::operator=(const KGameSvgDocument &doc) 0148 { 0149 QDomDocument::operator=(doc); 0150 *d = *doc.d; 0151 return *this; 0152 } 0153 0154 QDomNode KGameSvgDocument::elementByUniqueAttributeValue(const QString &attributeName, const QString &attributeValue) 0155 { 0156 /* DOM is always "live", so there maybe a new root node. We always have to ask for the 0157 * root node instead of keeping a pointer to it. 0158 */ 0159 QDomElement docElem = documentElement(); 0160 QDomNode n = docElem.firstChild(); 0161 0162 QDomNode node = d->findElementById(attributeName, attributeValue, n); 0163 setCurrentNode(node); 0164 return node; 0165 } 0166 0167 QDomNode KGameSvgDocument::elementById(const QString &attributeValue) 0168 { 0169 return elementByUniqueAttributeValue(QStringLiteral("id"), attributeValue); 0170 } 0171 0172 void KGameSvgDocument::load() 0173 { 0174 if (d->m_svgFilename.isEmpty()) { 0175 qCDebug(KDEGAMESPRIVATE_LOG) << "KGameSvgDocument::load(): Filename not specified."; 0176 return; 0177 } 0178 0179 QFile file(d->m_svgFilename); 0180 if (!file.open(QIODevice::ReadOnly)) { 0181 return; 0182 } 0183 QByteArray content = file.readAll(); 0184 0185 // If the file is compressed, decompress the contents before loading it. 0186 if (!content.startsWith("<?xml")) // krazy:exclude=strings 0187 { 0188 QBuffer buf(&content); 0189 KCompressionDevice flt(&buf, /*autoDeleteInputDevice*/ false, KCompressionDevice::GZip); 0190 if (!flt.open(QIODevice::ReadOnly)) { 0191 flt.close(); 0192 return; 0193 } 0194 QByteArray ar = flt.readAll(); 0195 flt.close(); 0196 content = ar; 0197 } 0198 0199 if (!setContent(content)) { 0200 file.close(); 0201 qCDebug(KDEGAMESPRIVATE_LOG) << "DOM content not set."; 0202 return; 0203 } 0204 file.close(); 0205 } 0206 0207 void KGameSvgDocument::load(const QString &svgFilename) 0208 { 0209 setSvgFilename(svgFilename); 0210 load(); 0211 } 0212 0213 void KGameSvgDocument::rotate(double degrees, MatrixOptions options) 0214 { 0215 QTransform matrix; 0216 0217 if (options == ApplyToCurrentMatrix) { 0218 matrix = transformMatrix().QTransform::rotate(degrees); 0219 } else { 0220 matrix = QTransform(); 0221 matrix.QTransform::rotate(degrees); 0222 } 0223 setTransformMatrix(matrix, ReplaceCurrentMatrix); 0224 } 0225 0226 void KGameSvgDocument::translate(int xPixels, int yPixels, MatrixOptions options) 0227 { 0228 QTransform matrix; 0229 0230 if (options == ApplyToCurrentMatrix) { 0231 matrix = transformMatrix().QTransform::translate(xPixels, yPixels); 0232 } else { 0233 matrix = QTransform(); 0234 matrix.QTransform::translate(xPixels, yPixels); 0235 } 0236 setTransformMatrix(matrix, ReplaceCurrentMatrix); 0237 } 0238 0239 void KGameSvgDocument::shear(double xRadians, double yRadians, MatrixOptions options) 0240 { 0241 QTransform matrix; 0242 0243 if (options == ApplyToCurrentMatrix) { 0244 matrix = transformMatrix().QTransform::shear(xRadians, yRadians); 0245 } else { 0246 matrix = QTransform(); 0247 matrix.QTransform::shear(xRadians, yRadians); 0248 } 0249 setTransformMatrix(matrix, ReplaceCurrentMatrix); 0250 } 0251 0252 void KGameSvgDocument::skew(double xDegrees, double yDegrees, MatrixOptions options) 0253 { 0254 double xRadians = xDegrees * (M_PI / 180); 0255 double yRadians = yDegrees * (M_PI / 180); 0256 0257 shear(xRadians, yRadians, options); 0258 } 0259 0260 void KGameSvgDocument::scale(double xFactor, double yFactor, MatrixOptions options) 0261 { 0262 QTransform matrix; 0263 if ((xFactor == 0) || (yFactor == 0)) { 0264 qCWarning(KDEGAMESPRIVATE_LOG) << "KGameSvgDocument::scale: You cannot scale by zero"; 0265 } 0266 0267 if (options == ApplyToCurrentMatrix) { 0268 matrix = transformMatrix().QTransform::scale(xFactor, yFactor); 0269 } else { 0270 matrix = QTransform(); 0271 matrix.QTransform::scale(xFactor, yFactor); 0272 } 0273 setTransformMatrix(matrix, ReplaceCurrentMatrix); 0274 } 0275 0276 QDomNode KGameSvgDocument::currentNode() const 0277 { 0278 return d->m_currentNode; 0279 } 0280 0281 void KGameSvgDocument::setCurrentNode(const QDomNode &node) 0282 { 0283 d->m_currentNode = node; 0284 d->setCurrentElement(); 0285 } 0286 0287 QString KGameSvgDocument::svgFilename() const 0288 { 0289 return d->m_svgFilename; 0290 } 0291 0292 void KGameSvgDocument::setSvgFilename(const QString &svgFilename) 0293 { 0294 d->m_svgFilename = svgFilename; 0295 } 0296 0297 QString KGameSvgDocument::styleProperty(const QString &propertyName) const 0298 { 0299 return styleProperties().value(propertyName); 0300 } 0301 0302 void KGameSvgDocument::setStyleProperty(const QString &propertyName, const QString &propertyValue) 0303 { 0304 QHash<QString, QString> properties; 0305 0306 properties = styleProperties(); 0307 properties.insert(propertyName, propertyValue); 0308 0309 setStyleProperties(properties, UseInkscapeOrder); 0310 } 0311 0312 QString KGameSvgDocument::nodeToSvg() const 0313 { 0314 QString s, t, xml, defs, pattern; 0315 QTextStream str(&s); 0316 QTextStream str_t(&t); 0317 QStringList defsAdded; 0318 QRegularExpression rx; 0319 0320 currentNode().save(str, 1); 0321 xml = *str.string(); 0322 0323 // Find and add any required gradients or patterns 0324 pattern = QLatin1String("url") + WSP_ASTERISK + OPEN_PARENS + WSP_ASTERISK + QLatin1String("#(.*)") + WSP_ASTERISK + CLOSE_PARENS; 0325 rx.setPattern(pattern); 0326 if (rx.match(xml).hasMatch()) { 0327 QDomNode node, nodeBase; 0328 QString baseId; 0329 QDomNode n = def(); 0330 0331 QRegularExpressionMatchIterator i = rx.globalMatch(xml); 0332 while (i.hasNext()) { 0333 QRegularExpressionMatch match = i.next(); 0334 const QString id = match.captured(1); 0335 if (!defsAdded.contains(id)) { 0336 node = d->findElementById(QStringLiteral("id"), id, n); 0337 node.save(str_t, 1); 0338 defsAdded.append(id); 0339 } 0340 0341 // Find the gradient the above gradient is based on 0342 baseId = node.toElement().attribute(QStringLiteral("xlink:href")).mid(1); 0343 if (!defsAdded.contains(baseId)) { 0344 nodeBase = d->findElementById(QStringLiteral("id"), baseId, n); 0345 nodeBase.save(str_t, 1); 0346 defsAdded.append(baseId); 0347 } 0348 } 0349 defs = *str_t.string(); 0350 defs = QLatin1String("<defs>") + defs + QLatin1String("</defs>"); 0351 } 0352 0353 // Need to make node be a real svg document, so prepend and append required tags. 0354 xml = d->SVG_XML_PREPEND + defs + xml + d->SVG_XML_APPEND; 0355 return xml; 0356 } 0357 0358 QByteArray KGameSvgDocument::nodeToByteArray() const 0359 { 0360 return nodeToSvg().toUtf8(); 0361 } 0362 0363 QString KGameSvgDocument::style() const 0364 { 0365 return d->m_currentElement.attribute(QStringLiteral("style"), QStringLiteral("Element has no style attribute.")); 0366 } 0367 0368 void KGameSvgDocument::setStyle(const QString &styleAttribute) 0369 { 0370 d->m_currentElement.setAttribute(QStringLiteral("style"), styleAttribute); 0371 } 0372 0373 QDomNodeList KGameSvgDocument::patterns() const 0374 { 0375 return elementsByTagName(QStringLiteral("pattern")); 0376 } 0377 0378 QDomNodeList KGameSvgDocument::linearGradients() const 0379 { 0380 return elementsByTagName(QStringLiteral("linearGradient")); 0381 } 0382 0383 QDomNodeList KGameSvgDocument::radialGradients() const 0384 { 0385 return elementsByTagName(QStringLiteral("radialGradient")); 0386 } 0387 0388 QDomNodeList KGameSvgDocument::defs() const 0389 { 0390 return elementsByTagName(QStringLiteral("defs")); 0391 } 0392 0393 QDomNode KGameSvgDocument::def() const 0394 { 0395 return defs().at(0); 0396 } 0397 0398 QString KGameSvgDocument::transform() const 0399 { 0400 return d->m_currentElement.attribute(QStringLiteral("transform"), QStringLiteral("Element has no transform attribute.")); 0401 } 0402 0403 void KGameSvgDocument::setTransform(const QString &transformAttribute) 0404 { 0405 d->m_currentElement.setAttribute(QStringLiteral("transform"), transformAttribute); 0406 } 0407 0408 QHash<QString, QString> KGameSvgDocument::styleProperties() const 0409 { 0410 QHash<QString, QString> stylePropertiesHash; 0411 0412 QList<QStringView> styleProperties = QStringView(style()).split(QLatin1Char(';')); 0413 0414 /* The style attr may have a trailing semi-colon. If it does, split() 0415 * gives us an empty final element. Remove it or we get 'index out of range' errors 0416 */ 0417 if (styleProperties.at((styleProperties.count() - 1)).isEmpty()) { 0418 styleProperties.removeAt((styleProperties.count() - 1)); 0419 d->setStyleHasTrailingSemicolon(true); 0420 } else { 0421 d->setStyleHasTrailingSemicolon(false); 0422 } 0423 0424 for (const QStringView &styleProperty : std::as_const(styleProperties)) { 0425 const QList<QStringView> keyValuePair = styleProperty.split(QLatin1Char(':')); 0426 stylePropertiesHash.insert(keyValuePair.at(0).toString(), keyValuePair.at(1).toString()); 0427 } 0428 return stylePropertiesHash; 0429 } 0430 0431 void KGameSvgDocument::setStyleProperties(const QHash<QString, QString> &_styleProperties, const StylePropertySortOptions &options) 0432 { 0433 QHash<QString, QString> styleProperties = _styleProperties; 0434 QString styleBuffer; 0435 0436 d->m_inkscapeOrder << QStringLiteral("fill") << QStringLiteral("fill-opacity") << QStringLiteral("fill-rule") << QStringLiteral("stroke") 0437 << QStringLiteral("stroke-width") << QStringLiteral("stroke-linecap") << QStringLiteral("stroke-linejoin") 0438 << QStringLiteral("stroke-miterlimit") << QStringLiteral("stroke-dasharray") << QStringLiteral("stroke-opacity"); 0439 0440 if (options == UseInkscapeOrder) { 0441 for (const QString &property : std::as_const(d->m_inkscapeOrder)) { 0442 if (styleProperties.contains(property)) { 0443 styleBuffer += property + QLatin1Char(':') + styleProperties.take(property) + QLatin1Char(';'); 0444 } else { 0445 // Do Nothing 0446 } 0447 } 0448 } 0449 0450 // Append any style properties 0451 if (!styleProperties.isEmpty()) { 0452 QHashIterator<QString, QString> it(styleProperties); 0453 while (it.hasNext()) { 0454 it.next(); 0455 styleBuffer += it.key() + QLatin1Char(':') + it.value() + QLatin1Char(';'); 0456 } 0457 } 0458 0459 // Remove trailing semicolon if original didn't have one 0460 if (!d->styleHasTrailingSemicolon()) { 0461 styleBuffer.chop(1); 0462 } 0463 setStyle(styleBuffer); 0464 } 0465 0466 QTransform KGameSvgDocument::transformMatrix() const 0467 { 0468 /* 0469 * Transform attributes can be quite complex. Here, we assemble this tangled web of 0470 * complexity into an single matrix. 0471 * 0472 * The regex's that make this bearable live in kgamesvgdocument_p.h. As these regex's 0473 * get quite complex, we have some code in tests/kgamesvgdocumenttest.cpp to help verify 0474 * they are still correct after being edited. 0475 * 0476 * Warning: This code depends on the capturing parenthesis in the regex's not changing. 0477 * 0478 * For all the gory details, see http://www.w3.org/TR/SVG/coords.html#TransformAttribute 0479 */ 0480 QRegularExpression rx; 0481 QString transformAttribute; 0482 int i = 0; 0483 QTransform baseMatrix = QTransform(); 0484 0485 transformAttribute = transform(); 0486 if (transformAttribute == QLatin1String("Element has no transform attribute.")) { 0487 return QTransform(); 0488 } 0489 transformAttribute = transformAttribute.trimmed(); 0490 0491 rx.setPattern(TRANSFORMS); 0492 if (!rx.match(transformAttribute).hasMatch()) { 0493 qCWarning(KDEGAMESPRIVATE_LOG) << "Transform attribute seems to be invalid. Check your SVG file."; 0494 return QTransform(); 0495 } 0496 0497 rx.setPattern(TRANSFORM); 0498 0499 while (transformAttribute.size() > 0 && i < 32) // 32 is an arbitrary limit for the number of transforms for a single node 0500 { 0501 QRegularExpressionMatch match = rx.match(transformAttribute); 0502 int result = match.capturedStart(); 0503 if (result != -1) // Found left-most transform 0504 { 0505 if (match.captured(1) == QLatin1String("matrix")) { 0506 // If the first transform found is a matrix, use it as the base, 0507 // else we use a null matrix. 0508 if (i == 0) { 0509 baseMatrix = QTransform(match.captured(2).toDouble(), 0510 match.captured(3).toDouble(), 0511 match.captured(4).toDouble(), 0512 match.captured(5).toDouble(), 0513 match.captured(6).toDouble(), 0514 match.captured(7).toDouble()); 0515 } else { 0516 baseMatrix = QTransform(match.captured(2).toDouble(), 0517 match.captured(3).toDouble(), 0518 match.captured(4).toDouble(), 0519 match.captured(5).toDouble(), 0520 match.captured(6).toDouble(), 0521 match.captured(7).toDouble()) 0522 * baseMatrix; 0523 } 0524 } 0525 0526 if (match.captured(8) == QLatin1String("translate")) { 0527 double x = match.captured(9).toDouble(); 0528 double y = match.captured(10).toDouble(); 0529 if (match.captured(10).isEmpty()) // y defaults to zero per SVG standard 0530 { 0531 y = 0; 0532 } 0533 baseMatrix = baseMatrix.translate(x, y); 0534 } 0535 0536 if (match.captured(11) == QLatin1String("scale")) { 0537 double x = match.captured(12).toDouble(); 0538 double y = match.captured(12).toDouble(); 0539 if (match.captured(13).isEmpty()) // y defaults to x per SVG standard 0540 { 0541 y = x; 0542 } 0543 baseMatrix = baseMatrix.scale(x, y); 0544 } 0545 0546 if (match.captured(14) == QLatin1String("rotate")) { 0547 double a = match.captured(15).toDouble(); 0548 double cx = match.captured(16).toDouble(); 0549 double cy = match.captured(17).toDouble(); 0550 0551 if ((cx > 0) || (cy > 0)) // rotate around point (cx, cy) 0552 { 0553 baseMatrix.translate(cx, cy); 0554 baseMatrix.rotate(a); 0555 baseMatrix.translate((cx * -1), (cy * -1)); 0556 } else { 0557 baseMatrix = baseMatrix.rotate(a); // rotate around origin 0558 } 0559 } 0560 0561 if (match.captured(18) == QLatin1String("skewX")) { 0562 baseMatrix = baseMatrix.shear(match.captured(19).toDouble() * (M_PI / 180), 0); 0563 } 0564 0565 if (match.captured(20) == QLatin1String("skewY")) { 0566 baseMatrix = baseMatrix.shear(0, match.captured(21).toDouble() * (M_PI / 180)); 0567 } 0568 } 0569 transformAttribute = transformAttribute.mid(match.capturedLength() + result); 0570 i++; 0571 } 0572 0573 return baseMatrix; 0574 } 0575 0576 void KGameSvgDocument::setTransformMatrix(QTransform &matrix, MatrixOptions options) 0577 { 0578 QString transformBuffer, tmp; 0579 QTransform null = QTransform(); 0580 0581 if (options == ApplyToCurrentMatrix) { 0582 matrix = transformMatrix() * matrix; 0583 } 0584 0585 transformBuffer = QStringLiteral("matrix("); 0586 transformBuffer += tmp.setNum(matrix.m11(), 'g', 7) + QLatin1Char(','); 0587 transformBuffer += tmp.setNum(matrix.m12(), 'g', 7) + QLatin1Char(','); 0588 transformBuffer += tmp.setNum(matrix.m21(), 'g', 7) + QLatin1Char(','); 0589 transformBuffer += tmp.setNum(matrix.m22(), 'g', 7) + QLatin1Char(','); 0590 transformBuffer += tmp.setNum(matrix.dx(), 'g', 7) + QLatin1Char(','); 0591 transformBuffer += tmp.setNum(matrix.dy(), 'g', 7) + QLatin1Char(')'); 0592 0593 if ((transform() == QLatin1String("Element has no transform attribute.")) && (matrix == null)) { 0594 // Do not write a meaningless matrix to DOM 0595 } else { 0596 setTransform(transformBuffer); 0597 } 0598 } 0599 0600 // 0601 // Private 0602 // 0603 0604 QDomNode KGameSvgDocumentPrivate::findElementById(const QString &attributeName, const QString &attributeValue, const QDomNode &node) 0605 { 0606 QDomElement e = node.toElement(); // try to convert the node to an element. 0607 QString value = e.attribute(attributeName, QStringLiteral("Element has no attribute with that name.")); 0608 0609 if (value == attributeValue) { 0610 // We found our node. Stop recursion and return it. 0611 return node; 0612 } 0613 0614 if (!node.firstChild().isNull()) { 0615 QDomNode result = findElementById(attributeName, attributeValue, node.firstChild()); 0616 /** We have recursed, now we need to have this recursion end when 0617 * the function call above returns 0618 */ 0619 if (!result.isNull()) 0620 return result; // If we found the node with id, then return it 0621 } 0622 if (!node.nextSibling().isNull()) { 0623 QDomNode result = findElementById(attributeName, attributeValue, node.nextSibling()); 0624 /** We have recursed, now we need to have this recursion end when 0625 * the function call above returns */ 0626 if (!result.isNull()) 0627 return result; 0628 } 0629 if (!node.firstChild().isNull() && !node.nextSibling().isNull()) { 0630 // Do Nothing 0631 // qCDebug(KDEGAMESPRIVATE_LOG) << "No children or siblings."; 0632 } 0633 0634 // Matching node not found, so return a null node. 0635 return QDomNode(); 0636 } 0637 0638 QDomElement KGameSvgDocumentPrivate::currentElement() const 0639 { 0640 return m_currentElement; 0641 } 0642 0643 void KGameSvgDocumentPrivate::setCurrentElement() 0644 { 0645 m_currentElement = m_currentNode.toElement(); 0646 } 0647 0648 bool KGameSvgDocumentPrivate::styleHasTrailingSemicolon() const 0649 { 0650 return m_hasSemicolon; 0651 } 0652 0653 void KGameSvgDocumentPrivate::setStyleHasTrailingSemicolon(bool hasSemicolon) 0654 { 0655 m_hasSemicolon = hasSemicolon; 0656 }