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 }