diff --git a/libs/flake/KoOdfGradientBackground.cpp b/libs/flake/KoOdfGradientBackground.cpp index 40c496d01a..20f7aa727d 100644 --- a/libs/flake/KoOdfGradientBackground.cpp +++ b/libs/flake/KoOdfGradientBackground.cpp @@ -1,376 +1,386 @@ /* This file is part of the KDE project * * Copyright (C) 2011 Lukáš Tvrdý * * 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 "KoOdfGradientBackground.h" #include "KoShapeBackground_p.h" #include "KoShapeSavingContext.h" #include #include #include #include #include #include #include #include #include #include #include +#include "FlakeDebug.h" + class KoOdfGradientBackgroundPrivate : public KoShapeBackgroundPrivate { public: KoOdfGradientBackgroundPrivate() - : style(), cx(0), cy(0), startColor(), endColor(), angle(0), border(0), opacity(1.0) {}; + : style() + , cx(0) + , cy(0) + , startColor() + , endColor() + , angle(0) + , border(0) + , opacity(1.0) + {} ~KoOdfGradientBackgroundPrivate() override{}; //data QString style; int cx; int cy; QColor startColor; QColor endColor; qreal angle; qreal border; qreal opacity; }; KoOdfGradientBackground::KoOdfGradientBackground() : KoShapeBackground(*(new KoOdfGradientBackgroundPrivate())) { } KoOdfGradientBackground::~KoOdfGradientBackground() { } bool KoOdfGradientBackground::compareTo(const KoShapeBackground *other) const { Q_UNUSED(other); return false; } bool KoOdfGradientBackground::loadOdf(const KoXmlElement& e) { Q_D(KoOdfGradientBackground); d->style = e.attributeNS(KoXmlNS::draw, "style", QString()); //TODO: support ellipsoid here too if ((d->style != "rectangular") && (d->style != "square")) { return false; } d->cx = KoUnit::parseValue(e.attributeNS(KoXmlNS::draw, "cx", QString()).remove('%')); d->cy = KoUnit::parseValue(e.attributeNS(KoXmlNS::draw, "cy", QString()).remove('%')); d->border = qBound(0.0,0.01 * e.attributeNS(KoXmlNS::draw, "border", "0").remove('%').toDouble(),1.0); d->startColor = QColor(e.attributeNS(KoXmlNS::draw, "start-color", QString())); d->startColor.setAlphaF((0.01 * e.attributeNS(KoXmlNS::draw, "start-intensity", "100").remove('%').toDouble())); d->endColor = QColor(e.attributeNS(KoXmlNS::draw, "end-color", QString())); d->endColor.setAlphaF(0.01 * e.attributeNS(KoXmlNS::draw, "end-intensity", "100").remove('%').toDouble()); d->angle = e.attributeNS(KoXmlNS::draw, "angle", "0").toDouble() / 10; return true; } void KoOdfGradientBackground::saveOdf(KoGenStyle& styleFill, KoGenStyles& mainStyles) const { Q_D(const KoOdfGradientBackground); KoGenStyle::Type type = styleFill.type(); KoGenStyle::PropertyType propertyType = (type == KoGenStyle::GraphicStyle || type == KoGenStyle::GraphicAutoStyle || type == KoGenStyle::DrawingPageStyle || type == KoGenStyle::DrawingPageAutoStyle ) ? KoGenStyle::DefaultType : KoGenStyle::GraphicType; KoGenStyle gradientStyle(KoGenStyle::GradientStyle); gradientStyle.addAttribute("draw:style", d->style); // draw:style="square" gradientStyle.addAttribute("draw:cx", QString("%1%").arg(d->cx)); gradientStyle.addAttribute("draw:cy", QString("%1%").arg(d->cy)); gradientStyle.addAttribute("draw:start-color", d->startColor.name()); gradientStyle.addAttribute("draw:end-color", d->endColor.name()); gradientStyle.addAttribute("draw:start-intensity", QString("%1%").arg(qRound(d->startColor.alphaF() * 100)) ); gradientStyle.addAttribute("draw:end-intensity", QString("%1%").arg(qRound(d->endColor.alphaF() * 100)) ); gradientStyle.addAttribute("draw:angle", QString("%1").arg(d->angle * 10)); gradientStyle.addAttribute("draw:border", QString("%1%").arg(qRound(d->border * 100.0))); QString gradientStyleName = mainStyles.insert(gradientStyle, "gradient"); styleFill.addProperty("draw:fill", "gradient", propertyType); styleFill.addProperty("draw:fill-gradient-name", gradientStyleName, propertyType); if (d->opacity <= 1.0) { styleFill.addProperty("draw:opacity", QString("%1%").arg(d->opacity * 100.0), propertyType); } } void KoOdfGradientBackground::paint(QPainter& painter, const KoViewConverter &/*converter*/, KoShapePaintingContext &/*context*/, const QPainterPath& fillPath) const { Q_D(const KoOdfGradientBackground); QImage buffer; QRectF targetRect = fillPath.boundingRect(); QRectF pixels = painter.transform().mapRect(QRectF(0,0,targetRect.width(), targetRect.height())); QSize currentSize( qCeil(pixels.size().width()), qCeil(pixels.size().height()) ); if (buffer.isNull() || buffer.size() != currentSize){ buffer = QImage(currentSize, QImage::Format_ARGB32_Premultiplied); if (d->style == "square") { renderSquareGradient(buffer); } else { renderRectangleGradient(buffer); } } painter.setClipPath(fillPath); painter.setOpacity(d->opacity); painter.drawImage(targetRect, buffer, QRectF(QPointF(0,0), buffer.size())); } void KoOdfGradientBackground::fillStyle(KoGenStyle& style, KoShapeSavingContext& context) { saveOdf(style, context.mainStyles()); } bool KoOdfGradientBackground::loadStyle(KoOdfLoadingContext& context, const QSizeF& shapeSize) { Q_UNUSED(shapeSize); Q_D(KoOdfGradientBackground); KoStyleStack &styleStack = context.styleStack(); if (!styleStack.hasProperty(KoXmlNS::draw, "fill")) { return false; } QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle == "gradient") { if (styleStack.hasProperty(KoXmlNS::draw, "opacity")) { QString opacity = styleStack.property(KoXmlNS::draw, "opacity"); if (! opacity.isEmpty() && opacity.right(1) == "%") { d->opacity = qMin(opacity.left(opacity.length() - 1).toDouble(), 100.0) / 100; } } QString styleName = styleStack.property(KoXmlNS::draw, "fill-gradient-name"); KoXmlElement * e = context.stylesReader().drawStyles("gradient")[styleName]; return loadOdf(*e); } return false; } void KoOdfGradientBackground::renderSquareGradient(QImage& buffer) const { Q_D(const KoOdfGradientBackground); buffer.fill(d->startColor.rgba()); QPainter painter(&buffer); painter.setPen(Qt::NoPen); painter.setRenderHint(QPainter::Antialiasing, false); int width = buffer.width(); int height = buffer.height(); qreal gradientCenterX = qRound(width * d->cx * 0.01); qreal gradientCenterY = qRound(height * d->cy * 0.01); qreal centerX = width * 0.5; qreal centerY = height * 0.5; qreal areaCenterX = qRound(centerX); qreal areaCenterY = qRound(centerY); QTransform m; m.translate(gradientCenterX, gradientCenterY); m.rotate(-d->angle); m.scale(1.0 - d->border, 1.0 - d->border); m.translate(-gradientCenterX, -gradientCenterY); m.translate(gradientCenterX - areaCenterX,gradientCenterY - areaCenterY); painter.setTransform(m); QLinearGradient linearGradient; linearGradient.setColorAt(1, d->startColor); linearGradient.setColorAt(0, d->endColor); // from center going North linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(centerX, 0); painter.setBrush(linearGradient); painter.drawRect(0, 0, width, centerY); // from center going South linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(0, centerY, width, centerY); // clip the East and West portion QPainterPath clip; clip.moveTo(width, 0); clip.lineTo(width, height); clip.lineTo(0, 0); clip.lineTo(0, height); clip.closeSubpath(); painter.setClipPath(clip); // from center going East linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(centerX, 0, width, height); // from center going West linearGradient.setFinalStop( 0, centerY); painter.setBrush(linearGradient); painter.drawRect(0, 0, centerX, height); } void KoOdfGradientBackground::renderRectangleGradient(QImage& buffer) const { Q_D(const KoOdfGradientBackground); buffer.fill(d->startColor.rgba()); QPainter painter(&buffer); painter.setPen(Qt::NoPen); painter.setRenderHint(QPainter::Antialiasing, false); int width = buffer.width(); int height = buffer.height(); qreal gradientCenterX = qRound(width * d->cx * 0.01); qreal gradientCenterY = qRound(height * d->cy * 0.01); qreal centerX = width * 0.5; qreal centerY = height * 0.5; qreal areaCenterY = qRound(centerY); qreal areaCenterX = qRound(centerX); QTransform m; m.translate(gradientCenterX, gradientCenterY); // m.rotate(-d->angle); // OOo rotates the gradient differently m.scale(1.0 - d->border, 1.0 - d->border); m.translate(-gradientCenterX, -gradientCenterY); m.translate(gradientCenterX - areaCenterX,gradientCenterY - areaCenterY); painter.setTransform(m); QLinearGradient linearGradient; linearGradient.setColorAt(1, d->startColor); linearGradient.setColorAt(0, d->endColor); // render background QPainterPath clipPath; if (width < height) { QRectF west(0,0,centerX, height); QRectF east(centerX, 0, centerX, height); linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(0, centerY); painter.setBrush(linearGradient); painter.drawRect(west); linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(east); QRectF north(0,0,width, centerX); QRectF south(0,height - centerX, width, centerX); clipPath.moveTo(0,0); clipPath.lineTo(width, 0); clipPath.lineTo(centerX, centerX); clipPath.closeSubpath(); clipPath.moveTo(width, height); clipPath.lineTo(0, height); clipPath.lineTo(centerX, south.y()); clipPath.closeSubpath(); linearGradient.setStart(centerX, centerX); linearGradient.setFinalStop(centerX, 0); painter.setClipPath(clipPath); painter.setBrush(linearGradient); painter.drawRect(north); linearGradient.setStart(centerX, south.y()); linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(south); } else { QRectF north(0,0,width, centerY); QRectF south(0, centerY, width, centerY); linearGradient.setStart(centerX, centerY); linearGradient.setFinalStop(centerX, 0); painter.setBrush(linearGradient); painter.drawRect(north); linearGradient.setFinalStop(centerX, height); painter.setBrush(linearGradient); painter.drawRect(south); QRectF west(0,0,centerY, height); QRectF east(width - centerY, 0, centerY, height); clipPath.moveTo(0,0); clipPath.lineTo(centerY, centerY); clipPath.lineTo(0,height); clipPath.closeSubpath(); clipPath.moveTo(width, height); clipPath.lineTo(east.x(), centerY); clipPath.lineTo(width,0); clipPath.closeSubpath(); linearGradient.setStart(centerY, centerY); linearGradient.setFinalStop(0, centerY); painter.setClipPath(clipPath); painter.setBrush(linearGradient); painter.drawRect(west); linearGradient.setStart(east.x(), centerY); linearGradient.setFinalStop(width, centerY); painter.setBrush(linearGradient); painter.drawRect(east); } } void KoOdfGradientBackground::debug() const { Q_D(const KoOdfGradientBackground); - qDebug() << "cx,cy: "<< d->cx << d->cy; - qDebug() << "style" << d->style; - qDebug() << "colors" << d->startColor << d->endColor; - qDebug() << "angle:" << d->angle; - qDebug() << "border" << d->border; + debugFlake << "cx,cy: "<< d->cx << d->cy; + debugFlake << "style" << d->style; + debugFlake << "colors" << d->startColor << d->endColor; + debugFlake << "angle:" << d->angle; + debugFlake << "border" << d->border; } diff --git a/libs/flake/KoRTree.h b/libs/flake/KoRTree.h index 6844bd0277..06bf55e636 100644 --- a/libs/flake/KoRTree.h +++ b/libs/flake/KoRTree.h @@ -1,1165 +1,1165 @@ /* This file is part of the KDE project Copyright (c) 2006 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. Based on code from Wolfgang Baer - WBaer@gmx.de */ #ifndef KORTREE_H #define KORTREE_H #include #include #include #include #include #include #include #include #include "kis_assert.h" // #define CALLIGRA_RTREE_DEBUG #ifdef CALLIGRA_RTREE_DEBUG #include #endif /** * @brief The KoRTree class is a template class that provides a R-tree. * * This class implements a R-tree as described in * "R-TREES. A DYNAMIC INDEX STRUCTURE FOR SPATIAL SEARCHING" by Antomn Guttman * * It only supports 2 dimensional bounding boxes which are represented by a QRectF. * For node splitting the Quadratic-Cost Algorithm is used as described by Guttman. */ template class KoRTree { public: /** * @brief Constructor * * @param capacity the capacity a node can take * @param minimum the minimum filling of a node max 0.5 * capacity */ KoRTree(int capacity, int minimum); /** * @brief Destructor */ virtual ~KoRTree(); /** * @brief Insert data item into the tree * * This will insert a data item into the tree. If necessary the tree will * adjust itself. * * @param data * @param bb */ virtual void insert(const QRectF& bb, const T& data); /** * @brief Show if a shape is a part of the tree * @param data */ bool contains(const T &data); /** * @brief Remove a data item from the tree * * This removed a data item from the tree. If necessary the tree will * adjust itself. * * @param data */ void remove(const T& data); /** * @brief Find all data items which intersects rect * The items are sorted by insertion time in ascending order. * * @param rect where the objects have to be in * * @return objects intersecting the rect */ virtual QList intersects(const QRectF& rect) const; /** * @brief Find all data item which contain the point * The items are sorted by insertion time in ascending order. * * @param point which should be contained in the objects * * @return objects which contain the point */ QList contains(const QPointF &point) const; /** * @brief Find all data item which contain the point * The items are sorted by insertion time in ascending order. * * @param point which should be contained in the objects * * @return objects which contain the point */ QList contained(const QRectF &point) const; /** * @brief Find all data rectangles * The order is NOT guaranteed to be the same as that used by values(). * * @return a list containing all the data rectangles used in the tree */ QList keys() const; /** * @brief Find all data items * The order is NOT guaranteed to be the same as that used by keys(). * * @return a list containing all the data used in the tree */ QList values() const; virtual void clear() { delete m_root; m_root = createLeafNode(m_capacity + 1, 0, 0); m_leafMap.clear(); } #ifdef CALLIGRA_RTREE_DEBUG /** * @brief Paint the tree * * @param p painter which should be used for painting */ void paint(QPainter & p) const; /** * @brief Print the tree using qdebug */ void debug() const; #endif protected: class NonLeafNode; class LeafNode; class Node { public: #ifdef CALLIGRA_RTREE_DEBUG static int nodeIdCnt; #endif Node(int capacity, int level, Node * parent); virtual ~Node() {} virtual void remove(int index); // move node between nodes of the same type from node virtual void move(Node * node, int index) = 0; virtual LeafNode * chooseLeaf(const QRectF& bb) = 0; virtual NonLeafNode * chooseNode(const QRectF& bb, int level) = 0; virtual void intersects(const QRectF& rect, QMap & result) const = 0; virtual void contains(const QPointF & point, QMap & result) const = 0; virtual void contained(const QRectF & point, QMap & result) const = 0; virtual void keys(QList & result) const = 0; virtual void values(QMap & result) const = 0; virtual Node * parent() const { return m_parent; } virtual void setParent(Node * parent) { m_parent = parent; } virtual int childCount() const { return m_counter; } virtual const QRectF& boundingBox() const { return m_boundingBox; } virtual void updateBoundingBox(); virtual const QRectF& childBoundingBox(int index) const { return m_childBoundingBox[index]; } virtual void setChildBoundingBox(int index, const QRectF& rect) { m_childBoundingBox[index] = rect; } virtual void clear(); virtual bool isRoot() const { return m_parent == 0; } virtual bool isLeaf() const { return false; } virtual int place() const { return m_place; } virtual void setPlace(int place) { m_place = place; } virtual int level() const { return m_level; } virtual void setLevel(int level) { m_level = level; } #ifdef CALLIGRA_RTREE_DEBUG virtual int nodeId() const { return m_nodeId; } virtual void paint(QPainter & p, int level) const = 0; virtual void debug(QString line) const = 0; protected: #define levelColorSize 5 static QColor levelColor[levelColorSize]; virtual void paintRect(QPainter & p, int level) const; #endif protected: Node * m_parent; QRectF m_boundingBox; QVector m_childBoundingBox; int m_counter; // the position in the parent int m_place; #ifdef CALLIGRA_RTREE_DEBUG int m_nodeId; #endif int m_level; }; class NonLeafNode : virtual public Node { public: NonLeafNode(int capacity, int level, Node * parent); ~NonLeafNode() override; virtual void insert(const QRectF& bb, Node * data); void remove(int index) override; void move(Node * node, int index) override; LeafNode * chooseLeaf(const QRectF& bb) override; NonLeafNode * chooseNode(const QRectF& bb, int level) override; void intersects(const QRectF& rect, QMap & result) const override; void contains(const QPointF & point, QMap & result) const override; void contained(const QRectF & point, QMap & result) const override; void keys(QList & result) const override; void values(QMap & result) const override; virtual Node * getNode(int index) const; #ifdef CALLIGRA_RTREE_DEBUG virtual void paint(QPainter & p, int level) const; virtual void debug(QString line) const; #endif protected: virtual Node * getLeastEnlargement(const QRectF& bb) const; QVector m_childs; }; class LeafNode : virtual public Node { public: static int dataIdCounter; LeafNode(int capacity, int level, Node * parent); ~LeafNode() override; virtual void insert(const QRectF& bb, const T& data, int id); void remove(int index) override; virtual void remove(const T& data); void move(Node * node, int index) override; LeafNode * chooseLeaf(const QRectF& bb) override; NonLeafNode * chooseNode(const QRectF& bb, int level) override; void intersects(const QRectF& rect, QMap & result) const override; void contains(const QPointF & point, QMap & result) const override; void contained(const QRectF & point, QMap & result) const override; void keys(QList & result) const override; void values(QMap & result) const override; virtual const T& getData(int index) const; virtual int getDataId(int index) const; bool isLeaf() const override { return true; } #ifdef CALLIGRA_RTREE_DEBUG virtual void debug(QString line) const; virtual void paint(QPainter & p, int level) const; #endif protected: QVector m_data; QVector m_dataIds; }; // factory methods virtual LeafNode* createLeafNode(int capacity, int level, Node * parent) { return new LeafNode(capacity, level, parent); } virtual NonLeafNode* createNonLeafNode(int capacity, int level, Node * parent) { return new NonLeafNode(capacity, level, parent); } // methods for insert QPair splitNode(Node * node); QPair pickSeeds(Node * node); QPair pickNext(Node * node, QVector & marker, Node * group1, Node * group2); virtual void adjustTree(Node * node1, Node * node2); void insertHelper(const QRectF& bb, const T& data, int id); // methods for delete void insert(Node * node); virtual void condenseTree(Node * node, QVector & reinsert); int m_capacity; int m_minimum; Node * m_root; QMap m_leafMap; }; template KoRTree::KoRTree(int capacity, int minimum) : m_capacity(capacity) , m_minimum(minimum) , m_root(createLeafNode(m_capacity + 1, 0, 0)) { if (minimum > capacity / 2) qFatal("KoRTree::KoRTree minimum can be maximal capacity/2"); - //qDebug() << "root node " << m_root->nodeId(); + //debugFlake << "root node " << m_root->nodeId(); } template KoRTree::~KoRTree() { delete m_root; } template void KoRTree::insert(const QRectF& bb, const T& data) { // check if the shape is not already registered KIS_SAFE_ASSERT_RECOVER_NOOP(!m_leafMap[data]); insertHelper(bb, data, LeafNode::dataIdCounter++); } template void KoRTree::insertHelper(const QRectF& bb, const T& data, int id) { QRectF nbb(bb.normalized()); // This has to be done as it is not possible to use QRectF::united() with a isNull() if (nbb.isNull()) { nbb.setWidth(0.0001); nbb.setHeight(0.0001); qWarning() << "KoRTree::insert boundingBox isNull setting size to" << nbb.size(); } else { // This has to be done as QRectF::intersects() return false if the rect does not have any area overlapping. // If there is no width or height there is no area and therefore no overlapping. if ( nbb.width() == 0 ) { nbb.setWidth(0.0001); } if ( nbb.height() == 0 ) { nbb.setHeight(0.0001); } } LeafNode * leaf = m_root->chooseLeaf(nbb); - //qDebug() << " leaf" << leaf->nodeId() << nbb; + //debugFlake << " leaf" << leaf->nodeId() << nbb; if (leaf->childCount() < m_capacity) { leaf->insert(nbb, data, id); m_leafMap[data] = leaf; adjustTree(leaf, 0); } else { leaf->insert(nbb, data, id); m_leafMap[data] = leaf; QPair newNodes = splitNode(leaf); LeafNode * l = dynamic_cast(newNodes.first); if (l) for (int i = 0; i < l->childCount(); ++i) m_leafMap[l->getData(i)] = l; l = dynamic_cast(newNodes.second); if (l) for (int i = 0; i < l->childCount(); ++i) m_leafMap[l->getData(i)] = l; adjustTree(newNodes.first, newNodes.second); } } template void KoRTree::insert(Node * node) { if (node->level() == m_root->level()) { adjustTree(m_root, node); } else { QRectF bb(node->boundingBox()); NonLeafNode * newParent = m_root->chooseNode(bb, node->level() + 1); newParent->insert(bb, node); QPair newNodes(node, 0); if (newParent->childCount() > m_capacity) { newNodes = splitNode(newParent); } adjustTree(newNodes.first, newNodes.second); } } template bool KoRTree::contains(const T &data) { return m_leafMap[data]; } template void KoRTree::remove(const T&data) { - //qDebug() << "KoRTree remove"; + //debugFlake << "KoRTree remove"; LeafNode * leaf = m_leafMap[data]; // Trying to remove unexistent leaf. Most probably, this leaf hasn't been added // to the shape manager correctly KIS_SAFE_ASSERT_RECOVER_RETURN(leaf); m_leafMap.remove(data); leaf->remove(data); /** * WARNING: after calling condenseTree() the temporary enters an inconsistent state! * m_leafMap still points to the nodes now stored in 'reinsert' list, although * they are not a part of the hierarchy. This state does not cause any use * visible changes, but should be considered while implementing sanity checks. */ QVector reinsert; condenseTree(leaf, reinsert); for (int i = 0; i < reinsert.size(); ++i) { if (reinsert[i]->isLeaf()) { LeafNode * leaf = dynamic_cast(reinsert[i]); for (int j = 0; j < leaf->childCount(); ++j) { insertHelper(leaf->childBoundingBox(j), leaf->getData(j), leaf->getDataId(j)); } // clear is needed as the data items are not removed when insert into a new node leaf->clear(); delete leaf; } else { NonLeafNode * node = dynamic_cast(reinsert[i]); for (int j = 0; j < node->childCount(); ++j) { insert(node->getNode(j)); } // clear is needed as the data items are not removed when insert into a new node node->clear(); delete node; } } } template QList KoRTree::intersects(const QRectF& rect) const { QMap found; m_root->intersects(rect, found); return found.values(); } template QList KoRTree::contains(const QPointF &point) const { QMap found; m_root->contains(point, found); return found.values(); } template QList KoRTree::contained(const QRectF& rect) const { QMap found; m_root->contained(rect, found); return found.values(); } template QList KoRTree::keys() const { QList found; m_root->keys(found); return found; } template QList KoRTree::values() const { QMap found; m_root->values(found); return found.values(); } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::paint(QPainter & p) const { if (m_root) { m_root->paint(p, 0); } } template void KoRTree::debug() const { QString prefix(""); m_root->debug(prefix); } #endif template QPair< typename KoRTree::Node*, typename KoRTree::Node* > KoRTree::splitNode(typename KoRTree::Node* node) { - //qDebug() << "KoRTree::splitNode" << node; + //debugFlake << "KoRTree::splitNode" << node; Node * n1; Node * n2; if (node->isLeaf()) { n1 = createLeafNode(m_capacity + 1, node->level(), node->parent()); n2 = createLeafNode(m_capacity + 1, node->level(), node->parent()); } else { n1 = createNonLeafNode(m_capacity + 1, node->level(), node->parent()); n2 = createNonLeafNode(m_capacity + 1, node->level(), node->parent()); } - //qDebug() << " n1" << n1 << n1->nodeId(); - //qDebug() << " n2" << n2 << n2->nodeId(); + //debugFlake << " n1" << n1 << n1->nodeId(); + //debugFlake << " n2" << n2 << n2->nodeId(); QVector marker(m_capacity + 1); QPair seeds(pickSeeds(node)); n1->move(node, seeds.first); n2->move(node, seeds.second); marker[seeds.first] = true; marker[seeds.second] = true; // There is one more in a node to split than the capacity and as we // already put the seeds in the new nodes subtract them. int remaining = m_capacity + 1 - 2; while (remaining > 0) { if (m_minimum - n1->childCount() == remaining) { for (int i = 0; i < m_capacity + 1; ++i) { if (!marker[i]) { n1->move(node, i); --remaining; } } } else if (m_minimum - n2->childCount() == remaining) { for (int i = 0; i < m_capacity + 1; ++i) { if (!marker[i]) { n2->move(node, i); --remaining; } } } else { QPair next(pickNext(node, marker, n1, n2)); if (next.first == 0) { n1->move(node, next.second); } else { n2->move(node, next.second); } --remaining; } } Q_ASSERT(n1->childCount() + n2->childCount() == node->childCount()); // move the data back to the old node // this has to be done as the current node is already in the tree. node->clear(); for (int i = 0; i < n1->childCount(); ++i) { node->move(n1, i); } - //qDebug() << " delete n1" << n1 << n1->nodeId(); + //debugFlake << " delete n1" << n1 << n1->nodeId(); // clear is needed as the data items are not removed n1->clear(); delete n1; return qMakePair(node, n2); } template QPair KoRTree::pickSeeds(Node *node) { int s1 = 0; int s2 = 1; qreal max = 0; for (int i = 0; i < m_capacity + 1; ++i) { for (int j = i+1; j < m_capacity + 1; ++j) { if (i != j) { QRectF bb1(node->childBoundingBox(i)); QRectF bb2(node->childBoundingBox(j)); QRectF comp(node->childBoundingBox(i).united(node->childBoundingBox(j))); qreal area = comp.width() * comp.height() - bb1.width() * bb1.height() - bb2.width() * bb2.height(); - //qDebug() << " ps" << i << j << area; + //debugFlake << " ps" << i << j << area; if (area > max) { max = area; s1 = i; s2 = j; } } } } return qMakePair(s1, s2); } template QPair KoRTree::pickNext(Node * node, QVector & marker, Node * group1, Node * group2) { - //qDebug() << "KoRTree::pickNext" << marker; + //debugFlake << "KoRTree::pickNext" << marker; qreal max = -1.0; int select = 0; int group = 0; for (int i = 0; i < m_capacity + 1; ++i) { if (marker[i] == false) { QRectF bb1 = group1->boundingBox().united(node->childBoundingBox(i)); QRectF bb2 = group2->boundingBox().united(node->childBoundingBox(i)); qreal d1 = bb1.width() * bb1.height() - group1->boundingBox().width() * group1->boundingBox().height(); qreal d2 = bb2.width() * bb2.height() - group2->boundingBox().width() * group2->boundingBox().height(); qreal diff = qAbs(d1 - d2); - //qDebug() << " diff" << diff << i << d1 << d2; + //debugFlake << " diff" << diff << i << d1 << d2; if (diff > max) { max = diff; select = i; - //qDebug() << " i =" << i; + //debugFlake << " i =" << i; if (qAbs(d1) > qAbs(d2)) { group = 1; } else { group = 0; } - //qDebug() << " group =" << group; + //debugFlake << " group =" << group; } } } marker[select] = true; return qMakePair(group, select); } template void KoRTree::adjustTree(Node *node1, Node *node2) { - //qDebug() << "KoRTree::adjustTree"; + //debugFlake << "KoRTree::adjustTree"; if (node1->isRoot()) { - //qDebug() << " root"; + //debugFlake << " root"; if (node2) { NonLeafNode * newRoot = createNonLeafNode(m_capacity + 1, node1->level() + 1, 0); newRoot->insert(node1->boundingBox(), node1); newRoot->insert(node2->boundingBox(), node2); m_root = newRoot; - //qDebug() << "new root" << m_root->nodeId(); + //debugFlake << "new root" << m_root->nodeId(); } } else { NonLeafNode * parent = dynamic_cast(node1->parent()); if (!parent) { qFatal("KoRTree::adjustTree: no parent node found!"); return; } //QRectF pbbold( parent->boundingBox() ); parent->setChildBoundingBox(node1->place(), node1->boundingBox()); parent->updateBoundingBox(); - //qDebug() << " bb1 =" << node1->boundingBox() << node1->place() << pbbold << "->" << parent->boundingBox() << parent->nodeId(); + //debugFlake << " bb1 =" << node1->boundingBox() << node1->place() << pbbold << "->" << parent->boundingBox() << parent->nodeId(); if (!node2) { - //qDebug() << " update"; + //debugFlake << " update"; adjustTree(parent, 0); } else { if (parent->childCount() < m_capacity) { - //qDebug() << " no split needed"; + //debugFlake << " no split needed"; parent->insert(node2->boundingBox(), node2); adjustTree(parent, 0); } else { - //qDebug() << " split again"; + //debugFlake << " split again"; parent->insert(node2->boundingBox(), node2); QPair newNodes = splitNode(parent); adjustTree(newNodes.first, newNodes.second); } } } } template void KoRTree::condenseTree(Node *node, QVector & reinsert) { - //qDebug() << "KoRTree::condenseTree begin reinsert.size()" << reinsert.size(); + //debugFlake << "KoRTree::condenseTree begin reinsert.size()" << reinsert.size(); if (!node->isRoot()) { Node * parent = node->parent(); - //qDebug() << " !node->isRoot us" << node->childCount(); + //debugFlake << " !node->isRoot us" << node->childCount(); if (node->childCount() < m_minimum) { - //qDebug() << " remove node"; + //debugFlake << " remove node"; parent->remove(node->place()); reinsert.push_back(node); /** * WARNING: here we leave the tree in an inconsistent state! 'reinsert' * nodes may still be kept in m_leafMap structure, but we will * *not* remove them for the efficiency reasons. They are guaranteed * to be readded in remove(). */ } else { - //qDebug() << " update BB parent is root" << parent->isRoot(); + //debugFlake << " update BB parent is root" << parent->isRoot(); parent->setChildBoundingBox(node->place(), node->boundingBox()); parent->updateBoundingBox(); } condenseTree(parent, reinsert); } else { - //qDebug() << " node->isRoot us" << node->childCount(); + //debugFlake << " node->isRoot us" << node->childCount(); if (node->childCount() == 1 && !node->isLeaf()) { - //qDebug() << " usedSpace = 1"; + //debugFlake << " usedSpace = 1"; NonLeafNode * n = dynamic_cast(node); if (n) { Node * kid = n->getNode(0); // clear is needed as the data items are not removed m_root->clear(); delete m_root; m_root = kid; m_root->setParent(0); - //qDebug() << " new root" << m_root; + //debugFlake << " new root" << m_root; } else { qFatal("KoRTree::condenseTree cast to NonLeafNode failed"); } } } - //qDebug() << "KoRTree::condenseTree end reinsert.size()" << reinsert.size(); + //debugFlake << "KoRTree::condenseTree end reinsert.size()" << reinsert.size(); } #ifdef CALLIGRA_RTREE_DEBUG template QColor KoRTree::Node::levelColor[] = { QColor(Qt::green), QColor(Qt::red), QColor(Qt::cyan), QColor(Qt::magenta), QColor(Qt::yellow), }; template int KoRTree::Node::nodeIdCnt = 0; #endif template KoRTree::Node::Node(int capacity, int level, Node * parent) : m_parent(parent) , m_childBoundingBox(capacity) , m_counter(0) #ifdef CALLIGRA_RTREE_DEBUG , m_nodeId(nodeIdCnt++) #endif , m_level(level) { } template void KoRTree::Node::remove(int index) { for (int i = index + 1; i < m_counter; ++i) { m_childBoundingBox[i-1] = m_childBoundingBox[i]; } --m_counter; updateBoundingBox(); } template void KoRTree::Node::updateBoundingBox() { m_boundingBox = QRectF(); for (int i = 0; i < m_counter; ++i) { m_boundingBox = m_boundingBox.united(m_childBoundingBox[i]); } } template void KoRTree::Node::clear() { m_counter = 0; m_boundingBox = QRectF(); } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::Node::paintRect(QPainter & p, int level) const { QColor c(Qt::black); if (level < levelColorSize) { c = levelColor[level]; } QPen pen(c, 0); p.setPen(pen); QRectF bbdraw(this->m_boundingBox); bbdraw.adjust(level * 2, level * 2, -level * 2, -level * 2); p.drawRect(bbdraw); } #endif template KoRTree::NonLeafNode::NonLeafNode(int capacity, int level, Node * parent) : Node(capacity, level, parent) , m_childs(capacity) { - //qDebug() << "NonLeafNode::NonLeafNode()" << this; + //debugFlake << "NonLeafNode::NonLeafNode()" << this; } template KoRTree::NonLeafNode::~NonLeafNode() { - //qDebug() << "NonLeafNode::~NonLeafNode()" << this; + //debugFlake << "NonLeafNode::~NonLeafNode()" << this; for (int i = 0; i < this->m_counter; ++i) { delete m_childs[i]; } } template void KoRTree::NonLeafNode::insert(const QRectF& bb, Node * data) { m_childs[this->m_counter] = data; data->setPlace(this->m_counter); data->setParent(this); this->m_childBoundingBox[this->m_counter] = bb; this->m_boundingBox = this->m_boundingBox.united(bb); - //qDebug() << "NonLeafNode::insert" << this->nodeId() << data->nodeId(); + //debugFlake << "NonLeafNode::insert" << this->nodeId() << data->nodeId(); ++this->m_counter; } template void KoRTree::NonLeafNode::remove(int index) { for (int i = index + 1; i < this->m_counter; ++i) { m_childs[i-1] = m_childs[i]; m_childs[i-1]->setPlace(i - 1); } Node::remove(index); } template void KoRTree::NonLeafNode::move(Node * node, int index) { - //qDebug() << "NonLeafNode::move" << this << node << index << node->nodeId() << "->" << this->nodeId(); + //debugFlake << "NonLeafNode::move" << this << node << index << node->nodeId() << "->" << this->nodeId(); NonLeafNode * n = dynamic_cast(node); if (n) { QRectF bb = n->childBoundingBox(index); insert(bb, n->getNode(index)); } } template typename KoRTree::LeafNode * KoRTree::NonLeafNode::chooseLeaf(const QRectF& bb) { return getLeastEnlargement(bb)->chooseLeaf(bb); } template typename KoRTree::NonLeafNode * KoRTree::NonLeafNode::chooseNode(const QRectF& bb, int level) { if (this->m_level > level) { return getLeastEnlargement(bb)->chooseNode(bb, level); } else { return this; } } template void KoRTree::NonLeafNode::intersects(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { m_childs[i]->intersects(rect, result); } } } template void KoRTree::NonLeafNode::contains(const QPointF & point, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].contains(point)) { m_childs[i]->contains(point, result); } } } template void KoRTree::NonLeafNode::contained(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { m_childs[i]->contained(rect, result); } } } template void KoRTree::NonLeafNode::keys(QList & result) const { for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->keys(result); } } template void KoRTree::NonLeafNode::values(QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->values(result); } } template typename KoRTree::Node * KoRTree::NonLeafNode::getNode(int index) const { return m_childs[index]; } template typename KoRTree::Node * KoRTree::NonLeafNode::getLeastEnlargement(const QRectF& bb) const { - //qDebug() << "NonLeafNode::getLeastEnlargement"; + //debugFlake << "NonLeafNode::getLeastEnlargement"; QVarLengthArray area(this->m_counter); for (int i = 0; i < this->m_counter; ++i) { QSizeF big(this->m_childBoundingBox[i].united(bb).size()); area[i] = big.width() * big.height() - this->m_childBoundingBox[i].width() * this->m_childBoundingBox[i].height(); } int minIndex = 0; qreal minArea = area[minIndex]; - //qDebug() << " min" << minIndex << minArea; + //debugFlake << " min" << minIndex << minArea; for (int i = 1; i < this->m_counter; ++i) { if (area[i] < minArea) { minIndex = i; minArea = area[i]; - //qDebug() << " min" << minIndex << minArea; + //debugFlake << " min" << minIndex << minArea; } } return m_childs[minIndex]; } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::NonLeafNode::debug(QString line) const { for (int i = 0; i < this->m_counter; ++i) { qDebug("%s %d %d", qPrintable(line), this->nodeId(), i); m_childs[i]->debug(line + " "); } } template void KoRTree::NonLeafNode::paint(QPainter & p, int level) const { this->paintRect(p, level); for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->paint(p, level + 1); } } #endif template int KoRTree::LeafNode::dataIdCounter = 0; template KoRTree::LeafNode::LeafNode(int capacity, int level, Node * parent) : Node(capacity, level, parent) , m_data(capacity) , m_dataIds(capacity) { - //qDebug() << "LeafNode::LeafNode" << this; + //debugFlake << "LeafNode::LeafNode" << this; } template KoRTree::LeafNode::~LeafNode() { - //qDebug() << "LeafNode::~LeafNode" << this; + //debugFlake << "LeafNode::~LeafNode" << this; } template void KoRTree::LeafNode::insert(const QRectF& bb, const T& data, int id) { m_data[this->m_counter] = data; m_dataIds[this->m_counter] = id; this->m_childBoundingBox[this->m_counter] = bb; this->m_boundingBox = this->m_boundingBox.united(bb); ++this->m_counter; } template void KoRTree::LeafNode::remove(int index) { for (int i = index + 1; i < this->m_counter; ++i) { m_data[i-1] = m_data[i]; m_dataIds[i-1] = m_dataIds[i]; } Node::remove(index); } template void KoRTree::LeafNode::remove(const T& data) { int old_counter = this->m_counter; for (int i = 0; i < this->m_counter; ++i) { if (m_data[i] == data) { - //qDebug() << "LeafNode::remove id" << i; + //debugFlake << "LeafNode::remove id" << i; remove(i); break; } } if (old_counter == this->m_counter) { qWarning() << "LeafNode::remove( const T&data) data not found"; } } template void KoRTree::LeafNode::move(Node * node, int index) { LeafNode * n = dynamic_cast(node); if (n) { - //qDebug() << "LeafNode::move" << this << node << index + //debugFlake << "LeafNode::move" << this << node << index // << node->nodeId() << "->" << this->nodeId() << n->childBoundingBox( index ); QRectF bb = n->childBoundingBox(index); insert(bb, n->getData(index), n->getDataId(index)); } } template typename KoRTree::LeafNode * KoRTree::LeafNode::chooseLeaf(const QRectF& bb) { Q_UNUSED(bb); return this; } template typename KoRTree::NonLeafNode * KoRTree::LeafNode::chooseNode(const QRectF& bb, int level) { Q_UNUSED(bb); Q_UNUSED(level); qFatal("LeafNode::chooseNode called. This should not happen!"); return 0; } template void KoRTree::LeafNode::intersects(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::contains(const QPointF & point, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].contains(point)) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::contained(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (rect.contains(this->m_childBoundingBox[i])) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::keys(QList & result) const { for (int i = 0; i < this->m_counter; ++i) { result.push_back(this->m_childBoundingBox[i]); } } template void KoRTree::LeafNode::values(QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { result.insert(m_dataIds[i], m_data[i]); } } template const T& KoRTree::LeafNode::getData(int index) const { return m_data[ index ]; } template int KoRTree::LeafNode::getDataId(int index) const { return m_dataIds[ index ]; } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::LeafNode::debug(QString line) const { for (int i = 0; i < this->m_counter; ++i) { qDebug("%s %d %d %p", qPrintable(line), this->nodeId(), i, &(m_data[i])); - qDebug() << this->m_childBoundingBox[i].toRect(); + debugFlake << this->m_childBoundingBox[i].toRect(); } } template void KoRTree::LeafNode::paint(QPainter & p, int level) const { if (this->m_counter) { this->paintRect(p, level); } } #endif #endif /* KORTREE_H */ diff --git a/libs/flake/KoToolBase.cpp b/libs/flake/KoToolBase.cpp index 0853b242f0..722a0f63ed 100644 --- a/libs/flake/KoToolBase.cpp +++ b/libs/flake/KoToolBase.cpp @@ -1,429 +1,429 @@ /* This file is part of the KDE project * Copyright (C) 2006, 2010 Thomas Zander * Copyright (C) 2011 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 #include #include "KoToolBase.h" #include "KoToolBase_p.h" #include "KoCanvasBase.h" #include "KoPointerEvent.h" #include "KoDocumentResourceManager.h" #include "KoCanvasResourceManager.h" #include "KoViewConverter.h" #include "KoShapeController.h" #include "KoShapeBasedDocumentBase.h" #include "KoToolSelection.h" #include #include #include #include #include #include KoToolBase::KoToolBase(KoCanvasBase *canvas) : d_ptr(new KoToolBasePrivate(this, canvas)) { Q_D(KoToolBase); d->connectSignals(); } KoToolBase::KoToolBase(KoToolBasePrivate &dd) : d_ptr(&dd) { Q_D(KoToolBase); d->connectSignals(); } KoToolBase::~KoToolBase() { // Enable this to easily generate action files for tools // if (actions().size() > 0) { // QDomDocument doc; // QDomElement e = doc.createElement("Actions"); // e.setAttribute("name", toolId()); // e.setAttribute("version", "2"); // doc.appendChild(e); // Q_FOREACH (QAction *action, actions().values()) { // QDomElement a = doc.createElement("Action"); // a.setAttribute("name", action->objectName()); // // But seriously, XML is the worst format ever designed // auto addElement = [&](QString title, QString content) { // QDomElement newNode = doc.createElement(title); // QDomText newText = doc.createTextNode(content); // newNode.appendChild(newText); // a.appendChild(newNode); // }; // addElement("icon", action->icon().name()); // addElement("text", action->text()); // addElement("whatsThis" , action->whatsThis()); // addElement("toolTip" , action->toolTip()); // addElement("iconText" , action->iconText()); // addElement("shortcut" , action->shortcut().toString()); // addElement("isCheckable" , QString((action->isChecked() ? "true" : "false"))); // addElement("statusTip", action->statusTip()); // e.appendChild(a); // } // QFile f(toolId() + ".action"); // f.open(QFile::WriteOnly); // f.write(doc.toString().toUtf8()); // f.close(); // } // else { -// qDebug() << "Tool" << toolId() << "has no actions"; +// debugFlake << "Tool" << toolId() << "has no actions"; // } qDeleteAll(d_ptr->optionWidgets); delete d_ptr; } bool KoToolBase::isActivated() const { Q_D(const KoToolBase); return d->isActivated; } void KoToolBase::activate(KoToolBase::ToolActivation toolActivation, const QSet &shapes) { Q_UNUSED(toolActivation); Q_UNUSED(shapes); Q_D(KoToolBase); d->isActivated = true; } void KoToolBase::deactivate() { Q_D(KoToolBase); d->isActivated = false; } void KoToolBase::canvasResourceChanged(int key, const QVariant & res) { Q_UNUSED(key); Q_UNUSED(res); } void KoToolBase::documentResourceChanged(int key, const QVariant &res) { Q_UNUSED(key); Q_UNUSED(res); } bool KoToolBase::wantsAutoScroll() const { return true; } void KoToolBase::mouseDoubleClickEvent(KoPointerEvent *event) { event->ignore(); } void KoToolBase::mouseTripleClickEvent(KoPointerEvent *event) { event->ignore(); } void KoToolBase::keyPressEvent(QKeyEvent *e) { e->ignore(); } void KoToolBase::keyReleaseEvent(QKeyEvent *e) { e->ignore(); } void KoToolBase::explicitUserStrokeEndRequest() { } QVariant KoToolBase::inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &) const { Q_D(const KoToolBase); if (d->canvas->canvasWidget() == 0) return QVariant(); switch (query) { case Qt::ImMicroFocus: return QRect(d->canvas->canvasWidget()->width() / 2, 0, 1, d->canvas->canvasWidget()->height()); case Qt::ImFont: return d->canvas->canvasWidget()->font(); default: return QVariant(); } } void KoToolBase::inputMethodEvent(QInputMethodEvent * event) { if (! event->commitString().isEmpty()) { QKeyEvent ke(QEvent::KeyPress, -1, 0, event->commitString()); keyPressEvent(&ke); } event->accept(); } void KoToolBase::customPressEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::customReleaseEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::customMoveEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::useCursor(const QCursor &cursor) { Q_D(KoToolBase); d->currentCursor = cursor; emit cursorChanged(d->currentCursor); } QList > KoToolBase::optionWidgets() { Q_D(KoToolBase); if (d->optionWidgets.empty()) { d->optionWidgets = createOptionWidgets(); } return d->optionWidgets; } void KoToolBase::addAction(const QString &name, QAction *action) { Q_D(KoToolBase); if (action->objectName().isEmpty()) { action->setObjectName(name); } d->actions.insert(name, action); } QHash KoToolBase::actions() const { Q_D(const KoToolBase); return d->actions; } QAction *KoToolBase::action(const QString &name) const { Q_D(const KoToolBase); return d->actions.value(name); } QWidget * KoToolBase::createOptionWidget() { return 0; } QList > KoToolBase::createOptionWidgets() { QList > ow; if (QWidget *widget = createOptionWidget()) { if (widget->objectName().isEmpty()) { widget->setObjectName(toolId()); } ow.append(widget); } return ow; } void KoToolBase::setToolId(const QString &id) { Q_D(KoToolBase); d->toolId = id; } QString KoToolBase::toolId() const { Q_D(const KoToolBase); return d->toolId; } QCursor KoToolBase::cursor() const { Q_D(const KoToolBase); return d->currentCursor; } void KoToolBase::deleteSelection() { } void KoToolBase::cut() { copy(); deleteSelection(); } QMenu *KoToolBase::popupActionsMenu() { return 0; } KoCanvasBase * KoToolBase::canvas() const { Q_D(const KoToolBase); return d->canvas; } void KoToolBase::setStatusText(const QString &statusText) { emit statusTextChanged(statusText); } uint KoToolBase::handleRadius() const { Q_D(const KoToolBase); if(d->canvas->shapeController()->resourceManager()) { return d->canvas->shapeController()->resourceManager()->handleRadius(); } else { return 3; } } uint KoToolBase::grabSensitivity() const { Q_D(const KoToolBase); if(d->canvas->shapeController()->resourceManager()) { return d->canvas->shapeController()->resourceManager()->grabSensitivity(); } else { return 3; } } QRectF KoToolBase::handleGrabRect(const QPointF &position) const { Q_D(const KoToolBase); const KoViewConverter * converter = d->canvas->viewConverter(); uint handleSize = 2*grabSensitivity(); QRectF r = converter->viewToDocument(QRectF(0, 0, handleSize, handleSize)); r.moveCenter(position); return r; } QRectF KoToolBase::handlePaintRect(const QPointF &position) const { Q_D(const KoToolBase); const KoViewConverter * converter = d->canvas->viewConverter(); uint handleSize = 2*handleRadius(); QRectF r = converter->viewToDocument(QRectF(0, 0, handleSize, handleSize)); r.moveCenter(position); return r; } void KoToolBase::setTextMode(bool value) { Q_D(KoToolBase); d->isInTextMode=value; } bool KoToolBase::paste() { return false; } void KoToolBase::copy() const { } void KoToolBase::dragMoveEvent(QDragMoveEvent *event, const QPointF &point) { Q_UNUSED(event); Q_UNUSED(point); } void KoToolBase::dragLeaveEvent(QDragLeaveEvent *event) { Q_UNUSED(event); } void KoToolBase::dropEvent(QDropEvent *event, const QPointF &point) { Q_UNUSED(event); Q_UNUSED(point); } bool KoToolBase::hasSelection() { KoToolSelection *sel = selection(); return (sel && sel->hasSelection()); } KoToolSelection *KoToolBase::selection() { return 0; } void KoToolBase::repaintDecorations() { } bool KoToolBase::isInTextMode() const { Q_D(const KoToolBase); return d->isInTextMode; } void KoToolBase::requestUndoDuringStroke() { /** * Default implementation just cancells the stroke */ requestStrokeCancellation(); } void KoToolBase::requestStrokeCancellation() { } void KoToolBase::requestStrokeEnd() { } bool KoToolBase::maskSyntheticEvents() const { Q_D(const KoToolBase); return d->maskSyntheticEvents; } void KoToolBase::setMaskSyntheticEvents(bool value) { Q_D(KoToolBase); d->maskSyntheticEvents = value; } diff --git a/libs/flake/resources/KoSvgSymbolCollectionResource.cpp b/libs/flake/resources/KoSvgSymbolCollectionResource.cpp index 5407aca043..2c4f082fe5 100644 --- a/libs/flake/resources/KoSvgSymbolCollectionResource.cpp +++ b/libs/flake/resources/KoSvgSymbolCollectionResource.cpp @@ -1,255 +1,256 @@ /* This file is part of the KDE project Copyright (c) 2017 L. E. Segovia This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include +#include void paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { QList shapes = group->shapes(); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); Q_FOREACH (KoShape *child, shapes) { // we paint recursively here, so we do not have to check recursively for visibility if (!child->isVisible(false)) continue; KoShapeGroup *childGroup = dynamic_cast(child); if (childGroup) { paintGroup(childGroup, painter, converter, paintContext); } else { painter.save(); KoShapeManager::renderSingleShape(child, painter, converter, paintContext); painter.restore(); } } } QImage KoSvgSymbol::icon() { KoShapeGroup *group = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(group, QImage()); QRectF rc = group->boundingRect().normalized(); QImage image(rc.width(), rc.height(), QImage::Format_ARGB32_Premultiplied); QPainter gc(&image); image.fill(Qt::gray); KoViewConverter vc; KoShapePaintingContext ctx; -// qDebug() << "Going to render. Original bounding rect:" << group->boundingRect() +// debugFlake << "Going to render. Original bounding rect:" << group->boundingRect() // << "Normalized: " << rc // << "Scale W" << 256 / rc.width() << "Scale H" << 256 / rc.height(); gc.translate(-rc.x(), -rc.y()); paintGroup(group, gc, vc, ctx); gc.end(); image = image.scaled(128, 128, Qt::KeepAspectRatio); return image; } struct KoSvgSymbolCollectionResource::Private { QVector symbols; QString title; QString description; }; KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource(const QString& filename) : KoResource(filename) , d(new Private()) { } KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource() : KoResource(QString()) , d(new Private()) { } KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource(const KoSvgSymbolCollectionResource& rhs) : QObject(0) , KoResource(QString()) , d(new Private()) { setFilename(rhs.filename()); d->symbols = rhs.d->symbols; setValid(true); } KoSvgSymbolCollectionResource::~KoSvgSymbolCollectionResource() { } bool KoSvgSymbolCollectionResource::load() { QFile file(filename()); if (file.size() == 0) return false; if (!file.open(QIODevice::ReadOnly)) { return false; } bool res = loadFromDevice(&file); file.close(); return res; } bool KoSvgSymbolCollectionResource::loadFromDevice(QIODevice *dev) { if (!dev->isOpen()) dev->open(QIODevice::ReadOnly); KoXmlDocument doc; QString errorMsg; int errorLine = 0; int errorColumn; bool ok = doc.setContent(dev->readAll(), false, &errorMsg, &errorLine, &errorColumn); if (!ok) { errKrita << "Parsing error in " << filename() << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" , errorLine , errorColumn , errorMsg); return false; } KoDocumentResourceManager manager; SvgParser parser(&manager); parser.setResolution(QRectF(0,0,100,100), 72); // initialize with default values QSizeF fragmentSize; // We're not interested in the shapes themselves qDeleteAll(parser.parseSvg(doc.documentElement(), &fragmentSize)); d->symbols = parser.takeSymbols(); -// qDebug() << "Loaded" << filename() << "\n\t" +// debugFlake << "Loaded" << filename() << "\n\t" // << "Title" << parser.documentTitle() << "\n\t" // << "Description" << parser.documentDescription() // << "\n\tgot" << d->symbols.size() << "symbols" // << d->symbols[0]->shape->outlineRect() // << d->symbols[0]->shape->size(); d->title = parser.documentTitle(); setName(d->title); d->description = parser.documentDescription(); if (d->symbols.size() < 1) { setValid(false); return false; } setValid(true); setImage(d->symbols[0]->icon()); return true; } bool KoSvgSymbolCollectionResource::save() { QFile file(filename()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } saveToDevice(&file); file.close(); return true; } bool KoSvgSymbolCollectionResource::saveToDevice(QIODevice *dev) const { bool res = false; // XXX if (res) { KoResource::saveToDevice(dev); } return res; } QString KoSvgSymbolCollectionResource::defaultFileExtension() const { return QString(".svg"); } QString KoSvgSymbolCollectionResource::title() const { return d->title; } QString KoSvgSymbolCollectionResource::description() const { return d->description; } QString KoSvgSymbolCollectionResource::creator() const { return ""; } QString KoSvgSymbolCollectionResource::rights() const { return ""; } QString KoSvgSymbolCollectionResource::language() const { return ""; } QStringList KoSvgSymbolCollectionResource::subjects() const { return QStringList(); } QString KoSvgSymbolCollectionResource::license() const { return ""; } QStringList KoSvgSymbolCollectionResource::permits() const { return QStringList(); } QVector KoSvgSymbolCollectionResource::symbols() const { return d->symbols; } diff --git a/libs/flake/svg/SvgLoadingContext.cpp b/libs/flake/svg/SvgLoadingContext.cpp index b45ee37740..bf38bec314 100644 --- a/libs/flake/svg/SvgLoadingContext.cpp +++ b/libs/flake/svg/SvgLoadingContext.cpp @@ -1,302 +1,302 @@ /* This file is part of the KDE project * Copyright (C) 2011 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 "SvgLoadingContext.h" #include #include #include #include #include #include #include #include #include "SvgGraphicContext.h" #include "SvgUtil.h" #include "SvgCssHelper.h" #include "SvgStyleParser.h" #include "kis_debug.h" class Q_DECL_HIDDEN SvgLoadingContext::Private { public: Private() : zIndex(0), styleParser(0) { } ~Private() { if (! gcStack.isEmpty() && !gcStack.top()->isResolutionFrame) { // Resolution frame is usually the first and is not removed. warnFlake << "the context stack is not empty (current count" << gcStack.size() << ", expected 0)"; } qDeleteAll(gcStack); gcStack.clear(); delete styleParser; } QStack gcStack; QString initialXmlBaseDir; int zIndex; KoDocumentResourceManager *documentResourceManager; QHash loadedShapes; QHash definitions; QHash profiles; SvgCssHelper cssStyles; SvgStyleParser *styleParser; FileFetcherFunc fileFetcher; }; SvgLoadingContext::SvgLoadingContext(KoDocumentResourceManager *documentResourceManager) : d(new Private()) { d->documentResourceManager = documentResourceManager; d->styleParser = new SvgStyleParser(*this); Q_ASSERT(d->documentResourceManager); } SvgLoadingContext::~SvgLoadingContext() { delete d; } SvgGraphicsContext *SvgLoadingContext::currentGC() const { if (d->gcStack.isEmpty()) return 0; return d->gcStack.top(); } #include "parsers/SvgTransformParser.h" SvgGraphicsContext *SvgLoadingContext::pushGraphicsContext(const KoXmlElement &element, bool inherit) { SvgGraphicsContext *gc = new SvgGraphicsContext; // copy data from current context if (! d->gcStack.isEmpty() && inherit) { *gc = *(d->gcStack.top()); } gc->textProperties.resetNonInheritableToDefault(); // some of the text properties are not inherited gc->filterId.clear(); // filters are not inherited gc->clipPathId.clear(); // clip paths are not inherited gc->clipMaskId.clear(); // clip masks are not inherited gc->display = true; // display is not inherited gc->opacity = 1.0; // opacity is not inherited if (!element.isNull()) { if (element.hasAttribute("transform")) { SvgTransformParser p(element.attribute("transform")); if (p.isValid()) { QTransform mat = p.transform(); gc->matrix = mat * gc->matrix; } } if (element.hasAttribute("xml:base")) gc->xmlBaseDir = element.attribute("xml:base"); if (element.hasAttribute("xml:space")) gc->preserveWhitespace = element.attribute("xml:space") == "preserve"; } d->gcStack.push(gc); return gc; } void SvgLoadingContext::popGraphicsContext() { delete(d->gcStack.pop()); } void SvgLoadingContext::setInitialXmlBaseDir(const QString &baseDir) { d->initialXmlBaseDir = baseDir; } QString SvgLoadingContext::xmlBaseDir() const { SvgGraphicsContext *gc = currentGC(); return (gc && !gc->xmlBaseDir.isEmpty()) ? gc->xmlBaseDir : d->initialXmlBaseDir; } QString SvgLoadingContext::absoluteFilePath(const QString &href) { QFileInfo info(href); if (! info.isRelative()) return href; SvgGraphicsContext *gc = currentGC(); if (!gc) return d->initialXmlBaseDir; QString baseDir = d->initialXmlBaseDir; if (! gc->xmlBaseDir.isEmpty()) baseDir = absoluteFilePath(gc->xmlBaseDir); QFileInfo pathInfo(QFileInfo(baseDir).filePath()); QString relFile = href; while (relFile.startsWith(QLatin1String("../"))) { relFile.remove(0, 3); pathInfo.setFile(pathInfo.dir(), QString()); } QString absFile = pathInfo.absolutePath() + '/' + relFile; return absFile; } QString SvgLoadingContext::relativeFilePath(const QString &href) { const SvgGraphicsContext *gc = currentGC(); if (!gc) return href; QString result = href; if (!gc->xmlBaseDir.isEmpty()) { result = gc->xmlBaseDir + QDir::separator() + href; } else if (!d->initialXmlBaseDir.isEmpty()) { result = d->initialXmlBaseDir + QDir::separator() + href; } return QDir::cleanPath(result); } int SvgLoadingContext::nextZIndex() { return d->zIndex++; } KoImageCollection* SvgLoadingContext::imageCollection() { return d->documentResourceManager->imageCollection(); } void SvgLoadingContext::registerShape(const QString &id, KoShape *shape) { if (!id.isEmpty()) d->loadedShapes.insert(id, shape); } KoShape* SvgLoadingContext::shapeById(const QString &id) { return d->loadedShapes.value(id); } void SvgLoadingContext::addDefinition(const KoXmlElement &element) { const QString id = element.attribute("id"); if (id.isEmpty() || d->definitions.contains(id)) return; d->definitions.insert(id, element); } KoXmlElement SvgLoadingContext::definition(const QString &id) const { return d->definitions.value(id); } bool SvgLoadingContext::hasDefinition(const QString &id) const { return d->definitions.contains(id); } void SvgLoadingContext::addStyleSheet(const KoXmlElement &styleSheet) { d->cssStyles.parseStylesheet(styleSheet); } QStringList SvgLoadingContext::matchingCssStyles(const KoXmlElement &element) const { return d->cssStyles.matchStyles(element); } SvgStyleParser &SvgLoadingContext::styleParser() { return *d->styleParser; } void SvgLoadingContext::parseProfile(const KoXmlElement &element) { const QString href = element.attribute("xlink:href"); const QByteArray uniqueId = QByteArray::fromHex(element.attribute("local").toLatin1()); const QString name = element.attribute("name"); if (element.attribute("rendering-intent", "auto") != "auto") { // WARNING: Krita does *not* treat rendering intents attributes of the profile! - qDebug() << "WARNING: we do *not* treat rendering intents attributes of the profile!"; + debugFlake << "WARNING: we do *not* treat rendering intents attributes of the profile!"; } if (d->profiles.contains(name)) { - qDebug() << "Profile already in the map!" << ppVar(name); + debugFlake << "Profile already in the map!" << ppVar(name); return; } const KoColorProfile *profile = KoColorSpaceRegistry::instance()->profileByUniqueId(uniqueId); if (!profile && d->fileFetcher) { KoColorSpaceEngine *engine = KoColorSpaceEngineRegistry::instance()->get("icc"); KIS_ASSERT(engine); if (engine) { const QString fileName = relativeFilePath(href); const QByteArray profileData = d->fileFetcher(fileName); if (!profileData.isEmpty()) { profile = engine->addProfile(profileData); if (profile->uniqueId() != uniqueId) { - qDebug() << "WARNING: ProfileID of the attached profile doesn't match the one mentioned in SVG element"; - qDebug() << " " << ppVar(profile->uniqueId().toHex()); - qDebug() << " " << ppVar(uniqueId.toHex()); + debugFlake << "WARNING: ProfileID of the attached profile doesn't match the one mentioned in SVG element"; + debugFlake << " " << ppVar(profile->uniqueId().toHex()); + debugFlake << " " << ppVar(uniqueId.toHex()); } } else { - qDebug() << "WARNING: couldn't fetch the ICCprofile file!" << fileName; + debugFlake << "WARNING: couldn't fetch the ICCprofile file!" << fileName; } } } if (profile) { d->profiles.insert(name, profile); } else { - qDebug() << "WARNING: couldn't load SVG profile" << ppVar(name) << ppVar(href) << ppVar(uniqueId); + debugFlake << "WARNING: couldn't load SVG profile" << ppVar(name) << ppVar(href) << ppVar(uniqueId); } } bool SvgLoadingContext::isRootContext() const { KIS_ASSERT(!d->gcStack.isEmpty()); return d->gcStack.size() == 1; } void SvgLoadingContext::setFileFetcher(SvgLoadingContext::FileFetcherFunc func) { d->fileFetcher = func; } QByteArray SvgLoadingContext::fetchExternalFile(const QString &url) { return d->fileFetcher ? d->fileFetcher(url) : QByteArray(); } diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index 7453b0043a..caa2095be3 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1858 +1,1858 @@ /* 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 #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 #include #include "kis_dom_utils.h" #include "kis_algebra_2d.h" #include "kis_debug.h" #include "kis_global.h" #include struct SvgParser::DeferredUseStore { struct El { El(const KoXmlElement* ue, const QString& key) : m_useElement(ue), m_key(key) { } const KoXmlElement* m_useElement; QString m_key; }; DeferredUseStore(SvgParser* p) : m_parse(p) { } void add(const KoXmlElement* useE, const QString& key) { m_uses.push_back(El(useE, key)); } bool empty() const { return m_uses.empty(); } void checkPendingUse(const KoXmlElement &b, QList& shapes) { KoShape* shape = 0; const QString id = b.attribute("id"); if (id.isEmpty()) return; - // qDebug() << "Checking id: " << id; + // debugFlake << "Checking id: " << id; auto i = std::partition(m_uses.begin(), m_uses.end(), [&](const El& e) -> bool {return e.m_key != id;}); while (i != m_uses.end()) { const El& el = m_uses.back(); if (m_parse->m_context.hasDefinition(el.m_key)) { - // qDebug() << "Found pending use for id: " << el.m_key; + // debugFlake << "Found pending use for id: " << el.m_key; shape = m_parse->resolveUse(*(el.m_useElement), el.m_key); if (shape) { shapes.append(shape); } } m_uses.pop_back(); } } ~DeferredUseStore() { while (!m_uses.empty()) { const El& el = m_uses.back(); debugFlake << "WARNING: could not find path in m_uses; }; SvgParser::SvgParser(KoDocumentResourceManager *documentResourceManager) : m_context(documentResourceManager) , m_documentResourceManager(documentResourceManager) { } SvgParser::~SvgParser() { qDeleteAll(m_symbols); } 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()->isResolutionFrame = true; 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; } void SvgParser::setForcedFontSizeResolution(qreal value) { if (qFuzzyCompare(value, 0.0)) return; m_context.currentGC()->forcedFontSizeCoeff = 72.0 / value; } QList SvgParser::shapes() const { return m_shapes; } QVector SvgParser::takeSymbols() { QVector symbols = m_symbols; m_symbols.clear(); return symbols; } // 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 (KoXml::childNodesCount(e) == 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); } qreal SvgParser::parseAngular(const QString &unit) { return SvgUtil::parseUnitAngular(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(); + debugFlake << "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); } /** * In Krita shapes X,Y coordinates are baked into the shape global transform, but * the pattern should be painted in "user" coordinates. Therefore, we should handle * this offfset separately. * * TODO: Please also note 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()); m_context.pushGraphicsContext(e); gc = m_context.currentGC(); gc->workaroundClearInheritedFillProperties(); // HACK! // 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 { marker->setExplicitOrientation(parseAngular(orientation)); } // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e, false); 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::parseSymbol(const KoXmlElement &e) { const QString id = e.attribute("id"); if (id.isEmpty()) return false; QScopedPointer svgSymbol(new KoSvgSymbol()); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e, false); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->currentBoundingBox = QRectF(0.0, 0.0, 1.0, 1.0); QString title = e.firstChildElement("title").toElement().text(); QScopedPointer symbolShape(parseGroup(e)); m_context.popGraphicsContext(); if (!symbolShape) return false; svgSymbol->shape = symbolShape.take(); svgSymbol->title = title; svgSymbol->id = id; if (title.isEmpty()) svgSymbol->title = id; if (svgSymbol->shape->boundingRect() == QRectF(0.0, 0.0, 0.0, 0.0)) { debugFlake << "Symbol" << id << "seems to be empty, discarding"; return false; } m_symbols << svgSymbol.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(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! 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(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! 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; applyCurrentBasicStyle(shape); if (KoPathShape *pathShape = dynamic_cast(shape)) { applyMarkers(pathShape); } applyFilter(shape); applyClipping(shape, shapeToOriginalUserCoordinates); applyMaskClipping(shape, shapeToOriginalUserCoordinates); } void SvgParser::applyCurrentBasicStyle(KoShape *shape) { if (!shape) return; SvgGraphicsContext *gc = m_context.currentGC(); KIS_ASSERT(gc); if (!dynamic_cast(shape)) { applyFillStyle(shape); applyStrokeStyle(shape); } 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(); const QRectF outlineRect = shape->outlineRect(); if (outlineRect.isEmpty()) return resultGradient; /** * If shape outline rect is valid, convert the gradient into OBB mode by * doing some magic conversions: we compensate non-uniform size of the shape * by applying an additional pre-transform */ QRadialGradient *rgradient = static_cast(resultGradient); const qreal maxDimension = KisAlgebra2D::maxDimension(outlineRect); const QRectF uniformSize(outlineRect.topLeft(), QSizeF(maxDimension, maxDimension)); const QTransform uniformizeTransform = QTransform::fromTranslate(-outlineRect.x(), -outlineRect.y()) * QTransform::fromScale(maxDimension / shape->outlineRect().width(), maxDimension / shape->outlineRect().height()) * QTransform::fromTranslate(outlineRect.x(), outlineRect.y()); const QPointF centerLocal = transform->map(rgradient->center()); const QPointF focalLocal = transform->map(rgradient->focalPoint()); const QPointF centerOBB = KisAlgebra2D::absoluteToRelative(centerLocal, uniformSize); const QPointF focalOBB = KisAlgebra2D::absoluteToRelative(focalLocal, uniformSize); rgradient->setCenter(centerOBB); rgradient->setFocalPoint(focalOBB); const qreal centerRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->centerRadius(), uniformSize); const qreal focalRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->focalRadius(), uniformSize); rgradient->setCenterRadius(centerRadiusOBB); rgradient->setFocalRadius(focalRadiusOBB); rgradient->setCoordinateMode(QGradient::ObjectBoundingMode); // Warning: should it really be pre-multiplication? *transform = uniformizeTransform * gradient->transform(); } } 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 transformation 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, DeferredUseStore* deferredUseStore) { QString href = e.attribute("xlink:href"); if (href.isEmpty()) return 0; QString key = href.mid(1); const bool gotDef = m_context.hasDefinition(key); if (gotDef) { return resolveUse(e, key); } else if (deferredUseStore) { deferredUseStore->add(&e, key); return 0; } - qDebug() << "WARNING: Did not find reference for svg 'use' element. Skipping. Id: " + debugFlake << "WARNING: Did not find reference for svg 'use' element. Skipping. Id: " << key; return 0; } KoShape* SvgParser::resolveUse(const KoXmlElement &e, const QString& key) { KoShape *result = 0; SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); // TODO: parse 'width' and 'height' 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, KoShapeContainer *group) { m_shapes += shapes; if (!group || shapes.isEmpty()) return; // not normalized KoShapeGroupCommand cmd(group, shapes, 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; // First find the metadata for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement b = n.toElement(); if (b.isNull()) continue; if (b.tagName() == "title") { m_documentTitle = b.text().trimmed(); } else if (b.tagName() == "desc") { m_documentDescription = b.text().trimmed(); } else if (b.tagName() == "metadata") { // TODO: parse the metadata } } // SVG 1.1: skip the rendering of the element if it has null viewBox; however an inverted viewbox is just peachy // and as mother makes them -- if mother is inkscape. if (gc->currentBoundingBox.normalized().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(); } QString SvgParser::documentTitle() const { return m_documentTitle; } QString SvgParser::documentDescription() const { return m_documentDescription; } 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, 0); } 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; } KoShape* SvgParser::parseTextNode(const KoXmlText &e) { QScopedPointer textChunk(new KoSvgTextChunkShape()); textChunk->setZIndex(m_context.nextZIndex()); if (!textChunk->loadSvgTextNode(e, m_context)) { return 0; } textChunk->applyAbsoluteTransformation(m_context.currentGC()->matrix); applyCurrentBasicStyle(textChunk.data()); // apply style to this group after size is set return textChunk.take(); } KoXmlText getTheOnlyTextChild(const KoXmlElement &e) { KoXmlNode firstChild = e.firstChild(); return !firstChild.isNull() && firstChild == e.lastChild() && firstChild.isText() ? firstChild.toText() : KoXmlText(); } KoShape *SvgParser::parseTextElement(const KoXmlElement &e, KoSvgTextShape *mergeIntoShape) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(e.tagName() == "text" || e.tagName() == "tspan", 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_isInsideTextSubtree || e.tagName() == "text", 0); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(e.tagName() == "text" || !mergeIntoShape, 0); KoSvgTextShape *rootTextShape = 0; if (e.tagName() == "text") { // XXX: Shapes need to be created by their factories rootTextShape = mergeIntoShape ? mergeIntoShape : new KoSvgTextShape(); } if (rootTextShape) { m_isInsideTextSubtree = true; } m_context.pushGraphicsContext(e); uploadStyleToContext(e); KoSvgTextChunkShape *textChunk = rootTextShape ? rootTextShape : new KoSvgTextChunkShape(); textChunk->setZIndex(m_context.nextZIndex()); textChunk->loadSvg(e, m_context); // 1) apply transformation only in case we are not overriding the shape! // 2) the transformation should be applied *before* the shape is added to the group! if (!mergeIntoShape) { // groups should also have their own coordinate system! textChunk->applyAbsoluteTransformation(m_context.currentGC()->matrix); const QPointF extraOffset = extraShapeOffset(textChunk, m_context.currentGC()->matrix); // handle id applyId(e.attribute("id"), textChunk); applyCurrentStyle(textChunk, extraOffset); // apply style to this group after size is set } else { m_context.currentGC()->matrix = mergeIntoShape->absoluteTransformation(0); applyCurrentBasicStyle(textChunk); } KoXmlText onlyTextChild = getTheOnlyTextChild(e); if (!onlyTextChild.isNull()) { textChunk->loadSvgTextNode(onlyTextChild, m_context); } else { QList childShapes = parseContainer(e, true); addToGroup(childShapes, textChunk); } m_context.popGraphicsContext(); textChunk->normalizeCharTransformations(); if (rootTextShape) { textChunk->simplifyFillStrokeInheritance(); m_isInsideTextSubtree = false; rootTextShape->relayout(); } return textChunk; } QList SvgParser::parseContainer(const KoXmlElement &e, bool parseTextNodes) { QList shapes; // are we parsing a switch container bool isSwitch = e.tagName() == "switch"; DeferredUseStore deferredUseStore(this); for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement b = n.toElement(); if (b.isNull()) { if (parseTextNodes && n.isText()) { KoShape *shape = parseTextNode(n.toText()); if (shape) { shapes += shape; } } 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 implemented yet } } QList currentShapes = parseSingleElement(b, &deferredUseStore); shapes.append(currentShapes); // if we are parsing a switch, stop after the first supported element if (isSwitch && !currentShapes.isEmpty()) break; } return shapes; } void SvgParser::parseDefsElement(const KoXmlElement &e) { KIS_SAFE_ASSERT_RECOVER_RETURN(e.tagName() == "defs"); parseSingleElement(e); } QList SvgParser::parseSingleElement(const KoXmlElement &b, DeferredUseStore* deferredUseStore) { QList shapes; // save definition for later instantiation with 'use' m_context.addDefinition(b); if (deferredUseStore) { deferredUseStore->checkPendingUse(b, shapes); } if (b.tagName() == "svg") { shapes += parseSvg(b); } else if (b.tagName() == "g" || b.tagName() == "a") { // 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 (KoXml::childNodesCount(b) > 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); m_defsShapes << defsShape; // TODO: where to delete the shape!? } } else if (b.tagName() == "linearGradient" || b.tagName() == "radialGradient") { } 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() == "symbol") { parseSymbol(b); } else if (b.tagName() == "style") { m_context.addStyleSheet(b); } else if (b.tagName() == "text" || b.tagName() == "tspan") { shapes += parseTextElement(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") { KoShape *shape = createObjectDirect(b); if (shape) shapes.append(shape); } else if (b.tagName() == "use") { KoShape* s = parseUse(b, deferredUseStore); if (s) { shapes += s; } } 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; QStringList pointList = SvgUtil::simplifyList(element.attribute("points")); for (QStringList::Iterator it = pointList.begin(); it != pointList.end(); ++it) { QPointF point; point.setX(SvgUtil::fromUserSpace(KisDomUtils::toDouble(*it))); ++it; if (it == pointList.end()) break; point.setY(SvgUtil::fromUserSpace(KisDomUtils::toDouble(*it))); 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; const QString tagName = SvgUtil::mapExtendedShapeTag(element.tagName(), element); QList factories = KoShapeRegistry::instance()->factoriesForElement(KoXmlNS::svg, 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 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)); 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/text/KoSvgTextChunkShape.cpp b/libs/flake/text/KoSvgTextChunkShape.cpp index 7ed4bf7684..0bd21a464e 100644 --- a/libs/flake/text/KoSvgTextChunkShape.cpp +++ b/libs/flake/text/KoSvgTextChunkShape.cpp @@ -1,946 +1,947 @@ /* * Copyright (c) 2017 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 "KoSvgTextChunkShape.h" #include "KoSvgTextChunkShape_p.h" #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include namespace { void appendLazy(QVector *list, boost::optional value, int iteration, bool hasDefault = true, qreal defaultValue = 0.0) { if (!value) return; if (value && *value == defaultValue && hasDefault == true && list->isEmpty()) return; while (list->size() < iteration) { list->append(defaultValue); } list->append(*value); } void fillTransforms(QVector *xPos, QVector *yPos, QVector *dxPos, QVector *dyPos, QVector *rotate, QVector localTransformations) { for (int i = 0; i < localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = localTransformations[i]; appendLazy(xPos, t.xPos, i, false); appendLazy(yPos, t.yPos, i, false); appendLazy(dxPos, t.dxPos, i); appendLazy(dyPos, t.dyPos, i); appendLazy(rotate, t.rotate, i); } } QVector parseListAttributeX(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitX(context.currentGC(), str); } return result; } QVector parseListAttributeY(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitY(context.currentGC(), str); } return result; } QVector parseListAttributeAngular(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitAngular(context.currentGC(), str); } return result; } QString convertListAttribute(const QVector &values) { QStringList stringValues; Q_FOREACH (qreal value, values) { stringValues.append(KisDomUtils::toString(value)); } return stringValues.join(','); } } struct KoSvgTextChunkShapePrivate::LayoutInterface : public KoSvgTextChunkShapeLayoutInterface { LayoutInterface(KoSvgTextChunkShape *_q) : q(_q) {} KoSvgText::AutoValue textLength() const { return q->d_func()->textLength; } KoSvgText::LengthAdjust lengthAdjust() const { return q->d_func()->lengthAdjust; } int numChars() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), 0); int result = 0; if (!q->shapeCount()) { result = q->d_func()->text.size(); } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); result += chunkShape->layoutInterface()->numChars(); } } return result; } int relativeCharPos(KoSvgTextChunkShape *child, int pos) const { QList childShapes = q->shapes(); int result = -1; int numCharsPassed = 0; Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); if (chunkShape == child) { result = pos + numCharsPassed; break; } else { numCharsPassed += chunkShape->layoutInterface()->numChars(); } } return result; } bool isTextNode() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), false); return !q->shapeCount(); } QString nodeText() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), 0); return !q->shapeCount() ? q->d_func()->text : QString(); } QVector localCharTransformations() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(isTextNode(), QVector()); const QVector t = q->d_func()->localTransformations; return t.mid(0, qMin(t.size(), q->d_func()->text.size())); } static QString getBidiOpening(KoSvgText::Direction direction, KoSvgText::UnicodeBidi bidi) { using namespace KoSvgText; QString result; if (bidi == BidiEmbed) { result = direction == DirectionLeftToRight ? "\u202a" : "\u202b"; } else if (bidi == BidiOverride) { result = direction == DirectionLeftToRight ? "\u202d" : "\u202e"; } return result; } QVector collectSubChunks() const { QVector result; if (isTextNode()) { const QString text = q->d_func()->text; const KoSvgText::KoSvgCharChunkFormat format = q->d_func()->fetchCharFormat(); QVector transforms = q->d_func()->localTransformations; /** * Sometimes SVG can contain the X,Y offsets for the pieces of text that * do not exist, just skip them. */ if (text.size() <= transforms.size()) { transforms.resize(text.size()); } KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(q->d_func()->properties.propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt()); KoSvgText::Direction direction = KoSvgText::Direction(q->d_func()->properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); const QString bidiOpening = getBidiOpening(direction, bidi); if (!bidiOpening.isEmpty()) { result << SubChunk(bidiOpening, format); } if (transforms.isEmpty()) { result << SubChunk(text, format); } else { for (int i = 0; i < transforms.size(); i++) { const KoSvgText::CharTransformation baseTransform = transforms[i]; int subChunkLength = 1; for (int j = i + 1; j < transforms.size(); j++) { if (transforms[j].isNull()) { subChunkLength++; } else { break; } } if (i + subChunkLength >= transforms.size()) { subChunkLength = text.size() - i; } result << SubChunk(text.mid(i, subChunkLength), format, baseTransform); i += subChunkLength - 1; } } if (!bidiOpening.isEmpty()) { result << SubChunk("\u202c", format); } } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result += chunkShape->layoutInterface()->collectSubChunks(); } } return result; } void addAssociatedOutline(const QRectF &rect) override { KIS_SAFE_ASSERT_RECOVER_RETURN(isTextNode()); QPainterPath path; path.addRect(rect); path |= q->d_func()->associatedOutline; path.setFillRule(Qt::WindingFill); path = path.simplified(); q->d_func()->associatedOutline = path; q->d_func()->size = path.boundingRect().size(); q->notifyChanged(); q->d_func()->shapeChanged(KoShape::SizeChanged); } void clearAssociatedOutline() override { q->d_func()->associatedOutline = QPainterPath(); q->d_func()->size = QSizeF(); q->notifyChanged(); q->d_func()->shapeChanged(KoShape::SizeChanged); } private: KoSvgTextChunkShape *q; }; KoSvgTextChunkShape::KoSvgTextChunkShape() : KoShapeContainer(new KoSvgTextChunkShapePrivate(this)) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::KoSvgTextChunkShape(const KoSvgTextChunkShape &rhs) : KoShapeContainer(new KoSvgTextChunkShapePrivate(*rhs.d_func(), this)) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::KoSvgTextChunkShape(KoSvgTextChunkShapePrivate *dd) : KoShapeContainer(dd) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::~KoSvgTextChunkShape() { } KoShape *KoSvgTextChunkShape::cloneShape() const { return new KoSvgTextChunkShape(*this); } QSizeF KoSvgTextChunkShape::size() const { return outlineRect().size(); } void KoSvgTextChunkShape::setSize(const QSizeF &size) { Q_UNUSED(size); // we do not support resizing! } QRectF KoSvgTextChunkShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoSvgTextChunkShape::outline() const { Q_D(const KoSvgTextChunkShape); QPainterPath result; result.setFillRule(Qt::WindingFill); if (d->layoutInterface->isTextNode()) { result = d->associatedOutline; } else { Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result |= chunkShape->outline(); } } return result.simplified(); } void KoSvgTextChunkShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); } void KoSvgTextChunkShape::saveOdf(KoShapeSavingContext &context) const { Q_UNUSED(context); } bool KoSvgTextChunkShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_UNUSED(element); Q_UNUSED(context); return false; } bool KoSvgTextChunkShape::saveHtml(HtmlSavingContext &context) { Q_D(KoSvgTextChunkShape); // Should we add a newline? Check for vertical movement if we're using rtl or ltr text // XXX: if vertical text, check horizontal movement. QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); for (int i = 0; i < d->localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = d->localTransformations[i]; appendLazy(&xPos, t.xPos, i, false); appendLazy(&yPos, t.yPos, i, false); appendLazy(&dxPos, t.dxPos, i); appendLazy(&dyPos, t.dyPos, i); } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); // XXX: we don't save fill, stroke, text length, length adjust or spacing and glyphs. KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); if (isRootTextNode()) { context.shapeWriter().startElement("body", false); if (layoutInterface()->isTextNode()) { context.shapeWriter().startElement("p", false); } // XXX: Save the style? } else if (parent->isRootTextNode()) { context.shapeWriter().startElement("p", false); } else { context.shapeWriter().startElement("span", false); // XXX: Save the style? } QMap attributes = ownProperties.convertToSvgTextAttributes(); if (attributes.size() > 0) { QString styleString; for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { if (QString(it.key().toLatin1().data()).contains("text-anchor")) { QString val = it.value(); if (it.value()=="middle") { val = "center"; } else if (it.value()=="end") { val = "right"; } else { val = "left"; } styleString.append("text-align") .append(": ") .append(val) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("fill")){ styleString.append("color") .append(": ") .append(it.value()) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("font-size")){ QString val = it.value(); if (QRegExp ("\\d*").exactMatch(val)) { val.append("pt"); } styleString.append(it.key().toLatin1().data()) .append(": ") .append(val) .append(";" ); } else { styleString.append(it.key().toLatin1().data()) .append(": ") .append(it.value()) .append(";" ); } } context.shapeWriter().addAttribute("style", styleString); } if (layoutInterface()->isTextNode()) { - qDebug() << "saveHTML" << this << d->text << xPos << yPos << dxPos << dyPos; + debugFlake << "saveHTML" << this << d->text << xPos << yPos << dxPos << dyPos; // After adding all the styling to the

element, add the text context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveHtml(context); } } if (isRootTextNode() && layoutInterface()->isTextNode()) { context.shapeWriter().endElement(); // body } context.shapeWriter().endElement(); // p or span return true; } void writeTextListAttribute(const QString &attribute, const QVector &values, KoXmlWriter &writer) { const QString value = convertListAttribute(values); if (!value.isEmpty()) { writer.addAttribute(attribute.toLatin1().data(), value); } } bool KoSvgTextChunkShape::saveSvg(SvgSavingContext &context) { Q_D(KoSvgTextChunkShape); if (isRootTextNode()) { context.shapeWriter().startElement("text", false); if (!context.strippedTextMode()) { context.shapeWriter().addAttribute("id", context.getID(this)); SvgUtil::writeTransformAttributeLazy("transform", transformation(), context.shapeWriter()); SvgStyleWriter::saveSvgStyle(this, context); } else { SvgStyleWriter::saveSvgFill(this, context); SvgStyleWriter::saveSvgStroke(this, context); } } else { context.shapeWriter().startElement("tspan", false); if (!context.strippedTextMode()) { SvgStyleWriter::saveSvgBasicStyle(this, context); } } if (layoutInterface()->isTextNode()) { QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); writeTextListAttribute("x", xPos, context.shapeWriter()); writeTextListAttribute("y", yPos, context.shapeWriter()); writeTextListAttribute("dx", dxPos, context.shapeWriter()); writeTextListAttribute("dy", dyPos, context.shapeWriter()); writeTextListAttribute("rotate", rotate, context.shapeWriter()); } if (!d->textLength.isAuto) { context.shapeWriter().addAttribute("textLength", KisDomUtils::toString(d->textLength.customValue)); if (d->lengthAdjust == KoSvgText::LengthAdjustSpacingAndGlyphs) { context.shapeWriter().addAttribute("lengthAdjust", "spacingAndGlyphs"); } } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); // we write down stroke/fill iff they are different from the parent's value if (!isRootTextNode()) { if (ownProperties.hasProperty(KoSvgTextProperties::FillId)) { SvgStyleWriter::saveSvgFill(this, context); } if (ownProperties.hasProperty(KoSvgTextProperties::StrokeId)) { SvgStyleWriter::saveSvgStroke(this, context); } } QMap attributes = ownProperties.convertToSvgTextAttributes(); for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { context.shapeWriter().addAttribute(it.key().toLatin1().data(), it.value()); } if (layoutInterface()->isTextNode()) { context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveSvg(context); } } context.shapeWriter().endElement(); return true; } void KoSvgTextChunkShapePrivate::loadContextBasedProperties(SvgGraphicsContext *gc) { properties = gc->textProperties; font = gc->font; fontFamiliesList = gc->fontFamiliesList; } void KoSvgTextChunkShape::resetTextShape() { Q_D(KoSvgTextChunkShape); using namespace KoSvgText; d->properties = KoSvgTextProperties(); d->font = QFont(); d->fontFamiliesList = QStringList(); d->textLength = AutoValue(); d->lengthAdjust = LengthAdjustSpacing; d->localTransformations.clear(); d->text.clear(); // all the subchunks are destroyed! // (first detach, then destroy) QList shapesToReset = shapes(); Q_FOREACH (KoShape *shape, shapesToReset) { shape->setParent(0); delete shape; } } bool KoSvgTextChunkShape::loadSvg(const KoXmlElement &e, SvgLoadingContext &context) { Q_D(KoSvgTextChunkShape); SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); d->textLength = KoSvgText::parseAutoValueXY(e.attribute("textLength", ""), context, ""); d->lengthAdjust = KoSvgText::parseLengthAdjust(e.attribute("lengthAdjust", "spacing")); QVector xPos = parseListAttributeX(e.attribute("x", ""), context); QVector yPos = parseListAttributeY(e.attribute("y", ""), context); QVector dxPos = parseListAttributeX(e.attribute("dx", ""), context); QVector dyPos = parseListAttributeY(e.attribute("dy", ""), context); QVector rotate = parseListAttributeAngular(e.attribute("rotate", ""), context); const int numLocalTransformations = std::max({xPos.size(), yPos.size(), dxPos.size(), dyPos.size(), rotate.size()}); d->localTransformations.resize(numLocalTransformations); for (int i = 0; i < numLocalTransformations; i++) { if (i < xPos.size()) { d->localTransformations[i].xPos = xPos[i]; } if (i < yPos.size()) { d->localTransformations[i].yPos = yPos[i]; } if (i < dxPos.size() && dxPos[i] != 0.0) { d->localTransformations[i].dxPos = dxPos[i]; } if (i < dyPos.size() && dyPos[i] != 0.0) { d->localTransformations[i].dyPos = dyPos[i]; } if (i < rotate.size()) { d->localTransformations[i].rotate = rotate[i]; } } return true; } namespace { bool hasNextSibling(const KoXmlNode &node) { if (!node.nextSibling().isNull()) return true; KoXmlNode parentNode = node.parentNode(); if (!parentNode.isNull() && parentNode.isElement() && parentNode.toElement().tagName() == "tspan") { return hasNextSibling(parentNode); } return false; } bool hasPreviousSibling(const KoXmlNode &node) { if (!node.previousSibling().isNull()) return true; KoXmlNode parentNode = node.parentNode(); if (!parentNode.isNull() && parentNode.isElement() && parentNode.toElement().tagName() == "tspan") { return hasPreviousSibling(parentNode); } return false; } } bool KoSvgTextChunkShape::loadSvgTextNode(const KoXmlText &text, SvgLoadingContext &context) { Q_D(KoSvgTextChunkShape); SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); QString data = text.data(); data.replace(QRegExp("[\\r\\n]"), ""); data.replace(QRegExp("\\s{2,}"), " "); if (data.startsWith(' ') && !hasPreviousSibling(text)) { data.remove(0, 1); } if (data.endsWith(' ') && !hasNextSibling(text)) { data.remove(data.size() - 1, 1); } if (data == " ") { data = ""; } //ENTER_FUNCTION() << text.data() << "-->" << data; d->text = data; return !data.isEmpty(); } void KoSvgTextChunkShape::normalizeCharTransformations() { Q_D(KoSvgTextChunkShape); d->applyParentCharTransformations(d->localTransformations); } void KoSvgTextChunkShape::simplifyFillStrokeInheritance() { Q_D(KoSvgTextChunkShape); if (!isRootTextNode()) { KoShape *parentShape = parent(); KIS_SAFE_ASSERT_RECOVER_RETURN(parentShape); QSharedPointer bg = background(); QSharedPointer parentBg = parentShape->background(); if (!inheritBackground() && ((!bg && !parentBg) || (bg && parentBg && bg->compareTo(parentShape->background().data())))) { setInheritBackground(true); } KoShapeStrokeModelSP stroke = this->stroke(); KoShapeStrokeModelSP parentStroke= parentShape->stroke(); if (!inheritStroke() && ((!stroke && !parentStroke) || (stroke && parentStroke && stroke->compareFillTo(parentShape->stroke().data()) && stroke->compareStyleTo(parentShape->stroke().data())))) { setInheritStroke(true); } } Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->simplifyFillStrokeInheritance(); } } KoSvgTextProperties KoSvgTextChunkShape::textProperties() const { Q_D(const KoSvgTextChunkShape); KoSvgTextProperties properties = d->properties; properties.setProperty(KoSvgTextProperties::FillId, QVariant::fromValue(KoSvgText::BackgroundProperty(background()))); properties.setProperty(KoSvgTextProperties::StrokeId, QVariant::fromValue(KoSvgText::StrokeProperty(stroke()))); return properties; } bool KoSvgTextChunkShape::isTextNode() const { Q_D(const KoSvgTextChunkShape); return d->layoutInterface->isTextNode(); } KoSvgTextChunkShapeLayoutInterface *KoSvgTextChunkShape::layoutInterface() { Q_D(KoSvgTextChunkShape); return d->layoutInterface.data(); } bool KoSvgTextChunkShape::isRootTextNode() const { return false; } /**************************************************************************************************/ /* KoSvgTextChunkShapePrivate */ /**************************************************************************************************/ #include "SimpleShapeContainerModel.h" KoSvgTextChunkShapePrivate::KoSvgTextChunkShapePrivate(KoSvgTextChunkShape *_q) : KoShapeContainerPrivate(_q) { } KoSvgTextChunkShapePrivate::KoSvgTextChunkShapePrivate(const KoSvgTextChunkShapePrivate &rhs, KoSvgTextChunkShape *q) : KoShapeContainerPrivate(rhs, q), properties(rhs.properties), font(rhs.font), fontFamiliesList(rhs.fontFamiliesList), localTransformations(rhs.localTransformations), textLength(rhs.textLength), lengthAdjust(rhs.lengthAdjust), text(rhs.text) { if (rhs.model) { SimpleShapeContainerModel *otherModel = dynamic_cast(rhs.model); KIS_ASSERT_RECOVER_RETURN(otherModel); model = new SimpleShapeContainerModel(*otherModel); } } KoSvgTextChunkShapePrivate::~KoSvgTextChunkShapePrivate() { } #include #include #include KoSvgText::KoSvgCharChunkFormat KoSvgTextChunkShapePrivate::fetchCharFormat() const { Q_Q(const KoSvgTextChunkShape); KoSvgText::KoSvgCharChunkFormat format; format.setFont(font); format.setTextAnchor(KoSvgText::TextAnchor(properties.propertyOrDefault(KoSvgTextProperties::TextAnchorId).toInt())); KoSvgText::Direction direction = KoSvgText::Direction(properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); format.setLayoutDirection(direction == KoSvgText::DirectionLeftToRight ? Qt::LeftToRight : Qt::RightToLeft); KoSvgText::BaselineShiftMode shiftMode = KoSvgText::BaselineShiftMode(properties.propertyOrDefault(KoSvgTextProperties::BaselineShiftModeId).toInt()); // FIXME: we support only 'none', 'sub' and 'super' shifts at the moment. // Please implement 'percentage' as well! // WARNING!!! Qt's setVerticalAlignment() also changes the size of the font! And SVG does not(!) imply it! if (shiftMode == KoSvgText::ShiftSub) { format.setVerticalAlignment(QTextCharFormat::AlignSubScript); } else if (shiftMode == KoSvgText::ShiftSuper) { format.setVerticalAlignment(QTextCharFormat::AlignSuperScript); } KoSvgText::AutoValue letterSpacing = properties.propertyOrDefault(KoSvgTextProperties::LetterSpacingId).value(); if (!letterSpacing.isAuto) { format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(letterSpacing.customValue); } KoSvgText::AutoValue wordSpacing = properties.propertyOrDefault(KoSvgTextProperties::WordSpacingId).value(); if (!wordSpacing.isAuto) { format.setFontWordSpacing(wordSpacing.customValue); } KoSvgText::AutoValue kerning = properties.propertyOrDefault(KoSvgTextProperties::KerningId).value(); if (!kerning.isAuto) { format.setFontKerning(false); format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(format.fontLetterSpacing() + kerning.customValue); } QBrush textBrush = Qt::NoBrush; if (q->background()) { KoColorBackground *colorBackground = dynamic_cast(q->background().data()); KIS_SAFE_ASSERT_RECOVER (colorBackground) { textBrush = Qt::red; } if (colorBackground) { textBrush = colorBackground->brush(); } } format.setForeground(textBrush); QPen textPen = Qt::NoPen; if (q->stroke()) { KoShapeStroke *stroke = dynamic_cast(q->stroke().data()); if (stroke) { textPen = stroke->resultLinePen(); } } format.setTextOutline(textPen); // TODO: avoid const_cast somehow... format.setAssociatedShape(const_cast(q)); return format; } void KoSvgTextChunkShapePrivate::applyParentCharTransformations(const QVector transformations) { Q_Q(KoSvgTextChunkShape); if (q->shapeCount()) { int numCharsPassed = 0; Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); const int numCharsInSubtree = chunkShape->layoutInterface()->numChars(); QVector t = transformations.mid(numCharsPassed, numCharsInSubtree); if (t.isEmpty()) break; chunkShape->d_func()->applyParentCharTransformations(t); numCharsPassed += numCharsInSubtree; if (numCharsPassed >= transformations.size()) break; } } else { for (int i = 0; i < qMin(transformations.size(), text.size()); i++) { KIS_SAFE_ASSERT_RECOVER_RETURN(localTransformations.size() >= i); if (localTransformations.size() == i) { localTransformations.append(transformations[i]); } else { localTransformations[i].mergeInParentTransformation(transformations[i]); } } } } diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp index 9f9fdcaed1..3fa2af0e26 100644 --- a/libs/flake/text/KoSvgTextShape.cpp +++ b/libs/flake/text/KoSvgTextShape.cpp @@ -1,629 +1,630 @@ /* * Copyright (c) 2017 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 "KoSvgTextShape.h" #include #include #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include struct KoSvgTextShapePrivate : public KoSvgTextChunkShapePrivate { KoSvgTextShapePrivate(KoSvgTextShape *_q) : KoSvgTextChunkShapePrivate(_q) { } KoSvgTextShapePrivate(const KoSvgTextShapePrivate &rhs, KoSvgTextShape *q) : KoSvgTextChunkShapePrivate(rhs, q) { } std::vector> cachedLayouts; std::vector cachedLayoutsOffsets; QThread *cachedLayoutsWorkingThread = 0; void clearAssociatedOutlines(KoShape *rootShape); Q_DECLARE_PUBLIC(KoSvgTextShape) }; KoSvgTextShape::KoSvgTextShape() : KoSvgTextChunkShape(new KoSvgTextShapePrivate(this)) { Q_D(KoSvgTextShape); setShapeId(KoSvgTextShape_SHAPEID); } KoSvgTextShape::KoSvgTextShape(const KoSvgTextShape &rhs) : KoSvgTextChunkShape(new KoSvgTextShapePrivate(*rhs.d_func(), this)) { Q_D(KoSvgTextShape); setShapeId(KoSvgTextShape_SHAPEID); // QTextLayout has no copy-ctor, so just relayout everything! relayout(); } KoSvgTextShape::~KoSvgTextShape() { } KoShape *KoSvgTextShape::cloneShape() const { return new KoSvgTextShape(*this); } void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape) { KoSvgTextChunkShape::shapeChanged(type, shape); if (type == StrokeChanged || type == BackgroundChanged || type == ContentChanged) { relayout(); } } void KoSvgTextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoSvgTextShape); Q_UNUSED(paintContext); /** * HACK ALERT: * QTextLayout should only be accessed from the tread it has been created in. * If the cached layout has been created in a different thread, we should just * recreate the layouts in the current thread to be able to render them. */ if (QThread::currentThread() != d->cachedLayoutsWorkingThread) { relayout(); } applyConversion(painter, converter); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { d->cachedLayouts[i]->draw(&painter, d->cachedLayoutsOffsets[i]); } /** * HACK ALERT: * The layouts of non-gui threads must be destroyed in the same thread * they have been created. Because the thread might be restarted in the * meantime or just destroyed, meaning that the per-thread freetype data * will not be available. */ if (QThread::currentThread() != qApp->thread()) { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = 0; } } void KoSvgTextShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); // do nothing! everything is painted in paintComponent() } QPainterPath KoSvgTextShape::textOutline() { Q_D(KoSvgTextShape); QPainterPath result; result.setFillRule(Qt::WindingFill); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; const QTextLayout *layout = d->cachedLayouts[i].get(); for (int j = 0; j < layout->lineCount(); j++) { QTextLine line = layout->lineAt(j); Q_FOREACH (const QGlyphRun &run, line.glyphRuns()) { const QVector indexes = run.glyphIndexes(); const QVector positions = run.positions(); const QRawFont font = run.rawFont(); KIS_SAFE_ASSERT_RECOVER(indexes.size() == positions.size()) { continue; } for (int k = 0; k < indexes.size(); k++) { QPainterPath glyph = font.pathForGlyph(indexes[k]); glyph.translate(positions[k] + layoutOffset); result += glyph; } const qreal thickness = font.lineThickness(); const QRectF runBounds = run.boundingRect(); if (run.overline()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y(); QRectF overlineBlob(runBounds.x(), y, runBounds.width(), thickness); overlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(overlineBlob); // don't use direct addRect, because it does't care about Qt::WindingFill result += path; } if (run.strikeOut()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y() + 0.5 * line.height(); QRectF strikeThroughBlob(runBounds.x(), y, runBounds.width(), thickness); strikeThroughBlob.translate(layoutOffset); QPainterPath path; path.addRect(strikeThroughBlob); // don't use direct addRect, because it does't care about Qt::WindingFill result += path; } if (run.underline()) { const qreal y = line.y() + line.ascent() + font.underlinePosition(); QRectF underlineBlob(runBounds.x(), y, runBounds.width(), thickness); underlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(underlineBlob); // don't use direct addRect, because it does't care about Qt::WindingFill result += path; } } } } return result; } void KoSvgTextShape::resetTextShape() { KoSvgTextChunkShape::resetTextShape(); relayout(); } struct TextChunk { QString text; QVector formats; Qt::LayoutDirection direction = Qt::LeftToRight; Qt::Alignment alignment = Qt::AlignLeading; struct SubChunkOffset { QPointF offset; int start = 0; }; QVector offsets; boost::optional xStartPos; boost::optional yStartPos; QPointF applyStartPosOverride(const QPointF &pos) const { QPointF result = pos; if (xStartPos) { result.rx() = *xStartPos; } if (yStartPos) { result.ry() = *yStartPos; } return result; } }; QVector mergeIntoChunks(const QVector &subChunks) { QVector chunks; for (auto it = subChunks.begin(); it != subChunks.end(); ++it) { if (it->transformation.startsNewChunk() || it == subChunks.begin()) { TextChunk newChunk = TextChunk(); newChunk.direction = it->format.layoutDirection(); newChunk.alignment = it->format.calculateAlignment(); newChunk.xStartPos = it->transformation.xPos; newChunk.yStartPos = it->transformation.yPos; chunks.append(newChunk); } TextChunk ¤tChunk = chunks.last(); if (it->transformation.hasRelativeOffset()) { TextChunk::SubChunkOffset o; o.start = currentChunk.text.size(); o.offset = it->transformation.relativeOffset(); KIS_SAFE_ASSERT_RECOVER_NOOP(!o.offset.isNull()); currentChunk.offsets.append(o); } QTextLayout::FormatRange formatRange; formatRange.start = currentChunk.text.size(); formatRange.length = it->text.size(); formatRange.format = it->format; currentChunk.formats.append(formatRange); currentChunk.text += it->text; } return chunks; } /** * Qt's QTextLayout has a weird trait, it doesn't count space characters as * distinct characters in QTextLayout::setNumColumns(), that is, if we want to * position a block of text that starts with a space character in a specific * position, QTextLayout will drop this space and will move the text to the left. * * That is why we have a special wrapper object that ensures that no spaces are * dropped and their horizontal advance parameter is taken into account. */ struct LayoutChunkWrapper { LayoutChunkWrapper(QTextLayout *layout) : m_layout(layout) { } QPointF addTextChunk(int startPos, int length, const QPointF &textChunkStartPos) { QPointF currentTextPos = textChunkStartPos; const int lastPos = startPos + length - 1; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(startPos == m_addedChars, currentTextPos); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastPos < m_layout->text().size(), currentTextPos); QTextLine line; std::swap(line, m_danglingLine); if (!line.isValid()) { line = m_layout->createLine(); } // skip all the space characters that were not included into the Qt's text line const int currentLineStart = line.isValid() ? line.textStart() : startPos + length; while (startPos < currentLineStart && startPos <= lastPos) { currentTextPos.rx() += skipSpaceCharacter(startPos); startPos++; } if (startPos <= lastPos) { const int numChars = lastPos - startPos + 1; line.setNumColumns(numChars); line.setPosition(currentTextPos - QPointF(0, line.ascent())); currentTextPos.rx() += line.horizontalAdvance(); // skip all the space characters that were not included into the Qt's text line for (int i = line.textStart() + line.textLength(); i < lastPos; i++) { currentTextPos.rx() += skipSpaceCharacter(i); } } else { // keep the created but unused line for future use std::swap(line, m_danglingLine); } m_addedChars += length; return currentTextPos; } private: qreal skipSpaceCharacter(int pos) { const QTextCharFormat format = formatForPos(pos, m_layout->formats()); const QChar skippedChar = m_layout->text()[pos]; KIS_SAFE_ASSERT_RECOVER_NOOP(skippedChar.isSpace() || !skippedChar.isPrint()); QFontMetrics metrics(format.font()); return metrics.width(skippedChar); } static QTextCharFormat formatForPos(int pos, const QVector &formats) { Q_FOREACH (const QTextLayout::FormatRange &range, formats) { if (pos >= range.start && pos < range.start + range.length) { return range.format; } } KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "pos should be within the bounds of the layouted text"); return QTextCharFormat(); } private: int m_addedChars = 0; QTextLayout *m_layout; QTextLine m_danglingLine; }; void KoSvgTextShape::relayout() { Q_D(KoSvgTextShape); d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = QThread::currentThread(); QPointF currentTextPos; QVector textChunks = mergeIntoChunks(layoutInterface()->collectSubChunks()); Q_FOREACH (const TextChunk &chunk, textChunks) { std::unique_ptr layout(new QTextLayout()); QTextOption option; // WARNING: never activate this option! It breaks the RTL text layout! //option.setFlags(QTextOption::ShowTabsAndSpaces); option.setWrapMode(QTextOption::WrapAnywhere); option.setUseDesignMetrics(true); // TODO: investigate if it is needed? option.setTextDirection(chunk.direction); layout->setText(chunk.text); layout->setTextOption(option); layout->setFormats(chunk.formats); layout->setCacheEnabled(true); layout->beginLayout(); currentTextPos = chunk.applyStartPosOverride(currentTextPos); const QPointF anchorPointPos = currentTextPos; int lastSubChunkStart = 0; QPointF lastSubChunkOffset; LayoutChunkWrapper wrapper(layout.get()); for (int i = 0; i <= chunk.offsets.size(); i++) { const bool isFinalPass = i == chunk.offsets.size(); const int length = !isFinalPass ? chunk.offsets[i].start - lastSubChunkStart : chunk.text.size() - lastSubChunkStart; if (length > 0) { currentTextPos += lastSubChunkOffset; currentTextPos = wrapper.addTextChunk(lastSubChunkStart, length, currentTextPos); } if (!isFinalPass) { lastSubChunkOffset = chunk.offsets[i].offset; lastSubChunkStart = chunk.offsets[i].start; } } layout->endLayout(); QPointF diff; if (chunk.alignment & Qt::AlignTrailing || chunk.alignment & Qt::AlignHCenter) { if (chunk.alignment & Qt::AlignTrailing) { diff = currentTextPos - anchorPointPos; } else if (chunk.alignment & Qt::AlignHCenter) { diff = 0.5 * (currentTextPos - anchorPointPos); } // TODO: fix after t2b text implemented diff.ry() = 0; } d->cachedLayouts.push_back(std::move(layout)); d->cachedLayoutsOffsets.push_back(-diff); } d->clearAssociatedOutlines(this); for (int i = 0; i < int(d->cachedLayouts.size()); i++) { const QTextLayout &layout = *d->cachedLayouts[i]; const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; using namespace KoSvgText; Q_FOREACH (const QTextLayout::FormatRange &range, layout.formats()) { const KoSvgCharChunkFormat &format = static_cast(range.format); AssociatedShapeWrapper wrapper = format.associatedShapeWrapper(); const int rangeStart = range.start; const int safeRangeLength = range.length > 0 ? range.length : layout.text().size() - rangeStart; if (safeRangeLength <= 0) continue; const int rangeEnd = range.start + safeRangeLength - 1; const int firstLineIndex = layout.lineForTextPosition(rangeStart).lineNumber(); const int lastLineIndex = layout.lineForTextPosition(rangeEnd).lineNumber(); for (int i = firstLineIndex; i <= lastLineIndex; i++) { const QTextLine line = layout.lineAt(i); // It might happen that the range contains only one (or two) // symbol that is a whitespace symbol. In such a case we should // just skip this (invalid) line. if (!line.isValid()) continue; const int posStart = qMax(line.textStart(), rangeStart); const int posEnd = qMin(line.textStart() + line.textLength() - 1, rangeEnd); const QList glyphRuns = line.glyphRuns(posStart, posEnd - posStart + 1); Q_FOREACH (const QGlyphRun &run, glyphRuns) { const QPointF firstPosition = run.positions().first(); const quint32 firstGlyphIndex = run.glyphIndexes().first(); const QPointF lastPosition = run.positions().last(); const quint32 lastGlyphIndex = run.glyphIndexes().last(); const QRawFont rawFont = run.rawFont(); const QRectF firstGlyphRect = rawFont.boundingRect(firstGlyphIndex).translated(firstPosition); const QRectF lastGlyphRect = rawFont.boundingRect(lastGlyphIndex).translated(lastPosition); QRectF rect = run.boundingRect(); /** * HACK ALERT: there is a bug in a way how Qt calculates boundingRect() * of the glyph run. It doesn't care about left and right bearings * of the border chars in the run, therefore it becomes cropped. * * Here we just add a half-char width margin to both sides * of the glyph run to make sure the glyphs are fully painted. * * BUG: 389528 * BUG: 392068 */ rect.setLeft(qMin(rect.left(), lastGlyphRect.left()) - 0.5 * firstGlyphRect.width()); rect.setRight(qMax(rect.right(), lastGlyphRect.right()) + 0.5 * lastGlyphRect.width()); wrapper.addCharacterRect(rect.translated(layoutOffset)); } } } } } void KoSvgTextShapePrivate::clearAssociatedOutlines(KoShape *rootShape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(rootShape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->layoutInterface()->clearAssociatedOutline(); Q_FOREACH (KoShape *child, chunkShape->shapes()) { clearAssociatedOutlines(child); } } bool KoSvgTextShape::isRootTextNode() const { return true; } KoSvgTextShapeFactory::KoSvgTextShapeFactory() : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18n("Text")) { setToolTip(i18n("SVG Text Shape")); setIconName(koIconNameCStr("x-shape-text")); setLoadingPriority(5); setXmlElementNames(KoXmlNS::svg, QStringList("text")); KoShapeTemplate t; t.name = i18n("SVG Text"); t.iconName = koIconName("x-shape-text"); t.toolTip = i18n("SVG Text Shape"); addTemplate(t); } KoShape *KoSvgTextShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const { - qDebug() << "Create default svg text shape"; + debugFlake << "Create default svg text shape"; KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "", QRectF(0, 0, 200, 60), documentResources->shapeController()->pixelsPerInch()); - qDebug() << converter.errors() << converter.warnings(); + debugFlake << converter.errors() << converter.warnings(); return shape; } KoShape *KoSvgTextShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const { KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); QString svgText = params->stringProperty("svgText", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); QString defs = params->stringProperty("defs" , ""); QRectF shapeRect = QRectF(0, 0, 200, 60); QVariant rect = params->property("shapeRect"); if (rect.type()==QVariant::RectF) { shapeRect = rect.toRectF(); } KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg(svgText, defs, shapeRect, documentResources->shapeController()->pixelsPerInch()); shape->setBackground(QSharedPointer(new KoColorBackground(QColor(Qt::black)))); shape->setPosition(shapeRect.topLeft()); return shape; } bool KoSvgTextShapeFactory::supports(const KoXmlElement &/*e*/, KoShapeLoadingContext &/*context*/) const { return false; } diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index 8537248590..43ae0b3969 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1231 +1,1232 @@ /* * Copyright (c) 2017 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 "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include +#include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { - qDebug() << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; + debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); KoXmlDocument doc; QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); if (!doc.setContent(fullText, &errorMessage, &errorLine, &errorColumn)) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); - qDebug() << "\t\t" << *htmlText; + debugFlake << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { - qDebug() << ">>>>>>>>>>>" << htmlText; + debugFlake << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { - qDebug() << "\tdoing br"; + debugFlake << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { - qDebug() << "\tstart Element" << elementName; + debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line - qDebug() << "\t\tstart Element" << elementName; + debugFlake << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { - qDebug() << "\tstart Element" << elementName; + debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { - qDebug() << "\tstart Element" << elementName; + debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { - qDebug() << "\tstart Element" << elementName; + debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { - qDebug() << "\tstart Element" << elementName; + debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { - qDebug() << "\t\tAdvancing to the next line"; + debugFlake << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { - qDebug() << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; + debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { - qDebug() << "\tCharacters:" << htmlReader.text(); + debugFlake << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape * (accordign to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) if (svgReader.name() != "text" && elementAttributes.hasAttribute("dy") && elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } if (KisDomUtils::toDouble(xString) == 0.0) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } } } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() == 1, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; SvgGraphicsContext *context = new SvgGraphicsContext(); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) if (it.value() == test.property(it.key())) diff.clearProperty(it.key()); return diff; } diff --git a/libs/flake/tools/KoPathTool.cpp b/libs/flake/tools/KoPathTool.cpp index f6cbc3e002..109f18e8fb 100644 --- a/libs/flake/tools/KoPathTool.cpp +++ b/libs/flake/tools/KoPathTool.cpp @@ -1,1315 +1,1315 @@ /* This file is part of the KDE project * Copyright (C) 2006-2012 Jan Hambrecht * Copyright (C) 2006,2007 Thorsten Zachmann * Copyright (C) 2007, 2010 Thomas Zander * Copyright (C) 2007 Boudewijn Rempt * * 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 "KoPathTool.h" #include "KoToolBase_p.h" #include "KoPathShape_p.h" #include "KoPathToolHandle.h" #include "KoCanvasBase.h" #include "KoShapeManager.h" #include "KoSelectedShapesProxy.h" #include "KoDocumentResourceManager.h" #include "KoViewConverter.h" #include "KoSelection.h" #include "KoPointerEvent.h" #include "commands/KoPathPointTypeCommand.h" #include "commands/KoPathPointInsertCommand.h" #include "commands/KoPathPointRemoveCommand.h" #include "commands/KoPathSegmentTypeCommand.h" #include "commands/KoPathBreakAtPointCommand.h" #include "commands/KoPathSegmentBreakCommand.h" #include "commands/KoParameterToPathCommand.h" #include "commands/KoSubpathJoinCommand.h" #include #include #include #include "KoParameterShape.h" #include #include "KoPathPoint.h" #include "KoPathPointRubberSelectStrategy.h" #include "KoPathSegmentChangeStrategy.h" #include "KoPathConnectionPointStrategy.h" #include "KoParameterChangeStrategy.h" #include "PathToolOptionWidget.h" #include "KoConnectionShape.h" #include "KoSnapGuide.h" #include "KoShapeController.h" #include "kis_action_registry.h" #include #include #include "kis_command_utils.h" #include "kis_pointer_utils.h" #include #include #include #include #include #include #include #include #include static const unsigned char needle_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x00, 0x0f, 0x00, 0x1f, 0x00, 0x3e, 0x00, 0x7e, 0x00, 0x7c, 0x00, 0x1c, 0x00, 0x18, 0x00, 0x00 }; static const unsigned char needle_move_bits[] = { 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x60, 0x00, 0xc0, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x80, 0x07, 0x10, 0x0f, 0x38, 0x1f, 0x54, 0x3e, 0xfe, 0x7e, 0x54, 0x7c, 0x38, 0x1c, 0x10, 0x18, 0x00, 0x00 }; // helper function to calculate the squared distance between two points qreal squaredDistance(const QPointF& p1, const QPointF &p2) { qreal dx = p1.x()-p2.x(); qreal dy = p1.y()-p2.y(); return dx*dx + dy*dy; } struct KoPathTool::PathSegment { PathSegment() : path(0), segmentStart(0), positionOnSegment(0) { } bool isValid() { return path && segmentStart; } KoPathShape *path; KoPathPoint *segmentStart; qreal positionOnSegment; }; KoPathTool::KoPathTool(KoCanvasBase *canvas) : KoToolBase(canvas) , m_pointSelection(this) , m_activeHandle(0) , m_handleRadius(3) , m_activeSegment(0) , m_currentStrategy(0) , m_activatedTemporarily(false) { QActionGroup *points = new QActionGroup(this); // m_pointTypeGroup->setExclusive(true); KisActionRegistry *actionRegistry = KisActionRegistry::instance(); m_actionPathPointCorner = actionRegistry->makeQAction("pathpoint-corner", this); addAction("pathpoint-corner", m_actionPathPointCorner); m_actionPathPointCorner->setData(KoPathPointTypeCommand::Corner); points->addAction(m_actionPathPointCorner); m_actionPathPointSmooth = actionRegistry->makeQAction("pathpoint-smooth", this); addAction("pathpoint-smooth", m_actionPathPointSmooth); m_actionPathPointSmooth->setData(KoPathPointTypeCommand::Smooth); points->addAction(m_actionPathPointSmooth); m_actionPathPointSymmetric = actionRegistry->makeQAction("pathpoint-symmetric", this); addAction("pathpoint-symmetric", m_actionPathPointSymmetric); m_actionPathPointSymmetric->setData(KoPathPointTypeCommand::Symmetric); points->addAction(m_actionPathPointSymmetric); m_actionCurvePoint = actionRegistry->makeQAction("pathpoint-curve", this); addAction("pathpoint-curve", m_actionCurvePoint); connect(m_actionCurvePoint, SIGNAL(triggered()), this, SLOT(pointToCurve())); m_actionLinePoint = actionRegistry->makeQAction("pathpoint-line", this); addAction("pathpoint-line", m_actionLinePoint); connect(m_actionLinePoint, SIGNAL(triggered()), this, SLOT(pointToLine())); m_actionLineSegment = actionRegistry->makeQAction("pathsegment-line", this); addAction("pathsegment-line", m_actionLineSegment); connect(m_actionLineSegment, SIGNAL(triggered()), this, SLOT(segmentToLine())); m_actionCurveSegment = actionRegistry->makeQAction("pathsegment-curve", this); addAction("pathsegment-curve", m_actionCurveSegment); connect(m_actionCurveSegment, SIGNAL(triggered()), this, SLOT(segmentToCurve())); m_actionAddPoint = actionRegistry->makeQAction("pathpoint-insert", this); addAction("pathpoint-insert", m_actionAddPoint); connect(m_actionAddPoint, SIGNAL(triggered()), this, SLOT(insertPoints())); m_actionRemovePoint = actionRegistry->makeQAction("pathpoint-remove", this); addAction("pathpoint-remove", m_actionRemovePoint); connect(m_actionRemovePoint, SIGNAL(triggered()), this, SLOT(removePoints())); m_actionBreakPoint = actionRegistry->makeQAction("path-break-point", this); addAction("path-break-point", m_actionBreakPoint); connect(m_actionBreakPoint, SIGNAL(triggered()), this, SLOT(breakAtPoint())); m_actionBreakSegment = actionRegistry->makeQAction("path-break-segment", this); addAction("path-break-segment", m_actionBreakSegment); connect(m_actionBreakSegment, SIGNAL(triggered()), this, SLOT(breakAtSegment())); m_actionJoinSegment = actionRegistry->makeQAction("pathpoint-join", this); addAction("pathpoint-join", m_actionJoinSegment); connect(m_actionJoinSegment, SIGNAL(triggered()), this, SLOT(joinPoints())); m_actionMergePoints = actionRegistry->makeQAction("pathpoint-merge", this); addAction("pathpoint-merge", m_actionMergePoints); connect(m_actionMergePoints, SIGNAL(triggered()), this, SLOT(mergePoints())); m_actionConvertToPath = actionRegistry->makeQAction("convert-to-path", this); addAction("convert-to-path", m_actionConvertToPath); connect(m_actionConvertToPath, SIGNAL(triggered()), this, SLOT(convertToPath())); m_contextMenu.reset(new QMenu()); connect(points, SIGNAL(triggered(QAction*)), this, SLOT(pointTypeChanged(QAction*))); connect(&m_pointSelection, SIGNAL(selectionChanged()), this, SLOT(pointSelectionChanged())); QBitmap b = QBitmap::fromData(QSize(16, 16), needle_bits); QBitmap m = b.createHeuristicMask(false); m_selectCursor = QCursor(b, m, 2, 0); b = QBitmap::fromData(QSize(16, 16), needle_move_bits); m = b.createHeuristicMask(false); m_moveCursor = QCursor(b, m, 2, 0); } KoPathTool::~KoPathTool() { delete m_activeHandle; delete m_activeSegment; delete m_currentStrategy; } QList > KoPathTool::createOptionWidgets() { QList > list; PathToolOptionWidget * toolOptions = new PathToolOptionWidget(this); connect(this, SIGNAL(typeChanged(int)), toolOptions, SLOT(setSelectionType(int))); connect(this, SIGNAL(singleShapeChanged(KoPathShape*)), toolOptions, SLOT(setCurrentShape(KoPathShape*))); connect(toolOptions, SIGNAL(sigRequestUpdateActions()), this, SLOT(updateActions())); updateOptionsWidget(); toolOptions->setWindowTitle(i18n("Edit Shape")); list.append(toolOptions); return list; } void KoPathTool::pointTypeChanged(QAction *type) { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *initialConversionCommand = createPointToCurveCommand(selectedPoints); // conversion should happen before the c-tor // of KoPathPointTypeCommand is executed! if (initialConversionCommand) { initialConversionCommand->redo(); } KUndo2Command *command = new KoPathPointTypeCommand(selectedPoints, static_cast(type->data().toInt())); if (initialConversionCommand) { using namespace KisCommandUtils; CompositeCommand *parent = new CompositeCommand(); parent->setText(command->text()); parent->addCommand(new SkipFirstRedoWrapper(initialConversionCommand)); parent->addCommand(command); command = parent; } d->canvas->addCommand(command); } } void KoPathTool::insertPoints() { Q_D(KoToolBase); QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { qreal positionInSegment = 0.5; if (m_activeSegment && m_activeSegment->isValid()) { positionInSegment = m_activeSegment->positionOnSegment; } KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, positionInSegment); d->canvas->addCommand(cmd); // TODO: this construction is dangerous. The canvas can remove the command right after // it has been added to it! m_pointSelection.clear(); foreach (KoPathPoint * p, cmd->insertedPoints()) { m_pointSelection.add(p, false); } } } void KoPathTool::removePoints() { Q_D(KoToolBase); if (m_pointSelection.size() > 0) { KUndo2Command *cmd = KoPathPointRemoveCommand::createCommand(m_pointSelection.selectedPointsData(), d->canvas->shapeController()); PointHandle *pointHandle = dynamic_cast(m_activeHandle); if (pointHandle && m_pointSelection.contains(pointHandle->activePoint())) { delete m_activeHandle; m_activeHandle = 0; } clearActivePointSelectionReferences(); d->canvas->addCommand(cmd); } } void KoPathTool::pointToLine() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); QList pointToChange; QList::const_iterator it(selectedPoints.constBegin()); for (; it != selectedPoints.constEnd(); ++it) { KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); if (point && (point->activeControlPoint1() || point->activeControlPoint2())) pointToChange.append(*it); } if (! pointToChange.isEmpty()) { d->canvas->addCommand(new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Line)); } } } void KoPathTool::pointToCurve() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); KUndo2Command *command = createPointToCurveCommand(selectedPoints); if (command) { d->canvas->addCommand(command); } } } KUndo2Command* KoPathTool::createPointToCurveCommand(const QList &points) { KUndo2Command *command = 0; QList pointToChange; QList::const_iterator it(points.constBegin()); for (; it != points.constEnd(); ++it) { KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); if (point && (! point->activeControlPoint1() || ! point->activeControlPoint2())) pointToChange.append(*it); } if (!pointToChange.isEmpty()) { command = new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Curve); } return command; } void KoPathTool::segmentToLine() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Line)); } } } void KoPathTool::segmentToCurve() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Curve)); } } } void KoPathTool::convertToPath() { Q_D(KoToolBase); KoSelection *selection = canvas()->selectedShapesProxy()->selection(); QList parameterShapes; Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameteric = dynamic_cast(shape); if (parameteric && parameteric->isParametricShape()) { parameterShapes.append(parameteric); } } if (!parameterShapes.isEmpty()) { d->canvas->addCommand(new KoParameterToPathCommand(parameterShapes)); } QList textShapes; Q_FOREACH (KoShape *shape, selection->selectedEditableShapes()) { if (KoSvgTextShape *text = dynamic_cast(shape)) { textShapes.append(text); } } if (!textShapes.isEmpty()) { KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Convert to Path")); // TODO: reuse the text from KoParameterToPathCommand const QList oldSelectedShapes = implicitCastList(textShapes); new KoKeepShapesSelectedCommand(oldSelectedShapes, {}, canvas()->selectedShapesProxy(), false, cmd); QList newSelectedShapes; Q_FOREACH (KoSvgTextShape *shape, textShapes) { const QPainterPath path = shape->textOutline(); if (path.isEmpty()) continue; KoPathShape *pathShape = KoPathShape::createShapeFromPainterPath(path); pathShape->setBackground(shape->background()); pathShape->setStroke(shape->stroke()); pathShape->setZIndex(shape->zIndex()); pathShape->setTransformation(shape->transformation()); KoShapeContainer *parent = shape->parent(); canvas()->shapeController()->addShapeDirect(pathShape, parent, cmd); newSelectedShapes << pathShape; } canvas()->shapeController()->removeShapes(oldSelectedShapes, cmd); new KoKeepShapesSelectedCommand({}, newSelectedShapes, canvas()->selectedShapesProxy(), true, cmd); canvas()->addCommand(cmd); } updateOptionsWidget(); } namespace { bool checkCanJoinToPoints(const KoPathPointData & pd1, const KoPathPointData & pd2) { const KoPathPointIndex & index1 = pd1.pointIndex; const KoPathPointIndex & index2 = pd2.pointIndex; KoPathShape *path1 = pd1.pathShape; KoPathShape *path2 = pd2.pathShape; // check if subpaths are already closed if (path1->isClosedSubpath(index1.first) || path2->isClosedSubpath(index2.first)) return false; // check if first point is an endpoint if (index1.second != 0 && index1.second != path1->subpathPointCount(index1.first)-1) return false; // check if second point is an endpoint if (index2.second != 0 && index2.second != path2->subpathPointCount(index2.first)-1) return false; return true; } } void KoPathTool::mergePointsImpl(bool doJoin) { Q_D(KoToolBase); if (m_pointSelection.size() != 2) return; QList pointData = m_pointSelection.selectedPointsData(); if (pointData.size() != 2) return; const KoPathPointData & pd1 = pointData.at(0); const KoPathPointData & pd2 = pointData.at(1); if (!checkCanJoinToPoints(pd1, pd2)) { return; } clearActivePointSelectionReferences(); KUndo2Command *cmd = 0; if (doJoin) { cmd = new KoMultiPathPointJoinCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); } else { cmd = new KoMultiPathPointMergeCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); } d->canvas->addCommand(cmd); } void KoPathTool::joinPoints() { mergePointsImpl(true); } void KoPathTool::mergePoints() { mergePointsImpl(false); } void KoPathTool::breakAtPoint() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { d->canvas->addCommand(new KoPathBreakAtPointCommand(m_pointSelection.selectedPointsData())); } } void KoPathTool::breakAtSegment() { Q_D(KoToolBase); // only try to break a segment when 2 points of the same object are selected if (m_pointSelection.objectCount() == 1 && m_pointSelection.size() == 2) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { d->canvas->addCommand(new KoPathSegmentBreakCommand(segments.at(0))); } } } void KoPathTool::paint(QPainter &painter, const KoViewConverter &converter) { Q_D(KoToolBase); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::primarySelection()); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { parameterShape->paintHandles(helper); } else { shape->paintPoints(helper); } if (!shape->stroke() || !shape->stroke()->isVisible()) { helper.setHandleStyle(KisHandleStyle::secondarySelection()); helper.drawPath(shape->outline()); } } if (m_currentStrategy) { painter.save(); m_currentStrategy->paint(painter, converter); painter.restore(); } m_pointSelection.paint(painter, converter, m_handleRadius); if (m_activeHandle) { if (m_activeHandle->check(m_pointSelection.selectedShapes())) { m_activeHandle->paint(painter, converter, m_handleRadius); } else { delete m_activeHandle; m_activeHandle = 0; } } else if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; // if the stroke is invisible, then we already painted the outline of the shape! if (shape->stroke() && shape->stroke()->isVisible()) { KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = shape->segmentByIndex(index).toCubic(); KIS_SAFE_ASSERT_RECOVER_RETURN(segment.isValid()); KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); helper.setHandleStyle(KisHandleStyle::secondarySelection()); QPainterPath path; path.moveTo(segment.first()->point()); path.cubicTo(segment.first()->controlPoint2(), segment.second()->controlPoint1(), segment.second()->point()); helper.drawPath(path); } } if (m_currentStrategy) { painter.save(); KoShape::applyConversion(painter, converter); d->canvas->snapGuide()->paint(painter, converter); painter.restore(); } } void KoPathTool::repaintDecorations() { Q_FOREACH (KoShape *shape, m_pointSelection.selectedShapes()) { repaint(shape->boundingRect()); } m_pointSelection.repaint(); updateOptionsWidget(); } void KoPathTool::mousePressEvent(KoPointerEvent *event) { // we are moving if we hit a point and use the left mouse button event->ignore(); if (m_activeHandle) { m_currentStrategy = m_activeHandle->handleMousePress(event); event->accept(); } else { if (event->button() & Qt::LeftButton) { // check if we hit a path segment if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = shape->segmentByIndex(index); m_pointSelection.add(segment.first(), !(event->modifiers() & Qt::ShiftModifier)); m_pointSelection.add(segment.second(), false); KoPathPointData data(shape, index); m_currentStrategy = new KoPathSegmentChangeStrategy(this, event->point, data, m_activeSegment->positionOnSegment); event->accept(); } else { KoShapeManager *shapeManager = canvas()->shapeManager(); KoSelection *selection = shapeManager->selection(); KoShape *shape = shapeManager->shapeAt(event->point, KoFlake::ShapeOnTop); if (shape && !selection->isSelected(shape)) { if (!(event->modifiers() & Qt::ShiftModifier)) { selection->deselectAll(); } selection->select(shape); } else { KIS_ASSERT_RECOVER_RETURN(m_currentStrategy == 0); m_currentStrategy = new KoPathPointRubberSelectStrategy(this, event->point); event->accept(); } } } } } void KoPathTool::mouseMoveEvent(KoPointerEvent *event) { if (event->button() & Qt::RightButton) return; if (m_currentStrategy) { m_lastPoint = event->point; m_currentStrategy->handleMouseMove(event->point, event->modifiers()); // repaint new handle positions m_pointSelection.repaint(); if (m_activeHandle) { m_activeHandle->repaint(); } if (m_activeSegment) { repaintSegment(m_activeSegment); } return; } if (m_activeSegment) { KoPathPointIndex index = m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart); KoPathSegment segment = m_activeSegment->path->segmentByIndex(index); repaint(segment.boundingRect()); delete m_activeSegment; m_activeSegment = 0; } Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF roi = handleGrabRect(shape->documentToShape(event->point)); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { int handleId = parameterShape->handleIdAt(roi); if (handleId != -1) { useCursor(m_moveCursor); emit statusTextChanged(i18n("Drag to move handle.")); if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; if (KoConnectionShape * connectionShape = dynamic_cast(parameterShape)) { - //qDebug() << "handleId" << handleId; + //debugFlake << "handleId" << handleId; m_activeHandle = new ConnectionHandle(this, connectionShape, handleId); m_activeHandle->repaint(); return; } else { - //qDebug() << "handleId" << handleId; + //debugFlake << "handleId" << handleId; m_activeHandle = new ParameterHandle(this, parameterShape, handleId); m_activeHandle->repaint(); return; } } } else { QList points = shape->pointsAt(roi); if (! points.empty()) { // find the nearest control point from all points within the roi KoPathPoint * bestPoint = 0; KoPathPoint::PointType bestPointType = KoPathPoint::Node; qreal minDistance = HUGE_VAL; Q_FOREACH (KoPathPoint *p, points) { // the node point must be hit if the point is not selected yet if (! m_pointSelection.contains(p) && ! roi.contains(p->point())) continue; // check for the control points first as otherwise it is no longer // possible to change the control points when they are the same as the point if (p->activeControlPoint1() && roi.contains(p->controlPoint1())) { qreal dist = squaredDistance(roi.center(), p->controlPoint1()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint1; minDistance = dist; } } if (p->activeControlPoint2() && roi.contains(p->controlPoint2())) { qreal dist = squaredDistance(roi.center(), p->controlPoint2()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::ControlPoint2; minDistance = dist; } } // check the node point at last qreal dist = squaredDistance(roi.center(), p->point()); if (dist < minDistance) { bestPoint = p; bestPointType = KoPathPoint::Node; minDistance = dist; } } if (! bestPoint) return; useCursor(m_moveCursor); if (bestPointType == KoPathPoint::Node) emit statusTextChanged(i18n("Drag to move point. Shift click to change point type.")); else emit statusTextChanged(i18n("Drag to move control point.")); PointHandle *prev = dynamic_cast(m_activeHandle); if (prev && prev->activePoint() == bestPoint && prev->activePointType() == bestPointType) return; // no change; if (m_activeHandle) m_activeHandle->repaint(); delete m_activeHandle; m_activeHandle = new PointHandle(this, bestPoint, bestPointType); m_activeHandle->repaint(); return; } } } useCursor(m_selectCursor); if (m_activeHandle) { m_activeHandle->repaint(); } delete m_activeHandle; m_activeHandle = 0; PathSegment *hoveredSegment = segmentAtPoint(event->point); if(hoveredSegment) { useCursor(Qt::PointingHandCursor); emit statusTextChanged(i18n("Drag to change curve directly. Double click to insert new path point.")); m_activeSegment = hoveredSegment; repaintSegment(m_activeSegment); } else { uint selectedPointCount = m_pointSelection.size(); if (selectedPointCount == 0) emit statusTextChanged(QString()); else if (selectedPointCount == 1) emit statusTextChanged(i18n("Press B to break path at selected point.")); else emit statusTextChanged(i18n("Press B to break path at selected segments.")); } } void KoPathTool::repaintSegment(PathSegment *pathSegment) { if (!pathSegment || !pathSegment->isValid()) return; KoPathPointIndex index = pathSegment->path->pathPointIndex(pathSegment->segmentStart); KoPathSegment segment = pathSegment->path->segmentByIndex(index); repaint(segment.boundingRect()); } void KoPathTool::mouseReleaseEvent(KoPointerEvent *event) { Q_D(KoToolBase); if (m_currentStrategy) { const bool hadNoSelection = !m_pointSelection.hasSelection(); m_currentStrategy->finishInteraction(event->modifiers()); KUndo2Command *command = m_currentStrategy->createCommand(); if (command) d->canvas->addCommand(command); if (hadNoSelection && dynamic_cast(m_currentStrategy) && !m_pointSelection.hasSelection()) { // the click didn't do anything at all. Allow it to be used by others. event->ignore(); } delete m_currentStrategy; m_currentStrategy = 0; } } void KoPathTool::keyPressEvent(QKeyEvent *event) { Q_D(KoToolBase); if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, event->modifiers()); } break; case Qt::Key_Escape: m_currentStrategy->cancelInteraction(); delete m_currentStrategy; m_currentStrategy = 0; break; default: event->ignore(); return; } } else { switch (event->key()) { #ifndef NDEBUG case Qt::Key_D: if (m_pointSelection.objectCount() == 1) { QList selectedPoints = m_pointSelection.selectedPointsData(); KoPathShapePrivate *p = static_cast(selectedPoints[0].pathShape->priv()); p->debugPath(); } break; #endif case Qt::Key_B: if (m_pointSelection.size() == 1) breakAtPoint(); else if (m_pointSelection.size() >= 2) breakAtSegment(); break; default: event->ignore(); return; } } event->accept(); } void KoPathTool::keyReleaseEvent(QKeyEvent *event) { if (m_currentStrategy) { switch (event->key()) { case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Shift: case Qt::Key_Meta: if (! event->isAutoRepeat()) { m_currentStrategy->handleMouseMove(m_lastPoint, Qt::NoModifier); } break; default: break; } } event->accept(); } void KoPathTool::mouseDoubleClickEvent(KoPointerEvent *event) { Q_D(KoToolBase); event->ignore(); // check if we are doing something else at the moment if (m_currentStrategy) return; if (!m_activeHandle && m_activeSegment && m_activeSegment->isValid()) { QList segments; segments.append( KoPathPointData(m_activeSegment->path, m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart))); KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, m_activeSegment->positionOnSegment); d->canvas->addCommand(cmd); m_pointSelection.clear(); foreach (KoPathPoint * p, cmd->insertedPoints()) { m_pointSelection.add(p, false); } updateActions(); event->accept(); } else if (!m_activeHandle && !m_activeSegment && m_activatedTemporarily) { emit done(); event->accept(); } else if (!m_activeHandle && !m_activeSegment) { KoShapeManager *shapeManager = canvas()->shapeManager(); KoSelection *selection = shapeManager->selection(); selection->deselectAll(); event->accept(); } } KoPathTool::PathSegment* KoPathTool::segmentAtPoint(const QPointF &point) { Q_D(KoToolBase); // the max allowed distance from a segment const QRectF grabRoi = handleGrabRect(point); const qreal distanceThreshold = 0.5 * KisAlgebra2D::maxDimension(grabRoi); QScopedPointer segment(new PathSegment); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) continue; // convert document point to shape coordinates const QPointF p = shape->documentToShape(point); // our region of interest, i.e. a region around our mouse position const QRectF roi = shape->documentToShape(grabRoi); qreal minDistance = std::numeric_limits::max(); // check all segments of this shape which intersect the region of interest const QList segments = shape->segmentsAt(roi); foreach (const KoPathSegment &s, segments) { const qreal nearestPointParam = s.nearestPoint(p); const QPointF nearestPoint = s.pointAt(nearestPointParam); const qreal distance = kisDistance(p, nearestPoint); // are we within the allowed distance ? if (distance > distanceThreshold) continue; // are we closer to the last closest point ? if (distance < minDistance) { segment->path = shape; segment->segmentStart = s.first(); segment->positionOnSegment = nearestPointParam; } } } if (!segment->isValid()) { segment.reset(); } return segment.take(); } void KoPathTool::activate(ToolActivation activation, const QSet &shapes) { KoToolBase::activate(activation, shapes); Q_D(KoToolBase); m_activatedTemporarily = activation == TemporaryActivation; // retrieve the actual global handle radius m_handleRadius = handleRadius(); d->canvas->snapGuide()->reset(); useCursor(m_selectCursor); m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), this, SLOT(updateActions())); m_shapeFillResourceConnector.connectToCanvas(d->canvas); initializeWithShapes(shapes.toList()); } void KoPathTool::slotSelectionChanged() { Q_D(KoToolBase); QList shapes = d->canvas->selectedShapesProxy()->selection()->selectedEditableShapesAndDelegates(); initializeWithShapes(shapes); } void KoPathTool::notifyPathPointsChanged(KoPathShape *shape) { Q_UNUSED(shape); // active handle and selection might have already become invalid, so just // delete them without dereferencing anything... delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; } void KoPathTool::clearActivePointSelectionReferences() { delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; m_pointSelection.clear(); } void KoPathTool::initializeWithShapes(const QList shapes) { QList selectedShapes; Q_FOREACH (KoShape *shape, shapes) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape && pathShape->isShapeEditable()) { selectedShapes.append(pathShape); } } const QRectF oldBoundingRect = KoShape::boundingRect(implicitCastList(m_pointSelection.selectedShapes())); if (selectedShapes != m_pointSelection.selectedShapes()) { clearActivePointSelectionReferences(); m_pointSelection.setSelectedShapes(selectedShapes); repaintDecorations(); } Q_FOREACH (KoPathShape *shape, selectedShapes) { // as the tool is just in activation repaintDecorations does not yet get called // so we need to use repaint of the tool and it is only needed to repaint the // current canvas repaint(shape->boundingRect()); } repaint(oldBoundingRect); updateOptionsWidget(); updateActions(); } void KoPathTool::updateOptionsWidget() { PathToolOptionWidget::Types type; QList selectedShapes = m_pointSelection.selectedShapes(); Q_FOREACH (KoPathShape *shape, selectedShapes) { KoParameterShape * parameterShape = dynamic_cast(shape); type |= parameterShape && parameterShape->isParametricShape() ? PathToolOptionWidget::ParametricShape : PathToolOptionWidget::PlainPath; } emit singleShapeChanged(selectedShapes.size() == 1 ? selectedShapes.first() : 0); emit typeChanged(type); } void KoPathTool::updateActions() { QList pointData = m_pointSelection.selectedPointsData(); bool canBreakAtPoint = false; bool hasNonSmoothPoints = false; bool hasNonSymmetricPoints = false; bool hasNonSplitPoints = false; bool hasNonLinePoints = false; bool hasNonCurvePoints = false; bool canJoinSubpaths = false; if (!pointData.isEmpty()) { Q_FOREACH (const KoPathPointData &pd, pointData) { const int subpathIndex = pd.pointIndex.first; const int pointIndex = pd.pointIndex.second; canBreakAtPoint |= pd.pathShape->isClosedSubpath(subpathIndex) || (pointIndex > 0 && pointIndex < pd.pathShape->subpathPointCount(subpathIndex) - 1); KoPathPoint *point = pd.pathShape->pointByIndex(pd.pointIndex); hasNonSmoothPoints |= !(point->properties() & KoPathPoint::IsSmooth); hasNonSymmetricPoints |= !(point->properties() & KoPathPoint::IsSymmetric); hasNonSplitPoints |= point->properties() & KoPathPoint::IsSymmetric || point->properties() & KoPathPoint::IsSmooth; hasNonLinePoints |= point->activeControlPoint1() || point->activeControlPoint2(); hasNonCurvePoints |= !point->activeControlPoint1() && !point->activeControlPoint2(); } if (pointData.size() == 2) { const KoPathPointData & pd1 = pointData.at(0); const KoPathPointData & pd2 = pointData.at(1); canJoinSubpaths = checkCanJoinToPoints(pd1, pd2); } } m_actionPathPointCorner->setEnabled(hasNonSplitPoints); m_actionPathPointSmooth->setEnabled(hasNonSmoothPoints); m_actionPathPointSymmetric->setEnabled(hasNonSymmetricPoints); m_actionRemovePoint->setEnabled(!pointData.isEmpty()); m_actionBreakPoint->setEnabled(canBreakAtPoint); m_actionCurvePoint->setEnabled(hasNonCurvePoints); m_actionLinePoint->setEnabled(hasNonLinePoints); m_actionJoinSegment->setEnabled(canJoinSubpaths); m_actionMergePoints->setEnabled(canJoinSubpaths); QList segments(m_pointSelection.selectedSegmentsData()); bool canSplitAtSegment = false; bool canConvertSegmentToLine = false; bool canConvertSegmentToCurve= false; if (!segments.isEmpty()) { canSplitAtSegment = segments.size() == 1; bool hasLines = false; bool hasCurves = false; Q_FOREACH (const KoPathPointData &pd, segments) { KoPathSegment segment = pd.pathShape->segmentByIndex(pd.pointIndex); hasLines |= segment.degree() == 1; hasCurves |= segment.degree() > 1; } canConvertSegmentToLine = !segments.isEmpty() && hasCurves; canConvertSegmentToCurve= !segments.isEmpty() && hasLines; } m_actionAddPoint->setEnabled(canSplitAtSegment); m_actionLineSegment->setEnabled(canConvertSegmentToLine); m_actionCurveSegment->setEnabled(canConvertSegmentToCurve); m_actionBreakSegment->setEnabled(canSplitAtSegment); KoSelection *selection = canvas()->selectedShapesProxy()->selection(); bool haveConvertibleShapes = false; Q_FOREACH (KoShape *shape, selection->selectedEditableShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); KoSvgTextShape *textShape = dynamic_cast(shape); if (textShape || (parameterShape && parameterShape->isParametricShape())) { haveConvertibleShapes = true; break; } } m_actionConvertToPath->setEnabled(haveConvertibleShapes); } void KoPathTool::deactivate() { Q_D(KoToolBase); m_shapeFillResourceConnector.disconnect(); m_canvasConnections.clear(); m_pointSelection.clear(); m_pointSelection.setSelectedShapes(QList()); delete m_activeHandle; m_activeHandle = 0; delete m_activeSegment; m_activeSegment = 0; delete m_currentStrategy; m_currentStrategy = 0; d->canvas->snapGuide()->reset(); KoToolBase::deactivate(); } void KoPathTool::documentResourceChanged(int key, const QVariant & res) { if (key == KoDocumentResourceManager::HandleRadius) { int oldHandleRadius = m_handleRadius; m_handleRadius = res.toUInt(); // repaint with the bigger of old and new handle radius int maxRadius = qMax(m_handleRadius, oldHandleRadius); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF controlPointRect = shape->absoluteTransformation(0).map(shape->outline()).controlPointRect(); repaint(controlPointRect.adjusted(-maxRadius, -maxRadius, maxRadius, maxRadius)); } } } void KoPathTool::pointSelectionChanged() { Q_D(KoToolBase); updateActions(); d->canvas->snapGuide()->setIgnoredPathPoints(m_pointSelection.selectedPoints().toList()); emit selectionChanged(m_pointSelection.hasSelection()); } void KoPathTool::repaint(const QRectF &repaintRect) { Q_D(KoToolBase); //debugFlake <<"KoPathTool::repaint(" << repaintRect <<")" << m_handleRadius; // widen border to take antialiasing into account qreal radius = m_handleRadius + 1; d->canvas->updateCanvas(repaintRect.adjusted(-radius, -radius, radius, radius)); } namespace { void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2) { if (a1->isEnabled() || a2->isEnabled()) { menu->addAction(a1); menu->addAction(a2); menu->addSeparator(); } } void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2, QAction *a3) { if (a1->isEnabled() || a2->isEnabled()) { menu->addAction(a1); menu->addAction(a2); menu->addAction(a3); menu->addSeparator(); } } } QMenu *KoPathTool::popupActionsMenu() { if (m_activeHandle) { m_activeHandle->trySelectHandle(); } if (m_activeSegment && m_activeSegment->isValid()) { KoPathShape *shape = m_activeSegment->path; KoPathSegment segment = shape->segmentByIndex(shape->pathPointIndex(m_activeSegment->segmentStart)); m_pointSelection.add(segment.first(), true); m_pointSelection.add(segment.second(), false); } if (m_contextMenu) { m_contextMenu->clear(); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionPathPointCorner, m_actionPathPointSmooth, m_actionPathPointSymmetric); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionCurvePoint, m_actionLinePoint); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionAddPoint, m_actionRemovePoint); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionLineSegment, m_actionCurveSegment); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionBreakPoint, m_actionBreakSegment); addActionsGroupIfEnabled(m_contextMenu.data(), m_actionJoinSegment, m_actionMergePoints); m_contextMenu->addAction(m_actionConvertToPath); m_contextMenu->addSeparator(); } return m_contextMenu.data(); } void KoPathTool::deleteSelection() { removePoints(); } KoToolSelection * KoPathTool::selection() { return &m_pointSelection; } void KoPathTool::requestUndoDuringStroke() { // noop! } void KoPathTool::requestStrokeCancellation() { explicitUserStrokeEndRequest(); } void KoPathTool::requestStrokeEnd() { // noop! } void KoPathTool::explicitUserStrokeEndRequest() { if (m_activatedTemporarily) { emit done(); } }