File indexing completed on 2024-06-09 04:20:52

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2009 Jan Hambrecht <jaham@gmx.net>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "SvgUtil.h"
0008 #include "SvgGraphicContext.h"
0009 
0010 #include <KoUnit.h>
0011 
0012 #include <QString>
0013 #include <QRectF>
0014 #include <QStringList>
0015 #include <QFontMetrics>
0016 #include <QRegExp>
0017 
0018 #include <math.h>
0019 #include "kis_debug.h"
0020 #include "kis_global.h"
0021 
0022 #include <KoXmlWriter.h>
0023 #include "kis_dom_utils.h"
0024 
0025 #define DPI 72.0
0026 
0027 #define DEG2RAD(degree) degree/180.0*M_PI
0028 
0029 double SvgUtil::fromUserSpace(double value)
0030 {
0031     return value;
0032 }
0033 
0034 double SvgUtil::toUserSpace(double value)
0035 {
0036     return value;
0037 }
0038 
0039 double SvgUtil::ptToPx(SvgGraphicsContext *gc, double value)
0040 {
0041     return value * gc->pixelsPerInch / DPI;
0042 }
0043 
0044 QPointF SvgUtil::toUserSpace(const QPointF &point)
0045 {
0046     return QPointF(toUserSpace(point.x()), toUserSpace(point.y()));
0047 }
0048 
0049 QRectF SvgUtil::toUserSpace(const QRectF &rect)
0050 {
0051     return QRectF(toUserSpace(rect.topLeft()), toUserSpace(rect.size()));
0052 }
0053 
0054 QSizeF SvgUtil::toUserSpace(const QSizeF &size)
0055 {
0056     return QSizeF(toUserSpace(size.width()), toUserSpace(size.height()));
0057 }
0058 
0059 QString SvgUtil::toPercentage(qreal value)
0060 {
0061     return KisDomUtils::toString(value * 100.0) + "%";
0062 }
0063 
0064 double SvgUtil::fromPercentage(QString s, bool *ok)
0065 {
0066     if (s.endsWith('%'))
0067         return KisDomUtils::toDouble(s.remove('%'), ok) / 100.0;
0068     else
0069         return KisDomUtils::toDouble(s, ok);
0070 }
0071 
0072 QPointF SvgUtil::objectToUserSpace(const QPointF &position, const QRectF &objectBound)
0073 {
0074     qreal x = objectBound.left() + position.x() * objectBound.width();
0075     qreal y = objectBound.top() + position.y() * objectBound.height();
0076     return QPointF(x, y);
0077 }
0078 
0079 QSizeF SvgUtil::objectToUserSpace(const QSizeF &size, const QRectF &objectBound)
0080 {
0081     qreal w = size.width() * objectBound.width();
0082     qreal h = size.height() * objectBound.height();
0083     return QSizeF(w, h);
0084 }
0085 
0086 QPointF SvgUtil::userSpaceToObject(const QPointF &position, const QRectF &objectBound)
0087 {
0088     qreal x = 0.0;
0089     if (objectBound.width() != 0)
0090         x = (position.x() - objectBound.x()) / objectBound.width();
0091     qreal y = 0.0;
0092     if (objectBound.height() != 0)
0093         y = (position.y() - objectBound.y()) / objectBound.height();
0094     return QPointF(x, y);
0095 }
0096 
0097 QSizeF SvgUtil::userSpaceToObject(const QSizeF &size, const QRectF &objectBound)
0098 {
0099     qreal w = objectBound.width() != 0 ? size.width() / objectBound.width() : 0.0;
0100     qreal h = objectBound.height() != 0 ? size.height() / objectBound.height() : 0.0;
0101     return QSizeF(w, h);
0102 }
0103 
0104 QString SvgUtil::transformToString(const QTransform &transform)
0105 {
0106     if (transform.isIdentity())
0107         return QString();
0108 
0109     if (transform.type() == QTransform::TxTranslate) {
0110         return QString("translate(%1, %2)")
0111                      .arg(KisDomUtils::toString(toUserSpace(transform.dx())))
0112                      .arg(KisDomUtils::toString(toUserSpace(transform.dy())));
0113     } else {
0114         return QString("matrix(%1 %2 %3 %4 %5 %6)")
0115                      .arg(KisDomUtils::toString(transform.m11()))
0116                      .arg(KisDomUtils::toString(transform.m12()))
0117                      .arg(KisDomUtils::toString(transform.m21()))
0118                      .arg(KisDomUtils::toString(transform.m22()))
0119                      .arg(KisDomUtils::toString(toUserSpace(transform.dx())))
0120                      .arg(KisDomUtils::toString(toUserSpace(transform.dy())));
0121     }
0122 }
0123 
0124 void SvgUtil::writeTransformAttributeLazy(const QString &name, const QTransform &transform, KoXmlWriter &shapeWriter)
0125 {
0126     const QString value = transformToString(transform);
0127 
0128     if (!value.isEmpty()) {
0129         shapeWriter.addAttribute(name.toLatin1().data(), value);
0130     }
0131 }
0132 
0133 bool SvgUtil::parseViewBox(const QDomElement &e,
0134                            const QRectF &elementBounds,
0135                            QRectF *_viewRect, QTransform *_viewTransform)
0136 {
0137     KIS_ASSERT(_viewRect);
0138     KIS_ASSERT(_viewTransform);
0139 
0140     QString viewBoxStr = e.attribute("viewBox");
0141     if (viewBoxStr.isEmpty()) return false;
0142 
0143     bool result = false;
0144 
0145     QRectF viewBoxRect;
0146     // this is a workaround for bug 260429 for a file generated by blender
0147     // who has px in the viewbox which is wrong.
0148     // reported as bug https://developer.blender.org/T30971
0149     viewBoxStr.remove("px");
0150 
0151     QStringList points = viewBoxStr.replace(',', ' ').simplified().split(' ');
0152     if (points.count() == 4) {
0153         viewBoxRect.setX(SvgUtil::fromUserSpace(points[0].toDouble()));
0154         viewBoxRect.setY(SvgUtil::fromUserSpace(points[1].toDouble()));
0155         viewBoxRect.setWidth(SvgUtil::fromUserSpace(points[2].toDouble()));
0156         viewBoxRect.setHeight(SvgUtil::fromUserSpace(points[3].toDouble()));
0157 
0158         result = true;
0159     } else {
0160         // TODO: WARNING!
0161     }
0162 
0163     if (!result) return false;
0164 
0165     qreal scaleX = 1;
0166     if (!qFuzzyCompare(elementBounds.width(), viewBoxRect.width())) {
0167         scaleX = elementBounds.width() / viewBoxRect.width();
0168     }
0169     qreal scaleY = 1;
0170     if (!qFuzzyCompare(elementBounds.height(), viewBoxRect.height())) {
0171         scaleY = elementBounds.height() / viewBoxRect.height();
0172     }
0173 
0174     QTransform viewBoxTransform =
0175         QTransform::fromTranslate(-viewBoxRect.x(), -viewBoxRect.y()) *
0176         QTransform::fromScale(scaleX, scaleY) *
0177         QTransform::fromTranslate(elementBounds.x(), elementBounds.y());
0178 
0179     const QString aspectString = e.attribute("preserveAspectRatio");
0180     // give initial value if value not defined
0181     PreserveAspectRatioParser p( (!aspectString.isEmpty())? aspectString : QString("xMidYMid meet"));
0182     parseAspectRatio(p, elementBounds, viewBoxRect, &viewBoxTransform);
0183 
0184     *_viewRect = viewBoxRect;
0185     *_viewTransform = viewBoxTransform;
0186 
0187     return result;
0188 }
0189 
0190 void SvgUtil::parseAspectRatio(const PreserveAspectRatioParser &p, const QRectF &elementBounds, const QRectF &viewBoxRect, QTransform *_viewTransform)
0191 {
0192     if (p.mode != Qt::IgnoreAspectRatio) {
0193         QTransform viewBoxTransform = *_viewTransform;
0194 
0195         const qreal tan1 = viewBoxRect.height() / viewBoxRect.width();
0196         const qreal tan2 = elementBounds.height() / elementBounds.width();
0197 
0198         const qreal uniformScale =
0199             (p.mode == Qt::KeepAspectRatioByExpanding) ^ (tan1 > tan2) ?
0200                 elementBounds.height() / viewBoxRect.height() :
0201                 elementBounds.width() / viewBoxRect.width();
0202 
0203         viewBoxTransform =
0204             QTransform::fromTranslate(-viewBoxRect.x(), -viewBoxRect.y()) *
0205             QTransform::fromScale(uniformScale, uniformScale) *
0206             QTransform::fromTranslate(elementBounds.x(), elementBounds.y());
0207 
0208         const QPointF viewBoxAnchor = viewBoxTransform.map(p.rectAnchorPoint(viewBoxRect));
0209         const QPointF elementAnchor = p.rectAnchorPoint(elementBounds);
0210         const QPointF offset = elementAnchor - viewBoxAnchor;
0211 
0212         viewBoxTransform = viewBoxTransform * QTransform::fromTranslate(offset.x(), offset.y());
0213 
0214         *_viewTransform = viewBoxTransform;
0215     }
0216 }
0217 
0218 qreal SvgUtil::parseUnit(SvgGraphicsContext *gc, const QString &unit, bool horiz, bool vert, const QRectF &bbox)
0219 {
0220     if (unit.isEmpty())
0221         return 0.0;
0222     QByteArray unitLatin1 = unit.toLatin1();
0223     // TODO : percentage?
0224     const char *start = unitLatin1.data();
0225     if (!start) {
0226         return 0.0;
0227     }
0228     qreal value = 0.0;
0229     const char *end = parseNumber(start, value);
0230 
0231     if (int(end - start) < unit.length()) {
0232         if (unit.right(2) == "px")
0233             value = SvgUtil::fromUserSpace(value);
0234         else if (unit.right(2) == "pt")
0235             value = ptToPx(gc, value);
0236         else if (unit.right(2) == "cm")
0237             value = ptToPx(gc, CM_TO_POINT(value));
0238         else if (unit.right(2) == "pc")
0239             value = ptToPx(gc, PI_TO_POINT(value));
0240         else if (unit.right(2) == "mm")
0241             value = ptToPx(gc, MM_TO_POINT(value));
0242         else if (unit.right(2) == "in")
0243             value = ptToPx(gc, INCH_TO_POINT(value));
0244         else if (unit.right(2) == "em") {
0245             value = value * gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontSizeId).toReal();
0246         }
0247         else if (unit.right(2) == "ex") {
0248 
0249             QFontMetrics metrics(gc->textProperties.generateFont());
0250             value = value * metrics.xHeight();
0251         } else if (unit.right(1) == "%") {
0252             if (horiz && vert)
0253                 value = (value / 100.0) * (sqrt(pow(bbox.width(), 2) + pow(bbox.height(), 2)) / sqrt(2.0));
0254             else if (horiz)
0255                 value = (value / 100.0) * bbox.width();
0256             else if (vert)
0257                 value = (value / 100.0) * bbox.height();
0258         }
0259     } else {
0260         value = SvgUtil::fromUserSpace(value);
0261     }
0262     /*else
0263     {
0264         if( m_gc.top() )
0265         {
0266             if( horiz && vert )
0267                 value *= sqrt( pow( m_gc.top()->matrix.m11(), 2 ) + pow( m_gc.top()->matrix.m22(), 2 ) ) / sqrt( 2.0 );
0268             else if( horiz )
0269                 value /= m_gc.top()->matrix.m11();
0270             else if( vert )
0271                 value /= m_gc.top()->matrix.m22();
0272         }
0273     }*/
0274     //value *= 90.0 / DPI;
0275 
0276     return value;
0277 }
0278 
0279 qreal SvgUtil::parseUnitX(SvgGraphicsContext *gc, const QString &unit)
0280 {
0281     if (gc->forcePercentage) {
0282         return SvgUtil::fromPercentage(unit) * gc->currentBoundingBox.width();
0283     } else {
0284         return SvgUtil::parseUnit(gc, unit, true, false, gc->currentBoundingBox);
0285     }
0286 }
0287 
0288 qreal SvgUtil::parseUnitY(SvgGraphicsContext *gc, const QString &unit)
0289 {
0290     if (gc->forcePercentage) {
0291         return SvgUtil::fromPercentage(unit) * gc->currentBoundingBox.height();
0292     } else {
0293         return SvgUtil::parseUnit(gc, unit, false, true, gc->currentBoundingBox);
0294     }
0295 }
0296 
0297 qreal SvgUtil::parseUnitXY(SvgGraphicsContext *gc, const QString &unit)
0298 {
0299     if (gc->forcePercentage) {
0300         const qreal value = SvgUtil::fromPercentage(unit);
0301         return value * sqrt(pow(gc->currentBoundingBox.width(), 2) + pow(gc->currentBoundingBox.height(), 2)) / sqrt(2.0);
0302     } else {
0303         return SvgUtil::parseUnit(gc, unit, true, true, gc->currentBoundingBox);
0304     }
0305 }
0306 
0307 qreal SvgUtil::parseUnitAngular(SvgGraphicsContext *gc, const QString &unit)
0308 {
0309     Q_UNUSED(gc);
0310 
0311     qreal value = 0.0;
0312 
0313     if (unit.isEmpty()) return value;
0314     QByteArray unitLatin1 = unit.toLower().toLatin1();
0315 
0316     const char *start = unitLatin1.data();
0317     if (!start) return value;
0318 
0319     const char *end = parseNumber(start, value);
0320 
0321     if (int(end - start) < unit.length()) {
0322         if (unit.right(3) == "deg") {
0323             value = kisDegreesToRadians(value);
0324         } else if (unit.right(4) == "grad") {
0325             value *= M_PI / 200;
0326         } else if (unit.right(3) == "rad") {
0327             // noop!
0328         } else {
0329             value = kisDegreesToRadians(value);
0330         }
0331     } else {
0332         value = kisDegreesToRadians(value);
0333     }
0334 
0335     return value;
0336 }
0337 
0338 qreal SvgUtil::parseNumber(const QString &string)
0339 {
0340     qreal value = 0.0;
0341 
0342     if (string.isEmpty()) return value;
0343     QByteArray unitLatin1 = string.toLatin1();
0344 
0345     const char *start = unitLatin1.data();
0346     if (!start) return value;
0347 
0348     const char *end = parseNumber(start, value);
0349     KIS_SAFE_ASSERT_RECOVER_NOOP(int(end - start) == string.length());
0350     return value;
0351 }
0352 
0353 const char * SvgUtil::parseNumber(const char *ptr, qreal &number)
0354 {
0355     int integer, exponent;
0356     qreal decimal, frac;
0357     int sign, expsign;
0358 
0359     exponent = 0;
0360     integer = 0;
0361     frac = 1.0;
0362     decimal = 0;
0363     sign = 1;
0364     expsign = 1;
0365 
0366     // read the sign
0367     if (*ptr == '+') {
0368         ptr++;
0369     } else if (*ptr == '-') {
0370         ptr++;
0371         sign = -1;
0372     }
0373 
0374     // read the integer part
0375     while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9')
0376         integer = (integer * 10) + *(ptr++) - '0';
0377     if (*ptr == '.') { // read the decimals
0378         ptr++;
0379         while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9')
0380             decimal += (*(ptr++) - '0') * (frac *= 0.1);
0381     }
0382 
0383     if (*ptr == 'e' || *ptr == 'E') { // read the exponent part
0384         ptr++;
0385 
0386         // read the sign of the exponent
0387         if (*ptr == '+') {
0388             ptr++;
0389         } else if (*ptr == '-') {
0390             ptr++;
0391             expsign = -1;
0392         }
0393 
0394         exponent = 0;
0395         while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9') {
0396             exponent *= 10;
0397             exponent += *ptr - '0';
0398             ptr++;
0399         }
0400     }
0401     number = integer + decimal;
0402     number *= sign * pow((double)10, double(expsign * exponent));
0403 
0404     return ptr;
0405 }
0406 
0407 QString SvgUtil::mapExtendedShapeTag(const QString &tagName, const QDomElement &element)
0408 {
0409     QString result = tagName;
0410 
0411     if (tagName == "path") {
0412         QString kritaType = element.attribute("krita:type", "");
0413         QString sodipodiType = element.attribute("sodipodi:type", "");
0414 
0415         if (kritaType == "arc") {
0416             result = "krita:arc";
0417         } else if (sodipodiType == "arc") {
0418             result = "sodipodi:arc";
0419         }
0420     }
0421 
0422     return result;
0423 }
0424 
0425 QStringList SvgUtil::simplifyList(const QString &str)
0426 {
0427     QString attribute = str;
0428     attribute.replace(',', ' ');
0429     attribute.remove('\r');
0430     attribute.remove('\n');
0431 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0432     return attribute.simplified().split(' ', Qt::SkipEmptyParts);
0433 #else
0434     return attribute.simplified().split(' ', QString::SkipEmptyParts);
0435 #endif
0436 }
0437 
0438 SvgUtil::PreserveAspectRatioParser::PreserveAspectRatioParser(const QString &str)
0439 {
0440     QRegExp rexp("(defer)?\\s*(none|(x(Min|Max|Mid)Y(Min|Max|Mid)))\\s*(meet|slice)?", Qt::CaseInsensitive);
0441     int index = rexp.indexIn(str.toLower());
0442 
0443     if (index >= 0) {
0444         if (rexp.cap(1) == "defer") {
0445             defer = true;
0446         }
0447 
0448         if (rexp.cap(2) != "none") {
0449             xAlignment = alignmentFromString(rexp.cap(4));
0450             yAlignment = alignmentFromString(rexp.cap(5));
0451             mode = rexp.cap(6) == "slice" ?
0452                 Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio;
0453         }
0454     }
0455 }
0456 
0457 QPointF SvgUtil::PreserveAspectRatioParser::rectAnchorPoint(const QRectF &rc) const
0458 {
0459     return QPointF(alignedValue(rc.x(), rc.x() + rc.width(), xAlignment),
0460                    alignedValue(rc.y(), rc.y() + rc.height(), yAlignment));
0461 }
0462 
0463 QString SvgUtil::PreserveAspectRatioParser::toString() const
0464 {
0465     QString result;
0466 
0467     if (!defer &&
0468         xAlignment == Middle &&
0469         yAlignment == Middle &&
0470         mode == Qt::KeepAspectRatio) {
0471 
0472         return result;
0473     }
0474 
0475     if (defer) {
0476         result += "defer ";
0477     }
0478 
0479     if (mode == Qt::IgnoreAspectRatio) {
0480         result += "none";
0481     } else {
0482         result += QString("x%1Y%2")
0483             .arg(alignmentToString(xAlignment))
0484             .arg(alignmentToString(yAlignment));
0485 
0486         if (mode == Qt::KeepAspectRatioByExpanding) {
0487             result += " slice";
0488         }
0489     }
0490 
0491     return result;
0492 }
0493 
0494 SvgUtil::PreserveAspectRatioParser::Alignment SvgUtil::PreserveAspectRatioParser::alignmentFromString(const QString &str) const {
0495     return
0496         str == "max" ? Max :
0497         str == "mid" ? Middle : Min;
0498 }
0499 
0500 QString SvgUtil::PreserveAspectRatioParser::alignmentToString(SvgUtil::PreserveAspectRatioParser::Alignment alignment) const
0501 {
0502     return
0503         alignment == Max ? "Max" :
0504         alignment == Min ? "Min" :
0505         "Mid";
0506 
0507 }
0508 
0509 qreal SvgUtil::PreserveAspectRatioParser::alignedValue(qreal min, qreal max, SvgUtil::PreserveAspectRatioParser::Alignment alignment)
0510 {
0511     qreal result = min;
0512 
0513     switch (alignment) {
0514     case Min:
0515         result = min;
0516         break;
0517     case Middle:
0518         result = 0.5 * (min + max);
0519         break;
0520     case Max:
0521         result = max;
0522         break;
0523     }
0524 
0525     return result;
0526 }