diff --git a/libs/flake/KoShapeRegistry.cpp b/libs/flake/KoShapeRegistry.cpp index bacc598b39..5ce3a3b143 100644 --- a/libs/flake/KoShapeRegistry.cpp +++ b/libs/flake/KoShapeRegistry.cpp @@ -1,573 +1,575 @@ /* This file is part of the KDE project * Copyright (c) 2006 Boudewijn Rempt (boud@valdyas.org) * Copyright (C) 2006-2007, 2010 Thomas Zander * Copyright (C) 2006,2008-2010 Thorsten Zachmann * Copyright (C) 2007 Jan Hambrecht * Copyright (C) 2010 Inge Wallin * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // Own #include "KoShapeRegistry.h" #include "KoSvgTextShape.h" #include "KoPathShapeFactory.h" #include "KoConnectionShapeFactory.h" #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "KoShapeGroup.h" #include "KoShapeLayer.h" #include "SvgShapeFactory.h" #include #include #include #include #include #include #include #include #include #include #include Q_GLOBAL_STATIC(KoShapeRegistry, s_instance) class Q_DECL_HIDDEN KoShapeRegistry::Private { public: void insertFactory(KoShapeFactoryBase *factory); void init(KoShapeRegistry *q); KoShape *createShapeInternal(const KoXmlElement &fullElement, KoShapeLoadingContext &context, const KoXmlElement &element) const; // Map namespace,tagname to priority:factory QHash, QMultiMap > factoryMap; }; KoShapeRegistry::KoShapeRegistry() : d(new Private()) { } KoShapeRegistry::~KoShapeRegistry() { qDeleteAll(doubleEntries()); qDeleteAll(values()); delete d; } void KoShapeRegistry::Private::init(KoShapeRegistry *q) { KoPluginLoader::PluginsConfig config; config.whiteList = "FlakePlugins"; config.blacklist = "FlakePluginsDisabled"; config.group = "calligra"; KoPluginLoader::instance()->load(QString::fromLatin1("Calligra/Flake"), QString::fromLatin1("[X-Flake-PluginVersion] == 28"), config); config.whiteList = "ShapePlugins"; config.blacklist = "ShapePluginsDisabled"; KoPluginLoader::instance()->load(QString::fromLatin1("Calligra/Shape"), QString::fromLatin1("[X-Flake-PluginVersion] == 28"), config); // Also add our hard-coded basic shapes q->add(new KoSvgTextShapeFactory()); q->add(new KoPathShapeFactory(QStringList())); q->add(new KoConnectionShapeFactory()); // As long as there is no shape dealing with embedded svg images // we add the svg shape factory here by default q->add(new SvgShapeFactory); // Now all shape factories are registered with us, determine their // associated odf tagname & priority and prepare ourselves for // loading ODF. QList factories = q->values(); for (int i = 0; i < factories.size(); ++i) { insertFactory(factories[i]); } } KoShapeRegistry* KoShapeRegistry::instance() { if (!s_instance.exists()) { s_instance->d->init(s_instance); } return s_instance; } void KoShapeRegistry::addFactory(KoShapeFactoryBase * factory) { add(factory); d->insertFactory(factory); } void KoShapeRegistry::Private::insertFactory(KoShapeFactoryBase *factory) { const QList > odfElements(factory->odfElements()); if (odfElements.isEmpty()) { debugFlake << "Shape factory" << factory->id() << " does not have OdfNamespace defined, ignoring"; } else { int priority = factory->loadingPriority(); for (QList >::const_iterator it(odfElements.begin()); it != odfElements.end(); ++it) { foreach (const QString &elementName, (*it).second) { QPair p((*it).first, elementName); QMultiMap & priorityMap = factoryMap[p]; priorityMap.insert(priority, factory); debugFlake << "Inserting factory" << factory->id() << " for" << p << " with priority " << priority << " into factoryMap making " << priorityMap.size() << " entries. "; } } } } #include #include #include #include namespace { struct ObjectEntry { ObjectEntry() { } ObjectEntry(const ObjectEntry &rhs) : objectXmlContents(rhs.objectXmlContents), objectName(rhs.objectName), isDir(rhs.isDir) { } ~ObjectEntry() { } QByteArray objectXmlContents; // the XML tree in the object QString objectName; // object name in the frame without "./" // This is extracted from objectXmlContents. bool isDir = false; }; // A FileEntry is used to store information about embedded files // inside (i.e. referred to by) an object. struct FileEntry { FileEntry() {} FileEntry(const FileEntry &rhs) : path(rhs.path), mimeType(rhs.mimeType), isDir(rhs.isDir), contents(rhs.contents) { } QString path; // Normalized filename, i.e. without "./". QString mimeType; bool isDir; QByteArray contents; }; QByteArray loadFile(const QString &fileName, KoShapeLoadingContext &context) { // Can't load a file which is a directory, return an invalid QByteArray if (fileName.endsWith('/')) return QByteArray(); KoStore *store = context.odfLoadingContext().store(); QByteArray fileContent; if (!store->open(fileName)) { store->close(); return QByteArray(); } int fileSize = store->size(); fileContent = store->read(fileSize); store->close(); //debugFlake << "File content: " << fileContent; return fileContent; } boost::optional storeFile(const QString &fileName, KoShapeLoadingContext &context) { debugFlake << "Saving file: " << fileName; boost::optional result; QByteArray fileContent = loadFile(fileName, context); if (!fileContent.isNull()) { // Actually store the file in the list. FileEntry entry; entry.path = fileName; if (entry.path.startsWith(QLatin1String("./"))) { entry.path.remove(0, 2); } entry.mimeType = context.odfLoadingContext().mimeTypeForPath(entry.path); entry.isDir = false; entry.contents = fileContent; result = entry; } return result; } void storeXmlRecursive(const KoXmlElement &el, KoXmlWriter &writer, ObjectEntry *object, QHash &unknownNamespaces) { // Start the element; // keep the name in a QByteArray so that it stays valid until end element is called. const QByteArray name(el.nodeName().toLatin1()); writer.startElement(name.constData()); // Child elements // Loop through all the child elements of the draw:frame. KoXmlNode n = el.firstChild(); for (; !n.isNull(); n = n.nextSibling()) { if (n.isElement()) { storeXmlRecursive(n.toElement(), writer, object, unknownNamespaces); } else if (n.isText()) { writer.addTextNode(n.toText().data()/*.toUtf8()*/); } } // End the element writer.endElement(); } QVector storeObjects(const KoXmlElement &element) { QVector result; // Loop through all the child elements of the draw:frame and save them. KoXmlNode n = element.firstChild(); for (; !n.isNull(); n = n.nextSibling()) { debugFlake << "In draw:frame, node =" << n.nodeName(); // This disregards #text, but that's not in the spec anyway so // it doesn't need to be saved. if (!n.isElement()) continue; KoXmlElement el = n.toElement(); ObjectEntry object; QByteArray contentsTmp; QBuffer buffer(&contentsTmp); // the member KoXmlWriter writer(&buffer); // 1. Find out the objectName // Save the normalized filename, i.e. without a starting "./". // An empty string is saved if no name is found. QString name = el.attributeNS(KoXmlNS::xlink, "href", QString()); if (name.startsWith(QLatin1String("./"))) name.remove(0, 2); object.objectName = name; // 2. Copy the XML code. QHash unknownNamespaces; storeXmlRecursive(el, writer, &object, unknownNamespaces); object.objectXmlContents = contentsTmp; // 3, 4: the isDir and manifestEntry members are not set here, // but initialize them anyway. . object.isDir = false; // Has to be initialized to something. result.append(object); } return result; } } #include #include "kis_debug.h" #include #include #include #include #include KoShape * KoShapeRegistry::createShapeFromOdf(const KoXmlElement & e, KoShapeLoadingContext & context) const { debugFlake << "Going to check for" << e.namespaceURI() << ":" << e.tagName(); KoShape * shape = 0; // Handle the case where the element is a draw:frame differently from other cases. if (e.tagName() == "frame" && e.namespaceURI() == KoXmlNS::draw) { // If the element is in a frame, the frame is already added by the // application and we only want to create a shape from the // embedded element. The very first shape we create is accepted. // // FIXME: we might want to have some code to determine which is // the "best" of the creatable shapes. if (e.hasChildNodes()) { // if we don't ignore white spaces it can be that the first child is not a element so look for the first element KoXmlNode node = e.firstChild(); KoXmlElement element; while (!node.isNull() && element.isNull()) { element = node.toElement(); node = node.nextSibling(); } if (!element.isNull()) { // Check for draw:object if (element.tagName() == "object" && element.namespaceURI() == KoXmlNS::draw && element.hasChildNodes()) { // Loop through the elements and find the first one // that is handled by any shape. KoXmlNode n = element.firstChild(); for (; !n.isNull(); n = n.nextSibling()) { if (n.isElement()) { debugFlake << "trying for element " << n.toElement().tagName(); shape = d->createShapeInternal(e, context, n.toElement()); break; } } if (shape) debugFlake << "Found a shape for draw:object"; else debugFlake << "Found NO shape shape for draw:object"; } else { // If not draw:object, e.g draw:image or draw:plugin shape = d->createShapeInternal(e, context, element); } } if (shape) { debugFlake << "A shape supporting the requested type was found."; } else { // If none of the registered shapes could handle the frame // contents, try to fetch SVG it from an embedded link const KoXmlElement &frameElement = e; const int frameZIndex = SvgShapeFactory::calculateZIndex(frameElement, context); QList resultShapes; QVector objects = storeObjects(frameElement); Q_FOREACH (const ObjectEntry &object, objects) { if (object.objectName.isEmpty()) continue; boost::optional file = storeFile(object.objectName, context); if (file && !file->contents.isEmpty()) { QMimeDatabase db; QMimeType mime = db.mimeTypeForData(file->contents); const int zIndex = SvgShapeFactory::calculateZIndex(element, context); if (mime.inherits("image/svg+xml")) { KoXmlDocument xmlDoc; int line, col; QString errormessage; const bool parsed = xmlDoc.setContent(file->contents, &errormessage, &line, &col); if (!parsed) continue; const QRectF bounds = context.documentResourceManager()->shapeController()->documentRectInPixels(); // WARNING: Krita 3.x expects all the embedded objects to // be loaded in default resolution of 72.0 ppi. // Don't change it to the correct data in the image, // it will change back compatibility (and this code will // be deprecated some time soon // UPDATE (DK): There is actually no difference in what resolution we // load these shapes, because they will be scaled into // the bounds of the parent odf-frame const qreal pixelsPerInch = 72.0; + const qreal forcedFontSizeResolution = 72.0; QPointF pos; pos.setX(KoUnit::parseValue(frameElement.attributeNS(KoXmlNS::svg, "x", QString::number(bounds.x())))); pos.setY(KoUnit::parseValue(frameElement.attributeNS(KoXmlNS::svg, "y", QString::number(bounds.y())))); QSizeF size; size.setWidth(KoUnit::parseValue(frameElement.attributeNS(KoXmlNS::svg, "width", QString::number(bounds.width())))); size.setHeight(KoUnit::parseValue(frameElement.attributeNS(KoXmlNS::svg, "height", QString::number(bounds.height())))); KoShape *shape = SvgShapeFactory::createShapeFromSvgDirect(xmlDoc.documentElement(), QRectF(pos, size), pixelsPerInch, + forcedFontSizeResolution, zIndex, context); if (shape) { // NOTE: here we are expected to stretch the internal to the bounds of // the frame! Sounds weird, but it is what Krita 3.x did. const QRectF shapeRect = shape->absoluteOutlineRect(0); const QPointF offset = shapeRect.topLeft(); const QSizeF fragmentSize = shapeRect.size(); if (fragmentSize.isValid()) { /** * Yes, you see what you see. The previous versions of Krita used * QSvgRenderer to render the object, which allegedly truncated the * object on sides. Even though we don't use pre-rendering now, * we should still reproduce the old way... */ const QSizeF newSize = QSizeF(int(size.width()), int(size.height())); shape->applyAbsoluteTransformation( QTransform::fromTranslate(-offset.x(), -offset.y()) * QTransform::fromScale( newSize.width() / fragmentSize.width(), newSize.height() / fragmentSize.height()) * QTransform::fromTranslate(pos.x(), pos.y())); resultShapes.append(shape); } } } else { // TODO: implement raster images? } } } if (resultShapes.size() == 1) { shape = resultShapes.takeLast(); } else if (resultShapes.size() > 1) { KoShapeGroup *groupShape = new KoShapeGroup; KoShapeGroupCommand cmd(groupShape, resultShapes); cmd.redo(); groupShape->setZIndex(frameZIndex); shape = groupShape; } } } } // Hardwire the group shape into the loading as it should not appear // in the shape selector else if (e.localName() == "g" && e.namespaceURI() == KoXmlNS::draw) { KoShapeGroup * group = new KoShapeGroup(); context.odfLoadingContext().styleStack().save(); bool loaded = group->loadOdf(e, context); context.odfLoadingContext().styleStack().restore(); if (loaded) { shape = group; } else { delete group; } } else { shape = d->createShapeInternal(e, context, e); } if (shape) { context.shapeLoaded(shape); } return shape; } KoShape *KoShapeRegistry::Private::createShapeInternal(const KoXmlElement &fullElement, KoShapeLoadingContext &context, const KoXmlElement &element) const { // Pair of namespace, tagname QPair p = QPair(element.namespaceURI(), element.tagName()); // Remove duplicate lookup. if (!factoryMap.contains(p)) return 0; QMultiMap priorityMap = factoryMap.value(p); QList factories = priorityMap.values(); #ifndef NDEBUG debugFlake << "Supported factories for=" << p; foreach (KoShapeFactoryBase *f, factories) debugFlake << f->id() << f->name(); #endif // Loop through all shape factories. If any of them supports this // element, then we let the factory create a shape from it. This // may fail because the element itself is too generic to draw any // real conclusions from it - we actually have to try to load it. // An example of this is the draw:image element which have // potentially hundreds of different image formats to support, // including vector formats. // // If it succeeds, then we use this shape, if it fails, then just // try the next. // // Higher numbers are more specific, map is sorted by keys. for (int i = factories.size() - 1; i >= 0; --i) { KoShapeFactoryBase * factory = factories[i]; if (factory->supports(element, context)) { KoShape *shape = factory->createShapeFromOdf(fullElement, context); if (shape) { debugFlake << "Shape found for factory " << factory->id() << factory->name(); // we return the top-level most shape as that's the one that we'll have to // add to the KoShapeManager for painting later (and also to avoid memory leaks) // but don't go past a KoShapeLayer as KoShape adds those from the context // during loading and those are already added. while (shape->parent() && dynamic_cast(shape->parent()) == 0) shape = shape->parent(); return shape; } // Maybe a shape with a lower priority can load our // element, but this attempt has failed. } else { debugFlake << "No support for" << p << "by" << factory->id(); } } return 0; } QList KoShapeRegistry::factoriesForElement(const QString &nameSpace, const QString &elementName) { // Pair of namespace, tagname QPair p = QPair(nameSpace, elementName); QMultiMap priorityMap = d->factoryMap.value(p); QList shapeFactories; // sort list by priority Q_FOREACH (KoShapeFactoryBase *f, priorityMap.values()) { shapeFactories.prepend(f); } return shapeFactories; } diff --git a/libs/flake/svg/SvgGraphicContext.cpp b/libs/flake/svg/SvgGraphicContext.cpp index d4b5b60117..f2b36c9bb0 100644 --- a/libs/flake/svg/SvgGraphicContext.cpp +++ b/libs/flake/svg/SvgGraphicContext.cpp @@ -1,84 +1,85 @@ /* This file is part of the KDE project * Copyright (C) 2003,2005 Rob Buis * Copyright (C) 2007,2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgGraphicContext.h" #include "kis_pointer_utils.h" SvgGraphicsContext::SvgGraphicsContext() { strokeType = None; stroke = toQShared(new KoShapeStroke()); stroke->setLineStyle(Qt::NoPen, QVector()); // default is no stroke stroke->setLineWidth(1.0); stroke->setCapStyle(Qt::FlatCap); stroke->setJoinStyle(Qt::MiterJoin); fillType = Solid; fillRule = Qt::WindingFill; fillColor = QColor(Qt::black); // default is black fill as per svg spec opacity = 1.0; currentColor = Qt::black; forcePercentage = false; display = true; visible = true; isResolutionFrame = false; clipRule = Qt::WindingFill; preserveWhitespace = false; pixelsPerInch = 72.0; + forcedFontSizeCoeff = 1.0; // no workaround by default autoFillMarkers = false; textProperties = KoSvgTextProperties::defaultProperties(); } void SvgGraphicsContext::workaroundClearInheritedFillProperties() { /** * HACK ALERT: according to SVG patterns, clip paths and clip masks * must not inherit any properties from the referencing element. * We still don't support it, therefore we reset only fill/stroke * properties to avoid cyclic fill inheritance, which may cause * infinite recursion. */ strokeType = None; stroke = toQShared(new KoShapeStroke()); stroke->setLineStyle(Qt::NoPen, QVector()); // default is no stroke stroke->setLineWidth(1.0); stroke->setCapStyle(Qt::FlatCap); stroke->setJoinStyle(Qt::MiterJoin); fillType = Solid; fillRule = Qt::WindingFill; fillColor = QColor(Qt::black); // default is black fill as per svg spec opacity = 1.0; currentColor = Qt::black; } diff --git a/libs/flake/svg/SvgGraphicContext.h b/libs/flake/svg/SvgGraphicContext.h index d9cf364ef5..5fd31065af 100644 --- a/libs/flake/svg/SvgGraphicContext.h +++ b/libs/flake/svg/SvgGraphicContext.h @@ -1,83 +1,84 @@ /* This file is part of the KDE project * Copyright (C) 2003,2005 Rob Buis * Copyright (C) 2007,2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGGRAPHICCONTEXT_H #define SVGGRAPHICCONTEXT_H #include "kritaflake_export.h" #include #include #include #include class KRITAFLAKE_EXPORT SvgGraphicsContext { public: // Fill/stroke styles enum StyleType { None, ///< no style Solid, ///< solid style Complex ///< gradient or pattern style }; SvgGraphicsContext(); void workaroundClearInheritedFillProperties(); StyleType fillType; ///< the current fill type Qt::FillRule fillRule; ///< the current fill rule QColor fillColor; ///< the current fill color QString fillId; ///< the current fill id (used for gradient/pattern fills) StyleType strokeType;///< the current stroke type QString strokeId; ///< the current stroke id (used for gradient strokes) KoShapeStrokeSP stroke; ///< the current stroke QString filterId; ///< the current filter id QString clipPathId; ///< the current clip path id QString clipMaskId; ///< the current clip mask id Qt::FillRule clipRule; ///< the current clip rule qreal opacity; ///< the shapes opacity QTransform matrix; ///< the current transformation matrix QFont font; ///< the current font QStringList fontFamiliesList; ///< the full list of all the families to search glyphs in QColor currentColor; ///< the current color QString xmlBaseDir; ///< the current base directory (used for loading external content) bool preserveWhitespace;///< preserve whitespace in element text QRectF currentBoundingBox; ///< the current bound box used for bounding box units bool forcePercentage; ///< force parsing coordinates/length as percentages of currentBoundbox QTransform viewboxTransform; ///< view box transformation bool display; ///< controls display of shape bool visible; ///< controls visibility of the shape (inherited) bool isResolutionFrame; qreal pixelsPerInch; ///< controls the resolution of the image raster + qreal forcedFontSizeCoeff; ///< workaround for a Krita 3.3 odf-based files that use different resolution for font size QString markerStartId; QString markerMidId; QString markerEndId; bool autoFillMarkers; KoSvgTextProperties textProperties; }; #endif // SVGGRAPHICCONTEXT_H diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index 01ca694b4c..16d08212c7 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1853 +1,1860 @@ /* 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; 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; 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(); } // 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; KoSvgSymbol *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(); KoShape *symbolShape = parseGroup(e); m_context.popGraphicsContext(); if (!symbolShape) return false; svgSymbol->shape = symbolShape; 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"; delete svgSymbol; return false; } m_symbols << svgSymbol; 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); } if (!gotDef && deferredUseStore) { deferredUseStore->add(&e, key); return 0; } qDebug() << "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/svg/SvgParser.h b/libs/flake/svg/SvgParser.h index 11c69613d5..c93e6df8ea 100644 --- a/libs/flake/svg/SvgParser.h +++ b/libs/flake/svg/SvgParser.h @@ -1,218 +1,222 @@ /* This file is part of the KDE project * Copyright (C) 2002-2003,2005 Rob Buis * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005,2007-2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGPARSER_H #define SVGPARSER_H #include #include #include #include #include #include #include "kritaflake_export.h" #include "SvgGradientHelper.h" #include "SvgFilterHelper.h" #include "SvgClipPathHelper.h" #include "SvgLoadingContext.h" #include "SvgStyleParser.h" #include "KoClipMask.h" #include class KoShape; class KoShapeGroup; class KoShapeContainer; class KoDocumentResourceManager; class KoVectorPatternBackground; class KoMarker; class KoPathShape; class KoSvgTextShape; class KRITAFLAKE_EXPORT SvgParser { class DeferredUseStore; public: explicit SvgParser(KoDocumentResourceManager *documentResourceManager); virtual ~SvgParser(); /// Parses a svg fragment, returning the list of top level child shapes QList parseSvg(const KoXmlElement &e, QSizeF * fragmentSize = 0); /// Sets the initial xml base directory (the directory form where the file is read) void setXmlBaseDir(const QString &baseDir); void setResolution(const QRectF boundsInPixels, qreal pixelsPerInch); + /// A special workaround coeff for usign when loading old ODF-embedded SVG files, + /// which used hard-coded 96 ppi for font size + void setForcedFontSizeResolution(qreal value); + /// Returns the list of all shapes of the svg document QList shapes() const; /// Takes the collection of symbols contained in the svg document. The parser will /// no longer know about the symbols. QVector takeSymbols(); QString documentTitle() const; QString documentDescription() const; typedef std::function FileFetcherFunc; void setFileFetcher(FileFetcherFunc func); QList> knownMarkers() const; void parseDefsElement(const KoXmlElement &e); KoShape* parseTextElement(const KoXmlElement &e, KoSvgTextShape *mergeIntoShape = 0); protected: /// Parses a group-like element element, saving all its topmost properties KoShape* parseGroup(const KoXmlElement &e, const KoXmlElement &overrideChildrenFrom = KoXmlElement()); // XXX KoShape* parseTextNode(const KoXmlText &e); /// Parses a container element, returning a list of child shapes QList parseContainer(const KoXmlElement &, bool parseTextNodes = false); /// XXX QList parseSingleElement(const KoXmlElement &b, DeferredUseStore* deferredUseStore = 0); /// Parses a use element, returning a list of child shapes KoShape* parseUse(const KoXmlElement &, DeferredUseStore* deferredUseStore); KoShape* resolveUse(const KoXmlElement &e, const QString& key); /// Parses a gradient element SvgGradientHelper *parseGradient(const KoXmlElement &); /// Parses a pattern element QSharedPointer parsePattern(const KoXmlElement &e, const KoShape *__shape); /// Parses a filter element bool parseFilter(const KoXmlElement &, const KoXmlElement &referencedBy = KoXmlElement()); /// Parses a clip path element bool parseClipPath(const KoXmlElement &); bool parseClipMask(const KoXmlElement &e); bool parseMarker(const KoXmlElement &e); bool parseSymbol(const KoXmlElement &e); /// parses a length attribute qreal parseUnit(const QString &, bool horiz = false, bool vert = false, const QRectF &bbox = QRectF()); /// parses a length attribute in x-direction qreal parseUnitX(const QString &unit); /// parses a length attribute in y-direction qreal parseUnitY(const QString &unit); /// parses a length attribute in xy-direction qreal parseUnitXY(const QString &unit); /// parses a angular attribute values, result in radians qreal parseAngular(const QString &unit); KoShape *createObjectDirect(const KoXmlElement &b); /// Creates an object from the given xml element KoShape * createObject(const KoXmlElement &, const SvgStyles &style = SvgStyles()); /// Create path object from the given xml element KoShape * createPath(const KoXmlElement &); /// find gradient with given id in gradient map SvgGradientHelper* findGradient(const QString &id); /// find pattern with given id in pattern map QSharedPointer findPattern(const QString &id, const KoShape *shape); /// find filter with given id in filter map SvgFilterHelper* findFilter(const QString &id, const QString &href = QString()); /// find clip path with given id in clip path map SvgClipPathHelper* findClipPath(const QString &id); /// Adds list of shapes to the given group shape void addToGroup(QList shapes, KoShapeContainer *group); /// creates a shape from the given shape id KoShape * createShape(const QString &shapeID); /// Creates shape from specified svg element KoShape * createShapeFromElement(const KoXmlElement &element, SvgLoadingContext &context); /// Builds the document from the given shapes list void buildDocument(QList shapes); void uploadStyleToContext(const KoXmlElement &e); void applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyCurrentBasicStyle(KoShape *shape); /// Applies styles to the given shape void applyStyle(KoShape *, const KoXmlElement &, const QPointF &shapeToOriginalUserCoordinates); /// Applies styles to the given shape void applyStyle(KoShape *, const SvgStyles &, const QPointF &shapeToOriginalUserCoordinates); /// Applies the current fill style to the object void applyFillStyle(KoShape * shape); /// Applies the current stroke style to the object void applyStrokeStyle(KoShape * shape); /// Applies the current filter to the object void applyFilter(KoShape * shape); /// Applies the current clip path to the object void applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); void applyMarkers(KoPathShape *shape); /// Applies id to specified shape void applyId(const QString &id, KoShape *shape); /// Applies viewBox transformation to the current graphical context /// NOTE: after applying the function currectBoundingBox can become null! void applyViewBoxTransform(const KoXmlElement &element); private: QSizeF m_documentSize; SvgLoadingContext m_context; QMap m_gradients; QMap m_filters; QMap m_clipPaths; QMap> m_clipMasks; QMap> m_markers; KoDocumentResourceManager *m_documentResourceManager; QList m_shapes; QVector m_symbols; QList m_toplevelShapes; QList m_defsShapes; bool m_isInsideTextSubtree = false; QString m_documentTitle; QString m_documentDescription; }; #endif diff --git a/libs/flake/svg/SvgShapeFactory.cpp b/libs/flake/svg/SvgShapeFactory.cpp index 551977f8e6..e4fb34b2c8 100644 --- a/libs/flake/svg/SvgShapeFactory.cpp +++ b/libs/flake/svg/SvgShapeFactory.cpp @@ -1,166 +1,174 @@ /* 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 "SvgShapeFactory.h" #include "SvgParser.h" #include "KoShapeGroup.h" #include "KoShapeGroupCommand.h" #include "KoShapeLoadingContext.h" #include "KoShapeRegistry.h" #include "FlakeDebug.h" #include #include #include #include #include #define SVGSHAPEFACTORYID "SvgShapeFactory" SvgShapeFactory::SvgShapeFactory() : KoShapeFactoryBase(SVGSHAPEFACTORYID, i18n("Embedded svg shape")) { setLoadingPriority(4); setXmlElementNames(QString(KoXmlNS::draw), QStringList("image")); // hide from add shapes docker as the shape is not able to be dragged onto // the canvas as createDefaultShape returns 0. setHidden(true); } SvgShapeFactory::~SvgShapeFactory() { } -void SvgShapeFactory::addToRegistry() -{ - KoShapeRegistry *registry = KoShapeRegistry::instance(); - if (!registry->contains(QString(SVGSHAPEFACTORYID))) { - registry->addFactory(new SvgShapeFactory); - } -} - bool SvgShapeFactory::supports(const KoXmlElement &element, KoShapeLoadingContext &context) const { if (element.localName() == "image" && element.namespaceURI() == KoXmlNS::draw) { QString href = element.attribute("href"); if (href.isEmpty()) return false; // check the mimetype if (href.startsWith(QLatin1String("./"))) { href.remove(0,2); } QString mimetype = context.odfLoadingContext().mimeTypeForPath(href, true); return (mimetype == "image/svg+xml"); } return false; } KoShape *SvgShapeFactory::createShapeFromOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { const KoXmlElement & imageElement(KoXml::namedItemNS(element, KoXmlNS::draw, "image")); if (imageElement.isNull()) { errorFlake << "svg image element not found"; return 0; } if (imageElement.tagName() == "image") { debugFlake << "trying to create shapes form svg image"; QString href = imageElement.attribute("href"); if (href.isEmpty()) return 0; // check the mimetype if (href.startsWith(QLatin1String("./"))) { href.remove(0,2); } QString mimetype = context.odfLoadingContext().mimeTypeForPath(href); debugFlake << mimetype; if (mimetype != "image/svg+xml") return 0; if (!context.odfLoadingContext().store()->open(href)) return 0; KoStoreDevice dev(context.odfLoadingContext().store()); KoXmlDocument xmlDoc; int line, col; QString errormessage; const bool parsed = xmlDoc.setContent(&dev, &errormessage, &line, &col); context.odfLoadingContext().store()->close(); if (! parsed) { errorFlake << "Error while parsing file: " << "at line " << line << " column: " << col << " message: " << errormessage << endl; return 0; } const int zIndex = calculateZIndex(element, context); - return createShapeFromSvgDirect(xmlDoc.documentElement(), QRect(0,0,30,30), 72.0, zIndex, context); + + + /** + * In Krita 3.x we used hardcoded values for shape resolution and font resolution. + * Override them here explicitly, because ODF-based files can be created only in + * Krita 3.x. + * + * NOTE: don't ask me why they differ... + */ + const qreal hardcodedImageResolution = 90.0; + const qreal hardcodedFontResolution = 96.0; + + return createShapeFromSvgDirect(xmlDoc.documentElement(), QRect(0,0,300,300), + hardcodedImageResolution, + hardcodedFontResolution, zIndex, context); } return 0; } int SvgShapeFactory::calculateZIndex(const KoXmlElement &element, KoShapeLoadingContext &context) { int zIndex = 0; if (element.hasAttributeNS(KoXmlNS::draw, "z-index")) { zIndex = element.attributeNS(KoXmlNS::draw, "z-index").toInt(); } else { zIndex = context.zIndex(); } return zIndex; } KoShape *SvgShapeFactory::createShapeFromSvgDirect(const KoXmlElement &root, const QRectF &boundsInPixels, const qreal pixelsPerInch, + const qreal forcedFontSizeResolution, int zIndex, KoShapeLoadingContext &context, QSizeF *fragmentSize) { SvgParser parser(context.documentResourceManager()); parser.setResolution(boundsInPixels, pixelsPerInch); + parser.setForcedFontSizeResolution(forcedFontSizeResolution); QList shapes = parser.parseSvg(root, fragmentSize); if (shapes.isEmpty()) return 0; if (shapes.count() == 1) { KoShape *shape = shapes.first(); shape->setZIndex(zIndex); return shape; } KoShapeGroup *svgGroup = new KoShapeGroup; KoShapeGroupCommand cmd(svgGroup, shapes); cmd.redo(); svgGroup->setZIndex(zIndex); return svgGroup; } diff --git a/libs/flake/svg/SvgShapeFactory.h b/libs/flake/svg/SvgShapeFactory.h index d7ef75898a..164a4cdd4e 100644 --- a/libs/flake/svg/SvgShapeFactory.h +++ b/libs/flake/svg/SvgShapeFactory.h @@ -1,45 +1,42 @@ /* 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. */ #ifndef SVGSHAPEFACTORY_H #define SVGSHAPEFACTORY_H #include "kritaflake_export.h" #include "KoShapeFactoryBase.h" /// Use this shape factory to load embedded svg files from odf class KRITAFLAKE_EXPORT SvgShapeFactory : public KoShapeFactoryBase { public: SvgShapeFactory(); ~SvgShapeFactory() override; // reimplemented from KoShapeFactoryBase bool supports(const KoXmlElement &element, KoShapeLoadingContext &context) const override; // reimplemented from KoShapeFactoryBase KoShape *createShapeFromOdf(const KoXmlElement &element, KoShapeLoadingContext &context) override; static int calculateZIndex(const KoXmlElement &element, KoShapeLoadingContext &context); - static KoShape *createShapeFromSvgDirect(const KoXmlElement &root, const QRectF &boundsInPixels, const qreal pixelsPerInch, int zIndex, KoShapeLoadingContext &context, QSizeF *fragmentSize = 0); - - /// Adds an instance of this factory to the shape registry, if not already registered - static void addToRegistry(); + static KoShape *createShapeFromSvgDirect(const KoXmlElement &root, const QRectF &boundsInPixels, const qreal pixelsPerInch, const qreal forcedFontSizeResolution, int zIndex, KoShapeLoadingContext &context, QSizeF *fragmentSize = 0); }; #endif // SVGSHAPEFACTORY_H diff --git a/libs/flake/svg/SvgStyleParser.cpp b/libs/flake/svg/SvgStyleParser.cpp index bdb1703e4c..478b38bb25 100644 --- a/libs/flake/svg/SvgStyleParser.cpp +++ b/libs/flake/svg/SvgStyleParser.cpp @@ -1,547 +1,552 @@ /* This file is part of the KDE project * Copyright (C) 2002-2005,2007 Rob Buis * Copyright (C) 2002-2004 Nicolas Goutte * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2009 Jan Hambrecht * Copyright (C) 2005,2007 Thomas Zander * Copyright (C) 2006-2007 Inge Wallin * Copyright (C) 2007-2008,2010 Thorsten Zachmann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgStyleParser.h" #include "SvgLoadingContext.h" #include "SvgGraphicContext.h" #include "SvgUtil.h" #include "kis_dom_utils.h" #include #include #include #include #include class Q_DECL_HIDDEN SvgStyleParser::Private { public: Private(SvgLoadingContext &loadingContext) : context(loadingContext) { textAttributes << KoSvgTextProperties::supportedXmlAttributes(); // the order of the font attributes is important, don't change without reason !!! fontAttributes << "font-family" << "font-size" << "font-weight" << "font-style" << "font-variant" << "font-stretch" << "font-size-adjust" << "font" << "text-decoration" << "letter-spacing" << "word-spacing" << "baseline-shift"; // the order of the style attributes is important, don't change without reason !!! styleAttributes << "color" << "display" << "visibility"; styleAttributes << "fill" << "fill-rule" << "fill-opacity"; styleAttributes << "stroke" << "stroke-width" << "stroke-linejoin" << "stroke-linecap"; styleAttributes << "stroke-dasharray" << "stroke-dashoffset" << "stroke-opacity" << "stroke-miterlimit"; styleAttributes << "opacity" << "filter" << "clip-path" << "clip-rule" << "mask"; styleAttributes << "marker" << "marker-start" << "marker-mid" << "marker-end" << "krita:marker-fill-method"; } SvgLoadingContext &context; QStringList textAttributes; ///< text related attributes QStringList fontAttributes; ///< font related attributes QStringList styleAttributes; ///< style related attributes }; SvgStyleParser::SvgStyleParser(SvgLoadingContext &context) : d(new Private(context)) { } SvgStyleParser::~SvgStyleParser() { delete d; } void SvgStyleParser::parseStyle(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure we parse the style attributes in the right order Q_FOREACH (const QString & command, d->styleAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } void SvgStyleParser::parseFont(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure to only parse font attributes here Q_FOREACH (const QString & command, d->fontAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } Q_FOREACH (const QString & command, d->textAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } #include void SvgStyleParser::parsePA(SvgGraphicsContext *gc, const QString &command, const QString ¶ms) { QColor fillcolor = gc->fillColor; QColor strokecolor = gc->stroke->color(); if (params == "inherit") return; if (command == "fill") { if (params == "none") { gc->fillType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->fillId = params.mid(start, end - start); gc->fillType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(fillcolor, params.mid(end + 1).trimmed()); } else { // great we have a solid fill gc->fillType = SvgGraphicsContext::Solid; parseColor(fillcolor, params); } } else if (command == "fill-rule") { if (params == "nonzero") gc->fillRule = Qt::WindingFill; else if (params == "evenodd") gc->fillRule = Qt::OddEvenFill; } else if (command == "stroke") { if (params == "none") { gc->strokeType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->strokeId = params.mid(start, end - start); gc->strokeType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(strokecolor, params.mid(end + 1).trimmed()); } else { // great we have a solid stroke gc->strokeType = SvgGraphicsContext::Solid; parseColor(strokecolor, params); } } else if (command == "stroke-width") { gc->stroke->setLineWidth(SvgUtil::parseUnitXY(gc, params)); } else if (command == "stroke-linejoin") { if (params == "miter") gc->stroke->setJoinStyle(Qt::MiterJoin); else if (params == "round") gc->stroke->setJoinStyle(Qt::RoundJoin); else if (params == "bevel") gc->stroke->setJoinStyle(Qt::BevelJoin); } else if (command == "stroke-linecap") { if (params == "butt") gc->stroke->setCapStyle(Qt::FlatCap); else if (params == "round") gc->stroke->setCapStyle(Qt::RoundCap); else if (params == "square") gc->stroke->setCapStyle(Qt::SquareCap); } else if (command == "stroke-miterlimit") { gc->stroke->setMiterLimit(params.toFloat()); } else if (command == "stroke-dasharray") { QVector array; if (params != "none") { QString dashString = params; QStringList dashes = dashString.replace(',', ' ').simplified().split(' '); for (QStringList::Iterator it = dashes.begin(); it != dashes.end(); ++it) { array.append(SvgUtil::parseUnitXY(gc, *it)); } // if the array is odd repeat it according to the standard if (array.size() & 1) { array << array; } } gc->stroke->setLineStyle(Qt::CustomDashLine, array); } else if (command == "stroke-dashoffset") { gc->stroke->setDashOffset(params.toFloat()); } // handle opacity else if (command == "stroke-opacity") strokecolor.setAlphaF(SvgUtil::fromPercentage(params)); else if (command == "fill-opacity") { float opacity = SvgUtil::fromPercentage(params); if (opacity < 0.0) opacity = 0.0; if (opacity > 1.0) opacity = 1.0; fillcolor.setAlphaF(opacity); } else if (command == "opacity") { gc->opacity = SvgUtil::fromPercentage(params); } else if (command == "font-family") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); QStringList familiesList = gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontFamiliesId).toStringList(); if (!familiesList.isEmpty()) { gc->font.setFamily(familiesList.first()); gc->fontFamiliesList = familiesList; } } else if (command == "font-size") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); - gc->font.setPointSizeF(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontSizeId).toReal()); + + /** + * In ODF-based Krita vectors (<= 3.x) we used hardcoded font size values set to 96 ppi, + * so, when loading old files, we should adjust it accordingly. + */ + gc->font.setPointSizeF(gc->forcedFontSizeCoeff * gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontSizeId).toReal()); } else if (command == "font-style") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); const QFont::Style style = QFont::Style(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontStyleId).toInt()); gc->font.setStyle(style); } else if (command == "font-variant") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); gc->font.setCapitalization( gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontIsSmallCapsId).toBool() ? QFont::SmallCaps : QFont::MixedCase); } else if (command == "font-stretch") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); gc->font.setStretch(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontStretchId).toInt()); } else if (command == "font-weight") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); //ENTER_FUNCTION() << ppVar(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontWeightId).toInt()); gc->font.setWeight(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontWeightId).toInt()); } else if (command == "font-size-adjust") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); warnFile << "WARNING: \'font-size-adjust\' SVG attribute is not supported!"; } else if (command == "font") { warnFile << "WARNING: \'font\' SVG attribute is not yet implemented! Please report a bug!"; } else if (command == "text-decoration") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); using namespace KoSvgText; TextDecorations deco = gc->textProperties.propertyOrDefault(KoSvgTextProperties::TextDecorationId) .value(); gc->font.setStrikeOut(deco & DecorationLineThrough); gc->font.setUnderline(deco & DecorationUnderline); gc->font.setOverline(deco & DecorationOverline); } else if (command == "color") { QColor color; parseColor(color, params); gc->currentColor = color; } else if (command == "display") { if (params == "none") gc->display = false; } else if (command == "visibility") { // visible is inherited! gc->visible = params == "visible"; } else if (command == "filter") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->filterId = params.mid(start, end - start); } } else if (command == "clip-path") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipPathId = params.mid(start, end - start); } } else if (command == "clip-rule") { if (params == "nonzero") gc->clipRule = Qt::WindingFill; else if (params == "evenodd") gc->clipRule = Qt::OddEvenFill; } else if (command == "mask") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipMaskId = params.mid(start, end - start); } } else if (command == "marker-start") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); } } else if (command == "marker-end") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerEndId = params.mid(start, end - start); } } else if (command == "marker-mid") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerMidId = params.mid(start, end - start); } } else if (command == "marker") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); gc->markerMidId = gc->markerStartId; gc->markerEndId = gc->markerStartId; } } else if (command == "krita:marker-fill-method") { gc->autoFillMarkers = params == "auto"; } else if (d->textAttributes.contains(command)) { gc->textProperties.parseSvgTextAttribute(d->context, command, params); } gc->fillColor = fillcolor; gc->stroke->setColor(strokecolor); } bool SvgStyleParser::parseColor(QColor &color, const QString &s) { if (s.isEmpty() || s == "none") return false; if (s.startsWith(QLatin1String("rgb("))) { QString parse = s.trimmed(); QStringList colors = parse.split(','); QString r = colors[0].right((colors[0].length() - 4)); QString g = colors[1]; QString b = colors[2].left((colors[2].length() - 1)); if (r.contains('%')) { r = r.left(r.length() - 1); r = QString::number(int((double(255 * KisDomUtils::toDouble(r)) / 100.0))); } if (g.contains('%')) { g = g.left(g.length() - 1); g = QString::number(int((double(255 * KisDomUtils::toDouble(g)) / 100.0))); } if (b.contains('%')) { b = b.left(b.length() - 1); b = QString::number(int((double(255 * KisDomUtils::toDouble(b)) / 100.0))); } color = QColor(r.toInt(), g.toInt(), b.toInt()); } else if (s == "currentColor") { color = d->context.currentGC()->currentColor; } else { // QColor understands #RRGGBB and svg color names color.setNamedColor(s.trimmed()); } return true; } void SvgStyleParser::parseColorStops(QGradient *gradient, const KoXmlElement &e, SvgGraphicsContext *context, const QGradientStops &defaultStops) { QGradientStops stops; qreal previousOffset = 0.0; KoXmlElement stop; forEachElement(stop, e) { if (stop.tagName() == "stop") { qreal offset = 0.0; QString offsetStr = stop.attribute("offset").trimmed(); if (offsetStr.endsWith('%')) { offsetStr = offsetStr.left(offsetStr.length() - 1); offset = offsetStr.toFloat() / 100.0; } else { offset = offsetStr.toFloat(); } // according to SVG the value must be within [0; 1] interval offset = qBound(0.0, offset, 1.0); // according to SVG the stops' offset must be non-decreasing offset = qMax(offset, previousOffset); previousOffset = offset; QColor color; QString stopColorStr = stop.attribute("stop-color"); QString stopOpacityStr = stop.attribute("stop-opacity"); const QStringList attributes({"stop-color", "stop-opacity"}); SvgStyles styles = parseOneCssStyle(stop.attribute("style"), attributes); // SVG: CSS values have precedence over presentation attributes! if (styles.contains("stop-color")) { stopColorStr = styles.value("stop-color"); } if (styles.contains("stop-opacity")) { stopOpacityStr = styles.value("stop-opacity"); } if (stopColorStr.isEmpty() && stopColorStr == "inherit") { color = context->currentColor; } else { parseColor(color, stopColorStr); } if (!stopOpacityStr.isEmpty() && stopOpacityStr != "inherit") { color.setAlphaF(qBound(0.0, KisDomUtils::toDouble(stopOpacityStr), 1.0)); } stops.append(QPair(offset, color)); } } if (!stops.isEmpty()) { gradient->setStops(stops); } else { gradient->setStops(defaultStops); } } SvgStyles SvgStyleParser::parseOneCssStyle(const QString &style, const QStringList &interestingAttributes) { SvgStyles parsedStyles; if (style.isEmpty()) return parsedStyles; QStringList substyles = style.simplified().split(';', QString::SkipEmptyParts); if (!substyles.count()) return parsedStyles; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); if (interestingAttributes.isEmpty() || interestingAttributes.contains(command)) { parsedStyles[command] = params; } } return parsedStyles; } SvgStyles SvgStyleParser::collectStyles(const KoXmlElement &e) { SvgStyles styleMap; // collect individual presentation style attributes which have the priority 0 // according to SVG standard // NOTE: font attributes should be parsed the first, because they defines 'em' and 'ex' Q_FOREACH (const QString & command, d->fontAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } Q_FOREACH (const QString &command, d->styleAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } Q_FOREACH (const QString & command, d->textAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } // match css style rules to element QStringList cssStyles = d->context.matchingCssStyles(e); // collect all css style attributes Q_FOREACH (const QString &style, cssStyles) { QStringList substyles = style.split(';', QString::SkipEmptyParts); if (!substyles.count()) continue; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); // toggle the namespace selector into the xml-like one command.replace("|", ":"); // only use style and font attributes if (d->styleAttributes.contains(command) || d->fontAttributes.contains(command) || d->textAttributes.contains(command)) { styleMap[command] = params; } } } // FIXME: if 'inherit' we should just remove the property and use the one from the context! // replace keyword "inherit" for style values QMutableMapIterator it(styleMap); while (it.hasNext()) { it.next(); if (it.value() == "inherit") { it.setValue(inheritedAttribute(it.key(), e)); } } return styleMap; } SvgStyles SvgStyleParser::mergeStyles(const SvgStyles &referencedBy, const SvgStyles &referencedStyles) { // 1. use all styles of the referencing styles SvgStyles mergedStyles = referencedBy; // 2. use all styles of the referenced style which are not in the referencing styles SvgStyles::const_iterator it = referencedStyles.constBegin(); for (; it != referencedStyles.constEnd(); ++it) { if (!referencedBy.contains(it.key())) { mergedStyles.insert(it.key(), it.value()); } } return mergedStyles; } SvgStyles SvgStyleParser::mergeStyles(const KoXmlElement &e1, const KoXmlElement &e2) { return mergeStyles(collectStyles(e1), collectStyles(e2)); } QString SvgStyleParser::inheritedAttribute(const QString &attributeName, const KoXmlElement &e) { KoXmlNode parent = e.parentNode(); while (!parent.isNull()) { KoXmlElement currentElement = parent.toElement(); if (currentElement.hasAttribute(attributeName)) { return currentElement.attribute(attributeName); } parent = currentElement.parentNode(); } return QString(); }