diff --git a/libs/flake/KoMarker.cpp b/libs/flake/KoMarker.cpp index 450267aea2..0945e51471 100644 --- a/libs/flake/KoMarker.cpp +++ b/libs/flake/KoMarker.cpp @@ -1,336 +1,410 @@ /* This file is part of the KDE project Copyright (C) 2011 Thorsten Zachmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoMarker.h" #include #include #include #include #include "KoPathShape.h" #include "KoPathShapeLoader.h" #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "KoOdfWorkaround.h" #include "KoShapePainter.h" #include "KoViewConverter.h" +#include +#include +#include + #include #include #include #include #include "kis_global.h" #include "kis_algebra_2d.h" + class Q_DECL_HIDDEN KoMarker::Private { public: Private() : coordinateSystem(StrokeWidth), referenceSize(3,3), hasAutoOrientation(false), explicitOrientation(0) {} ~Private() { qDeleteAll(shapes); } bool operator==(const KoMarker::Private &other) const { // WARNING: comparison of shapes is extremely fuzzy! Don't // trust it in life-critical cases! return name == other.name && coordinateSystem == other.coordinateSystem && referencePoint == other.referencePoint && referenceSize == other.referenceSize && hasAutoOrientation == other.hasAutoOrientation && explicitOrientation == other.explicitOrientation && compareShapesTo(other.shapes); } Private(const Private &rhs) : name(rhs.name), coordinateSystem(rhs.coordinateSystem), referencePoint(rhs.referencePoint), referenceSize(rhs.referenceSize), hasAutoOrientation(rhs.hasAutoOrientation), explicitOrientation(rhs.explicitOrientation) { Q_FOREACH (KoShape *shape, rhs.shapes) { shapes << shape->cloneShape(); } } QString name; MarkerCoordinateSystem coordinateSystem; QPointF referencePoint; QSizeF referenceSize; bool hasAutoOrientation; qreal explicitOrientation; QList shapes; bool compareShapesTo(const QList other) const { if (shapes.size() != other.size()) return false; for (int i = 0; i < shapes.size(); i++) { if (shapes[i]->outline() != other[i]->outline() || shapes[i]->absoluteTransformation(0) != other[i]->absoluteTransformation(0)) { return false; } } return true; } QTransform markerTransform(qreal strokeWidth, qreal nodeAngle, const QPointF &pos = QPointF()) { - const QTransform translate = QTransform::fromTranslate(referencePoint.x(), referencePoint.y()); + const QTransform translate = QTransform::fromTranslate(-referencePoint.x(), -referencePoint.y()); QTransform t = translate; if (coordinateSystem == StrokeWidth) { t *= QTransform::fromScale(strokeWidth, strokeWidth); } const qreal angle = hasAutoOrientation ? nodeAngle : explicitOrientation; if (angle != 0.0) { QTransform r; r.rotateRadians(angle); t *= r; } t *= QTransform::fromTranslate(pos.x(), pos.y()); return t; } }; KoMarker::KoMarker() : d(new Private()) { } KoMarker::~KoMarker() { delete d; } QString KoMarker::name() const { return d->name; } KoMarker::KoMarker(const KoMarker &rhs) : QSharedData(rhs), d(new Private(*rhs.d)) { } bool KoMarker::operator==(const KoMarker &other) const { return *d == *other.d; } void KoMarker::setCoordinateSystem(KoMarker::MarkerCoordinateSystem value) { d->coordinateSystem = value; } KoMarker::MarkerCoordinateSystem KoMarker::coordinateSystem() const { return d->coordinateSystem; } KoMarker::MarkerCoordinateSystem KoMarker::coordinateSystemFromString(const QString &value) { MarkerCoordinateSystem result = StrokeWidth; if (value == "userSpaceOnUse") { result = UserSpaceOnUse; } return result; } QString KoMarker::coordinateSystemToString(KoMarker::MarkerCoordinateSystem value) { return value == StrokeWidth ? "strokeWidth" : "userSpaceOnUse"; } void KoMarker::setReferencePoint(const QPointF &value) { d->referencePoint = value; } QPointF KoMarker::referencePoint() const { return d->referencePoint; } void KoMarker::setReferenceSize(const QSizeF &size) { d->referenceSize = size; } QSizeF KoMarker::referenceSize() const { return d->referenceSize; } bool KoMarker::hasAutoOtientation() const { return d->hasAutoOrientation; } void KoMarker::setAutoOrientation(bool value) { d->hasAutoOrientation = value; } qreal KoMarker::explicitOrientation() const { return d->explicitOrientation; } void KoMarker::setExplicitOrientation(qreal value) { d->explicitOrientation = value; } void KoMarker::setShapes(const QList &shapes) { d->shapes = shapes; } QList KoMarker::shapes() const { return d->shapes; } void KoMarker::paintAtPosition(QPainter *painter, const QPointF &pos, qreal strokeWidth, qreal nodeAngle) { QTransform oldTransform = painter->transform(); KoViewConverter converter; KoShapePainter p; p.setShapes(d->shapes); - painter->translate(pos); - - const qreal angle = d->hasAutoOrientation ? nodeAngle : d->explicitOrientation; - painter->rotate(kisRadiansToDegrees(angle)); + painter->setTransform(d->markerTransform(strokeWidth, nodeAngle, pos), true); - if (d->coordinateSystem == StrokeWidth) { - painter->scale(strokeWidth, strokeWidth); - } - - painter->translate(-d->referencePoint); p.paint(*painter, converter); painter->setTransform(oldTransform); } qreal KoMarker::maxInset(qreal strokeWidth) const { QRectF shapesBounds = boundingRect(strokeWidth, 0.0); // normalized to 0,0 qreal result = 0.0; result = qMax(KisAlgebra2D::norm(shapesBounds.topLeft()), result); result = qMax(KisAlgebra2D::norm(shapesBounds.topRight()), result); result = qMax(KisAlgebra2D::norm(shapesBounds.bottomLeft()), result); result = qMax(KisAlgebra2D::norm(shapesBounds.bottomRight()), result); if (d->coordinateSystem == StrokeWidth) { result *= strokeWidth; } return result; } QRectF KoMarker::boundingRect(qreal strokeWidth, qreal nodeAngle) const { QRectF shapesBounds; Q_FOREACH (KoShape *shape, d->shapes) { shapesBounds |= shape->boundingRect(); } const QTransform t = d->markerTransform(strokeWidth, nodeAngle); if (!t.isIdentity()) { shapesBounds = t.mapRect(shapesBounds); } return shapesBounds; } QPainterPath KoMarker::outline(qreal strokeWidth, qreal nodeAngle) const { QPainterPath outline; Q_FOREACH (KoShape *shape, d->shapes) { outline |= shape->absoluteTransformation(0).map(shape->outline()); } const QTransform t = d->markerTransform(strokeWidth, nodeAngle); if (!t.isIdentity()) { outline = t.map(outline); } return outline; } void KoMarker::drawPreview(QPainter *painter, const QRectF &previewRect, const QPen &pen, KoFlake::MarkerPosition position) { const QRectF outlineRect = outline(pen.widthF(), 0).boundingRect(); // normalized to 0,0 QPointF marker; QPointF start; QPointF end; if (position == KoFlake::StartMarker) { marker = QPointF(-outlineRect.left() + previewRect.left(), previewRect.center().y()); start = marker; end = QPointF(previewRect.right(), start.y()); } else if (position == KoFlake::MidMarker) { start = QPointF(previewRect.left(), previewRect.center().y()); marker = QPointF(-outlineRect.center().x() + previewRect.center().x(), start.y()); end = QPointF(previewRect.right(), start.y()); } else if (position == KoFlake::EndMarker) { start = QPointF(previewRect.left(), previewRect.center().y()); marker = QPointF(-outlineRect.right() + previewRect.right(), start.y()); end = marker; } painter->save(); painter->setPen(pen); painter->setClipRect(previewRect); painter->drawLine(start, end); paintAtPosition(painter, marker, pen.widthF(), 0); painter->restore(); } + +void KoMarker::applyShapeStroke(KoShape *parentShape, KoShapeStroke *stroke, const QPointF &pos, qreal strokeWidth, qreal nodeAngle) +{ + const QGradient *originalGradient = stroke->lineBrush().gradient(); + + if (!originalGradient) { + QList linearizedShapes = KoShape::linearizeSubtree(d->shapes); + Q_FOREACH(KoShape *shape, linearizedShapes) { + // update the stroke + KoShapeStrokeSP shapeStroke = shape->stroke() ? + qSharedPointerDynamicCast(shape->stroke()) : + KoShapeStrokeSP(); + + if (shapeStroke) { + shapeStroke = toQShared(new KoShapeStroke(*shapeStroke)); + + shapeStroke->setLineBrush(QBrush()); + shapeStroke->setColor(stroke->color()); + + shape->setStroke(shapeStroke); + } + + // update the background + if (shape->background()) { + QSharedPointer bg(new KoColorBackground(stroke->color())); + shape->setBackground(bg); + } + } + } else { + QScopedPointer g(KoFlake::cloneGradient(originalGradient)); + KIS_ASSERT_RECOVER_RETURN(g); + + const QTransform markerTransformInverted = + d->markerTransform(strokeWidth, nodeAngle, pos).inverted(); + + QTransform gradientToUser; + + // Unwrap the gradient to work in global mode + if (g->coordinateMode() == QGradient::ObjectBoundingMode) { + const QRectF boundingRect = + KisAlgebra2D::ensureRectNotSmaller(parentShape->outline().boundingRect(), QSizeF(1.0, 1.0)); + + gradientToUser = QTransform(boundingRect.width(), 0, 0, boundingRect.height(), + boundingRect.x(), boundingRect.y()); + + g->setCoordinateMode(QGradient::LogicalMode); + } + + QList linearizedShapes = KoShape::linearizeSubtree(d->shapes); + Q_FOREACH(KoShape *shape, linearizedShapes) { + // shape-unwinding transform + QTransform t = gradientToUser * markerTransformInverted * shape->absoluteTransformation(0).inverted(); + + // update the stroke + KoShapeStrokeSP shapeStroke = shape->stroke() ? + qSharedPointerDynamicCast(shape->stroke()) : + KoShapeStrokeSP(); + + if (shapeStroke) { + shapeStroke = toQShared(new KoShapeStroke(*shapeStroke)); + + QBrush brush(*g); + brush.setTransform(t); + shapeStroke->setLineBrush(brush); + shapeStroke->setColor(Qt::transparent); + shape->setStroke(shapeStroke); + } + + // update the background + if (shape->background()) { + + QSharedPointer bg(new KoGradientBackground(KoFlake::cloneGradient(g.data()), t)); + shape->setBackground(bg); + } + } + } +} diff --git a/libs/flake/KoMarker.h b/libs/flake/KoMarker.h index c1054feb6f..a50bc27b4f 100644 --- a/libs/flake/KoMarker.h +++ b/libs/flake/KoMarker.h @@ -1,118 +1,122 @@ /* This file is part of the KDE project Copyright (C) 2011 Thorsten Zachmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOMARKER_H #define KOMARKER_H #include #include #include "kritaflake_export.h" #include class KoXmlElement; class KoShapeLoadingContext; class KoShapeSavingContext; class QString; class QPainterPath; class KoShape; class QPainter; +class KoShapeStroke; class KRITAFLAKE_EXPORT KoMarker : public QSharedData { public: KoMarker(); ~KoMarker(); /** * Display name of the marker * * @return Display name of the marker */ QString name() const; KoMarker(const KoMarker &rhs); bool operator==(const KoMarker &other) const; enum MarkerCoordinateSystem { StrokeWidth, UserSpaceOnUse }; void setCoordinateSystem(MarkerCoordinateSystem value); MarkerCoordinateSystem coordinateSystem() const; static MarkerCoordinateSystem coordinateSystemFromString(const QString &value); static QString coordinateSystemToString(MarkerCoordinateSystem value); void setReferencePoint(const QPointF &value); QPointF referencePoint() const; void setReferenceSize(const QSizeF &size); QSizeF referenceSize() const; bool hasAutoOtientation() const; void setAutoOrientation(bool value); // measured in radians! qreal explicitOrientation() const; // measured in radians! void setExplicitOrientation(qreal value); void setShapes(const QList &shapes); QList shapes() const; /** * @brief paintAtOrigin paints the marker at the position \p pos. * Scales and rotates the masrker if needed. */ void paintAtPosition(QPainter *painter, const QPointF &pos, qreal strokeWidth, qreal nodeAngle); /** * Return maximum distance that the marker can take outside the shape itself */ qreal maxInset(qreal strokeWidth) const; /** * Bounding rect of the marker in local coordinates. It is assumed that the marker * is painted with the reference point placed at position (0,0) */ QRectF boundingRect(qreal strokeWidth, qreal nodeAngle) const; /** * Outline of the marker in local coordinates. It is assumed that the marker * is painted with the reference point placed at position (0,0) */ QPainterPath outline(qreal strokeWidth, qreal nodeAngle) const; /** * Draws a preview of the marker in \p previewRect of \p painter */ void drawPreview(QPainter *painter, const QRectF &previewRect, const QPen &pen, KoFlake::MarkerPosition position); + + void applyShapeStroke(KoShape *shape, KoShapeStroke *stroke, const QPointF &pos, qreal strokeWidth, qreal nodeAngle); + private: class Private; Private * const d; }; Q_DECLARE_METATYPE(KoMarker*) #endif /* KOMARKER_H */ diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index 5568428953..e568fb0cb3 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1669 +1,1678 @@ /* This file is part of the KDE project Copyright (C) 2006-2008, 2010-2011 Thorsten Zachmann Copyright (C) 2006-2011 Jan Hambrecht Copyright (C) 2007-2009 Thomas Zander Copyright (C) 2011 Jean-Nicolas Artaud This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathShape.h" #include "KoPathShape_p.h" #include "KoPathSegment.h" #include "KoOdfWorkaround.h" #include "KoPathPoint.h" #include "KoShapeStrokeModel.h" #include "KoViewConverter.h" #include "KoPathShapeLoader.h" #include "KoShapeSavingContext.h" #include "KoShapeLoadingContext.h" #include "KoShapeShadow.h" #include "KoShapeBackground.h" #include "KoShapeContainer.h" #include "KoFilterEffectStack.h" #include "KoMarker.h" #include "KoMarkerSharedLoadingData.h" #include "KoShapeStroke.h" #include "KoInsets.h" #include #include #include #include #include #include #include #include #include #include "kis_global.h" #include // for qIsNaN static bool qIsNaNPoint(const QPointF &p) { return qIsNaN(p.x()) || qIsNaN(p.y()); } static const qreal DefaultMarkerWidth = 3.0; KoPathShapePrivate::KoPathShapePrivate(KoPathShape *q) : KoTosContainerPrivate(q), fillRule(Qt::OddEvenFill), - startMarker(KoMarkerData::MarkerStart), - endMarker(KoMarkerData::MarkerEnd) + autoFillMarkers(false) { } KoPathShapePrivate::KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q) : KoTosContainerPrivate(rhs, q), fillRule(rhs.fillRule), - startMarker(rhs.startMarker), - endMarker(rhs.endMarker), - markersNew(rhs.markersNew) + markersNew(rhs.markersNew), + autoFillMarkers(rhs.autoFillMarkers) { Q_FOREACH (KoSubpath *subPath, rhs.subpaths) { KoSubpath *clonedSubPath = new KoSubpath(); Q_FOREACH (KoPathPoint *point, *subPath) { *clonedSubPath << new KoPathPoint(*point, q); } subpaths << clonedSubPath; } } QRectF KoPathShapePrivate::handleRect(const QPointF &p, qreal radius) const { return QRectF(p.x() - radius, p.y() - radius, 2*radius, 2*radius); } void KoPathShapePrivate::applyViewboxTransformation(const KoXmlElement &element) { // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { // load the desired size QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // load the desired position QPointF pos; pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); viewMatrix.translate(pos.x(), pos.y()); // transform the path data map(viewMatrix); } } KoPathShape::KoPathShape() :KoTosContainer(new KoPathShapePrivate(this)) { } KoPathShape::KoPathShape(KoPathShapePrivate *dd) : KoTosContainer(dd) { } KoPathShape::KoPathShape(const KoPathShape &rhs) : KoTosContainer(new KoPathShapePrivate(*rhs.d_func(), this)) { } KoPathShape::~KoPathShape() { clear(); } KoShape *KoPathShape::cloneShape() const { return new KoPathShape(*this); } void KoPathShape::saveContourOdf(KoShapeSavingContext &context, const QSizeF &scaleFactor) const { Q_D(const KoPathShape); if (d->subpaths.length() <= 1) { QTransform matrix; matrix.scale(scaleFactor.width(), scaleFactor.height()); QString points; KoSubpath *subPath = d->subpaths.first(); KoSubpath::const_iterator pointIt(subPath->constBegin()); KoPathPoint *currPoint= 0; // iterate over all points for (; pointIt != subPath->constEnd(); ++pointIt) { currPoint = *pointIt; if (currPoint->activeControlPoint1() || currPoint->activeControlPoint2()) { break; } const QPointF p = matrix.map(currPoint->point()); points += QString("%1,%2 ").arg(qRound(1000*p.x())).arg(qRound(1000*p.y())); } if (currPoint && !(currPoint->activeControlPoint1() || currPoint->activeControlPoint2())) { context.xmlWriter().startElement("draw:contour-polygon"); context.xmlWriter().addAttributePt("svg:width", size().width()); context.xmlWriter().addAttributePt("svg:height", size().height()); const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(1000*s.width())).arg(qRound(1000*s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); context.xmlWriter().addAttribute("draw:points", points); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); return; } } // if we get here we couldn't save as polygon - let-s try contour-path context.xmlWriter().startElement("draw:contour-path"); saveOdfAttributes(context, OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); } void KoPathShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoPathShape); context.xmlWriter().startElement("draw:path"); saveOdfAttributes(context, OdfAllAttributes | OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoPathShape::loadContourOdf(const KoXmlElement &element, KoShapeLoadingContext &, const QSizeF &scaleFactor) { Q_D(KoPathShape); // first clear the path data from the default path clear(); if (element.localName() == "contour-polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } close(); } else if (element.localName() == "contour-path") { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(scaleFactor.width(), scaleFactor.height()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); // transform the path data d->map(viewMatrix); } setTransformation(QTransform()); return true; } bool KoPathShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); loadOdfAttributes(element, context, OdfMandatories | OdfAdditionalAttributes | OdfCommonChildElements); // first clear the path data from the default path clear(); if (element.localName() == "line") { QPointF start; start.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", ""))); start.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", ""))); QPointF end; end.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", ""))); end.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", ""))); moveTo(start); lineTo(end); } else if (element.localName() == "polyline" || element.localName() == "polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } if (element.localName() == "polygon") close(); } else { // path loading KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } d->applyViewboxTransformation(element); QPointF pos = normalize(); setTransformation(QTransform()); if (element.hasAttributeNS(KoXmlNS::svg, "x") || element.hasAttributeNS(KoXmlNS::svg, "y")) { pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); } setPosition(pos); loadOdfAttributes(element, context, OdfTransformation); // now that the correct transformation is set up // apply that matrix to the path geometry so that // we don't transform the stroke d->map(transformation()); setTransformation(QTransform()); normalize(); loadText(element, context); return true; } QString KoPathShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { Q_D(const KoPathShape); style.addProperty("svg:fill-rule", d->fillRule == Qt::OddEvenFill ? "evenodd" : "nonzero"); QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } - d->startMarker.saveStyle(style, lineWidth, context); - d->endMarker.saveStyle(style, lineWidth, context); + + Q_UNUSED(lineWidth) return KoTosContainer::saveStyle(style, context); } void KoPathShape::loadStyle(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoPathShape); KoTosContainer::loadStyle(element, context); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); if (styleStack.hasProperty(KoXmlNS::svg, "fill-rule")) { QString rule = styleStack.property(KoXmlNS::svg, "fill-rule"); d->fillRule = (rule == "nonzero") ? Qt::WindingFill : Qt::OddEvenFill; } else { d->fillRule = Qt::WindingFill; #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixMissingFillRule(d->fillRule, context); #endif } QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } - d->startMarker.loadOdf(lineWidth, context); - d->endMarker.loadOdf(lineWidth, context); + Q_UNUSED(lineWidth); } QRect KoPathShape::loadOdfViewbox(const KoXmlElement & element) { QRect viewbox; QString data = element.attributeNS(KoXmlNS::svg, QLatin1String("viewBox")); if (! data.isEmpty()) { data.replace(QLatin1Char(','), QLatin1Char(' ')); const QStringList coordinates = data.simplified().split(QLatin1Char(' '), QString::SkipEmptyParts); if (coordinates.count() == 4) { viewbox.setRect(coordinates.at(0).toInt(), coordinates.at(1).toInt(), coordinates.at(2).toInt(), coordinates.at(3).toInt()); } } return viewbox; } void KoPathShape::clear() { Q_D(KoPathShape); Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } d->subpaths.clear(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoPathShape); applyConversion(painter, converter); QPainterPath path(outline()); path.setFillRule(d->fillRule); if (background()) { background()->paint(painter, converter, paintContext, path); } //d->paintDebug(painter); } #ifndef NDEBUG void KoPathShapePrivate::paintDebug(QPainter &painter) { Q_Q(KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); int i = 0; QPen pen(Qt::black, 0); painter.save(); painter.setPen(pen); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { ++i; KoPathPoint *point = (*it); QRectF r(point->point(), QSizeF(5, 5)); r.translate(-2.5, -2.5); QPen pen(Qt::black, 0); painter.setPen(pen); if (point->activeControlPoint1() && point->activeControlPoint2()) { QBrush b(Qt::red); painter.setBrush(b); } else if (point->activeControlPoint1()) { QBrush b(Qt::yellow); painter.setBrush(b); } else if (point->activeControlPoint2()) { QBrush b(Qt::darkYellow); painter.setBrush(b); } painter.drawEllipse(r); } } painter.restore(); debugFlake << "nop =" << i; } void KoPathShapePrivate::debugPath() const { Q_Q(const KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { debugFlake << "p:" << (*pathIt) << "," << *it << "," << (*it)->point() << "," << (*it)->properties(); } } } #endif void KoPathShape::paintPoints(QPainter &painter, const KoViewConverter &converter, int handleRadius) { Q_D(KoPathShape); applyConversion(painter, converter); KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) (*it)->paint(painter, handleRadius, KoPathPoint::Node); } } QRectF KoPathShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoPathShape::outline() const { Q_D(const KoPathShape); QPainterPath path; Q_FOREACH (KoSubpath * subpath, d->subpaths) { KoPathPoint * lastPoint = subpath->first(); bool activeCP = false; Q_FOREACH (KoPathPoint * currPoint, *subpath) { KoPathPoint::PointProperties currProperties = currPoint->properties(); if (currPoint == subpath->first()) { if (currProperties & KoPathPoint::StartSubpath) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.moveTo(currPoint->point()); } } else if (activeCP && currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.cubicTo( lastPoint->controlPoint2(), currPoint->controlPoint1(), currPoint->point()); } else if (activeCP || currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( activeCP ? lastPoint->controlPoint2() : currPoint->controlPoint1(), currPoint->point()); } else { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.lineTo(currPoint->point()); } if (currProperties & KoPathPoint::CloseSubpath && currProperties & KoPathPoint::StopSubpath) { // add curve when there is a curve on the way to the first point KoPathPoint * firstPoint = subpath->first(); Q_ASSERT(!qIsNaNPoint(firstPoint->point())); if (currPoint->activeControlPoint2() && firstPoint->activeControlPoint1()) { path.cubicTo( currPoint->controlPoint2(), firstPoint->controlPoint1(), firstPoint->point()); } else if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( currPoint->activeControlPoint2() ? currPoint->controlPoint2() : firstPoint->controlPoint1(), firstPoint->point()); } path.closeSubpath(); } if (currPoint->activeControlPoint2()) { activeCP = true; } else { activeCP = false; } lastPoint = currPoint; } } return path; } QRectF KoPathShape::boundingRect() const { QTransform transform = absoluteTransformation(0); // calculate the bounding rect of the transformed outline QRectF bb; const QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); QPen pen; if (lineBorder) { pen.setWidthF(lineBorder->lineWidth()); } bb = transform.map(pathStroke(pen)).boundingRect(); if (stroke()) { KoInsets inset; stroke()->strokeInsets(this, inset); // calculate transformed border insets QPointF center = transform.map(QPointF()); QPointF tl = transform.map(QPointF(-inset.left,-inset.top)) - center; QPointF br = transform.map(QPointF(inset.right,inset.bottom)) -center; qreal left = qMin(tl.x(),br.x()); qreal right = qMax(tl.x(),br.x()); qreal top = qMin(tl.y(),br.y()); qreal bottom = qMax(tl.y(),br.y()); bb.adjust(left, top, right, bottom); // TODO: take care about transformations! // take care about markers! bb = kisGrowRect(bb, stroke()->strokeMaxMarkersInset(this)); } if (shadow()) { KoInsets insets; shadow()->insets(insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (filterEffectStack()) { QRectF clipRect = filterEffectStack()->clipRectForBoundingRect(QRectF(QPointF(), size())); bb |= transform.mapRect(clipRect); } return bb; } QSizeF KoPathShape::size() const { // don't call boundingRect here as it uses absoluteTransformation // which itself uses size() -> leads to infinite reccursion return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) { Q_D(KoPathShape); QTransform matrix(resizeMatrix(newSize)); KoShape::setSize(newSize); d->map(matrix); } QTransform KoPathShape::resizeMatrix(const QSizeF & newSize) const { QSizeF oldSize = size(); if (oldSize.width() == 0.0) { oldSize.setWidth(0.000001); } if (oldSize.height() == 0.0) { oldSize.setHeight(0.000001); } QSizeF sizeNew(newSize); if (sizeNew.width() == 0.0) { sizeNew.setWidth(0.000001); } if (sizeNew.height() == 0.0) { sizeNew.setHeight(0.000001); } return QTransform(sizeNew.width() / oldSize.width(), 0, 0, sizeNew.height() / oldSize.height(), 0, 0); } KoPathPoint * KoPathShape::moveTo(const QPointF &p) { Q_D(KoPathShape); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StartSubpath | KoPathPoint::StopSubpath); KoSubpath * path = new KoSubpath; path->push_back(point); d->subpaths.push_back(path); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) moveTo(QPointF(0, 0)); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); if (lastPoint->properties() & KoPathPoint::CloseSubpath) { lastPoint = d->subpaths.last()->first(); } QPointF startpoint(lastPoint->point()); KoPathPoint * newEndPoint = lastPoint; QPointF curvePoints[12]; int pointCnt = arcToCurve(rx, ry, startAngle, sweepAngle, startpoint, curvePoints); for (int i = 0; i < pointCnt; i += 3) { newEndPoint = curveTo(curvePoints[i], curvePoints[i+1], curvePoints[i+2]); } return newEndPoint; } int KoPathShape::arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF & offset, QPointF * curvePoints) const { int pointCnt = 0; // check Parameters if (sweepAngle == 0) return pointCnt; if (sweepAngle > 360) sweepAngle = 360; else if (sweepAngle < -360) sweepAngle = - 360; if (rx == 0 || ry == 0) { //TODO } // split angles bigger than 90° so that it gives a good aproximation to the circle qreal parts = ceil(qAbs(sweepAngle / 90.0)); qreal sa_rad = startAngle * M_PI / 180.0; qreal partangle = sweepAngle / parts; qreal endangle = startAngle + partangle; qreal se_rad = endangle * M_PI / 180.0; qreal sinsa = sin(sa_rad); qreal cossa = cos(sa_rad); qreal kappa = 4.0 / 3.0 * tan((se_rad - sa_rad) / 4); // startpoint is at the last point is the path but when it is closed // it is at the first point QPointF startpoint(offset); //center berechnen QPointF center(startpoint - QPointF(cossa * rx, -sinsa * ry)); //debugFlake <<"kappa" << kappa <<"parts" << parts; for (int part = 0; part < parts; ++part) { // start tangent curvePoints[pointCnt++] = QPointF(startpoint - QPointF(sinsa * rx * kappa, cossa * ry * kappa)); qreal sinse = sin(se_rad); qreal cosse = cos(se_rad); // end point QPointF endpoint(center + QPointF(cosse * rx, -sinse * ry)); // end tangent curvePoints[pointCnt++] = QPointF(endpoint - QPointF(-sinse * rx * kappa, -cosse * ry * kappa)); curvePoints[pointCnt++] = endpoint; // set the endpoint as next start point startpoint = endpoint; sinsa = sinse; cossa = cosse; endangle += partangle; se_rad = endangle * M_PI / 180.0; } return pointCnt; } void KoPathShape::close() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeSubpath(d->subpaths.last()); } void KoPathShape::closeMerge() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeMergeSubpath(d->subpaths.last()); } QPointF KoPathShape::normalize() { Q_D(KoPathShape); QPointF tl(outline().boundingRect().topLeft()); QTransform matrix; matrix.translate(-tl.x(), -tl.y()); d->map(matrix); // keep the top left point of the object applyTransformation(matrix.inverted()); d->shapeChanged(ContentChanged); return tl; } void KoPathShapePrivate::map(const QTransform &matrix) { Q_Q(KoPathShape); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { (*it)->map(matrix); } } } void KoPathShapePrivate::updateLast(KoPathPoint **lastPoint) { Q_Q(KoPathShape); // check if we are about to add a new point to a closed subpath if ((*lastPoint)->properties() & KoPathPoint::StopSubpath && (*lastPoint)->properties() & KoPathPoint::CloseSubpath) { // get the first point of the subpath KoPathPoint *subpathStart = subpaths.last()->first(); // clone the first point of the subpath... KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart, q); // ... and make it a normal point newLastPoint->setProperties(KoPathPoint::Normal); // now start a new subpath with the cloned start point KoSubpath *path = new KoSubpath; path->push_back(newLastPoint); subpaths.push_back(path); *lastPoint = newLastPoint; } else { // the subpath was not closed so the formerly last point // of the subpath is no end point anymore (*lastPoint)->unsetProperty(KoPathPoint::StopSubpath); } (*lastPoint)->unsetProperty(KoPathPoint::CloseSubpath); } QList KoPathShape::pointsAt(const QRectF &r) const { Q_D(const KoPathShape); QList result; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (r.contains((*it)->point())) result.append(*it); else if ((*it)->activeControlPoint1() && r.contains((*it)->controlPoint1())) result.append(*it); else if ((*it)->activeControlPoint2() && r.contains((*it)->controlPoint2())) result.append(*it); } } return result; } QList KoPathShape::segmentsAt(const QRectF &r) const { Q_D(const KoPathShape); QList segments; int subpathCount = d->subpaths.count(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { KoSubpath * subpath = d->subpaths[subpathIndex]; int pointCount = subpath->count(); bool subpathClosed = isClosedSubpath(subpathIndex); for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { if (pointIndex == (pointCount - 1) && ! subpathClosed) break; KoPathSegment s(subpath->at(pointIndex), subpath->at((pointIndex + 1) % pointCount)); QRectF controlRect = s.controlPointRect(); if (! r.intersects(controlRect) && ! controlRect.contains(r)) continue; QRectF bound = s.boundingRect(); if (! r.intersects(bound) && ! bound.contains(r)) continue; segments.append(s); } } return segments; } KoPathPointIndex KoPathShape::pathPointIndex(const KoPathPoint *point) const { Q_D(const KoPathShape); for (int subpathIndex = 0; subpathIndex < d->subpaths.size(); ++subpathIndex) { KoSubpath * subpath = d->subpaths.at(subpathIndex); for (int pointPos = 0; pointPos < subpath->size(); ++pointPos) { if (subpath->at(pointPos) == point) { return KoPathPointIndex(subpathIndex, pointPos); } } } return KoPathPointIndex(-1, -1); } KoPathPoint * KoPathShape::pointByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; return subpath->at(pointIndex.second); } KoPathSegment KoPathShape::segmentByIndex(const KoPathPointIndex &pointIndex) const { Q_D(const KoPathShape); KoPathSegment segment(0, 0); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath != 0 && pointIndex.second >= 0 && pointIndex.second < subpath->size()) { KoPathPoint * point = subpath->at(pointIndex.second); int index = pointIndex.second; // check if we have a (closing) segment starting from the last point if ((index == subpath->size() - 1) && point->properties() & KoPathPoint::CloseSubpath) index = 0; else ++index; if (index < subpath->size()) { segment = KoPathSegment(point, subpath->at(index)); } } return segment; } int KoPathShape::pointCount() const { Q_D(const KoPathShape); int i = 0; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { i += (*pathIt)->size(); } return i; } int KoPathShape::subpathCount() const { Q_D(const KoPathShape); return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return -1; return subpath->size(); } bool KoPathShape::isClosedSubpath(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; const bool firstClosed = subpath->first()->properties() & KoPathPoint::CloseSubpath; const bool lastClosed = subpath->last()->properties() & KoPathPoint::CloseSubpath; return firstClosed && lastClosed; } bool KoPathShape::insertPoint(KoPathPoint* point, const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second > subpath->size()) return false; KoPathPoint::PointProperties properties = point->properties(); properties &= ~KoPathPoint::StartSubpath; properties &= ~KoPathPoint::StopSubpath; properties &= ~KoPathPoint::CloseSubpath; // check if new point starts subpath if (pointIndex.second == 0) { properties |= KoPathPoint::StartSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties |= KoPathPoint::CloseSubpath; } // old first point does not start the subpath anymore subpath->first()->unsetProperty(KoPathPoint::StartSubpath); } // check if new point stops subpath else if (pointIndex.second == subpath->size()) { properties |= KoPathPoint::StopSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties = properties | KoPathPoint::CloseSubpath; } // old last point does not end subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); } point->setProperties(properties); point->setParent(this); subpath->insert(pointIndex.second , point); return true; } KoPathPoint * KoPathShape::removePoint(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; KoPathPoint * point = subpath->takeAt(pointIndex.second); point->setParent(0); //don't do anything (not even crash), if there was only one point if (pointCount()==0) { return point; } // check if we removed the first point else if (pointIndex.second == 0) { // first point removed, set new StartSubpath subpath->first()->setProperty(KoPathPoint::StartSubpath); // check if path was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->first()->setProperty(KoPathPoint::CloseSubpath); } } // check if we removed the last point else if (pointIndex.second == subpath->size()) { // use size as point is already removed // last point removed, set new StopSubpath subpath->last()->setProperty(KoPathPoint::StopSubpath); // check if path was closed if (point->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->last()->setProperty(KoPathPoint::CloseSubpath); } } return point; } bool KoPathShape::breakAfter(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second > subpath->size() - 2 || isClosedSubpath(pointIndex.first)) return false; KoSubpath * newSubpath = new KoSubpath; int size = subpath->size(); for (int i = pointIndex.second + 1; i < size; ++i) { newSubpath->append(subpath->takeAt(pointIndex.second + 1)); } // now make the first point of the new subpath a starting node newSubpath->first()->setProperty(KoPathPoint::StartSubpath); // the last point of the old subpath is now an ending node subpath->last()->setProperty(KoPathPoint::StopSubpath); // insert the new subpath after the broken one d->subpaths.insert(pointIndex.first + 1, newSubpath); return true; } bool KoPathShape::join(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); KoSubpath *nextSubpath = d->subPath(subpathIndex + 1); if (!subpath || !nextSubpath || isClosedSubpath(subpathIndex) || isClosedSubpath(subpathIndex+1)) return false; // the last point of the subpath does not end the subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // the first point of the next subpath does not start a subpath anymore nextSubpath->first()->unsetProperty(KoPathPoint::StartSubpath); // append the second subpath to the first Q_FOREACH (KoPathPoint * p, *nextSubpath) subpath->append(p); // remove the nextSubpath from path d->subpaths.removeAt(subpathIndex + 1); // delete it as it is no longer possible to use it delete nextSubpath; return true; } bool KoPathShape::moveSubpath(int oldSubpathIndex, int newSubpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(oldSubpathIndex); if (subpath == 0 || newSubpathIndex >= d->subpaths.size()) return false; if (oldSubpathIndex == newSubpathIndex) return true; d->subpaths.removeAt(oldSubpathIndex); d->subpaths.insert(newSubpathIndex, subpath); return true; } KoPathPointIndex KoPathShape::openSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || !isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer closes the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } // make the first point a start node subpath->first()->setProperty(KoPathPoint::StartSubpath); // make the last point an end node subpath->last()->setProperty(KoPathPoint::StopSubpath); return pathPointIndex(oldStartPoint); } KoPathPointIndex KoPathShape::closeSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer ends the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } subpath->first()->setProperty(KoPathPoint::StartSubpath); subpath->last()->setProperty(KoPathPoint::StopSubpath); d->closeSubpath(subpath); return pathPointIndex(oldStartPoint); } bool KoPathShape::reverseSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; int size = subpath->size(); for (int i = 0; i < size; ++i) { KoPathPoint *p = subpath->takeAt(i); p->reverse(); subpath->prepend(p); } // adjust the position dependent properties KoPathPoint *first = subpath->first(); KoPathPoint *last = subpath->last(); KoPathPoint::PointProperties firstProps = first->properties(); KoPathPoint::PointProperties lastProps = last->properties(); firstProps |= KoPathPoint::StartSubpath; firstProps &= ~KoPathPoint::StopSubpath; lastProps |= KoPathPoint::StopSubpath; lastProps &= ~KoPathPoint::StartSubpath; if (firstProps & KoPathPoint::CloseSubpath) { firstProps |= KoPathPoint::CloseSubpath; lastProps |= KoPathPoint::CloseSubpath; } first->setProperties(firstProps); last->setProperties(lastProps); return true; } KoSubpath * KoPathShape::removeSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath != 0) { Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.removeAt(subpathIndex); } return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { Q_D(KoPathShape); if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.insert(subpathIndex, subpath); return true; } bool KoPathShape::combine(KoPathShape *path) { Q_D(KoPathShape); if (! path) return false; QTransform pathMatrix = path->absoluteTransformation(0); QTransform myMatrix = absoluteTransformation(0).inverted(); Q_FOREACH (KoSubpath* subpath, path->d_func()->subpaths) { KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, this); newPoint->map(pathMatrix); newPoint->map(myMatrix); newSubpath->append(newPoint); } d->subpaths.append(newSubpath); } normalize(); return true; } bool KoPathShape::separate(QList & separatedPaths) { Q_D(KoPathShape); if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); if (! shape) continue; shape->setStroke(stroke()); shape->setShapeId(shapeId()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d_func()->subpaths.append(newSubpath); shape->normalize(); separatedPaths.append(shape); } return true; } void KoPathShapePrivate::closeSubpath(KoSubpath *subpath) { if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); } void KoPathShapePrivate::closeMergeSubpath(KoSubpath *subpath) { if (! subpath || subpath->size() < 2) return; KoPathPoint * lastPoint = subpath->last(); KoPathPoint * firstPoint = subpath->first(); // check if first and last points are coincident if (lastPoint->point() == firstPoint->point()) { // we are removing the current last point and // reuse its first control point if active firstPoint->setProperty(KoPathPoint::StartSubpath); firstPoint->setProperty(KoPathPoint::CloseSubpath); if (lastPoint->activeControlPoint1()) firstPoint->setControlPoint1(lastPoint->controlPoint1()); // remove last point delete subpath->takeLast(); // the new last point closes the subpath now lastPoint = subpath->last(); lastPoint->setProperty(KoPathPoint::StopSubpath); lastPoint->setProperty(KoPathPoint::CloseSubpath); } else { closeSubpath(subpath); } } KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { Q_Q(const KoPathShape); if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const { return KoPathShapeId; } QString KoPathShape::toString(const QTransform &matrix) const { Q_D(const KoPathShape); QString pathString; // iterate over all subpaths KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator pointIt((*pathIt)->constBegin()); // keep a pointer to the first point of the subpath KoPathPoint *firstPoint(*pointIt); // keep a pointer to the previous point of the subpath KoPathPoint *lastPoint = firstPoint; // keep track if the previous point has an active control point 2 bool activeControlPoint2 = false; // iterate over all points of the current subpath for (; pointIt != (*pathIt)->constEnd(); ++pointIt) { KoPathPoint *currPoint(*pointIt); // first point of subpath ? if (currPoint == firstPoint) { // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? else if (activeControlPoint2 || currPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = activeControlPoint2 && currPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(lastPoint, currPoint) : KoPathSegment(lastPoint, currPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } // end point of line segment! else { const QPointF p = matrix.map(currPoint->point()); pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath && currPoint->properties() & KoPathPoint::CloseSubpath) { // add curve when there is a curve on the way to the first point if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = currPoint->activeControlPoint2() && firstPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(currPoint, firstPoint) : KoPathSegment(currPoint, firstPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } return pathString; } char nodeType(const KoPathPoint * point) { if (point->properties() & KoPathPoint::IsSmooth) { return 's'; } else if (point->properties() & KoPathPoint::IsSymmetric) { return 'z'; } else { return 'c'; } } QString KoPathShapePrivate::nodeTypes() const { Q_Q(const KoPathShape); QString types; KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (it == (*pathIt)->constBegin()) { types.append('c'); } else { types.append(nodeType(*it)); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { KoPathPoint * firstPoint = (*pathIt)->first(); types.append(nodeType(firstPoint)); } } } return types; } void updateNodeType(KoPathPoint * point, const QChar & nodeType) { if (nodeType == 's') { point->setProperty(KoPathPoint::IsSmooth); } else if (nodeType == 'z') { point->setProperty(KoPathPoint::IsSymmetric); } } void KoPathShapePrivate::loadNodeTypes(const KoXmlElement &element) { Q_Q(KoPathShape); if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it, nIt++) { // be sure not to crash if there are not enough nodes in nodeTypes if (nIt == nodeTypes.constEnd()) { warnFlake << "not enough nodes in calligra:nodeTypes"; return; } // the first node is always of type 'c' if (it != (*pathIt)->constBegin()) { updateNodeType(*it, *nIt); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { ++nIt; updateNodeType((*pathIt)->first(), *nIt); } } } } } Qt::FillRule KoPathShape::fillRule() const { Q_D(const KoPathShape); return d->fillRule; } void KoPathShape::setFillRule(Qt::FillRule fillRule) { Q_D(KoPathShape); d->fillRule = fillRule; } KoPathShape * KoPathShape::createShapeFromPainterPath(const QPainterPath &path) { KoPathShape * shape = new KoPathShape(); int elementCount = path.elementCount(); for (int i = 0; i < elementCount; i++) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: shape->moveTo(QPointF(element.x, element.y)); break; case QPainterPath::LineToElement: shape->lineTo(QPointF(element.x, element.y)); break; case QPainterPath::CurveToElement: shape->curveTo(QPointF(element.x, element.y), QPointF(path.elementAt(i + 1).x, path.elementAt(i + 1).y), QPointF(path.elementAt(i + 2).x, path.elementAt(i + 2).y)); break; default: continue; } } //shape->normalize(); return shape; } bool KoPathShape::hitTest(const QPointF &position) const { if (parent() && parent()->isClipped(this) && ! parent()->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); const QPainterPath outlinePath = outline(); if (stroke()) { KoInsets insets; stroke()->strokeInsets(this, insets); QRectF roi(QPointF(-insets.left, -insets.top), QPointF(insets.right, insets.bottom)); roi.moveCenter(point); if (outlinePath.intersects(roi) || outlinePath.contains(roi)) return true; } else { if (outlinePath.contains(point)) return true; } // if there is no shadow we can as well just leave if (! shadow()) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - shadow()->offset()); return outlinePath.contains(point); } void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { Q_D(KoPathShape); if (!marker && d->markersNew.contains(pos)) { d->markersNew.remove(pos); } else { d->markersNew[pos] = marker; } } KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { Q_D(const KoPathShape); return d->markersNew[pos].data(); } bool KoPathShape::hasMarkers() const { Q_D(const KoPathShape); return !d->markersNew.isEmpty(); } +bool KoPathShape::autoFillMarkers() const +{ + Q_D(const KoPathShape); + return d->autoFillMarkers; +} + +void KoPathShape::setAutoFillMarkers(bool value) +{ + Q_D(KoPathShape); + d->autoFillMarkers = value; +} + QPainterPath KoPathShape::pathStroke(const QPen &pen) const { Q_D(const KoPathShape); if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; QPainterPathStroker stroker; stroker.setWidth(0); stroker.setJoinStyle(Qt::MiterJoin); QPair firstSegments; QPair lastSegments; KoPathPoint *firstPoint = 0; KoPathPoint *lastPoint = 0; KoPathPoint *secondPoint = 0; KoPathPoint *preLastPoint = 0; KoSubpath *firstSubpath = d->subpaths.first(); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); stroker.setMiterLimit(pen.miterLimit()); stroker.setCapStyle(pen.capStyle()); stroker.setDashOffset(pen.dashOffset()); stroker.setDashPattern(pen.dashPattern()); // shortent the path to make it look nice // replace the point temporarily in case there is an arrow // BE AWARE: this changes the content of the path so that outline give the correct values. if (firstPoint) { firstSubpath->first() = firstSegments.second.first(); if (secondPoint) { (*firstSubpath)[1] = firstSegments.second.second(); } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = lastSegments.first.first(); } firstSubpath->last() = lastSegments.first.second(); } QPainterPath path = stroker.createStroke(outline()); if (firstPoint) { firstSubpath->first() = firstPoint; if (secondPoint) { (*firstSubpath)[1] = secondPoint; } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = preLastPoint; } firstSubpath->last() = lastPoint; } pathOutline.addPath(path); pathOutline.setFillRule(Qt::WindingFill); return pathOutline; } diff --git a/libs/flake/KoPathShape.h b/libs/flake/KoPathShape.h index a3c2b305c5..c0b5c6ace7 100644 --- a/libs/flake/KoPathShape.h +++ b/libs/flake/KoPathShape.h @@ -1,507 +1,510 @@ /* This file is part of the KDE project Copyright (C) 2006, 2011 Thorsten Zachmann Copyright (C) 2007,2009 Thomas Zander Copyright (C) 2006-2008 Jan Hambrecht Copyright (C) 2011 Jean-Nicolas Artaud This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHSHAPE_H #define KOPATHSHAPE_H #include "kritaflake_export.h" #include #include #include "KoTosContainer.h" #include "KoMarkerData.h" #define KoPathShapeId "KoPathShape" class KoPathSegment; class KoPathPoint; class KoPathShapePrivate; class KoMarker; typedef QPair KoPathPointIndex; /// a KoSubpath contains a path from a moveTo until a close or a new moveTo typedef QList KoSubpath; typedef QList KoSubpathList; /// The position of a path point within a path shape /** * @brief This is the base for all graphical objects. * * All graphical objects are based on this object e.g. lines, rectangulars, pies * and so on. * * The KoPathShape uses KoPathPoint's to describe the path of the shape. * * Here a short example: * 3 points connected by a curveTo's described by the following svg: * M 100,200 C 100,100 250,100 250,200 C 250,200 400,300 400,200. * * This will be stored in 3 KoPathPoint's as * The first point contains in * point 100,200 * controlPoint2 100,100 * The second point contains in * point 250,200 * controlPoint1 250,100 * controlPoint2 250,300 * The third point contains in * point 400,300 * controlPoint1 400,200 * * Not the segments are stored but the points. Out of the points the segments are * generated. See the outline method. The reason for storing it like that is that * it is the points that are modified by the user and not the segments. */ class KRITAFLAKE_EXPORT KoPathShape : public KoTosContainer { public: /** * @brief constructor */ KoPathShape(); /** * @brief */ virtual ~KoPathShape(); KoShape *cloneShape() const; /// reimplemented virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); virtual void paintPoints(QPainter &painter, const KoViewConverter &converter, int handleRadius); /// reimplemented QRectF outlineRect() const override; /// reimplemented QPainterPath outline() const override; /// reimplemented QRectF boundingRect() const override; /// reimplemented QSizeF size() const override; QPainterPath pathStroke(const QPen &pen) const; /** * Resize the shape * * This makes sure that the pathshape will not be resized to 0 if the new size * is null as that makes it impossible to undo the change. * * All functions that overwrite this function should also use the resizeMatrix * function to get and use the same data in resizing. * * @see resizeMatrix() */ virtual void setSize(const QSizeF &size); /// reimplemented virtual bool hitTest(const QPointF &position) const; // reimplemented virtual void saveOdf(KoShapeSavingContext &context) const; // reimplemented virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context); // basically the same as loadOdf but adapted to the contour cases // tag needs to be either contour-polygon or contour-path from another shape bool loadContourOdf(const KoXmlElement & element, KoShapeLoadingContext &context, const QSizeF &scaleFactor); /** basically the equivalent saveOdf but adapted to the contour cases * @param originalSize the original size of the unscaled image. */ void saveContourOdf(KoShapeSavingContext &context, const QSizeF &originalSize) const; /// Removes all subpaths and their points from the path void clear(); /** * @brief Starts a new Subpath * * Moves the pen to p and starts a new subpath. * * @return the newly created point */ KoPathPoint *moveTo(const QPointF &p); /** * @brief Adds a new line segment * * Adds a straight line between the last point and the given point p. * * @return the newly created point */ KoPathPoint *lineTo(const QPointF &p); /** * @brief Adds a new cubic Bezier curve segment. * * Adds a cubic Bezier curve between the last point and the given point p, * using the control points specified by c1 and c2. * * @param c1 control point1 * @param c2 control point2 * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p); /** * @brief Adds a new quadratic Bezier curve segment. * * Adds a quadratic Bezier curve between the last point and the given point p, * using the control point specified by c. * * @param c control point * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c, const QPointF &p); /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * * @return The newly created point */ KoPathPoint *arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle); /** * @brief Closes the current subpath */ void close(); /** * @brief Closes the current subpath * * It tries to merge the last and first point of the subpath * to one point and then closes the subpath. If merging is not * possible as the two point are to far from each other a close * will be done. * TODO define a maximum distance between the two points until this is working */ void closeMerge(); /** * @brief Normalizes the path data. * * The path points are transformed so that the top-left corner * of the bounding rect is at (0,0). * This should be called after adding points to the path or changing * positions of path points. * @return the offset by which the points are moved in shape coordinates. */ virtual QPointF normalize(); /** * @brief Returns the path points within the given rectangle. * @param rect the rectangle the requested points are in * @return list of points within the rectangle */ QList pointsAt(const QRectF &rect) const; /** * @brief Returns the list of path segments within the given rectangle. * @param rect the rectangle the requested segments are in * @return list of segments within the rectangle */ QList segmentsAt(const QRectF &rect) const; /** * @brief Returns the path point index of a given path point * * @param point the point for which you want to get the index * @return path point index of the point if it exists * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex pathPointIndex(const KoPathPoint *point) const; /** * @brief Returns the path point specified by a path point index * * @param pointIndex index of the point to get * * @return KoPathPoint on success, 0 otherwise e.g. out of bounds */ KoPathPoint *pointByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the segment specified by a path point index * * A semgent is defined by the point index of the first point in the segment. * A segment contains the defined point and its following point. If the subpath is * closed and the and the pointIndex point to the last point in the subpath, the * following point is the first point in the subpath. * * @param pointIndex index of the first point of the segment * * @return Segment containing both points of the segment or KoPathSegment( 0, 0 ) on error e.g. out of bounds */ KoPathSegment segmentByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the number of points in the path * * @return The number of points in the path */ int pointCount() const; /** * @brief Returns the number of subpaths in the path * * @return The number of subpaths in the path */ int subpathCount() const; /** * @brief Returns the number of points in a subpath * * @return The number of points in the subpath or -1 if subpath out of bounds */ int subpathPointCount(int subpathIndex) const; /** * @brief Checks if a subpath is closed * * @param subpathIndex index of the subpath to check * * @return true when the subpath is closed, false otherwise */ bool isClosedSubpath(int subpathIndex) const; /** * @brief Inserts a new point into the given subpath at the specified position * * This method keeps the subpath closed if it is closed, and open when it was * open. So it can change the properties of the point inserted. * You might need to update the point before/after to get the desired result * e.g. when you insert the point into a curve. * * @param point to insert * @param pointIndex index at which the point should be inserted * * @return true on success, * false when pointIndex is out of bounds */ bool insertPoint(KoPathPoint *point, const KoPathPointIndex &pointIndex); /** * @brief Removes a point from the path. * * Note that the ownership of the point will pass to the caller. * * @param pointIndex index of the point which should be removed * * @return The removed point on success, * otherwise 0 */ KoPathPoint *removePoint(const KoPathPointIndex &pointIndex); /** * @brief Breaks the path after the point index * * The new subpath will be behind the one that was broken. The segment between * the given point and the one behind will be removed. If you want to split at * one point insert first a copy of the point behind it. * This does not work when the subpath is closed. Use openSubpath for this. * It does not break at the last position of a subpath or if there is only one * point in the subpath. * * @param pointIndex index of the point after which the path should be broken * * @return true if the subpath was broken, otherwise false */ bool breakAfter(const KoPathPointIndex &pointIndex); /** * @brief Joins the given subpath with the following one * * Joins the given subpath with the following one by inserting a segment between * the two subpaths. * This does nothing if the specified subpath is the last subpath * or one of both subpaths is closed. * * @param subpathIndex index of the subpath being joined with the following subpath * * @return true if the subpath was joined, otherwise false */ bool join(int subpathIndex); /** * @brief Moves the position of a subpath within a path * * @param oldSubpathIndex old index of the subpath * @param newSubpathIndex new index of the subpath * * @return true if the subpath was moved, otherwise false e.g. if an index is out of bounds */ bool moveSubpath(int oldSubpathIndex, int newSubpathIndex); /** * @brief Opens a closed subpath * * The subpath is opened by removing the segment before the given point, making * the given point the new start point of the subpath. * * @param pointIndex the index of the point at which to open the closed subpath * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex openSubpath(const KoPathPointIndex &pointIndex); /** * @brief Close a open subpath * * The subpath is closed be inserting a segment between the start and end point, making * the given point the new start point of the subpath. * * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex closeSubpath(const KoPathPointIndex &pointIndex); /** * @brief Reverse subpath * * The last point becomes the first point and the first one becomes the last one. * * @param subpathIndex the index of the subpath to reverse */ bool reverseSubpath(int subpathIndex); /** * @brief Removes subpath from the path * @param subpathIndex the index of the subpath to remove * @return the removed subpath on succes, 0 otherwise. */ KoSubpath *removeSubpath(int subpathIndex); /** * @brief Adds a subpath at the given index to the path * @param subpath the subpath to add * @param subpathIndex the index at which the new subpath should be inserted * @return true on success, false otherwise e.g. subpathIndex out of bounds */ bool addSubpath(KoSubpath *subpath, int subpathIndex); /** * @brief Combines two path shapes by appending the data of the specified path. * @param path the path to combine with * @return true if combining was successful, else false */ bool combine(KoPathShape *path); /** * @brief Creates separate path shapes, one for each existing subpath. * @param separatedPaths the list which contains the separated path shapes * @return true if separating the path was successful, false otherwise */ bool separate(QList &separatedPaths); /** * Returns the specific path shape id. * * Path shape derived shapes have a different shape id which link them * to their respective shape factories. In most cases they do not have * a special tool for editing them. * This function returns the specific shape id for finding the shape * factory from KoShapeRegistry. The default KoPathShapeId is returned * from KoShape::shapeId() so that the generic path editing tool gets * activated when the shape is selected. * * @return the specific shape id */ virtual QString pathShapeId() const; /// Returns a odf/svg string represenatation of the path data with the given matrix applied. QString toString(const QTransform &matrix = QTransform()) const; /// Returns the fill rule for the path object Qt::FillRule fillRule() const; /// Sets the fill rule to be used for painting the background void setFillRule(Qt::FillRule fillRule); /// Creates path shape from given QPainterPath static KoPathShape *createShapeFromPainterPath(const QPainterPath &path); /// Returns the viewbox from the given xml element. static QRect loadOdfViewbox(const KoXmlElement &element); void setMarker(KoMarker *marker, KoFlake::MarkerPosition pos); KoMarker* marker(KoFlake::MarkerPosition pos) const; bool hasMarkers() const; + bool autoFillMarkers() const; + void setAutoFillMarkers(bool value); + private: /// constructor: to be used in cloneShape(), not in descendants! /// \internal KoPathShape(const KoPathShape &rhs); protected: /// constructor:to be used in descendant shapes /// \internal KoPathShape(KoPathShapePrivate *); /// reimplemented virtual QString saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const; /// reimplemented virtual void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context); /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * @param offset to the first point in the arc * @param curvePoints a array which take the cuve points, pass a 'QPointF curvePoins[12]'; * * @return number of points created by the curve */ int arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF &offset, QPointF *curvePoints) const; /** * Get the resize matrix * * This makes sure that also if the newSize isNull that there will be a * very small size of 0.000001 pixels */ QTransform resizeMatrix( const QSizeF &newSize ) const; private: Q_DECLARE_PRIVATE(KoPathShape) }; Q_DECLARE_METATYPE(KoPathShape*) #endif /* KOPATHSHAPE_H */ diff --git a/libs/flake/KoPathShape_p.h b/libs/flake/KoPathShape_p.h index 065d57680e..f34194a89d 100644 --- a/libs/flake/KoPathShape_p.h +++ b/libs/flake/KoPathShape_p.h @@ -1,103 +1,101 @@ /* This file is part of the KDE project * Copyright (C) 2009 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHSHAPEPRIVATE_H #define KOPATHSHAPEPRIVATE_H #include "KoPathShape.h" #include "KoTosContainer_p.h" #include "KoMarker.h" #include "KoMarkerData.h" class KoPathShapePrivate : public KoTosContainerPrivate { public: explicit KoPathShapePrivate(KoPathShape *q); explicit KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q); QRectF handleRect(const QPointF &p, qreal radius) const; /// Applies the viewbox transformation defined in the given element void applyViewboxTransformation(const KoXmlElement &element); void map(const QTransform &matrix); void updateLast(KoPathPoint **lastPoint); /// closes specified subpath void closeSubpath(KoSubpath *subpath); /// close-merges specified subpath void closeMergeSubpath(KoSubpath *subpath); /** * @brief Saves the node types * * This is inspired by inkscape and uses the same mechanism as they do. * The only difference is that they use sodipodi:nodeTypes as element and * we use calligra:nodeTyes as attribute. * This attribute contains of a string which has the node type of each point * in it. The following node types exist: * * c corner * s smooth * z symetric * * The first point of a path is always of the type c. * If the path is closed the type of the first point is saved in the last elemeent * E.g. you have a closed path with 2 points in it. The first one (start/end of path) * is symetric and the second one is smooth that will result in the nodeType="czs" * So if there is a closed sub path the nodeTypes contain one more entry then there * are points. That is due to the first and the last pojnt of a closed sub path get * merged into one when they are on the same position. * * @return The node types as string */ QString nodeTypes() const; /** * @brief Loads node types */ void loadNodeTypes(const KoXmlElement &element); /** * @brief Returns subpath at given index * @param subpathIndex the index of the subpath to return * @return subPath on success, or 0 when subpathIndex is out of bounds */ KoSubpath *subPath(int subpathIndex) const; #ifndef NDEBUG /// \internal void paintDebug(QPainter &painter); /** * @brief print debug information about a the points of the path */ void debugPath() const; #endif Qt::FillRule fillRule; Q_DECLARE_PUBLIC(KoPathShape) - KoMarkerData startMarker; - KoMarkerData endMarker; - KoSubpathList subpaths; QMap> markersNew; + bool autoFillMarkers; }; #endif diff --git a/libs/flake/KoShapeStroke.cpp b/libs/flake/KoShapeStroke.cpp index 233539e5db..da6951c629 100644 --- a/libs/flake/KoShapeStroke.cpp +++ b/libs/flake/KoShapeStroke.cpp @@ -1,392 +1,405 @@ /* This file is part of the KDE project * * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2006-2008 Jan Hambrecht * Copyright (C) 2007,2009 Thorsten Zachmann * Copyright (C) 2012 Inge Wallin * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // Own #include "KoShapeStroke.h" // Posix #include // Qt #include #include // Calligra #include #include // Flake #include "KoViewConverter.h" #include "KoShape.h" #include "KoShapeSavingContext.h" #include "KoPathShape.h" #include "KoMarkerData.h" #include "KoMarker.h" #include "KoInsets.h" #include #include #include #include "kis_global.h" class Q_DECL_HIDDEN KoShapeStroke::Private { public: + Private(KoShapeStroke *_q) : q(_q) {} + KoShapeStroke *q; + void paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const; QColor color; QPen pen; QBrush brush; }; namespace { QPair anglesForSegment(KoPathSegment segment) { const qreal eps = 1e-6; if (segment.degree() < 3) { segment = segment.toCubic(); } QList points = segment.controlPoints(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(points.size() == 4, qMakePair(0.0, 0.0)); QPointF vec1 = points[1] - points[0]; QPointF vec2 = points[3] - points[2]; if (vec1.manhattanLength() < eps) { points[1] = segment.pointAt(eps); vec1 = points[1] - points[0]; } if (vec2.manhattanLength() < eps) { points[2] = segment.pointAt(1.0 - eps); vec2 = points[3] - points[2]; } const qreal angle1 = std::atan2(vec1.y(), vec1.x()); const qreal angle2 = std::atan2(vec2.y(), vec2.x()); return qMakePair(angle1, angle2); } } void KoShapeStroke::Private::paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const { if (!pen.isCosmetic() && pen.style() != Qt::NoPen) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape) { QPainterPath path = pathShape->pathStroke(pen); painter.fillPath(path, pen.brush()); if (!pathShape->hasMarkers()) return; + const bool autoFillMarkers = pathShape->autoFillMarkers(); KoMarker *startMarker = pathShape->marker(KoFlake::StartMarker); KoMarker *midMarker = pathShape->marker(KoFlake::MidMarker); KoMarker *endMarker = pathShape->marker(KoFlake::EndMarker); for (int i = 0; i < pathShape->subpathCount(); i++) { const int numSubPoints = pathShape->subpathPointCount(i); if (numSubPoints < 2) continue; const bool isClosedSubpath = pathShape->isClosedSubpath(i); qreal firstAngle = 0.0; { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, 0)); firstAngle= anglesForSegment(segment).first; } qreal lastAngle = 0.0; { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, numSubPoints - 2)); lastAngle = anglesForSegment(segment).second; } qreal previousAngle = 0.0; for (int j = 0; j < numSubPoints - 1; j++) { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, j)); QPair angles = anglesForSegment(segment); const qreal angle1 = angles.first; const qreal angle2 = angles.second; if (j == 0 && startMarker) { const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : firstAngle; + if (autoFillMarkers) { + startMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); + } startMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); } if (j > 0 && midMarker) { const qreal angle = bisectorAngle(previousAngle, angle1); + if (autoFillMarkers) { + midMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); + } midMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); } if (j == numSubPoints - 2 && endMarker) { const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : lastAngle; + if (autoFillMarkers) { + endMarker->applyShapeStroke(shape, q, segment.second()->point(), pen.widthF(), angle); + } endMarker->paintAtPosition(&painter, segment.second()->point(), pen.widthF(), angle); } previousAngle = angle2; } } return; } painter.strokePath(shape->outline(), pen); } } KoShapeStroke::KoShapeStroke() - : d(new Private()) + : d(new Private(this)) { d->color = QColor(Qt::black); // we are not rendering stroke with zero width anymore // so lets use a default width of 1.0 d->pen.setWidthF(1.0); } KoShapeStroke::KoShapeStroke(const KoShapeStroke &other) - : KoShapeStrokeModel(), d(new Private()) + : KoShapeStrokeModel(), d(new Private(this)) { d->color = other.d->color; d->pen = other.d->pen; d->brush = other.d->brush; } KoShapeStroke::KoShapeStroke(qreal lineWidth, const QColor &color) - : d(new Private()) + : d(new Private(this)) { d->pen.setWidthF(qMax(qreal(0.0), lineWidth)); d->pen.setJoinStyle(Qt::MiterJoin); d->color = color; } KoShapeStroke::~KoShapeStroke() { delete d; } KoShapeStroke &KoShapeStroke::operator = (const KoShapeStroke &rhs) { if (this == &rhs) return *this; d->pen = rhs.d->pen; d->color = rhs.d->color; d->brush = rhs.d->brush; return *this; } void KoShapeStroke::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) const { QPen pen = d->pen; if (d->brush.gradient()) pen.setBrush(d->brush); else pen.setColor(d->color); KoOdfGraphicStyles::saveOdfStrokeStyle(style, context.mainStyles(), pen); } void KoShapeStroke::strokeInsets(const KoShape *shape, KoInsets &insets) const { Q_UNUSED(shape); qreal lineWidth = d->pen.widthF(); if (lineWidth < 0) { lineWidth = 1; } lineWidth *= 0.5; // since we draw a line half inside, and half outside the object. // if we have square cap, we need a little more space // -> sqrt((0.5*penWidth)^2 + (0.5*penWidth)^2) if (capStyle() == Qt::SquareCap) { lineWidth *= M_SQRT2; } if (joinStyle() == Qt::MiterJoin) { lineWidth = qMax(lineWidth, miterLimit()); } insets.top = lineWidth; insets.bottom = lineWidth; insets.left = lineWidth; insets.right = lineWidth; } qreal KoShapeStroke::strokeMaxMarkersInset(const KoShape *shape) const { qreal result = 0.0; const KoPathShape *pathShape = dynamic_cast(shape); if (pathShape && pathShape->hasMarkers()) { const qreal lineWidth = d->pen.widthF(); QVector markers; markers << pathShape->marker(KoFlake::StartMarker); markers << pathShape->marker(KoFlake::MidMarker); markers << pathShape->marker(KoFlake::EndMarker); Q_FOREACH (const KoMarker *marker, markers) { if (marker) { result = qMax(result, marker->maxInset(lineWidth)); } } } return result; } bool KoShapeStroke::hasTransparency() const { return d->color.alpha() > 0; } void KoShapeStroke::paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter) { KoShape::applyConversion(painter, converter); QPen pen = d->pen; if (d->brush.gradient()) pen.setBrush(d->brush); else pen.setColor(d->color); d->paintBorder(shape, painter, pen); } bool KoShapeStroke::compareFillTo(const KoShapeStrokeModel *other) { if (!other) return false; const KoShapeStroke *stroke = dynamic_cast(other); if (!stroke) return false; return (d->brush.gradient() && d->brush == stroke->d->brush) || (!d->brush.gradient() && d->color == stroke->d->color); } bool KoShapeStroke::compareStyleTo(const KoShapeStrokeModel *other) { if (!other) return false; const KoShapeStroke *stroke = dynamic_cast(other); if (!stroke) return false; QPen pen1 = d->pen; QPen pen2 = stroke->d->pen; // just a random color top avoid comparison of that property pen1.setColor(Qt::magenta); pen2.setColor(Qt::magenta); return pen1 == pen2; } void KoShapeStroke::setCapStyle(Qt::PenCapStyle style) { d->pen.setCapStyle(style); } Qt::PenCapStyle KoShapeStroke::capStyle() const { return d->pen.capStyle(); } void KoShapeStroke::setJoinStyle(Qt::PenJoinStyle style) { d->pen.setJoinStyle(style); } Qt::PenJoinStyle KoShapeStroke::joinStyle() const { return d->pen.joinStyle(); } void KoShapeStroke::setLineWidth(qreal lineWidth) { d->pen.setWidthF(qMax(qreal(0.0), lineWidth)); } qreal KoShapeStroke::lineWidth() const { return d->pen.widthF(); } void KoShapeStroke::setMiterLimit(qreal miterLimit) { d->pen.setMiterLimit(miterLimit); } qreal KoShapeStroke::miterLimit() const { return d->pen.miterLimit(); } QColor KoShapeStroke::color() const { return d->color; } void KoShapeStroke::setColor(const QColor &color) { d->color = color; } void KoShapeStroke::setLineStyle(Qt::PenStyle style, const QVector &dashes) { if (style < Qt::CustomDashLine) { d->pen.setStyle(style); } else { d->pen.setDashPattern(dashes); } } Qt::PenStyle KoShapeStroke::lineStyle() const { return d->pen.style(); } QVector KoShapeStroke::lineDashes() const { return d->pen.dashPattern(); } void KoShapeStroke::setDashOffset(qreal dashOffset) { d->pen.setDashOffset(dashOffset); } qreal KoShapeStroke::dashOffset() const { return d->pen.dashOffset(); } void KoShapeStroke::setLineBrush(const QBrush &brush) { d->brush = brush; } QBrush KoShapeStroke::lineBrush() const { return d->brush; } diff --git a/libs/flake/commands/KoPathShapeMarkerCommand.cpp b/libs/flake/commands/KoPathShapeMarkerCommand.cpp index f1845a6d17..770ce629e2 100644 --- a/libs/flake/commands/KoPathShapeMarkerCommand.cpp +++ b/libs/flake/commands/KoPathShapeMarkerCommand.cpp @@ -1,97 +1,105 @@ /* This file is part of the KDE project * Copyright (C) 2010 Jeremy Lugagne * Copyright (C) 2011 Jean-Nicolas Artaud * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoPathShapeMarkerCommand.h" #include "KoMarker.h" #include "KoPathShape.h" -#include " +#include #include "kis_command_ids.h" #include class Q_DECL_HIDDEN KoPathShapeMarkerCommand::Private { public: QList shapes; ///< the shapes to set marker for QList> oldMarkers; ///< the old markers, one for each shape QExplicitlySharedDataPointer marker; ///< the new marker to set KoFlake::MarkerPosition position; + QList oldAutoFillMarkers; }; KoPathShapeMarkerCommand::KoPathShapeMarkerCommand(const QList &shapes, KoMarker *marker, KoFlake::MarkerPosition position, KUndo2Command *parent) : KUndo2Command(kundo2_i18n("Set marker"), parent), m_d(new Private) { m_d->shapes = shapes; m_d->marker = marker; m_d->position = position; // save old markers Q_FOREACH (KoPathShape *shape, m_d->shapes) { m_d->oldMarkers.append(QExplicitlySharedDataPointer(shape->marker(position))); + m_d->oldAutoFillMarkers.append(shape->autoFillMarkers()); } } KoPathShapeMarkerCommand::~KoPathShapeMarkerCommand() { } void KoPathShapeMarkerCommand::redo() { KUndo2Command::redo(); Q_FOREACH (KoPathShape *shape, m_d->shapes) { shape->update(); shape->setMarker(m_d->marker.data(), m_d->position); + + // we have no GUI for selection auto-filling yet! So just enable it! + shape->setAutoFillMarkers(true); + shape->update(); } } void KoPathShapeMarkerCommand::undo() { KUndo2Command::undo(); auto markerIt = m_d->oldMarkers.begin(); + auto autoFillIt = m_d->oldAutoFillMarkers.begin(); Q_FOREACH (KoPathShape *shape, m_d->shapes) { shape->update(); shape->setMarker((*markerIt).data(), m_d->position); + shape->setAutoFillMarkers(*autoFillIt); shape->update(); ++markerIt; } } int KoPathShapeMarkerCommand::id() const { return KisCommandUtils::ChangeShapeMarkersId; } bool KoPathShapeMarkerCommand::mergeWith(const KUndo2Command *command) { const KoPathShapeMarkerCommand *other = dynamic_cast(command); if (!other || other->m_d->shapes != m_d->shapes || other->m_d->position != m_d->position) { return false; } m_d->marker = other->m_d->marker; return true; } diff --git a/libs/flake/svg/SvgGraphicContext.cpp b/libs/flake/svg/SvgGraphicContext.cpp index 24a7af3c8b..0182ac878a 100644 --- a/libs/flake/svg/SvgGraphicContext.cpp +++ b/libs/flake/svg/SvgGraphicContext.cpp @@ -1,54 +1,56 @@ /* This file is part of the KDE project * Copyright (C) 2003,2005 Rob Buis * Copyright (C) 2007,2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgGraphicContext.h" #include "kis_pointer_utils.h" SvgGraphicsContext::SvgGraphicsContext() { strokeType = None; stroke = toQShared(new KoShapeStroke()); stroke->setLineStyle(Qt::NoPen, QVector()); // default is no stroke stroke->setLineWidth(1.0); stroke->setCapStyle(Qt::FlatCap); stroke->setJoinStyle(Qt::MiterJoin); fillType = Solid; fillRule = Qt::WindingFill; fillColor = QColor(Qt::black); // default is black fill as per svg spec opacity = 1.0; currentColor = Qt::black; forcePercentage = false; display = true; visible = true; clipRule = Qt::WindingFill; preserveWhitespace = false; letterSpacing = 0.0; wordSpacing = 0.0; pixelsPerInch = 72.0; + + autoFillMarkers = false; } diff --git a/libs/flake/svg/SvgGraphicContext.h b/libs/flake/svg/SvgGraphicContext.h index 012e6848b9..2e943db9c6 100644 --- a/libs/flake/svg/SvgGraphicContext.h +++ b/libs/flake/svg/SvgGraphicContext.h @@ -1,79 +1,81 @@ /* This file is part of the KDE project * Copyright (C) 2003,2005 Rob Buis * Copyright (C) 2007,2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGGRAPHICCONTEXT_H #define SVGGRAPHICCONTEXT_H #include "kritaflake_export.h" #include #include #include class KRITAFLAKE_EXPORT SvgGraphicsContext { public: // Fill/stroke styles enum StyleType { None, ///< no style Solid, ///< solid style Complex ///< gradient or pattern style }; SvgGraphicsContext(); StyleType fillType; ///< the current fill type Qt::FillRule fillRule; ///< the current fill rule QColor fillColor; ///< the current fill color QString fillId; ///< the current fill id (used for gradient/pattern fills) StyleType strokeType;///< the current stroke type QString strokeId; ///< the current stroke id (used for gradient strokes) KoShapeStrokeSP stroke; ///< the current stroke QString filterId; ///< the current filter id QString clipPathId; ///< the current clip path id QString clipMaskId; ///< the current clip mask id Qt::FillRule clipRule; ///< the current clip rule qreal opacity; ///< the shapes opacity QTransform matrix; ///< the current transformation matrix QFont font; ///< the current font QColor currentColor; ///< the current color QString xmlBaseDir; ///< the current base directory (used for loading external content) bool preserveWhitespace;///< preserve whitespace in element text QRectF currentBoundingBox; ///< the current bound box used for bounding box units bool forcePercentage; ///< force parsing coordinates/length as percentages of currentBoundbox QTransform viewboxTransform; ///< view box transformation qreal letterSpacing; ///< additional spacing between characters of text elements qreal wordSpacing; ///< additional spacing between words of text elements QString baselineShift; ///< basline shift mode for text elements bool display; ///< controls display of shape bool visible; ///< controls visibility of the shape (inherited) qreal pixelsPerInch; ///< controls the resolution of the image raster QString markerStartId; QString markerMidId; QString markerEndId; + + bool autoFillMarkers; }; #endif // SVGGRAPHICCONTEXT_H diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index 695b61f3c2..65edc1438d 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1533 +1,1535 @@ /* This file is part of the KDE project * Copyright (C) 2002-2005,2007 Rob Buis * Copyright (C) 2002-2004 Nicolas Goutte * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2009 Jan Hambrecht * Copyright (C) 2005,2007 Thomas Zander * Copyright (C) 2006-2007 Inge Wallin * Copyright (C) 2007-2008,2010 Thorsten Zachmann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgParser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoFilterEffectStack.h" #include "KoFilterEffectLoadingContext.h" #include #include #include #include "SvgUtil.h" #include "SvgShape.h" #include "SvgGraphicContext.h" #include "SvgFilterHelper.h" #include "SvgGradientHelper.h" #include "SvgClipPathHelper.h" #include "parsers/SvgTransformParser.h" #include "kis_pointer_utils.h" #include #include #include "kis_debug.h" #include "kis_global.h" SvgParser::SvgParser(KoDocumentResourceManager *documentResourceManager) : m_context(documentResourceManager) , m_documentResourceManager(documentResourceManager) { } SvgParser::~SvgParser() { } void SvgParser::setXmlBaseDir(const QString &baseDir) { m_context.setInitialXmlBaseDir(baseDir); setFileFetcher( [this](const QString &name) { const QString fileName = m_context.xmlBaseDir() + QDir::separator() + name; QFile file(fileName); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(file.exists(), QByteArray()); file.open(QIODevice::ReadOnly); return file.readAll(); }); } void SvgParser::setResolution(const QRectF boundsInPixels, qreal pixelsPerInch) { KIS_ASSERT(!m_context.currentGC()); m_context.pushGraphicsContext(); m_context.currentGC()->pixelsPerInch = pixelsPerInch; const qreal scale = 72.0 / pixelsPerInch; const QTransform t = QTransform::fromScale(scale, scale); m_context.currentGC()->currentBoundingBox = boundsInPixels; m_context.currentGC()->matrix = t; } QList SvgParser::shapes() const { return m_shapes; } // Helper functions // --------------------------------------------------------------------------------------- SvgGradientHelper* SvgParser::findGradient(const QString &id) { SvgGradientHelper *result = 0; // check if gradient was already parsed, and return it if (m_gradients.contains(id)) { result = &m_gradients[ id ]; } // check if gradient was stored for later parsing if (!result && m_context.hasDefinition(id)) { const KoXmlElement &e = m_context.definition(id); if (e.tagName().contains("Gradient")) { result = parseGradient(m_context.definition(id)); } } return result; } QSharedPointer SvgParser::findPattern(const QString &id, const KoShape *shape) { QSharedPointer result; // check if gradient was stored for later parsing if (m_context.hasDefinition(id)) { const KoXmlElement &e = m_context.definition(id); if (e.tagName() == "pattern") { result = parsePattern(m_context.definition(id), shape); } } return result; } SvgFilterHelper* SvgParser::findFilter(const QString &id, const QString &href) { // check if filter was already parsed, and return it if (m_filters.contains(id)) return &m_filters[ id ]; // check if filter was stored for later parsing if (!m_context.hasDefinition(id)) return 0; const KoXmlElement &e = m_context.definition(id); if (e.childNodesCount() == 0) { QString mhref = e.attribute("xlink:href").mid(1); if (m_context.hasDefinition(mhref)) return findFilter(mhref, id); else return 0; } else { // ok parse filter now if (! parseFilter(m_context.definition(id), m_context.definition(href))) return 0; } // return successfully parsed filter or 0 QString n; if (href.isEmpty()) n = id; else n = href; if (m_filters.contains(n)) return &m_filters[ n ]; else return 0; } SvgClipPathHelper* SvgParser::findClipPath(const QString &id) { return m_clipPaths.contains(id) ? &m_clipPaths[id] : 0; } // Parsing functions // --------------------------------------------------------------------------------------- qreal SvgParser::parseUnit(const QString &unit, bool horiz, bool vert, const QRectF &bbox) { return SvgUtil::parseUnit(m_context.currentGC(), unit, horiz, vert, bbox); } qreal SvgParser::parseUnitX(const QString &unit) { return SvgUtil::parseUnitX(m_context.currentGC(), unit); } qreal SvgParser::parseUnitY(const QString &unit) { return SvgUtil::parseUnitY(m_context.currentGC(), unit); } qreal SvgParser::parseUnitXY(const QString &unit) { return SvgUtil::parseUnitXY(m_context.currentGC(), unit); } SvgGradientHelper* SvgParser::parseGradient(const KoXmlElement &e) { // IMPROVEMENTS: // - Store the parsed colorstops in some sort of a cache so they don't need to be parsed again. // - A gradient inherits attributes it does not have from the referencing gradient. // - Gradients with no color stops have no fill or stroke. // - Gradients with one color stop have a solid color. SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return 0; SvgGradientHelper gradHelper; QString gradientId = e.attribute("id"); if (gradientId.isEmpty()) return 0; // check if we have this gradient already parsed // copy existing gradient if it exists if (m_gradients.contains(gradientId)) { return &m_gradients[gradientId]; } if (e.hasAttribute("xlink:href")) { // strip the '#' symbol QString href = e.attribute("xlink:href").mid(1); if (!href.isEmpty()) { // copy the referenced gradient if found SvgGradientHelper *pGrad = findGradient(href); if (pGrad) { gradHelper = *pGrad; } } } const QGradientStops defaultStops = gradHelper.gradient()->stops(); if (e.attribute("gradientUnits") == "userSpaceOnUse") { gradHelper.setGradientUnits(KoFlake::UserSpaceOnUse); } m_context.pushGraphicsContext(e); uploadStyleToContext(e); if (e.tagName() == "linearGradient") { QLinearGradient *g = new QLinearGradient(); if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setStart(QPointF(SvgUtil::fromPercentage(e.attribute("x1", "0%")), SvgUtil::fromPercentage(e.attribute("y1", "0%")))); g->setFinalStop(QPointF(SvgUtil::fromPercentage(e.attribute("x2", "100%")), SvgUtil::fromPercentage(e.attribute("y2", "0%")))); } else { g->setStart(QPointF(parseUnitX(e.attribute("x1")), parseUnitY(e.attribute("y1")))); g->setFinalStop(QPointF(parseUnitX(e.attribute("x2")), parseUnitY(e.attribute("y2")))); } gradHelper.setGradient(g); } else if (e.tagName() == "radialGradient") { QRadialGradient *g = new QRadialGradient(); if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setCenter(QPointF(SvgUtil::fromPercentage(e.attribute("cx", "50%")), SvgUtil::fromPercentage(e.attribute("cy", "50%")))); g->setRadius(SvgUtil::fromPercentage(e.attribute("r", "50%"))); g->setFocalPoint(QPointF(SvgUtil::fromPercentage(e.attribute("fx", "50%")), SvgUtil::fromPercentage(e.attribute("fy", "50%")))); } else { g->setCenter(QPointF(parseUnitX(e.attribute("cx")), parseUnitY(e.attribute("cy")))); g->setFocalPoint(QPointF(parseUnitX(e.attribute("fx")), parseUnitY(e.attribute("fy")))); g->setRadius(parseUnitXY(e.attribute("r"))); } gradHelper.setGradient(g); } else { qDebug() << "WARNING: Failed to parse gradient with tag" << e.tagName(); } // handle spread method QGradient::Spread spreadMethod = QGradient::PadSpread; QString spreadMethodStr = e.attribute("spreadMethod"); if (!spreadMethodStr.isEmpty()) { if (spreadMethodStr == "reflect") { spreadMethod = QGradient::ReflectSpread; } else if (spreadMethodStr == "repeat") { spreadMethod = QGradient::RepeatSpread; } } gradHelper.setSpreadMode(spreadMethod); // Parse the color stops. m_context.styleParser().parseColorStops(gradHelper.gradient(), e, gc, defaultStops); if (e.hasAttribute("gradientTransform")) { SvgTransformParser p(e.attribute("gradientTransform")); if (p.isValid()) { gradHelper.setTransform(p.transform()); } } m_context.popGraphicsContext(); m_gradients.insert(gradientId, gradHelper); return &m_gradients[gradientId]; } inline QPointF bakeShapeOffset(const QTransform &patternTransform, const QPointF &shapeOffset) { QTransform result = patternTransform * QTransform::fromTranslate(-shapeOffset.x(), -shapeOffset.y()) * patternTransform.inverted(); KIS_ASSERT_RECOVER_NOOP(result.type() <= QTransform::TxTranslate); return QPointF(result.dx(), result.dy()); } QSharedPointer SvgParser::parsePattern(const KoXmlElement &e, const KoShape *shape) { /** * Unlike the gradient parsing function, this method is called every time we * *reference* the pattern, not when we define it. Therefore we can already * use the coordinate system of the destination. */ QSharedPointer pattHelper; SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return pattHelper; const QString patternId = e.attribute("id"); if (patternId.isEmpty()) return pattHelper; pattHelper = toQShared(new KoVectorPatternBackground); if (e.hasAttribute("xlink:href")) { // strip the '#' symbol QString href = e.attribute("xlink:href").mid(1); if (!href.isEmpty() &&href != patternId) { // copy the referenced pattern if found QSharedPointer pPatt = findPattern(href, shape); if (pPatt) { pattHelper = pPatt; } } } pattHelper->setReferenceCoordinates( KoFlake::coordinatesFromString(e.attribute("patternUnits"), pattHelper->referenceCoordinates())); pattHelper->setContentCoordinates( KoFlake::coordinatesFromString(e.attribute("patternContentUnits"), pattHelper->contentCoordinates())); if (e.hasAttribute("patternTransform")) { SvgTransformParser p(e.attribute("patternTransform")); if (p.isValid()) { pattHelper->setPatternTransform(p.transform()); } } if (pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox) { QRectF referenceRect( SvgUtil::fromPercentage(e.attribute("x", "0%")), SvgUtil::fromPercentage(e.attribute("y", "0%")), SvgUtil::fromPercentage(e.attribute("width", "0%")), // 0% is according to SVG 1.1, don't ask me why! SvgUtil::fromPercentage(e.attribute("height", "0%"))); // 0% is according to SVG 1.1, don't ask me why! pattHelper->setReferenceRect(referenceRect); } else { QRectF referenceRect( parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0")), parseUnitX(e.attribute("width", "0")), // 0 is according to SVG 1.1, don't ask me why! parseUnitY(e.attribute("height", "0"))); // 0 is according to SVG 1.1, don't ask me why! pattHelper->setReferenceRect(referenceRect); } m_context.pushGraphicsContext(e); gc = m_context.currentGC(); /** * In Krita shapes X,Y coordinates are baked into the the shape global transform, but * the pattern should be painted in "user" coordinates. Therefore, we should handle * this offfset separately. * * TODO: Please also not that this offset is different from extraShapeOffset(), * because A.inverted() * B != A * B.inverted(). I'm not sure which variant is * correct (DK) */ const QTransform dstShapeTransform = shape->absoluteTransformation(0); const QTransform shapeOffsetTransform = dstShapeTransform * gc->matrix.inverted(); KIS_SAFE_ASSERT_RECOVER_NOOP(shapeOffsetTransform.type() <= QTransform::TxTranslate); const QPointF extraShapeOffset(shapeOffsetTransform.dx(), shapeOffsetTransform.dy()); // start building shape tree from scratch gc->matrix = QTransform(); const QRectF boundingRect = shape->outline().boundingRect()/*.translated(extraShapeOffset)*/; const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); // WARNING1: OBB and ViewBox transformations are *baked* into the pattern shapes! // although we expect the pattern be reusable, but it is not so! // WARNING2: the pattern shapes are stored in *User* coordinate system, although // the "official" content system might be either OBB or User. It means that // this baked transform should be stripped before writing the shapes back // into SVG if (e.hasAttribute("viewBox")) { gc->currentBoundingBox = pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox ? relativeToShape.mapRect(pattHelper->referenceRect()) : pattHelper->referenceRect(); applyViewBoxTransform(e); pattHelper->setContentCoordinates(pattHelper->referenceCoordinates()); } else if (pattHelper->contentCoordinates() == KoFlake::ObjectBoundingBox) { gc->matrix = relativeToShape * gc->matrix; } // We do *not* apply patternTransform here! Here we only bake the untransformed // version of the shape. The transformed one will be done in the very end while rendering. QList patternShapes = parseContainer(e); if (pattHelper->contentCoordinates() == KoFlake::UserSpaceOnUse) { // In Krita we normalize the shapes, bake this transform into the pattern shapes const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); Q_FOREACH (KoShape *shape, patternShapes) { shape->applyAbsoluteTransformation(QTransform::fromTranslate(offset.x(), offset.y())); } } if (pattHelper->referenceCoordinates() == KoFlake::UserSpaceOnUse) { // In Krita we normalize the shapes, bake this transform into reference rect // NOTE: this is possible *only* when pattern transform is not perspective // (which is always true for SVG) const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); QRectF ref = pattHelper->referenceRect(); ref.translate(offset); pattHelper->setReferenceRect(ref); } m_context.popGraphicsContext(); gc = m_context.currentGC(); if (!patternShapes.isEmpty()) { pattHelper->setShapes(patternShapes); } return pattHelper; } bool SvgParser::parseFilter(const KoXmlElement &e, const KoXmlElement &referencedBy) { SvgFilterHelper filter; // Use the filter that is referencing, or if there isn't one, the original filter KoXmlElement b; if (!referencedBy.isNull()) b = referencedBy; else b = e; // check if we are referencing another filter if (e.hasAttribute("xlink:href")) { QString href = e.attribute("xlink:href").mid(1); if (! href.isEmpty()) { // copy the referenced filter if found SvgFilterHelper *refFilter = findFilter(href); if (refFilter) filter = *refFilter; } } else { filter.setContent(b); } if (b.attribute("filterUnits") == "userSpaceOnUse") filter.setFilterUnits(KoFlake::UserSpaceOnUse); if (b.attribute("primitiveUnits") == "objectBoundingBox") filter.setPrimitiveUnits(KoFlake::ObjectBoundingBox); // parse filter region rectangle if (filter.filterUnits() == KoFlake::UserSpaceOnUse) { filter.setPosition(QPointF(parseUnitX(b.attribute("x")), parseUnitY(b.attribute("y")))); filter.setSize(QSizeF(parseUnitX(b.attribute("width")), parseUnitY(b.attribute("height")))); } else { // x, y, width, height are in percentages of the object referencing the filter // so we just parse the percentages filter.setPosition(QPointF(SvgUtil::fromPercentage(b.attribute("x", "-0.1")), SvgUtil::fromPercentage(b.attribute("y", "-0.1")))); filter.setSize(QSizeF(SvgUtil::fromPercentage(b.attribute("width", "1.2")), SvgUtil::fromPercentage(b.attribute("height", "1.2")))); } m_filters.insert(b.attribute("id"), filter); return true; } bool SvgParser::parseMarker(const KoXmlElement &e) { const QString id = e.attribute("id"); if (id.isEmpty()) return false; QScopedPointer marker(new KoMarker()); marker->setCoordinateSystem( KoMarker::coordinateSystemFromString(e.attribute("markerUnits", "strokeWidth"))); marker->setReferencePoint(QPointF(parseUnitX(e.attribute("refX")), parseUnitY(e.attribute("refY")))); marker->setReferenceSize(QSizeF(parseUnitX(e.attribute("markerWidth", "3")), parseUnitY(e.attribute("markerHeight", "3")))); const QString orientation = e.attribute("orient", "0"); if (orientation == "auto") { marker->setAutoOrientation(true); } else { // TODO: special angular values! marker->setExplicitOrientation(kisDegreesToRadians(parseUnitXY(orientation))); } // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->currentBoundingBox = QRectF(QPointF(0, 0), marker->referenceSize()); KoShape *markerShape = parseGroup(e); m_context.popGraphicsContext(); if (!markerShape) return false; marker->setShapes({markerShape}); m_markers.insert(id, QExplicitlySharedDataPointer(marker.take())); return true; } bool SvgParser::parseClipPath(const KoXmlElement &e) { SvgClipPathHelper clipPath; const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipPath.setClipPathUnits( KoFlake::coordinatesFromString(e.attribute("clipPathUnits"), KoFlake::UserSpaceOnUse)); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipPath.setShapes({clipShape}); m_clipPaths.insert(id, clipPath); return true; } bool SvgParser::parseClipMask(const KoXmlElement &e) { QSharedPointer clipMask(new KoClipMask); const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipMask->setCoordinates(KoFlake::coordinatesFromString(e.attribute("maskUnits"), KoFlake::ObjectBoundingBox)); clipMask->setContentCoordinates(KoFlake::coordinatesFromString(e.attribute("maskContentUnits"), KoFlake::UserSpaceOnUse)); QRectF maskRect; if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { maskRect.setRect( SvgUtil::fromPercentage(e.attribute("x", "-10%")), SvgUtil::fromPercentage(e.attribute("y", "-10%")), SvgUtil::fromPercentage(e.attribute("width", "120%")), SvgUtil::fromPercentage(e.attribute("height", "120%"))); } else { maskRect.setRect( parseUnitX(e.attribute("x", "-10%")), // yes, percents are insane in this case, parseUnitY(e.attribute("y", "-10%")), // but this is what SVG 1.1 tells us... parseUnitX(e.attribute("width", "120%")), parseUnitY(e.attribute("height", "120%"))); } clipMask->setMaskRect(maskRect); // ensure that the clip mask is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipMask->setShapes({clipShape}); m_clipMasks.insert(id, clipMask); return true; } void SvgParser::uploadStyleToContext(const KoXmlElement &e) { SvgStyles styles = m_context.styleParser().collectStyles(e); m_context.styleParser().parseFont(styles); m_context.styleParser().parseStyle(styles); } void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { if (!shape) return; SvgGraphicsContext *gc = m_context.currentGC(); KIS_ASSERT(gc); if (!dynamic_cast(shape)) { applyFillStyle(shape); applyStrokeStyle(shape); } if (KoPathShape *pathShape = dynamic_cast(shape)) { applyMarkers(pathShape); } applyFilter(shape); applyClipping(shape, shapeToOriginalUserCoordinates); applyMaskClipping(shape, shapeToOriginalUserCoordinates); if (!gc->display || !gc->visible) { /** * WARNING: here is a small inconsistency with the standard: * in the standard, 'display' is not inherited, but in * flake it is! * * NOTE: though the standard says: "A value of 'display:none' indicates * that the given element and ***its children*** shall not be * rendered directly". Therefore, using setVisible(false) is fully * legitimate here (DK 29.11.16). */ shape->setVisible(false); } shape->setTransparency(1.0 - gc->opacity); } void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e, const QPointF &shapeToOriginalUserCoordinates) { applyStyle(obj, m_context.styleParser().collectStyles(e), shapeToOriginalUserCoordinates); } void SvgParser::applyStyle(KoShape *obj, const SvgStyles &styles, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; m_context.styleParser().parseStyle(styles); if (!obj) return; if (!dynamic_cast(obj)) { applyFillStyle(obj); applyStrokeStyle(obj); } if (KoPathShape *pathShape = dynamic_cast(obj)) { applyMarkers(pathShape); } applyFilter(obj); applyClipping(obj, shapeToOriginalUserCoordinates); applyMaskClipping(obj, shapeToOriginalUserCoordinates); if (!gc->display || !gc->visible) { obj->setVisible(false); } obj->setTransparency(1.0 - gc->opacity); } QGradient* prepareGradientForShape(const SvgGradientHelper *gradient, const KoShape *shape, const SvgGraphicsContext *gc, QTransform *transform) { QGradient *resultGradient = 0; KIS_ASSERT(transform); if (gradient->gradientUnits() == KoFlake::ObjectBoundingBox) { resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform(); } else { if (gradient->gradient()->type() == QGradient::LinearGradient) { /** * Create a converted gradient that looks the same, but linked to the * bounding rect of the shape, so it would be transformed with the shape */ const QRectF boundingRect = shape->outline().boundingRect(); const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); const QTransform relativeToUser = relativeToShape * shape->transformation() * gc->matrix.inverted(); const QTransform userToRelative = relativeToUser.inverted(); const QLinearGradient *o = static_cast(gradient->gradient()); QLinearGradient *g = new QLinearGradient(); g->setStart(userToRelative.map(o->start())); g->setFinalStop(userToRelative.map(o->finalStop())); g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setStops(o->stops()); g->setSpread(o->spread()); resultGradient = g; *transform = relativeToUser * gradient->transform() * userToRelative; } else if (gradient->gradient()->type() == QGradient::RadialGradient) { // For radial and conical gradients such conversion is not possible resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform() * gc->matrix * shape->transformation().inverted(); } } return resultGradient; } void SvgParser::applyFillStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->fillType == SvgGraphicsContext::None) { shape->setBackground(QSharedPointer(0)); } else if (gc->fillType == SvgGraphicsContext::Solid) { shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } else if (gc->fillType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->fillId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QSharedPointer bg; bg = toQShared(new KoGradientBackground(result)); bg->setTransform(transform); shape->setBackground(bg); } } else { QSharedPointer pattern = findPattern(gc->fillId, shape); if (pattern) { shape->setBackground(pattern); } else { // no referenced fill found, use fallback color shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } } } KoPathShape *path = dynamic_cast(shape); if (path) path->setFillRule(gc->fillRule); } void applyDashes(const KoShapeStrokeSP srcStroke, KoShapeStrokeSP dstStroke) { const double lineWidth = srcStroke->lineWidth(); QVector dashes = srcStroke->lineDashes(); // apply line width to dashes and dash offset if (dashes.count() && lineWidth > 0.0) { const double dashOffset = srcStroke->dashOffset(); QVector dashes = srcStroke->lineDashes(); for (int i = 0; i < dashes.count(); ++i) { dashes[i] /= lineWidth; } dstStroke->setLineStyle(Qt::CustomDashLine, dashes); dstStroke->setDashOffset(dashOffset / lineWidth); } else { dstStroke->setLineStyle(Qt::SolidLine, QVector()); } } void SvgParser::applyStrokeStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->strokeType == SvgGraphicsContext::None) { shape->setStroke(KoShapeStrokeModelSP()); } else if (gc->strokeType == SvgGraphicsContext::Solid) { KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } else if (gc->strokeType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->strokeId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QBrush brush = *result; delete result; brush.setTransform(transform); KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); stroke->setLineBrush(brush); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } else { // no referenced stroke found, use fallback color KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } } void SvgParser::applyFilter(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->filterId.isEmpty()) return; SvgFilterHelper *filter = findFilter(gc->filterId); if (! filter) return; KoXmlElement content = filter->content(); // parse filter region QRectF bound(shape->position(), shape->size()); // work on bounding box without viewbox tranformation applied // so user space coordinates of bounding box and filter region match up bound = gc->viewboxTransform.inverted().mapRect(bound); QRectF filterRegion(filter->position(bound), filter->size(bound)); // convert filter region to boundingbox units QRectF objectFilterRegion; objectFilterRegion.setTopLeft(SvgUtil::userSpaceToObject(filterRegion.topLeft(), bound)); objectFilterRegion.setSize(SvgUtil::userSpaceToObject(filterRegion.size(), bound)); KoFilterEffectLoadingContext context(m_context.xmlBaseDir()); context.setShapeBoundingBox(bound); // enable units conversion context.enableFilterUnitsConversion(filter->filterUnits() == KoFlake::UserSpaceOnUse); context.enableFilterPrimitiveUnitsConversion(filter->primitiveUnits() == KoFlake::UserSpaceOnUse); KoFilterEffectRegistry *registry = KoFilterEffectRegistry::instance(); KoFilterEffectStack *filterStack = 0; QSet stdInputs; stdInputs << "SourceGraphic" << "SourceAlpha"; stdInputs << "BackgroundImage" << "BackgroundAlpha"; stdInputs << "FillPaint" << "StrokePaint"; QMap inputs; // create the filter effects and add them to the shape for (KoXmlNode n = content.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement primitive = n.toElement(); KoFilterEffect *filterEffect = registry->createFilterEffectFromXml(primitive, context); if (!filterEffect) { debugFlake << "filter effect" << primitive.tagName() << "is not implemented yet"; continue; } const QString input = primitive.attribute("in"); if (!input.isEmpty()) { filterEffect->setInput(0, input); } const QString output = primitive.attribute("result"); if (!output.isEmpty()) { filterEffect->setOutput(output); } QRectF subRegion; // parse subregion if (filter->primitiveUnits() == KoFlake::UserSpaceOnUse) { const QString xa = primitive.attribute("x"); const QString ya = primitive.attribute("y"); const QString wa = primitive.attribute("width"); const QString ha = primitive.attribute("height"); if (xa.isEmpty() || ya.isEmpty() || wa.isEmpty() || ha.isEmpty()) { bool hasStdInput = false; bool isFirstEffect = filterStack == 0; // check if one of the inputs is a standard input Q_FOREACH (const QString &input, filterEffect->inputs()) { if ((isFirstEffect && input.isEmpty()) || stdInputs.contains(input)) { hasStdInput = true; break; } } if (hasStdInput || primitive.tagName() == "feImage") { // default to 0%, 0%, 100%, 100% subRegion.setTopLeft(QPointF(0, 0)); subRegion.setSize(QSizeF(1, 1)); } else { // defaults to bounding rect of all referenced nodes Q_FOREACH (const QString &input, filterEffect->inputs()) { if (!inputs.contains(input)) continue; KoFilterEffect *inputFilter = inputs[input]; if (inputFilter) subRegion |= inputFilter->filterRect(); } } } else { const qreal x = parseUnitX(xa); const qreal y = parseUnitY(ya); const qreal w = parseUnitX(wa); const qreal h = parseUnitY(ha); subRegion.setTopLeft(SvgUtil::userSpaceToObject(QPointF(x, y), bound)); subRegion.setSize(SvgUtil::userSpaceToObject(QSizeF(w, h), bound)); } } else { // x, y, width, height are in percentages of the object referencing the filter // so we just parse the percentages const qreal x = SvgUtil::fromPercentage(primitive.attribute("x", "0")); const qreal y = SvgUtil::fromPercentage(primitive.attribute("y", "0")); const qreal w = SvgUtil::fromPercentage(primitive.attribute("width", "1")); const qreal h = SvgUtil::fromPercentage(primitive.attribute("height", "1")); subRegion = QRectF(QPointF(x, y), QSizeF(w, h)); } filterEffect->setFilterRect(subRegion); if (!filterStack) filterStack = new KoFilterEffectStack(); filterStack->appendFilterEffect(filterEffect); inputs[filterEffect->output()] = filterEffect; } if (filterStack) { filterStack->setClipRect(objectFilterRegion); shape->setFilterEffectStack(filterStack); } } void SvgParser::applyMarkers(KoPathShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (!gc->markerStartId.isEmpty() && m_markers.contains(gc->markerStartId)) { shape->setMarker(m_markers[gc->markerStartId].data(), KoFlake::StartMarker); } if (!gc->markerMidId.isEmpty() && m_markers.contains(gc->markerMidId)) { shape->setMarker(m_markers[gc->markerMidId].data(), KoFlake::MidMarker); } if (!gc->markerEndId.isEmpty() && m_markers.contains(gc->markerEndId)) { shape->setMarker(m_markers[gc->markerEndId].data(), KoFlake::EndMarker); } + + shape->setAutoFillMarkers(gc->autoFillMarkers); } void SvgParser::applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->clipPathId.isEmpty()) return; SvgClipPathHelper *clipPath = findClipPath(gc->clipPathId); if (!clipPath || clipPath->isEmpty()) return; QList shapes; Q_FOREACH (KoShape *item, clipPath->shapes()) { KoShape *clonedShape = item->cloneShape(); KIS_ASSERT_RECOVER(clonedShape) { continue; } shapes.append(clonedShape); } if (!shapeToOriginalUserCoordinates.isNull()) { const QTransform t = QTransform::fromTranslate(shapeToOriginalUserCoordinates.x(), shapeToOriginalUserCoordinates.y()); Q_FOREACH(KoShape *s, shapes) { s->applyAbsoluteTransformation(t); } } KoClipPath *clipPathObject = new KoClipPath(shapes, clipPath->clipPathUnits() == KoFlake::ObjectBoundingBox ? KoFlake::ObjectBoundingBox : KoFlake::UserSpaceOnUse); shape->setClipPath(clipPathObject); } void SvgParser::applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (gc->clipMaskId.isEmpty()) return; QSharedPointer originalClipMask = m_clipMasks.value(gc->clipMaskId); if (!originalClipMask || originalClipMask->isEmpty()) return; KoClipMask *clipMask = originalClipMask->clone(); clipMask->setExtraShapeOffset(shapeToOriginalUserCoordinates); shape->setClipMask(clipMask); } KoShape* SvgParser::parseUse(const KoXmlElement &e) { KoShape *result = 0; QString id = e.attribute("xlink:href"); if (!id.isEmpty()) { QString key = id.mid(1); if (m_context.hasDefinition(key)) { SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); // TODO: parse 'width' and 'hight' as well gc->matrix.translate(parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0"))); const KoXmlElement &referencedElement = m_context.definition(key); result = parseGroup(e, referencedElement); m_context.popGraphicsContext(); } } return result; } void SvgParser::addToGroup(QList shapes, KoShapeGroup *group) { m_shapes += shapes; if (!group || shapes.isEmpty()) return; // not clipped, but inherit transform KoShapeGroupCommand cmd(group, shapes, false, true, false); cmd.redo(); } QList SvgParser::parseSvg(const KoXmlElement &e, QSizeF *fragmentSize) { // check if we are the root svg element const bool isRootSvg = m_context.isRootContext(); // parse 'transform' field if preset SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); applyStyle(0, e, QPointF()); const QString w = e.attribute("width"); const QString h = e.attribute("height"); const qreal width = w.isEmpty() ? 666.0 : parseUnitX(w); const qreal height = h.isEmpty() ? 555.0 : parseUnitY(h); QSizeF svgFragmentSize(QSizeF(width, height)); if (fragmentSize) { *fragmentSize = svgFragmentSize; } gc->currentBoundingBox = QRectF(QPointF(0, 0), svgFragmentSize); if (!isRootSvg) { // x and y attribute has no meaning for outermost svg elements const qreal x = parseUnit(e.attribute("x", "0")); const qreal y = parseUnit(e.attribute("y", "0")); QTransform move = QTransform::fromTranslate(x, y); gc->matrix = move * gc->matrix; } applyViewBoxTransform(e); QList shapes; // SVG 1.1: skip the rendering of the element if it has null viewBox if (gc->currentBoundingBox.isValid()) { shapes = parseContainer(e); } m_context.popGraphicsContext(); return shapes; } void SvgParser::applyViewBoxTransform(const KoXmlElement &element) { SvgGraphicsContext *gc = m_context.currentGC(); QRectF viewRect = gc->currentBoundingBox; QTransform viewTransform; if (SvgUtil::parseViewBox(gc, element, gc->currentBoundingBox, &viewRect, &viewTransform)) { gc->matrix = viewTransform * gc->matrix; gc->currentBoundingBox = viewRect; } } QList > SvgParser::knownMarkers() const { return m_markers.values(); } void SvgParser::setFileFetcher(SvgParser::FileFetcherFunc func) { m_context.setFileFetcher(func); } inline QPointF extraShapeOffset(const KoShape *shape, const QTransform coordinateSystemOnLoading) { const QTransform shapeToOriginalUserCoordinates = shape->absoluteTransformation(0).inverted() * coordinateSystemOnLoading; KIS_SAFE_ASSERT_RECOVER_NOOP(shapeToOriginalUserCoordinates.type() <= QTransform::TxTranslate); return QPointF(shapeToOriginalUserCoordinates.dx(), shapeToOriginalUserCoordinates.dy()); } KoShape* SvgParser::parseGroup(const KoXmlElement &b, const KoXmlElement &overrideChildrenFrom) { m_context.pushGraphicsContext(b); KoShapeGroup *group = new KoShapeGroup(); group->setZIndex(m_context.nextZIndex()); // groups should also have their own coordinate system! group->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(group, m_context.currentGC()->matrix); uploadStyleToContext(b); QList childShapes; if (!overrideChildrenFrom.isNull()) { // we upload styles from both: and uploadStyleToContext(overrideChildrenFrom); childShapes = parseSingleElement(overrideChildrenFrom); } else { childShapes = parseContainer(b); } // handle id applyId(b.attribute("id"), group); addToGroup(childShapes, group); applyCurrentStyle(group, extraOffset); // apply style to this group after size is set m_context.popGraphicsContext(); return group; } QList SvgParser::parseContainer(const KoXmlElement &e) { QList shapes; // are we parsing a switch container bool isSwitch = e.tagName() == "switch"; for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement b = n.toElement(); if (b.isNull()) continue; if (isSwitch) { // if we are parsing a switch check the requiredFeatures, requiredExtensions // and systemLanguage attributes // TODO: evaluate feature list if (b.hasAttribute("requiredFeatures")) { continue; } if (b.hasAttribute("requiredExtensions")) { // we do not support any extensions continue; } if (b.hasAttribute("systemLanguage")) { // not implemeted yet } } QList currentShapes = parseSingleElement(b); shapes.append(currentShapes); // if we are parsing a switch, stop after the first supported element if (isSwitch && !currentShapes.isEmpty()) break; } return shapes; } QList SvgParser::parseSingleElement(const KoXmlElement &b) { QList shapes; // save definition for later instantiation with 'use' m_context.addDefinition(b); if (b.tagName() == "svg") { shapes += parseSvg(b); } else if (b.tagName() == "g" || b.tagName() == "a" || b.tagName() == "symbol") { // treat svg link as group so we don't miss its child elements shapes += parseGroup(b); } else if (b.tagName() == "switch") { m_context.pushGraphicsContext(b); shapes += parseContainer(b); m_context.popGraphicsContext(); } else if (b.tagName() == "defs") { if (b.childNodesCount() > 0) { /** * WARNING: 'defs' are basically 'display:none' style, therefore they should not play * any role in shapes outline calculation. But setVisible(false) shapes do! * Should be fixed in the future! */ KoShape *defsShape = parseGroup(b); defsShape->setVisible(false); shapes += defsShape; } } else if (b.tagName() == "linearGradient" || b.tagName() == "radialGradient") { parseGradient(b); } else if (b.tagName() == "pattern") { } else if (b.tagName() == "filter") { parseFilter(b); } else if (b.tagName() == "clipPath") { parseClipPath(b); } else if (b.tagName() == "mask") { parseClipMask(b); } else if (b.tagName() == "marker") { parseMarker(b); } else if (b.tagName() == "style") { m_context.addStyleSheet(b); } else if (b.tagName() == "rect" || b.tagName() == "ellipse" || b.tagName() == "circle" || b.tagName() == "line" || b.tagName() == "polyline" || b.tagName() == "polygon" || b.tagName() == "path" || b.tagName() == "image" || b.tagName() == "text") { KoShape *shape = createObjectDirect(b); if (shape) shapes.append(shape); } else if (b.tagName() == "use") { shapes += parseUse(b); } else if (b.tagName() == "color-profile") { m_context.parseProfile(b); } else { // this is an unknown element, so try to load it anyway // there might be a shape that handles that element KoShape *shape = createObject(b); if (shape) { shapes.append(shape); } } return shapes; } // Creating functions // --------------------------------------------------------------------------------------- KoShape * SvgParser::createPath(const KoXmlElement &element) { KoShape *obj = 0; if (element.tagName() == "line") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { double x1 = element.attribute("x1").isEmpty() ? 0.0 : parseUnitX(element.attribute("x1")); double y1 = element.attribute("y1").isEmpty() ? 0.0 : parseUnitY(element.attribute("y1")); double x2 = element.attribute("x2").isEmpty() ? 0.0 : parseUnitX(element.attribute("x2")); double y2 = element.attribute("y2").isEmpty() ? 0.0 : parseUnitY(element.attribute("y2")); path->clear(); path->moveTo(QPointF(x1, y1)); path->lineTo(QPointF(x2, y2)); path->normalize(); obj = path; } } else if (element.tagName() == "polyline" || element.tagName() == "polygon") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { path->clear(); bool bFirst = true; QString points = element.attribute("points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); QStringList pointList = points.split(' ', QString::SkipEmptyParts); for (QStringList::Iterator it = pointList.begin(); it != pointList.end(); ++it) { QPointF point; point.setX(SvgUtil::fromUserSpace((*it).toDouble())); ++it; if (it == pointList.end()) break; point.setY(SvgUtil::fromUserSpace((*it).toDouble())); if (bFirst) { path->moveTo(point); bFirst = false; } else path->lineTo(point); } if (element.tagName() == "polygon") path->close(); path->setPosition(path->normalize()); obj = path; } } else if (element.tagName() == "path") { KoPathShape *path = static_cast(createShape(KoPathShapeId)); if (path) { path->clear(); KoPathShapeLoader loader(path); loader.parseSvg(element.attribute("d"), true); path->setPosition(path->normalize()); QPointF newPosition = QPointF(SvgUtil::fromUserSpace(path->position().x()), SvgUtil::fromUserSpace(path->position().y())); QSizeF newSize = QSizeF(SvgUtil::fromUserSpace(path->size().width()), SvgUtil::fromUserSpace(path->size().height())); path->setSize(newSize); path->setPosition(newPosition); obj = path; } } return obj; } KoShape * SvgParser::createObjectDirect(const KoXmlElement &b) { m_context.pushGraphicsContext(b); uploadStyleToContext(b); KoShape *obj = createShapeFromElement(b, m_context); if (obj) { obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); applyCurrentStyle(obj, extraOffset); // handle id applyId(b.attribute("id"), obj); obj->setZIndex(m_context.nextZIndex()); } m_context.popGraphicsContext(); return obj; } KoShape * SvgParser::createObject(const KoXmlElement &b, const SvgStyles &style) { m_context.pushGraphicsContext(b); KoShape *obj = createShapeFromElement(b, m_context); if (obj) { obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); SvgStyles objStyle = style.isEmpty() ? m_context.styleParser().collectStyles(b) : style; m_context.styleParser().parseFont(objStyle); applyStyle(obj, objStyle, extraOffset); // handle id applyId(b.attribute("id"), obj); obj->setZIndex(m_context.nextZIndex()); } m_context.popGraphicsContext(); return obj; } KoShape * SvgParser::createShapeFromElement(const KoXmlElement &element, SvgLoadingContext &context) { KoShape *object = 0; QList factories = KoShapeRegistry::instance()->factoriesForElement(KoXmlNS::svg, element.tagName()); foreach (KoShapeFactoryBase *f, factories) { KoShape *shape = f->createDefaultShape(m_documentResourceManager); if (!shape) continue; SvgShape *svgShape = dynamic_cast(shape); if (!svgShape) { delete shape; continue; } // reset transformation that might come from the default shape shape->setTransformation(QTransform()); // reset border KoShapeStrokeModelSP oldStroke = shape->stroke(); shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); if (!svgShape->loadSvg(element, context)) { delete shape; continue; } object = shape; break; } if (!object) { object = createPath(element); } return object; } KoShape * SvgParser::createShape(const QString &shapeID) { KoShapeFactoryBase *factory = KoShapeRegistry::instance()->get(shapeID); if (!factory) { debugFlake << "Could not find factory for shape id" << shapeID; return 0; } KoShape *shape = factory->createDefaultShape(m_documentResourceManager); if (!shape) { debugFlake << "Could not create Default shape for shape id" << shapeID; return 0; } if (shape->shapeId().isEmpty()) shape->setShapeId(factory->id()); // reset tranformation that might come from the default shape shape->setTransformation(QTransform()); // reset border KoShapeStrokeModelSP oldStroke = shape->stroke(); shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); return shape; } void SvgParser::applyId(const QString &id, KoShape *shape) { if (id.isEmpty()) return; shape->setName(id); m_context.registerShape(id, shape); } diff --git a/libs/flake/svg/SvgStyleParser.cpp b/libs/flake/svg/SvgStyleParser.cpp index 7ade60445c..b5b7a241b9 100644 --- a/libs/flake/svg/SvgStyleParser.cpp +++ b/libs/flake/svg/SvgStyleParser.cpp @@ -1,552 +1,558 @@ /* This file is part of the KDE project * Copyright (C) 2002-2005,2007 Rob Buis * Copyright (C) 2002-2004 Nicolas Goutte * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2009 Jan Hambrecht * Copyright (C) 2005,2007 Thomas Zander * Copyright (C) 2006-2007 Inge Wallin * Copyright (C) 2007-2008,2010 Thorsten Zachmann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgStyleParser.h" #include "SvgLoadingContext.h" #include "SvgGraphicContext.h" #include "SvgUtil.h" #include #include #include class Q_DECL_HIDDEN SvgStyleParser::Private { public: Private(SvgLoadingContext &loadingContext) : context(loadingContext) { // the order of the font attributes is important, don't change without reason !!! fontAttributes << "font-family" << "font-size" << "font-weight"; fontAttributes << "text-decoration" << "letter-spacing" << "word-spacing" << "baseline-shift"; // the order of the style attributes is important, don't change without reason !!! styleAttributes << "color" << "display" << "visibility"; styleAttributes << "fill" << "fill-rule" << "fill-opacity"; styleAttributes << "stroke" << "stroke-width" << "stroke-linejoin" << "stroke-linecap"; styleAttributes << "stroke-dasharray" << "stroke-dashoffset" << "stroke-opacity" << "stroke-miterlimit"; styleAttributes << "opacity" << "filter" << "clip-path" << "clip-rule" << "mask"; - styleAttributes << "marker" << "marker-start" << "marker-mid" << "marker-end"; + styleAttributes << "marker" << "marker-start" << "marker-mid" << "marker-end" << "krita:marker-fill-method"; } SvgLoadingContext &context; QStringList fontAttributes; ///< font related attributes QStringList styleAttributes; ///< style related attributes }; SvgStyleParser::SvgStyleParser(SvgLoadingContext &context) : d(new Private(context)) { } SvgStyleParser::~SvgStyleParser() { delete d; } void SvgStyleParser::parseStyle(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure we parse the style attributes in the right order Q_FOREACH (const QString & command, d->styleAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } void SvgStyleParser::parseFont(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure to only parse font attributes here Q_FOREACH (const QString & command, d->fontAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } void SvgStyleParser::parsePA(SvgGraphicsContext *gc, const QString &command, const QString ¶ms) { QColor fillcolor = gc->fillColor; QColor strokecolor = gc->stroke->color(); if (params == "inherit") return; if (command == "fill") { if (params == "none") { gc->fillType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->fillId = params.mid(start, end - start); gc->fillType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(fillcolor, params.mid(end + 1).trimmed()); } else { // great we have a solid fill gc->fillType = SvgGraphicsContext::Solid; parseColor(fillcolor, params); } } else if (command == "fill-rule") { if (params == "nonzero") gc->fillRule = Qt::WindingFill; else if (params == "evenodd") gc->fillRule = Qt::OddEvenFill; } else if (command == "stroke") { if (params == "none") { gc->strokeType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->strokeId = params.mid(start, end - start); gc->strokeType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(strokecolor, params.mid(end + 1).trimmed()); } else { // great we have a solid stroke gc->strokeType = SvgGraphicsContext::Solid; parseColor(strokecolor, params); } } else if (command == "stroke-width") { gc->stroke->setLineWidth(SvgUtil::parseUnitXY(gc, params)); } else if (command == "stroke-linejoin") { if (params == "miter") gc->stroke->setJoinStyle(Qt::MiterJoin); else if (params == "round") gc->stroke->setJoinStyle(Qt::RoundJoin); else if (params == "bevel") gc->stroke->setJoinStyle(Qt::BevelJoin); } else if (command == "stroke-linecap") { if (params == "butt") gc->stroke->setCapStyle(Qt::FlatCap); else if (params == "round") gc->stroke->setCapStyle(Qt::RoundCap); else if (params == "square") gc->stroke->setCapStyle(Qt::SquareCap); } else if (command == "stroke-miterlimit") { gc->stroke->setMiterLimit(params.toFloat()); } else if (command == "stroke-dasharray") { QVector array; if (params != "none") { QString dashString = params; QStringList dashes = dashString.replace(',', ' ').simplified().split(' '); for (QStringList::Iterator it = dashes.begin(); it != dashes.end(); ++it) { array.append(SvgUtil::parseUnitXY(gc, *it)); } // if the array is odd repeat it according to the standard if (array.size() & 1) { array << array; } } gc->stroke->setLineStyle(Qt::CustomDashLine, array); } else if (command == "stroke-dashoffset") { gc->stroke->setDashOffset(params.toFloat()); } // handle opacity else if (command == "stroke-opacity") strokecolor.setAlphaF(SvgUtil::fromPercentage(params)); else if (command == "fill-opacity") { float opacity = SvgUtil::fromPercentage(params); if (opacity < 0.0) opacity = 0.0; if (opacity > 1.0) opacity = 1.0; fillcolor.setAlphaF(opacity); } else if (command == "opacity") { gc->opacity = SvgUtil::fromPercentage(params); } else if (command == "font-family") { QString family = params; family.replace('\'' , ' '); gc->font.setFamily(family); } else if (command == "font-size") { float pointSize = SvgUtil::parseUnitY(gc, params); if (pointSize > 0.0f) gc->font.setPointSizeF(pointSize); } else if (command == "font-weight") { int weight = QFont::Normal; // map svg weight to qt weight // svg value qt value // 100,200,300 1, 17, 33 // 400 50 (normal) // 500,600 58,66 // 700 75 (bold) // 800,900 87,99 if (params == "bold") weight = QFont::Bold; else if (params == "lighter") { weight = gc->font.weight(); if (weight <= 17) weight = 1; else if (weight <= 33) weight = 17; else if (weight <= 50) weight = 33; else if (weight <= 58) weight = 50; else if (weight <= 66) weight = 58; else if (weight <= 75) weight = 66; else if (weight <= 87) weight = 75; else if (weight <= 99) weight = 87; } else if (params == "bolder") { weight = gc->font.weight(); if (weight >= 87) weight = 99; else if (weight >= 75) weight = 87; else if (weight >= 66) weight = 75; else if (weight >= 58) weight = 66; else if (weight >= 50) weight = 58; else if (weight >= 33) weight = 50; else if (weight >= 17) weight = 50; else if (weight >= 1) weight = 17; } else { bool ok; // try to read numerical weight value weight = params.toInt(&ok, 10); if (!ok) return; switch (weight) { case 100: weight = 1; break; case 200: weight = 17; break; case 300: weight = 33; break; case 400: weight = 50; break; case 500: weight = 58; break; case 600: weight = 66; break; case 700: weight = 75; break; case 800: weight = 87; break; case 900: weight = 99; break; } } gc->font.setWeight(weight); } else if (command == "text-decoration") { if (params == "line-through") gc->font.setStrikeOut(true); else if (params == "underline") gc->font.setUnderline(true); } else if (command == "letter-spacing") { gc->letterSpacing = SvgUtil::parseUnitX(gc, params); } else if (command == "baseline-shift") { gc->baselineShift = params; } else if (command == "word-spacing") { gc->wordSpacing = SvgUtil::parseUnitX(gc, params); } else if (command == "color") { QColor color; parseColor(color, params); gc->currentColor = color; } else if (command == "display") { if (params == "none") gc->display = false; } else if (command == "visibility") { // visible is inherited! gc->visible = params == "visible"; } else if (command == "filter") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->filterId = params.mid(start, end - start); } } else if (command == "clip-path") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipPathId = params.mid(start, end - start); } } else if (command == "clip-rule") { if (params == "nonzero") gc->clipRule = Qt::WindingFill; else if (params == "evenodd") gc->clipRule = Qt::OddEvenFill; } else if (command == "mask") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipMaskId = params.mid(start, end - start); } } else if (command == "marker-start") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); } } else if (command == "marker-end") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerEndId = params.mid(start, end - start); } } else if (command == "marker-mid") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerMidId = params.mid(start, end - start); } - } else if (command == "marker") { + } else if (command == "marker") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); gc->markerMidId = gc->markerStartId; gc->markerEndId = gc->markerStartId; } + } else if (command == "krita:marker-fill-method") { + gc->autoFillMarkers = params == "auto"; } gc->fillColor = fillcolor; gc->stroke->setColor(strokecolor); } bool SvgStyleParser::parseColor(QColor &color, const QString &s) { if (s.isEmpty() || s == "none") return false; if (s.startsWith(QLatin1String("rgb("))) { QString parse = s.trimmed(); QStringList colors = parse.split(','); QString r = colors[0].right((colors[0].length() - 4)); QString g = colors[1]; QString b = colors[2].left((colors[2].length() - 1)); if (r.contains('%')) { r = r.left(r.length() - 1); r = QString::number(int((double(255 * r.toDouble()) / 100.0))); } if (g.contains('%')) { g = g.left(g.length() - 1); g = QString::number(int((double(255 * g.toDouble()) / 100.0))); } if (b.contains('%')) { b = b.left(b.length() - 1); b = QString::number(int((double(255 * b.toDouble()) / 100.0))); } color = QColor(r.toInt(), g.toInt(), b.toInt()); } else if (s == "currentColor") { color = d->context.currentGC()->currentColor; } else { // QColor understands #RRGGBB and svg color names color.setNamedColor(s.trimmed()); } return true; } void SvgStyleParser::parseColorStops(QGradient *gradient, const KoXmlElement &e, SvgGraphicsContext *context, const QGradientStops &defaultStops) { QGradientStops stops; qreal previousOffset = 0.0; KoXmlElement stop; forEachElement(stop, e) { if (stop.tagName() == "stop") { qreal offset = 0.0; QString offsetStr = stop.attribute("offset").trimmed(); if (offsetStr.endsWith('%')) { offsetStr = offsetStr.left(offsetStr.length() - 1); offset = offsetStr.toFloat() / 100.0; } else { offset = offsetStr.toFloat(); } // according to SVG the value must be within [0; 1] interval offset = qBound(0.0, offset, 1.0); // according to SVG the stops' offset must be non-decreasing offset = qMax(offset, previousOffset); previousOffset = offset; QColor color; QString stopColorStr = stop.attribute("stop-color"); QString stopOpacityStr = stop.attribute("stop-opacity"); const QStringList attributes({"stop-color", "stop-opacity"}); SvgStyles styles = parseOneCssStyle(stop.attribute("style"), attributes); // SVG: CSS values have precedence over presentation attributes! if (styles.contains("stop-color")) { stopColorStr = styles.value("stop-color"); } if (styles.contains("stop-opacity")) { stopOpacityStr = styles.value("stop-opacity"); } if (stopColorStr.isEmpty() && stopColorStr == "inherit") { color = context->currentColor; } else { parseColor(color, stopColorStr); } if (!stopOpacityStr.isEmpty() && stopOpacityStr != "inherit") { color.setAlphaF(qBound(0.0, stopOpacityStr.toDouble(), 1.0)); } stops.append(QPair(offset, color)); } } if (!stops.isEmpty()) { gradient->setStops(stops); } else { gradient->setStops(defaultStops); } } SvgStyles SvgStyleParser::parseOneCssStyle(const QString &style, const QStringList &interestingAttributes) { SvgStyles parsedStyles; if (style.isEmpty()) return parsedStyles; QStringList substyles = style.simplified().split(';', QString::SkipEmptyParts); if (!substyles.count()) return parsedStyles; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); if (interestingAttributes.isEmpty() || interestingAttributes.contains(command)) { parsedStyles[command] = params; } } return parsedStyles; } SvgStyles SvgStyleParser::collectStyles(const KoXmlElement &e) { SvgStyles styleMap; // collect individual presentation style attributes which have the priority 0 // according to SVG standard Q_FOREACH (const QString &command, d->styleAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } Q_FOREACH (const QString & command, d->fontAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } // match css style rules to element QStringList cssStyles = d->context.matchingCssStyles(e); // collect all css style attributes Q_FOREACH (const QString &style, cssStyles) { QStringList substyles = style.split(';', QString::SkipEmptyParts); if (!substyles.count()) continue; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); + + // toggle the namespace selector into the xml-like one + command.replace("|", ":"); + // only use style and font attributes if (d->styleAttributes.contains(command) || d->fontAttributes.contains(command)) styleMap[command] = params; } } // FIXME: if 'inherit' we should just remove the property and use the one from the context! // replace keyword "inherit" for style values QMutableMapIterator it(styleMap); while (it.hasNext()) { it.next(); if (it.value() == "inherit") { it.setValue(inheritedAttribute(it.key(), e)); } } return styleMap; } SvgStyles SvgStyleParser::mergeStyles(const SvgStyles &referencedBy, const SvgStyles &referencedStyles) { // 1. use all styles of the referencing styles SvgStyles mergedStyles = referencedBy; // 2. use all styles of the referenced style which are not in the referencing styles SvgStyles::const_iterator it = referencedStyles.constBegin(); for (; it != referencedStyles.constEnd(); ++it) { if (!referencedBy.contains(it.key())) { mergedStyles.insert(it.key(), it.value()); } } return mergedStyles; } SvgStyles SvgStyleParser::mergeStyles(const KoXmlElement &e1, const KoXmlElement &e2) { return mergeStyles(collectStyles(e1), collectStyles(e2)); } QString SvgStyleParser::inheritedAttribute(const QString &attributeName, const KoXmlElement &e) { KoXmlNode parent = e.parentNode(); while (!parent.isNull()) { KoXmlElement currentElement = parent.toElement(); if (currentElement.hasAttribute(attributeName)) { return currentElement.attribute(attributeName); } parent = currentElement.parentNode(); } return QString(); } diff --git a/libs/flake/svg/SvgStyleWriter.cpp b/libs/flake/svg/SvgStyleWriter.cpp index a4cdaeded3..2cb7c6773e 100644 --- a/libs/flake/svg/SvgStyleWriter.cpp +++ b/libs/flake/svg/SvgStyleWriter.cpp @@ -1,543 +1,546 @@ /* This file is part of the KDE project Copyright (C) 2002 Lars Siebold Copyright (C) 2002-2003,2005 Rob Buis Copyright (C) 2002,2005-2006 David Faure Copyright (C) 2002 Werner Trobin Copyright (C) 2002 Lennart Kudling Copyright (C) 2004 Nicolas Goutte Copyright (C) 2005 Boudewijn Rempt Copyright (C) 2005 Raphael Langerhorst Copyright (C) 2005 Thomas Zander Copyright (C) 2005,2007-2008 Jan Hambrecht Copyright (C) 2006 Inge Wallin Copyright (C) 2006 Martin Pfeiffer Copyright (C) 2006 Gábor Lehel Copyright (C) 2006 Laurent Montel Copyright (C) 2006 Christian Mueller Copyright (C) 2006 Ariya Hidayat Copyright (C) 2010 Thorsten Zachmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgStyleWriter.h" #include "SvgSavingContext.h" #include "SvgUtil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include "kis_algebra_2d.h" #include #include void SvgStyleWriter::saveSvgStyle(KoShape *shape, SvgSavingContext &context) { saveSvgFill(shape, context); saveSvgStroke(shape, context); saveSvgEffects(shape, context); saveSvgClipping(shape, context); saveSvgMasking(shape, context); saveSvgMarkers(shape, context); if (! shape->isVisible()) context.shapeWriter().addAttribute("display", "none"); if (shape->transparency() > 0.0) context.shapeWriter().addAttribute("opacity", 1.0 - shape->transparency()); } void SvgStyleWriter::saveSvgFill(KoShape *shape, SvgSavingContext &context) { if (! shape->background()) { context.shapeWriter().addAttribute("fill", "none"); } QBrush fill(Qt::NoBrush); QSharedPointer cbg = qSharedPointerDynamicCast(shape->background()); if (cbg) { context.shapeWriter().addAttribute("fill", cbg->color().name()); if (cbg->color().alphaF() < 1.0) context.shapeWriter().addAttribute("fill-opacity", cbg->color().alphaF()); } QSharedPointer gbg = qSharedPointerDynamicCast(shape->background()); if (gbg) { QString gradientId = saveSvgGradient(gbg->gradient(), gbg->transform(), context); context.shapeWriter().addAttribute("fill", "url(#" + gradientId + ")"); } QSharedPointer pbg = qSharedPointerDynamicCast(shape->background()); if (pbg) { const QString patternId = saveSvgPattern(pbg, shape, context); context.shapeWriter().addAttribute("fill", "url(#" + patternId + ")"); } QSharedPointer vpbg = qSharedPointerDynamicCast(shape->background()); if (vpbg) { const QString patternId = saveSvgVectorPattern(vpbg, shape, context); context.shapeWriter().addAttribute("fill", "url(#" + patternId + ")"); } KoPathShape * path = dynamic_cast(shape); if (path && shape->background()) { // non-zero is default, so only write fillrule if evenodd is set if (path->fillRule() == Qt::OddEvenFill) context.shapeWriter().addAttribute("fill-rule", "evenodd"); } } void SvgStyleWriter::saveSvgStroke(KoShape *shape, SvgSavingContext &context) { const QSharedPointer lineBorder = qSharedPointerDynamicCast(shape->stroke()); if (! lineBorder) return; QString strokeStr("none"); if (lineBorder->lineBrush().gradient()) { QString gradientId = saveSvgGradient(lineBorder->lineBrush().gradient(), lineBorder->lineBrush().transform(), context); strokeStr = "url(#" + gradientId + ")"; } else { strokeStr = lineBorder->color().name(); } if (!strokeStr.isEmpty()) context.shapeWriter().addAttribute("stroke", strokeStr); if (lineBorder->color().alphaF() < 1.0) context.shapeWriter().addAttribute("stroke-opacity", lineBorder->color().alphaF()); context.shapeWriter().addAttribute("stroke-width", SvgUtil::toUserSpace(lineBorder->lineWidth())); if (lineBorder->capStyle() == Qt::FlatCap) context.shapeWriter().addAttribute("stroke-linecap", "butt"); else if (lineBorder->capStyle() == Qt::RoundCap) context.shapeWriter().addAttribute("stroke-linecap", "round"); else if (lineBorder->capStyle() == Qt::SquareCap) context.shapeWriter().addAttribute("stroke-linecap", "square"); if (lineBorder->joinStyle() == Qt::MiterJoin) { context.shapeWriter().addAttribute("stroke-linejoin", "miter"); context.shapeWriter().addAttribute("stroke-miterlimit", lineBorder->miterLimit()); } else if (lineBorder->joinStyle() == Qt::RoundJoin) context.shapeWriter().addAttribute("stroke-linejoin", "round"); else if (lineBorder->joinStyle() == Qt::BevelJoin) context.shapeWriter().addAttribute("stroke-linejoin", "bevel"); // dash if (lineBorder->lineStyle() > Qt::SolidLine) { qreal dashFactor = lineBorder->lineWidth(); if (lineBorder->dashOffset() != 0) context.shapeWriter().addAttribute("stroke-dashoffset", dashFactor * lineBorder->dashOffset()); QString dashStr; const QVector dashes = lineBorder->lineDashes(); int dashCount = dashes.size(); for (int i = 0; i < dashCount; ++i) { if (i > 0) dashStr += ","; dashStr += QString("%1").arg(KisDomUtils::toString(dashes[i] * dashFactor)); } context.shapeWriter().addAttribute("stroke-dasharray", dashStr); } } void SvgStyleWriter::saveSvgEffects(KoShape *shape, SvgSavingContext &context) { KoFilterEffectStack * filterStack = shape->filterEffectStack(); if (!filterStack) return; QList filterEffects = filterStack->filterEffects(); if (!filterEffects.count()) return; const QString uid = context.createUID("filter"); filterStack->save(context.styleWriter(), uid); context.shapeWriter().addAttribute("filter", "url(#" + uid + ")"); } void embedShapes(const QList &shapes, KoXmlWriter &outWriter) { QBuffer buffer; buffer.open(QIODevice::WriteOnly); { SvgWriter shapesWriter(shapes, QSizeF(666, 666)); shapesWriter.saveDetached(buffer); } buffer.close(); outWriter.addCompleteElement(&buffer); } void SvgStyleWriter::saveSvgClipping(KoShape *shape, SvgSavingContext &context) { KoClipPath *clipPath = shape->clipPath(); if (!clipPath) return; const QString uid = context.createUID("clippath"); context.styleWriter().startElement("clipPath"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("clipPathUnits", KoFlake::coordinateToString(clipPath->coordinates())); embedShapes(clipPath->clipShapes(), context.styleWriter()); context.styleWriter().endElement(); // clipPath context.shapeWriter().addAttribute("clip-path", "url(#" + uid + ")"); if (clipPath->clipRule() != Qt::WindingFill) context.shapeWriter().addAttribute("clip-rule", "evenodd"); } void SvgStyleWriter::saveSvgMasking(KoShape *shape, SvgSavingContext &context) { KoClipMask*clipMask = shape->clipMask(); if (!clipMask) return; const QString uid = context.createUID("clipmask"); context.styleWriter().startElement("mask"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("maskUnits", KoFlake::coordinateToString(clipMask->coordinates())); context.styleWriter().addAttribute("maskContentUnits", KoFlake::coordinateToString(clipMask->contentCoordinates())); const QRectF rect = clipMask->maskRect(); // think funny duplication? please note the 'pt' suffix! :) if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { context.styleWriter().addAttribute("x", rect.x()); context.styleWriter().addAttribute("y", rect.y()); context.styleWriter().addAttribute("width", rect.width()); context.styleWriter().addAttribute("height", rect.height()); } else { context.styleWriter().addAttributePt("x", rect.x()); context.styleWriter().addAttributePt("y", rect.y()); context.styleWriter().addAttributePt("width", rect.width()); context.styleWriter().addAttributePt("height", rect.height()); } embedShapes(clipMask->shapes(), context.styleWriter()); context.styleWriter().endElement(); // clipMask context.shapeWriter().addAttribute("mask", "url(#" + uid + ")"); } namespace { void writeMarkerStyle(KoXmlWriter &styleWriter, const KoMarker *marker, const QString &assignedId) { styleWriter.startElement("marker"); styleWriter.addAttribute("id", assignedId); styleWriter.addAttribute("markerUnits", KoMarker::coordinateSystemToString(marker->coordinateSystem())); const QPointF refPoint = marker->referencePoint(); styleWriter.addAttribute("refX", refPoint.x()); styleWriter.addAttribute("refY", refPoint.y()); const QSizeF refSize = marker->referenceSize(); styleWriter.addAttribute("markerWidth", refSize.width()); styleWriter.addAttribute("markerHeight", refSize.height()); if (marker->hasAutoOtientation()) { styleWriter.addAttribute("orient", "auto"); } else { // no suffix means 'degrees' styleWriter.addAttribute("orient", kisRadiansToDegrees(marker->explicitOrientation())); } embedShapes(marker->shapes(), styleWriter); styleWriter.endElement(); // marker } void tryEmbedMarker(const KoPathShape *pathShape, const QString &markerTag, KoFlake::MarkerPosition markerPosition, SvgSavingContext &context) { KoMarker *marker = pathShape->marker(markerPosition); if (marker) { const QString uid = context.createUID("lineMarker"); writeMarkerStyle(context.styleWriter(), marker, uid); context.shapeWriter().addAttribute(markerTag.toLatin1().data(), "url(#" + uid + ")"); } } } - void SvgStyleWriter::saveSvgMarkers(KoShape *shape, SvgSavingContext &context) { KoPathShape *pathShape = dynamic_cast(shape); if (!pathShape || !pathShape->hasMarkers()) return; tryEmbedMarker(pathShape, "marker-start", KoFlake::StartMarker, context); tryEmbedMarker(pathShape, "marker-mid", KoFlake::MidMarker, context); tryEmbedMarker(pathShape, "marker-end", KoFlake::EndMarker, context); + + if (pathShape->autoFillMarkers()) { + context.shapeWriter().addAttribute("krita:marker-fill-method", "auto"); + } } void SvgStyleWriter::saveSvgColorStops(const QGradientStops &colorStops, SvgSavingContext &context) { Q_FOREACH (const QGradientStop &stop, colorStops) { context.styleWriter().startElement("stop"); context.styleWriter().addAttribute("stop-color", stop.second.name()); context.styleWriter().addAttribute("offset", stop.first); context.styleWriter().addAttribute("stop-opacity", stop.second.alphaF()); context.styleWriter().endElement(); } } inline QString convertGradientMode(QGradient::CoordinateMode mode) { KIS_ASSERT_RECOVER_NOOP(mode != QGradient::StretchToDeviceMode); return mode == QGradient::ObjectBoundingMode ? "objectBoundingBox" : "userSpaceOnUse"; } QString SvgStyleWriter::saveSvgGradient(const QGradient *gradient, const QTransform &gradientTransform, SvgSavingContext &context) { if (! gradient) return QString(); Q_ASSERT(gradient->coordinateMode() == QGradient::ObjectBoundingMode); const QString spreadMethod[3] = { QString("pad"), QString("reflect"), QString("repeat") }; const QString uid = context.createUID("gradient"); if (gradient->type() == QGradient::LinearGradient) { const QLinearGradient * g = static_cast(gradient); context.styleWriter().startElement("linearGradient"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("gradientTransform", SvgUtil::transformToString(gradientTransform)); context.styleWriter().addAttribute("gradientUnits", convertGradientMode(g->coordinateMode())); context.styleWriter().addAttribute("x1", g->start().x()); context.styleWriter().addAttribute("y1", g->start().y()); context.styleWriter().addAttribute("x2", g->finalStop().x()); context.styleWriter().addAttribute("y2", g->finalStop().y()); context.styleWriter().addAttribute("spreadMethod", spreadMethod[g->spread()]); // color stops saveSvgColorStops(gradient->stops(), context); context.styleWriter().endElement(); } else if (gradient->type() == QGradient::RadialGradient) { const QRadialGradient * g = static_cast(gradient); context.styleWriter().startElement("radialGradient"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("gradientTransform", SvgUtil::transformToString(gradientTransform)); context.styleWriter().addAttribute("gradientUnits", convertGradientMode(g->coordinateMode())); context.styleWriter().addAttribute("cx", g->center().x()); context.styleWriter().addAttribute("cy", g->center().y()); context.styleWriter().addAttribute("fx", g->focalPoint().x()); context.styleWriter().addAttribute("fy", g->focalPoint().y()); context.styleWriter().addAttribute("r", g->radius()); context.styleWriter().addAttribute("spreadMethod", spreadMethod[g->spread()]); // color stops saveSvgColorStops(gradient->stops(), context); context.styleWriter().endElement(); } else if (gradient->type() == QGradient::ConicalGradient) { //const QConicalGradient * g = static_cast( gradient ); // fake conical grad as radial. // fugly but better than data loss. /* printIndentation( m_defs, m_indent2 ); *m_defs << "center().x() << "\" "; *m_defs << "cy=\"" << g->center().y() << "\" "; *m_defs << "fx=\"" << grad.focalPoint().x() << "\" "; *m_defs << "fy=\"" << grad.focalPoint().y() << "\" "; double r = sqrt( pow( grad.vector().x() - grad.origin().x(), 2 ) + pow( grad.vector().y() - grad.origin().y(), 2 ) ); *m_defs << "r=\"" << QString().setNum( r ) << "\" "; *m_defs << spreadMethod[g->spread()]; *m_defs << ">" << endl; // color stops getColorStops( gradient->stops() ); printIndentation( m_defs, m_indent2 ); *m_defs << "" << endl; *m_body << "url(#" << uid << ")"; */ } return uid; } QString SvgStyleWriter::saveSvgPattern(QSharedPointer pattern, KoShape *shape, SvgSavingContext &context) { const QString uid = context.createUID("pattern"); const QSizeF shapeSize = shape->size(); const QSizeF patternSize = pattern->patternDisplaySize(); const QSize imageSize = pattern->pattern().size(); // calculate offset in point QPointF offset = pattern->referencePointOffset(); offset.rx() = 0.01 * offset.x() * patternSize.width(); offset.ry() = 0.01 * offset.y() * patternSize.height(); // now take the reference point into account switch (pattern->referencePoint()) { case KoPatternBackground::TopLeft: break; case KoPatternBackground::Top: offset += QPointF(0.5 * shapeSize.width(), 0.0); break; case KoPatternBackground::TopRight: offset += QPointF(shapeSize.width(), 0.0); break; case KoPatternBackground::Left: offset += QPointF(0.0, 0.5 * shapeSize.height()); break; case KoPatternBackground::Center: offset += QPointF(0.5 * shapeSize.width(), 0.5 * shapeSize.height()); break; case KoPatternBackground::Right: offset += QPointF(shapeSize.width(), 0.5 * shapeSize.height()); break; case KoPatternBackground::BottomLeft: offset += QPointF(0.0, shapeSize.height()); break; case KoPatternBackground::Bottom: offset += QPointF(0.5 * shapeSize.width(), shapeSize.height()); break; case KoPatternBackground::BottomRight: offset += QPointF(shapeSize.width(), shapeSize.height()); break; } offset = shape->absoluteTransformation(0).map(offset); context.styleWriter().startElement("pattern"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("x", SvgUtil::toUserSpace(offset.x())); context.styleWriter().addAttribute("y", SvgUtil::toUserSpace(offset.y())); if (pattern->repeat() == KoPatternBackground::Stretched) { context.styleWriter().addAttribute("width", "100%"); context.styleWriter().addAttribute("height", "100%"); context.styleWriter().addAttribute("patternUnits", "objectBoundingBox"); } else { context.styleWriter().addAttribute("width", SvgUtil::toUserSpace(patternSize.width())); context.styleWriter().addAttribute("height", SvgUtil::toUserSpace(patternSize.height())); context.styleWriter().addAttribute("patternUnits", "userSpaceOnUse"); } context.styleWriter().addAttribute("viewBox", QString("0 0 %1 %2").arg(KisDomUtils::toString(imageSize.width())).arg(KisDomUtils::toString(imageSize.height()))); //*m_defs << " patternContentUnits=\"userSpaceOnUse\""; context.styleWriter().startElement("image"); context.styleWriter().addAttribute("x", "0"); context.styleWriter().addAttribute("y", "0"); context.styleWriter().addAttribute("width", QString("%1px").arg(KisDomUtils::toString(imageSize.width()))); context.styleWriter().addAttribute("height", QString("%1px").arg(KisDomUtils::toString(imageSize.height()))); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if (pattern->pattern().save(&buffer, "PNG")) { const QString mimeType = KisMimeDatabase::mimeTypeForSuffix("*.png"); context.styleWriter().addAttribute("xlink:href", "data:"+ mimeType + ";base64," + ba.toBase64()); } context.styleWriter().endElement(); // image context.styleWriter().endElement(); // pattern return uid; } QString SvgStyleWriter::saveSvgVectorPattern(QSharedPointer pattern, KoShape *parentShape, SvgSavingContext &context) { const QString uid = context.createUID("pattern"); context.styleWriter().startElement("pattern"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("patternUnits", KoFlake::coordinateToString(pattern->referenceCoordinates())); context.styleWriter().addAttribute("patternContentUnits", KoFlake::coordinateToString(pattern->contentCoordinates())); const QRectF rect = pattern->referenceRect(); if (pattern->referenceCoordinates() == KoFlake::ObjectBoundingBox) { context.styleWriter().addAttribute("x", rect.x()); context.styleWriter().addAttribute("y", rect.y()); context.styleWriter().addAttribute("width", rect.width()); context.styleWriter().addAttribute("height", rect.height()); } else { context.styleWriter().addAttributePt("x", rect.x()); context.styleWriter().addAttributePt("y", rect.y()); context.styleWriter().addAttributePt("width", rect.width()); context.styleWriter().addAttributePt("height", rect.height()); } context.styleWriter().addAttribute("patternTransform", SvgUtil::transformToString(pattern->patternTransform())); if (pattern->contentCoordinates() == KoFlake::ObjectBoundingBox) { // TODO: move this normalization into the KoVectorPatternBackground itself QList shapes = pattern->shapes(); QList clonedShapes; const QRectF dstShapeBoundingRect = parentShape->outlineRect(); const QTransform relativeToShape = KisAlgebra2D::mapToRect(dstShapeBoundingRect); const QTransform shapeToRelative = relativeToShape.inverted(); Q_FOREACH (KoShape *shape, shapes) { KoShape *clone = shape->cloneShape(); clone->applyAbsoluteTransformation(shapeToRelative); clonedShapes.append(clone); } embedShapes(clonedShapes, context.styleWriter()); qDeleteAll(clonedShapes); } else { QList shapes = pattern->shapes(); embedShapes(shapes, context.styleWriter()); } context.styleWriter().endElement(); // pattern return uid; } diff --git a/libs/flake/tests/TestSvgParser.cpp b/libs/flake/tests/TestSvgParser.cpp index 5f7850bf30..66f287deed 100644 --- a/libs/flake/tests/TestSvgParser.cpp +++ b/libs/flake/tests/TestSvgParser.cpp @@ -1,3001 +1,3102 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TestSvgParser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_algebra_2d.h" struct SvgTester { SvgTester (const QString &data) : parser(&resourceManager) { QVERIFY(doc.setContent(data.toLatin1())); root = doc.documentElement(); parser.setXmlBaseDir("./"); savedData = data; //printf("%s", savedData.toLatin1().data()); } ~SvgTester () { qDeleteAll(shapes); } void run() { shapes = parser.parseSvg(root, &fragmentSize); } KoShape* findShape(const QString &name, KoShape *parent = 0) { if (parent && parent->name() == name) { return parent; } QList children; if (!parent) { children = shapes; } else { KoShapeContainer *cont = dynamic_cast(parent); if (cont) { children = cont->shapes(); } } Q_FOREACH (KoShape *shape, children) { KoShape *result = findShape(name, shape); if (result) { return result; } } return 0; } KoShapeGroup* findGroup(const QString &name) { KoShapeGroup *group = 0; KoShape *shape = findShape(name); if (shape) { group = dynamic_cast(shape); } return group; } KoDocumentResourceManager resourceManager; SvgParser parser; KoXmlDocument doc; KoXmlElement root; QSizeF fragmentSize; QList shapes; QString savedData; }; void TestSvgParser::testUnitPx() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitPxResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testUnitPt() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitIn() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,720,1440), 36)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(72, 72)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(720,1440)); } void TestSvgParser::testUnitPercentInitial() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 80, 80) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testScalingViewport() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeetAlign() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 24) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,12)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,12)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,28)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,28)); } void TestSvgParser::testScalingViewportKeepSlice1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepSlice2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.25, 0.25)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(1,1)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(4,1)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(1,9)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(4,9)); } void TestSvgParser::testScalingViewportPercentInternal() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testParsePreserveAspectRatio() { { SvgUtil::PreserveAspectRatioParser p(" defer xMinYMax meet"); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Max); } { SvgUtil::PreserveAspectRatioParser p(" xMinYMid slice"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatioByExpanding); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" xmidYMid "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Middle); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" NoNe "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("defer NoNe "); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("sweet brown fox jumps over a nice svg file"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } } #include "parsers/SvgTransformParser.h" void TestSvgParser::testParseTransform() { { QString str("translate(-111.0, 33) translate(-111.0, 33) matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), true); } { // forget about one brace QString str("translate(-111.0, 33) translate(-111.0, 33 matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), false); } { SvgTransformParser p("translate(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 0)); } { SvgTransformParser p("scale(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 50)); } { SvgTransformParser p("scale(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 100)); } { SvgTransformParser p("rotate(90 70 74.0)"); QCOMPARE(p.isValid(), true); QTransform t; t.rotate(90); t = QTransform::fromTranslate(-70, -74) * t * QTransform::fromTranslate(70, 74); qDebug() << ppVar(p.transform()); QCOMPARE(p.transform(), t); } } void TestSvgParser::testScalingViewportTransform() { /** * Note: 'transform' affects all the attributes of the *current* * element, while 'viewBox' affects only the decendants! */ const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(10, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(11,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(5,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(11,18)); } void TestSvgParser::testTransformNesting() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformNestingGroups() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformRotation1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(-20,0,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(-20,10)); } void TestSvgParser::testTransformRotation2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(5,5,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,15)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(25,5)); } #include "../../sdk/tests/qimage_test_util.h" #ifdef USE_ROUND_TRIP #include "SvgWriter.h" #include #include #endif struct SvgRenderTester : public SvgTester { SvgRenderTester(const QString &data) : SvgTester(data) { } static void testRender(KoShape *shape, const QString &prefix, const QString &testName, const QSize canvasSize) { QImage canvas(canvasSize, QImage::Format_ARGB32); canvas.fill(0); KoViewConverter converter; QPainter painter(&canvas); KoShapePainter p; p.setShapes({shape}); painter.setClipRect(canvas.rect()); p.paint(painter, converter); QVERIFY(TestUtil::checkQImage(canvas, "svg_render", prefix, testName)); } void test_standard_30px_72ppi(const QString &testName, bool verifyGeometry = true, const QSize &canvasSize = QSize(30,30)) { parser.setResolution(QRectF(0, 0, 30, 30) /* px */, 72 /* ppi */); run(); #ifdef USE_CLONED_SHAPES { QList newShapes; Q_FOREACH (KoShape *shape, shapes) { KoShape *clonedShape = shape->cloneShape(); KIS_ASSERT(clonedShape); newShapes << clonedShape; } qDeleteAll(shapes); shapes = newShapes; } #endif /* USE_CLONED_SHAPES */ #ifdef USE_ROUND_TRIP const QSizeF sizeInPt(30,30); QBuffer writeBuf; writeBuf.open(QIODevice::WriteOnly); { SvgWriter writer(shapes, sizeInPt); writer.save(writeBuf); } QDomDocument prettyDoc; prettyDoc.setContent(savedData); qDebug(); printf("\n=== Original: ===\n\n%s\n", prettyDoc.toByteArray(4).data()); printf("\n=== Saved: ===\n\n%s\n", writeBuf.data().data()); qDebug(); QVERIFY(doc.setContent(writeBuf.data())); root = doc.documentElement(); run(); #endif /* USE_ROUND_TRIP */ KoShape *shape = findShape("testRect"); KIS_ASSERT(shape); if (verifyGeometry) { QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,5)); const QPointF bottomRight= shape->absolutePosition(KoFlake::BottomRight); const QPointF expectedBottomRight(15,25); if (KisAlgebra2D::norm(bottomRight - expectedBottomRight) > 0.0001 ) { QCOMPARE(bottomRight, expectedBottomRight); } } testRender(shape, "load", testName, canvasSize); } }; void TestSvgParser::testRenderStrokeNone() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeColorName() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex3() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex6() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbValues() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbPercent() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorCurrent() { const QString data = "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorNonexistentIri() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testRenderStrokeZeroWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_0_3_opacity"); } void TestSvgParser::testRenderStrokeJointRound() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_join_round"); } void TestSvgParser::testRenderStrokeLinecap() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_linecap_round"); } void TestSvgParser::testRenderStrokeMiterLimit() { // TODO:seems like doesn't work!! qWarning() << "WARNING: Miter limit test is skipped!!!"; return; const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_miter_limit"); } void TestSvgParser::testRenderStrokeDashArrayEven() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even"); } void TestSvgParser::testRenderStrokeDashArrayEvenOffset() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even_offset"); } void TestSvgParser::testRenderStrokeDashArrayOdd() { // SVG 1.1: if the dasharray is odd, repeat it const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_odd"); } void TestSvgParser::testRenderStrokeDashArrayRelative() { // SVG 1.1: relative to view box // (40 x 50) * sqrt(2) => dash length = 5 px const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_relative"); } void TestSvgParser::testRenderFillDefault() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_black"); } void TestSvgParser::testRenderFillRuleNonZero() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_non_zero"); } void TestSvgParser::testRenderFillRuleEvenOdd() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_even_odd"); } void TestSvgParser::testRenderFillOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_opacity_0_3"); } void TestSvgParser::testRenderDisplayAttribute() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } void TestSvgParser::testRenderVisibilityAttribute() { { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), true); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } } void TestSvgParser::testRenderVisibilityInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QCOMPARE(shape->isVisible(true), false); } void TestSvgParser::testRenderDisplayInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QEXPECT_FAIL("", "TODO: Fix 'display' attribute not to be inherited in shapes heirarchy!", Continue); QCOMPARE(shape->isVisible(true), true); } void TestSvgParser::testRenderStrokeWithInlineStyle() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testIccColor() { const QString data = "" "" " " " " " " "" ""; SvgRenderTester t (data); int numFetches = 0; t.parser.setFileFetcher( [&numFetches](const QString &name) { numFetches++; const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); }); t.test_standard_30px_72ppi("stroke_blue_width_2"); QCOMPARE(numFetches, 1); } void TestSvgParser::testRenderFillLinearGradientRelativePercent() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientRelativePortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientStopPortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientTransform() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical"); } void TestSvgParser::testRenderFillLinearGradientTransformUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical_in_user"); } void TestSvgParser::testRenderFillLinearGradientRotatedShape() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated", false); } void TestSvgParser::testRenderFillLinearGradientRotatedShapeUserCoord() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated_in_user", false); } void TestSvgParser::testRenderFillRadialGradient() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_radial"); } void TestSvgParser::testRenderFillRadialGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_radial_in_user"); } void TestSvgParser::testRenderFillLinearGradientUserCoordPercent() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderStrokeLinearGradient() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_gradient_dashed"); } QTransform rotateTransform(qreal degree, const QPointF ¢er) { QTransform rotate; rotate.rotate(degree); return QTransform::fromTranslate(-center.x(), -center.y()) * rotate * QTransform::fromTranslate(center.x(), center.y()); } QTransform viewTransform(const QRectF &src, const QRectF &dst) { return QTransform::fromTranslate(-src.x(), -src.y()) * QTransform::fromScale(dst.width() / src.width(), dst.height() / src.height()) * QTransform::fromTranslate(dst.x(), dst.y()); } QPainterPath bakeShape(const QPainterPath &path, const QTransform &bakeTransform, bool contentIsObb = false, const QRectF &shapeBoundingRect = QRectF(), bool contentIsViewBox = false, const QRectF &viewBoxRect= QRectF(), const QRectF &refRect = QRectF()) { const QTransform relativeToShape(shapeBoundingRect.width(), 0, 0, shapeBoundingRect.height(), shapeBoundingRect.x(), shapeBoundingRect.y()); QTransform newTransform = bakeTransform; if (contentIsObb) { newTransform = relativeToShape * newTransform; } if (contentIsViewBox) { newTransform = viewTransform(viewBoxRect, refRect) * newTransform; } return newTransform.map(path); } #include void renderBakedPath(QPainter &painter, const QPainterPath &bakedFillPath, const QTransform &bakedTransform, const QRect &shapeOutline, const QTransform &shapeTransform, const QRectF &referenceRect, bool contentIsObb, const QRectF &bakedShapeBoundingRect, bool referenceIsObb, const QTransform &patternTransform, QImage *stampResult) { painter.setTransform(QTransform()); painter.setPen(Qt::NoPen); QPainterPath shapeOutlinePath; shapeOutlinePath.addRect(shapeOutline); KoBakedShapeRenderer renderer( shapeOutlinePath, shapeTransform, bakedTransform, referenceRect, contentIsObb, bakedShapeBoundingRect, referenceIsObb, patternTransform); QPainter *patchPainter = renderer.bakeShapePainter(); patchPainter->fillPath(bakedFillPath, Qt::blue); patchPainter->end(); renderer.renderShape(painter); if (stampResult) { *stampResult = renderer.patchImage(); } } void TestSvgParser::testManualRenderPattern_ContentUser_RefObb() { const QRectF referenceRect(0, 0, 1.0, 0.5); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb() { const QRectF referenceRect(0.3, 0.3, 0.4, 0.4); QPainterPath fillPath; fillPath.addRect(QRectF(0.4, 0.4, 0.2, 0.2)); fillPath.addRect(QRectF(0.6, 0.5, 0.1, 0.1)); fillPath.addRect(QRectF(0.3, 0.4, 0.1, 0.1)); const QRect bakedShapeRect(2,2,10,10); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill1")); // Move to a different shape QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentUser_RefUser() { const QRectF referenceRect(5, 2, 8, 8); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb_Transform_Rotate() { const QRectF referenceRect(0.0, 0.0, 0.4, 0.2); QPainterPath fillPath; fillPath.addRect(QRectF(0.0, 0.0, 0.5, 0.1)); fillPath.addRect(QRectF(0.0, 0.1, 0.1, 0.1)); const QRect bakedShapeRect(2,1,10,10); QTransform bakedTransform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(10,10); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); QTransform patternTransform; patternTransform.rotate(90); patternTransform = patternTransform * QTransform::fromTranslate(0.5, 0.0); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefObb() { const QRectF referenceRect(0, 0, 0.5, 1.0/3.0); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, true, shape1OutlineRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, shape1OutlineRect, true, rotateTransform(90, QPointF(0, 1.0 / 3.0)), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefUser() { const QRectF referenceRect(60, 0, 30, 20); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, false, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, shape1OutlineRect, false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); QTransform patternTransform2 = rotateTransform(90, QPointF()) * QTransform::fromTranslate(40, 10); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, shape1OutlineRect, false, patternTransform2, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill2")); } void TestSvgParser::testRenderPattern_r_User_c_User() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View() { const QString data = "" "" " " " " " " // y is changed to 39 from 40 to fix a rounding issue! " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_Obb() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View_Rotated() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated", false, QSize(220, 160)); } void TestSvgParser::testRenderPattern_r_Obb_c_View_Rotated() { /** * This test case differs from any application existent in the world :( * * Chrome and Firefox premultiply the patternTransform instead of doing post- * multiplication. Photoshop forgets to multiply the reference rect on it. * * So... */ const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated_odd", false, QSize(220, 160)); } #include #include #include #include void TestSvgParser::testKoClipPathRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,7); clipPath2.lineTo(30,7); clipPath2.lineTo(15,30); clipPath2.lineTo(0,7); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false, true, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::UserSpaceOnUse); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "clip_render_test", QSize(30,30)); } void TestSvgParser::testKoClipPathRelativeRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,0); clipPath2.lineTo(1,0); clipPath2.lineTo(0.5,1); clipPath2.lineTo(0,0); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false, true, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::ObjectBoundingBox); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "relative_clip_render_test", QSize(30,30)); } void TestSvgParser::testRenderClipPath_User() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("relative_clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb_Transform() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test_rotated", false); } void TestSvgParser::testRenderClipMask_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_User() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } QByteArray fileFetcherFunc(const QString &name) { const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); } void TestSvgParser::testRenderImage_AspectDefault() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_default", false); } void TestSvgParser::testRenderImage_AspectNone() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_none", false); } void TestSvgParser::testRenderImage_AspectMeet() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_meet", false); } void TestSvgParser::testRectShapeRoundUniformX() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundUniformY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundXY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testRectShapeRoundXYOverflow() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testCircleShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("circle", false); } void TestSvgParser::testEllipseShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("ellipse", false); } void TestSvgParser::testLineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("line", false); } void TestSvgParser::testPolylineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polyline", false); } void TestSvgParser::testPolygonShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testPathShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testDefsHidden() { const QString data = "" "" " " " " " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("test_defs_hidden", false); } void TestSvgParser::testDefsUseInheritance() { const QString data = "" "" " " " " " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("defs_use_inheritance", false); } void TestSvgParser::testUseWithoutDefs() { const QString data = "" // technical rect for rendering "" "" " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("use_without_defs", false); } void TestSvgParser::testMarkersAutoOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers", false); } void TestSvgParser::testMarkersAutoOrientationScaled() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_scaled", false); } void TestSvgParser::testMarkersAutoOrientationScaledUserCoordinates() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_user_coordinates", false); } void TestSvgParser::testMarkersCustomOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_custom_orientation", false); } void TestSvgParser::testMarkersDifferent() { const QString data = "" "" " " " " " " "" "" " " " " " " "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_different", false); } +void TestSvgParser::testMarkersFillAsShape() +{ + const QString data = + "" + + "" + " " + " " + " " + " " + + " " + + " " + " " + " " + " " + + "" + + "" + //" d=\"M5,15 L25,15\"/>" + + ""; + + SvgRenderTester t (data); + + t.test_standard_30px_72ppi("markers_scaled_fill_as_shape", false); +} + + +void TestSvgParser::testGradientRecoveringTrasnform() +{ + // used for experimenting purposes only! + + QImage image(100,100,QImage::Format_ARGB32); + image.fill(0); + QPainter painter(&image); + + painter.setPen(QPen(Qt::black, 0)); + + + QLinearGradient gradient(0, 0.5, 1, 0.5); + gradient.setCoordinateMode(QGradient::ObjectBoundingMode); + + //QLinearGradient gradient(0, 50, 100, 50); + //gradient.setCoordinateMode(QGradient::LogicalMode); + + gradient.setColorAt(0.0, Qt::red); + gradient.setColorAt(1.0, Qt::blue); + + QTransform gradientTrasnform; + gradientTrasnform.shear(0.2, 0); + + { + QBrush brush(gradient); + brush.setTransform(gradientTrasnform); + painter.setBrush(brush); + } + + QRect mainShape(3,3,94,94); + painter.drawRect(mainShape); + + QTransform gradientToUser(mainShape.width(), 0, 0, mainShape.height(), + mainShape.x(), mainShape.y()); + + QRect smallShape(0,0,20,20); + QTransform smallShapeTransform; + + { + smallShapeTransform = + QTransform::fromTranslate(-smallShape.center().x(), -smallShape.center().y()); + + QTransform r; r.rotate(90); + smallShapeTransform *= r; + + smallShapeTransform *= + QTransform::fromTranslate(mainShape.center().x(), mainShape.center().y()); + } + + + { + gradient.setCoordinateMode(QGradient::LogicalMode); + QBrush brush(gradient); + brush.setTransform(gradientTrasnform * gradientToUser * smallShapeTransform.inverted()); + painter.setBrush(brush); + painter.setPen(Qt::NoPen); + } + + painter.setTransform(smallShapeTransform); + painter.drawRect(smallShape); + + //image.save("gradient_recovering_transform.png"); +} QTEST_MAIN(TestSvgParser) diff --git a/libs/flake/tests/TestSvgParser.h b/libs/flake/tests/TestSvgParser.h index 6ce35837d4..080b498eaf 100644 --- a/libs/flake/tests/TestSvgParser.h +++ b/libs/flake/tests/TestSvgParser.h @@ -1,161 +1,165 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TESTSVGPARSER_H #define TESTSVGPARSER_H #include class TestSvgParser : public QObject { Q_OBJECT private Q_SLOTS: void testUnitPx(); void testUnitPxResolution(); void testUnitPt(); void testUnitIn(); void testUnitPercentInitial(); void testScalingViewport(); void testScalingViewportKeepMeet1(); void testScalingViewportKeepMeet2(); void testScalingViewportKeepMeetAlign(); void testScalingViewportKeepSlice1(); void testScalingViewportKeepSlice2(); void testScalingViewportResolution(); void testScalingViewportPercentInternal(); void testParsePreserveAspectRatio(); void testParseTransform(); void testScalingViewportTransform(); void testTransformNesting(); void testTransformNestingGroups(); void testTransformRotation1(); void testTransformRotation2(); void testRenderStrokeNone(); void testRenderStrokeColorName(); void testRenderStrokeColorHex3(); void testRenderStrokeColorHex6(); void testRenderStrokeColorRgbValues(); void testRenderStrokeColorRgbPercent(); void testRenderStrokeColorCurrent(); void testRenderStrokeColorNonexistentIri(); void testRenderStrokeWidth(); void testRenderStrokeZeroWidth(); void testRenderStrokeOpacity(); void testRenderStrokeJointRound(); void testRenderStrokeLinecap(); void testRenderStrokeMiterLimit(); void testRenderStrokeDashArrayEven(); void testRenderStrokeDashArrayEvenOffset(); void testRenderStrokeDashArrayOdd(); void testRenderStrokeDashArrayRelative(); void testRenderFillDefault(); void testRenderFillRuleNonZero(); void testRenderFillRuleEvenOdd(); void testRenderFillOpacity(); void testRenderDisplayAttribute(); void testRenderVisibilityAttribute(); void testRenderVisibilityInheritance(); void testRenderDisplayInheritance(); void testRenderStrokeWithInlineStyle(); void testIccColor(); void testRenderFillLinearGradientRelativePercent(); void testRenderFillLinearGradientRelativePortion(); void testRenderFillLinearGradientUserCoord(); void testRenderFillLinearGradientStopPortion(); void testRenderFillLinearGradientTransform(); void testRenderFillLinearGradientTransformUserCoord(); void testRenderFillLinearGradientRotatedShape(); void testRenderFillLinearGradientRotatedShapeUserCoord(); void testRenderFillRadialGradient(); void testRenderFillRadialGradientUserCoord(); void testRenderFillLinearGradientUserCoordPercent(); void testRenderStrokeLinearGradient(); void testManualRenderPattern_ContentUser_RefObb(); void testManualRenderPattern_ContentObb_RefObb(); void testManualRenderPattern_ContentUser_RefUser(); void testManualRenderPattern_ContentObb_RefObb_Transform_Rotate(); void testManualRenderPattern_ContentView_RefObb(); void testManualRenderPattern_ContentView_RefUser(); void testRenderPattern_r_User_c_User(); void testRenderPattern_r_User_c_View(); void testRenderPattern_r_User_c_Obb(); void testRenderPattern_r_User_c_View_Rotated(); void testRenderPattern_r_Obb_c_View_Rotated(); void testKoClipPathRendering(); void testKoClipPathRelativeRendering(); void testRenderClipPath_User(); void testRenderClipPath_Obb(); void testRenderClipPath_Obb_Transform(); void testRenderClipMask_Obb(); void testRenderClipMask_User_Clip_Obb(); void testRenderClipMask_User_Clip_User(); void testRenderImage_AspectDefault(); void testRenderImage_AspectNone(); void testRenderImage_AspectMeet(); void testRectShapeRoundUniformX(); void testRectShapeRoundUniformY(); void testRectShapeRoundXY(); void testRectShapeRoundXYOverflow(); void testCircleShape(); void testEllipseShape(); void testLineShape(); void testPolylineShape(); void testPolygonShape(); void testPathShape(); void testDefsHidden(); void testDefsUseInheritance(); void testUseWithoutDefs(); void testMarkersAutoOrientation(); void testMarkersAutoOrientationScaled(); void testMarkersAutoOrientationScaledUserCoordinates(); void testMarkersCustomOrientation(); void testMarkersDifferent(); + + void testMarkersFillAsShape(); + + void testGradientRecoveringTrasnform(); }; #endif // TESTSVGPARSER_H diff --git a/libs/flake/tests/data/svg_render/load_markers_scaled_fill_as_shape.png b/libs/flake/tests/data/svg_render/load_markers_scaled_fill_as_shape.png new file mode 100644 index 0000000000..48fa548444 Binary files /dev/null and b/libs/flake/tests/data/svg_render/load_markers_scaled_fill_as_shape.png differ diff --git a/libs/global/kis_algebra_2d.cpp b/libs/global/kis_algebra_2d.cpp index 75c837a680..1ba61509d0 100644 --- a/libs/global/kis_algebra_2d.cpp +++ b/libs/global/kis_algebra_2d.cpp @@ -1,584 +1,570 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_algebra_2d.h" #include #include #include #include #include #include #include #include #include #define SANITY_CHECKS namespace KisAlgebra2D { void adjustIfOnPolygonBoundary(const QPolygonF &poly, int polygonDirection, QPointF *pt) { const int numPoints = poly.size(); for (int i = 0; i < numPoints; i++) { int nextI = i + 1; if (nextI >= numPoints) { nextI = 0; } const QPointF &p0 = poly[i]; const QPointF &p1 = poly[nextI]; QPointF edge = p1 - p0; qreal cross = crossProduct(edge, *pt - p0) / (0.5 * edge.manhattanLength()); if (cross < 1.0 && isInRange(pt->x(), p0.x(), p1.x()) && isInRange(pt->y(), p0.y(), p1.y())) { QPointF salt = 1.0e-3 * inwardUnitNormal(edge, polygonDirection); QPointF adjustedPoint = *pt + salt; // in case the polygon is self-intersecting, polygon direction // might not help if (kisDistanceToLine(adjustedPoint, QLineF(p0, p1)) < 1e-4) { adjustedPoint = *pt - salt; #ifdef SANITY_CHECKS if (kisDistanceToLine(adjustedPoint, QLineF(p0, p1)) < 1e-4) { dbgKrita << ppVar(*pt); dbgKrita << ppVar(adjustedPoint); dbgKrita << ppVar(QLineF(p0, p1)); dbgKrita << ppVar(salt); dbgKrita << ppVar(poly.containsPoint(*pt, Qt::OddEvenFill)); dbgKrita << ppVar(kisDistanceToLine(*pt, QLineF(p0, p1))); dbgKrita << ppVar(kisDistanceToLine(adjustedPoint, QLineF(p0, p1))); } *pt = adjustedPoint; KIS_ASSERT_RECOVER_NOOP(kisDistanceToLine(*pt, QLineF(p0, p1)) > 1e-4); #endif /* SANITY_CHECKS */ } } } } QPointF transformAsBase(const QPointF &pt, const QPointF &base1, const QPointF &base2) { qreal len1 = norm(base1); if (len1 < 1e-5) return pt; qreal sin1 = base1.y() / len1; qreal cos1 = base1.x() / len1; qreal len2 = norm(base2); if (len2 < 1e-5) return QPointF(); qreal sin2 = base2.y() / len2; qreal cos2 = base2.x() / len2; qreal sinD = sin2 * cos1 - cos2 * sin1; qreal cosD = cos1 * cos2 + sin1 * sin2; qreal scaleD = len2 / len1; QPointF result; result.rx() = scaleD * (pt.x() * cosD - pt.y() * sinD); result.ry() = scaleD * (pt.x() * sinD + pt.y() * cosD); return result; } qreal angleBetweenVectors(const QPointF &v1, const QPointF &v2) { qreal a1 = std::atan2(v1.y(), v1.x()); qreal a2 = std::atan2(v2.y(), v2.x()); return a2 - a1; } QPainterPath smallArrow() { QPainterPath p; p.moveTo(5, 2); p.lineTo(-3, 8); p.lineTo(-5, 5); p.lineTo( 2, 0); p.lineTo(-5,-5); p.lineTo(-3,-8); p.lineTo( 5,-2); p.arcTo(QRectF(3, -2, 4, 4), 90, -180); return p; } template inline Point ensureInRectImpl(Point pt, const Rect &bounds) { if (pt.x() > bounds.right()) { pt.rx() = bounds.right(); } else if (pt.x() < bounds.left()) { pt.rx() = bounds.left(); } if (pt.y() > bounds.bottom()) { pt.ry() = bounds.bottom(); } else if (pt.y() < bounds.top()) { pt.ry() = bounds.top(); } return pt; } QPoint ensureInRect(QPoint pt, const QRect &bounds) { return ensureInRectImpl(pt, bounds); } QPointF ensureInRect(QPointF pt, const QRectF &bounds) { return ensureInRectImpl(pt, bounds); } -QRect ensureRectNotSmaller(QRect rc, const QSize &size) -{ - if (rc.width() < size.width() || - rc.height() < size.height()) { - - int width = qMax(rc.width(), size.width()); - int height = qMax(rc.height(), size.height()); - - rc = QRect(rc.topLeft(), QSize(width, height)); - } - - return rc; -} - bool intersectLineRect(QLineF &line, const QRect rect) { QPointF pt1 = QPointF(), pt2 = QPointF(); QPointF tmp; if (line.intersect(QLineF(rect.topLeft(), rect.topRight()), &tmp) != QLineF::NoIntersection) { if (tmp.x() >= rect.left() && tmp.x() <= rect.right()) { pt1 = tmp; } } if (line.intersect(QLineF(rect.topRight(), rect.bottomRight()), &tmp) != QLineF::NoIntersection) { if (tmp.y() >= rect.top() && tmp.y() <= rect.bottom()) { if (pt1.isNull()) pt1 = tmp; else pt2 = tmp; } } if (line.intersect(QLineF(rect.bottomRight(), rect.bottomLeft()), &tmp) != QLineF::NoIntersection) { if (tmp.x() >= rect.left() && tmp.x() <= rect.right()) { if (pt1.isNull()) pt1 = tmp; else pt2 = tmp; } } if (line.intersect(QLineF(rect.bottomLeft(), rect.topLeft()), &tmp) != QLineF::NoIntersection) { if (tmp.y() >= rect.top() && tmp.y() <= rect.bottom()) { if (pt1.isNull()) pt1 = tmp; else pt2 = tmp; } } if (pt1.isNull() || pt2.isNull()) return false; // Attempt to retain polarity of end points if ((line.x1() < line.x2()) != (pt1.x() > pt2.x()) || (line.y1() < line.y2()) != (pt1.y() > pt2.y())) { tmp = pt1; pt1 = pt2; pt2 = tmp; } line.setP1(pt1); line.setP2(pt2); return true; } template QVector sampleRectWithPoints(const Rect &rect) { QVector points; Point m1 = 0.5 * (rect.topLeft() + rect.topRight()); Point m2 = 0.5 * (rect.bottomLeft() + rect.bottomRight()); points << rect.topLeft(); points << m1; points << rect.topRight(); points << 0.5 * (rect.topLeft() + rect.bottomLeft()); points << 0.5 * (m1 + m2); points << 0.5 * (rect.topRight() + rect.bottomRight()); points << rect.bottomLeft(); points << m2; points << rect.bottomRight(); return points; } QVector sampleRectWithPoints(const QRect &rect) { return sampleRectWithPoints(rect); } QVector sampleRectWithPoints(const QRectF &rect) { return sampleRectWithPoints(rect); } template Rect approximateRectFromPointsImpl(const QVector &points) { using namespace boost::accumulators; accumulator_set > accX; accumulator_set > accY; Q_FOREACH (const Point &pt, points) { accX(pt.x()); accY(pt.y()); } Rect resultRect; if (alignPixels) { resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)), std::ceil(max(accX)), std::ceil(max(accY))); } else { resultRect.setCoords(min(accX), min(accY), max(accX), max(accY)); } return resultRect; } QRect approximateRectFromPoints(const QVector &points) { return approximateRectFromPointsImpl(points); } QRectF approximateRectFromPoints(const QVector &points) { return approximateRectFromPointsImpl(points); } QRect approximateRectWithPointTransform(const QRect &rect, std::function func) { QVector points = sampleRectWithPoints(rect); using namespace boost::accumulators; accumulator_set > accX; accumulator_set > accY; Q_FOREACH (const QPoint &pt, points) { QPointF dstPt = func(pt); accX(dstPt.x()); accY(dstPt.y()); } QRect resultRect; resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)), std::ceil(max(accX)), std::ceil(max(accY))); return resultRect; } QRectF cutOffRect(const QRectF &rc, const KisAlgebra2D::RightHalfPlane &p) { QVector points; const QLineF cutLine = p.getLine(); points << rc.topLeft(); points << rc.topRight(); points << rc.bottomRight(); points << rc.bottomLeft(); QPointF p1 = points[3]; bool p1Valid = p.pos(p1) >= 0; QVector resultPoints; for (int i = 0; i < 4; i++) { const QPointF p2 = points[i]; const bool p2Valid = p.pos(p2) >= 0; if (p1Valid != p2Valid) { QPointF intersection; cutLine.intersect(QLineF(p1, p2), &intersection); resultPoints << intersection; } if (p2Valid) { resultPoints << p2; } p1 = p2; p1Valid = p2Valid; } return approximateRectFromPoints(resultPoints); } int quadraticEquation(qreal a, qreal b, qreal c, qreal *x1, qreal *x2) { int numSolutions = 0; const qreal D = pow2(b) - 4 * a * c; if (D < 0) { return 0; } else if (qFuzzyCompare(D, 0)) { *x1 = -b / (2 * a); numSolutions = 1; } else { const qreal sqrt_D = std::sqrt(D); *x1 = (-b + sqrt_D) / (2 * a); *x2 = (-b - sqrt_D) / (2 * a); numSolutions = 2; } return numSolutions; } QVector intersectTwoCircles(const QPointF ¢er1, qreal r1, const QPointF ¢er2, qreal r2) { QVector points; const QPointF diff = (center2 - center1); const QPointF c1; const QPointF c2 = diff; const qreal centerDistance = norm(diff); if (centerDistance > r1 + r2) return points; if (centerDistance < qAbs(r1 - r2)) return points; if (centerDistance < qAbs(r1 - r2) + 0.001) { qDebug() << "Skipping intersection" << ppVar(center1) << ppVar(center2) << ppVar(r1) << ppVar(r2) << ppVar(centerDistance) << ppVar(qAbs(r1-r2)); return points; } const qreal x_kp1 = diff.x(); const qreal y_kp1 = diff.y(); const qreal F2 = 0.5 * (pow2(x_kp1) + pow2(y_kp1) + pow2(r1) - pow2(r2)); if (qFuzzyCompare(diff.y(), 0)) { qreal x = F2 / diff.x(); qreal y1, y2; int result = KisAlgebra2D::quadraticEquation( 1, 0, pow2(x) - pow2(r2), &y1, &y2); KIS_SAFE_ASSERT_RECOVER(result > 0) { return points; } if (result == 1) { points << QPointF(x, y1); } else if (result == 2) { KisAlgebra2D::RightHalfPlane p(c1, c2); QPointF p1(x, y1); QPointF p2(x, y2); if (p.pos(p1) >= 0) { points << p1; points << p2; } else { points << p2; points << p1; } } } else { const qreal A = diff.x() / diff.y(); const qreal C = F2 / diff.y(); qreal x1, x2; int result = KisAlgebra2D::quadraticEquation( 1 + pow2(A), -2 * A * C, pow2(C) - pow2(r1), &x1, &x2); KIS_SAFE_ASSERT_RECOVER(result > 0) { return points; } if (result == 1) { points << QPointF(x1, C - x1 * A); } else if (result == 2) { KisAlgebra2D::RightHalfPlane p(c1, c2); QPointF p1(x1, C - x1 * A); QPointF p2(x2, C - x2 * A); if (p.pos(p1) >= 0) { points << p1; points << p2; } else { points << p2; points << p1; } } } for (int i = 0; i < points.size(); i++) { points[i] = center1 + points[i]; } return points; } QTransform mapToRect(const QRectF &rect) { return QTransform(rect.width(), 0, 0, rect.height(), rect.x(), rect.y()); } bool fuzzyMatrixCompare(const QTransform &t1, const QTransform &t2, qreal delta) { return qAbs(t1.m11() - t2.m11()) < delta && qAbs(t1.m12() - t2.m12()) < delta && qAbs(t1.m13() - t2.m13()) < delta && qAbs(t1.m21() - t2.m21()) < delta && qAbs(t1.m22() - t2.m22()) < delta && qAbs(t1.m23() - t2.m23()) < delta && qAbs(t1.m31() - t2.m31()) < delta && qAbs(t1.m32() - t2.m32()) < delta && qAbs(t1.m33() - t2.m33()) < delta; } /********************************************************/ /* DecomposedMatix */ /********************************************************/ DecomposedMatix::DecomposedMatix() { } DecomposedMatix::DecomposedMatix(const QTransform &t0) { QTransform t(t0); QTransform projMatrix; if (t.m33() == 0.0 || t0.determinant() == 0.0) { qWarning() << "Cannot decompose matrix!" << t; valid = false; return; } if (t.type() == QTransform::TxProject) { QTransform affineTransform(t.toAffine()); projMatrix = affineTransform.inverted() * t; t = affineTransform; proj[0] = projMatrix.m13(); proj[1] = projMatrix.m23(); proj[2] = projMatrix.m33(); } std::array rows; rows[0] = QVector3D(t.m11(), t.m12(), t.m13()); rows[1] = QVector3D(t.m21(), t.m22(), t.m23()); rows[2] = QVector3D(t.m31(), t.m32(), t.m33()); if (!qFuzzyCompare(t.m33(), 1.0)) { const qreal invM33 = 1.0 / t.m33(); for (auto row : rows) { row *= invM33; } } dx = rows[2].x(); dy = rows[2].y(); rows[2] = QVector3D(0,0,1); scaleX = rows[0].length(); rows[0] *= 1.0 / scaleX; shearXY = QVector3D::dotProduct(rows[0], rows[1]); rows[1] = rows[1] - shearXY * rows[0]; scaleY = rows[1].length(); rows[1] *= 1.0 / scaleY; shearXY *= 1.0 / scaleY; // If determinant is negative, one axis was flipped. qreal determinant = rows[0].x() * rows[1].y() - rows[0].y() * rows[1].x(); if (determinant < 0) { // Flip axis with minimum unit vector dot product. if (rows[0].x() < rows[1].y()) { scaleX = -scaleX; rows[0] = -rows[0]; } else { scaleY = -scaleY; rows[1] = -rows[1]; } shearXY = - shearXY; } angle = kisRadiansToDegrees(std::atan2(rows[0].y(), rows[0].x())); if (angle != 0.0) { // Rotate(-angle) = [cos(angle), sin(angle), -sin(angle), cos(angle)] // = [row0x, -row0y, row0y, row0x] // Thanks to the normalization above. qreal sn = -rows[0].y(); qreal cs = rows[0].x(); qreal m11 = rows[0].x(); qreal m12 = rows[0].y(); qreal m21 = rows[1].x(); qreal m22 = rows[1].y(); rows[0].setX(cs * m11 + sn * m21); rows[0].setY(cs * m12 + sn * m22); rows[1].setX(-sn * m11 + cs * m21); rows[1].setY(-sn * m12 + cs * m22); } QTransform leftOver( rows[0].x(), rows[0].y(), rows[0].z(), rows[1].x(), rows[1].y(), rows[1].z(), rows[2].x(), rows[2].y(), rows[2].z()); KIS_SAFE_ASSERT_RECOVER_NOOP(fuzzyMatrixCompare(leftOver, QTransform(), 1e-4)); } } diff --git a/libs/global/kis_algebra_2d.h b/libs/global/kis_algebra_2d.h index bc6575b70d..a64f7874c4 100644 --- a/libs/global/kis_algebra_2d.h +++ b/libs/global/kis_algebra_2d.h @@ -1,532 +1,548 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_ALGEBRA_2D_H #define __KIS_ALGEBRA_2D_H #include #include #include #include #include #include #include #include #include class QPainterPath; class QTransform; namespace KisAlgebra2D { template struct PointTypeTraits { }; template <> struct PointTypeTraits { typedef int value_type; typedef qreal calculation_type; typedef QRect rect_type; }; template <> struct PointTypeTraits { typedef qreal value_type; typedef qreal calculation_type; typedef QRectF rect_type; }; template typename PointTypeTraits::value_type dotProduct(const T &a, const T &b) { return a.x() * b.x() + a.y() * b.y(); } template typename PointTypeTraits::value_type crossProduct(const T &a, const T &b) { return a.x() * b.y() - a.y() * b.x(); } template qreal norm(const T &a) { return std::sqrt(pow2(a.x()) + pow2(a.y())); } template Point normalize(const Point &a) { const qreal length = norm(a); return (1.0 / length) * a; } /** * Usual sign() function with positive zero */ template T signPZ(T x) { return x >= T(0) ? T(1) : T(-1); } /** * Usual sign() function with zero returning zero */ template T signZZ(T x) { return x == T(0) ? T(0) : x > T(0) ? T(1) : T(-1); } /** * Copies the sign of \p y into \p x and returns the result */ template inline T copysign(T x, T y) { T strippedX = qAbs(x); return y >= T(0) ? strippedX : -strippedX; } template T leftUnitNormal(const T &a) { T result = a.x() != 0 ? T(-a.y() / a.x(), 1) : T(-1, 0); qreal length = norm(result); result *= (crossProduct(a, result) >= 0 ? 1 : -1) / length; return -result; } template T rightUnitNormal(const T &a) { return -leftUnitNormal(a); } template T inwardUnitNormal(const T &a, int polygonDirection) { return polygonDirection * leftUnitNormal(a); } /** * \return 1 if the polygon is counterclockwise * -1 if the polygon is clockwise * * Note: the sign is flipped because our 0y axis * is reversed */ template int polygonDirection(const QVector &polygon) { typename PointTypeTraits::value_type doubleSum = 0; const int numPoints = polygon.size(); for (int i = 1; i <= numPoints; i++) { int prev = i - 1; int next = i == numPoints ? 0 : i; doubleSum += (polygon[next].x() - polygon[prev].x()) * (polygon[next].y() + polygon[prev].y()); } return doubleSum >= 0 ? 1 : -1; } template bool isInRange(T x, T a, T b) { T length = qAbs(a - b); return qAbs(x - a) <= length && qAbs(x - b) <= length; } void KRITAGLOBAL_EXPORT adjustIfOnPolygonBoundary(const QPolygonF &poly, int polygonDirection, QPointF *pt); /** * Let \p pt, \p base1 are two vectors. \p base1 is uniformly scaled * and then rotated into \p base2 using transformation matrix S * * R. The function applies the same transformation to \pt and returns * the result. **/ QPointF KRITAGLOBAL_EXPORT transformAsBase(const QPointF &pt, const QPointF &base1, const QPointF &base2); qreal KRITAGLOBAL_EXPORT angleBetweenVectors(const QPointF &v1, const QPointF &v2); namespace Private { inline void resetEmptyRectangle(const QPoint &pt, QRect *rc) { *rc = QRect(pt, QSize(1, 1)); } inline void resetEmptyRectangle(const QPointF &pt, QRectF *rc) { static const qreal eps = 1e-10; *rc = QRectF(pt, QSizeF(eps, eps)); } } template inline void accumulateBounds(const Point &pt, Rect *bounds) { if (bounds->isEmpty()) { Private::resetEmptyRectangle(pt, bounds); } if (pt.x() > bounds->right()) { bounds->setRight(pt.x()); } if (pt.x() < bounds->left()) { bounds->setLeft(pt.x()); } if (pt.y() > bounds->bottom()) { bounds->setBottom(pt.y()); } if (pt.y() < bounds->top()) { bounds->setTop(pt.y()); } } template