diff --git a/libs/flake/KoMarkerCollection.cpp b/libs/flake/KoMarkerCollection.cpp index aaac81ac5c..c5e05b483d 100644 --- a/libs/flake/KoMarkerCollection.cpp +++ b/libs/flake/KoMarkerCollection.cpp @@ -1,138 +1,137 @@ /* This file is part of the KDE project Copyright (C) 2011 Thorsten Zachmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoMarkerCollection.h" #include #include #include "KoMarker.h" #include #include #include #include #include #include #include "kis_debug.h" // WARNING: there is a bug in GCC! It doesn't warn that we are // deleting an uninitialized type here! #include class Q_DECL_HIDDEN KoMarkerCollection::Private { public: ~Private() { } QList > markers; }; KoMarkerCollection::KoMarkerCollection(QObject *parent) : QObject(parent) , d(new Private) { // Add no marker so the user can remove a marker from the line. d->markers.append(QExplicitlySharedDataPointer(0)); // Add default markers loadDefaultMarkers(); } KoMarkerCollection::~KoMarkerCollection() { delete d; } void KoMarkerCollection::loadMarkersFromFile(const QString &svgFile) { QFile file(svgFile); if (!file.exists()) return; if (!file.open(QIODevice::ReadOnly)) return; QString errorMsg; int errorLine = 0; int errorColumn; - KoXmlDocument doc; - bool ok = doc.setContent(&file, false, &errorMsg, &errorLine, &errorColumn); - if (!ok) { + KoXmlDocument doc = SvgParser::createDocumentFromSvg(&file, &errorMsg, &errorLine, &errorColumn); + if (doc.isNull()) { errKrita << "Parsing error in " << svgFile << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" , errorLine , errorColumn , errorMsg); return; } KoDocumentResourceManager manager; SvgParser parser(&manager); parser.setResolution(QRectF(0,0,100,100), 72); // initialize with default values parser.setXmlBaseDir(QFileInfo(svgFile).absolutePath()); parser.setFileFetcher( [](const QString &fileName) { QFile file(fileName); if (!file.exists()) return QByteArray(); file.open(QIODevice::ReadOnly); return file.readAll(); }); QSizeF fragmentSize; QList shapes = parser.parseSvg(doc.documentElement(), &fragmentSize); qDeleteAll(shapes); Q_FOREACH (const QExplicitlySharedDataPointer &marker, parser.knownMarkers()) { addMarker(marker.data()); } } void KoMarkerCollection::loadDefaultMarkers() { QString filePath = KoResourcePaths::findResource("data", "styles/markers.svg"); loadMarkersFromFile(filePath); } QList KoMarkerCollection::markers() const { QList markerList; foreach (const QExplicitlySharedDataPointer& m, d->markers){ markerList.append(m.data()); } return markerList; } KoMarker * KoMarkerCollection::addMarker(KoMarker *marker) { foreach (const QExplicitlySharedDataPointer& m, d->markers) { if (marker == m.data()) { return marker; } if (m && *marker == *m) { debugFlake << "marker is the same as other"; return m.data(); } } d->markers.append(QExplicitlySharedDataPointer(marker)); return marker; } diff --git a/libs/flake/KoSvgPaste.cpp b/libs/flake/KoSvgPaste.cpp index 6da3f9abb7..21efa62187 100644 --- a/libs/flake/KoSvgPaste.cpp +++ b/libs/flake/KoSvgPaste.cpp @@ -1,82 +1,81 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgPaste.h" #include #include #include #include #include #include #include #include bool KoSvgPaste::hasShapes() { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); return mimeData && mimeData->hasFormat("image/svg+xml"); } QList KoSvgPaste::fetchShapes(const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize) { QList shapes; const QMimeData *mimeData = QApplication::clipboard()->mimeData(); if (!mimeData) return shapes; QByteArray data = mimeData->data("image/svg+xml"); if (data.isEmpty()) { return shapes; } return fetchShapesFromData(data, viewportInPx, resolutionPPI, fragmentSize); } QList KoSvgPaste::fetchShapesFromData(const QByteArray &data, const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize) { QList shapes; if (data.isEmpty()) { return shapes; } - KoXmlDocument doc; + QString errorMsg; int errorLine = 0; int errorColumn = 0; - const bool documentValid = doc.setContent(data, false, &errorMsg, &errorLine, &errorColumn); - - if (!documentValid) { + KoXmlDocument doc = SvgParser::createDocumentFromSvg(data, &errorMsg, &errorLine, &errorColumn); + if (doc.isNull()) { qWarning() << "Failed to process an SVG file at" << errorLine << ":" << errorColumn << "->" << errorMsg; return shapes; } KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(viewportInPx, resolutionPPI); shapes = parser.parseSvg(doc.documentElement(), fragmentSize); return shapes; } diff --git a/libs/flake/resources/KoSvgSymbolCollectionResource.cpp b/libs/flake/resources/KoSvgSymbolCollectionResource.cpp index 5407aca043..34b76c617c 100644 --- a/libs/flake/resources/KoSvgSymbolCollectionResource.cpp +++ b/libs/flake/resources/KoSvgSymbolCollectionResource.cpp @@ -1,255 +1,254 @@ /* This file is part of the KDE project Copyright (c) 2017 L. E. Segovia This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include void paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { QList shapes = group->shapes(); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); Q_FOREACH (KoShape *child, shapes) { // we paint recursively here, so we do not have to check recursively for visibility if (!child->isVisible(false)) continue; KoShapeGroup *childGroup = dynamic_cast(child); if (childGroup) { paintGroup(childGroup, painter, converter, paintContext); } else { painter.save(); KoShapeManager::renderSingleShape(child, painter, converter, paintContext); painter.restore(); } } } QImage KoSvgSymbol::icon() { KoShapeGroup *group = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(group, QImage()); QRectF rc = group->boundingRect().normalized(); QImage image(rc.width(), rc.height(), QImage::Format_ARGB32_Premultiplied); QPainter gc(&image); image.fill(Qt::gray); KoViewConverter vc; KoShapePaintingContext ctx; // qDebug() << "Going to render. Original bounding rect:" << group->boundingRect() // << "Normalized: " << rc // << "Scale W" << 256 / rc.width() << "Scale H" << 256 / rc.height(); gc.translate(-rc.x(), -rc.y()); paintGroup(group, gc, vc, ctx); gc.end(); image = image.scaled(128, 128, Qt::KeepAspectRatio); return image; } struct KoSvgSymbolCollectionResource::Private { QVector symbols; QString title; QString description; }; KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource(const QString& filename) : KoResource(filename) , d(new Private()) { } KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource() : KoResource(QString()) , d(new Private()) { } KoSvgSymbolCollectionResource::KoSvgSymbolCollectionResource(const KoSvgSymbolCollectionResource& rhs) : QObject(0) , KoResource(QString()) , d(new Private()) { setFilename(rhs.filename()); d->symbols = rhs.d->symbols; setValid(true); } KoSvgSymbolCollectionResource::~KoSvgSymbolCollectionResource() { } bool KoSvgSymbolCollectionResource::load() { QFile file(filename()); if (file.size() == 0) return false; if (!file.open(QIODevice::ReadOnly)) { return false; } bool res = loadFromDevice(&file); file.close(); return res; } bool KoSvgSymbolCollectionResource::loadFromDevice(QIODevice *dev) { if (!dev->isOpen()) dev->open(QIODevice::ReadOnly); - KoXmlDocument doc; QString errorMsg; int errorLine = 0; int errorColumn; - bool ok = doc.setContent(dev->readAll(), false, &errorMsg, &errorLine, &errorColumn); - if (!ok) { + KoXmlDocument doc = SvgParser::createDocumentFromSvg(dev, &errorMsg, &errorLine, &errorColumn); + if (doc.isNull()) { errKrita << "Parsing error in " << filename() << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" , errorLine , errorColumn , errorMsg); return false; } KoDocumentResourceManager manager; SvgParser parser(&manager); parser.setResolution(QRectF(0,0,100,100), 72); // initialize with default values QSizeF fragmentSize; // We're not interested in the shapes themselves qDeleteAll(parser.parseSvg(doc.documentElement(), &fragmentSize)); d->symbols = parser.takeSymbols(); // qDebug() << "Loaded" << filename() << "\n\t" // << "Title" << parser.documentTitle() << "\n\t" // << "Description" << parser.documentDescription() // << "\n\tgot" << d->symbols.size() << "symbols" // << d->symbols[0]->shape->outlineRect() // << d->symbols[0]->shape->size(); d->title = parser.documentTitle(); setName(d->title); d->description = parser.documentDescription(); if (d->symbols.size() < 1) { setValid(false); return false; } setValid(true); setImage(d->symbols[0]->icon()); return true; } bool KoSvgSymbolCollectionResource::save() { QFile file(filename()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } saveToDevice(&file); file.close(); return true; } bool KoSvgSymbolCollectionResource::saveToDevice(QIODevice *dev) const { bool res = false; // XXX if (res) { KoResource::saveToDevice(dev); } return res; } QString KoSvgSymbolCollectionResource::defaultFileExtension() const { return QString(".svg"); } QString KoSvgSymbolCollectionResource::title() const { return d->title; } QString KoSvgSymbolCollectionResource::description() const { return d->description; } QString KoSvgSymbolCollectionResource::creator() const { return ""; } QString KoSvgSymbolCollectionResource::rights() const { return ""; } QString KoSvgSymbolCollectionResource::language() const { return ""; } QStringList KoSvgSymbolCollectionResource::subjects() const { return QStringList(); } QString KoSvgSymbolCollectionResource::license() const { return ""; } QStringList KoSvgSymbolCollectionResource::permits() const { return QStringList(); } QVector KoSvgSymbolCollectionResource::symbols() const { return d->symbols; } diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index a188028d33..88734b570c 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1860 +1,1897 @@ /* 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 #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); } +KoXmlDocument SvgParser::createDocumentFromSvg(QIODevice *device, QString *errorMsg, int *errorLine, int *errorColumn) +{ + QXmlInputSource source(device); + return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); +} + +KoXmlDocument SvgParser::createDocumentFromSvg(const QByteArray &data, QString *errorMsg, int *errorLine, int *errorColumn) +{ + QXmlInputSource source; + source.setData(data); + + return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); +} + +KoXmlDocument SvgParser::createDocumentFromSvg(const QString &data, QString *errorMsg, int *errorLine, int *errorColumn) +{ + QXmlInputSource source; + source.setData(data); + + return createDocumentFromSvg(&source, errorMsg, errorLine, errorColumn); +} + +KoXmlDocument SvgParser::createDocumentFromSvg(QXmlInputSource *source, QString *errorMsg, int *errorLine, int *errorColumn) +{ + // we should read all spaces to parse text node correctly + QXmlSimpleReader reader; + reader.setFeature("http://qt-project.org/xml/features/report-whitespace-only-CharData", true); + + QDomDocument doc; + if (!doc.setContent(source, &reader, errorMsg, errorLine, errorColumn)) { + return QDomDocument(); + } + + return doc; +} + 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); if (!file.exists()) { return 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; QScopedPointer svgSymbol(new KoSvgSymbol()); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e, false); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->currentBoundingBox = QRectF(0.0, 0.0, 1.0, 1.0); QString title = e.firstChildElement("title").toElement().text(); QScopedPointer symbolShape(parseGroup(e)); m_context.popGraphicsContext(); if (!symbolShape) return false; svgSymbol->shape = symbolShape.take(); svgSymbol->title = title; svgSymbol->id = id; if (title.isEmpty()) svgSymbol->title = id; if (svgSymbol->shape->boundingRect() == QRectF(0.0, 0.0, 0.0, 0.0)) { debugFlake << "Symbol" << id << "seems to be empty, discarding"; return false; } m_symbols << svgSymbol.take(); return true; } bool SvgParser::parseClipPath(const KoXmlElement &e) { SvgClipPathHelper clipPath; const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipPath.setClipPathUnits( KoFlake::coordinatesFromString(e.attribute("clipPathUnits"), KoFlake::UserSpaceOnUse)); // ensure that the clip path is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipPath.setShapes({clipShape}); m_clipPaths.insert(id, clipPath); return true; } bool SvgParser::parseClipMask(const KoXmlElement &e) { QSharedPointer clipMask(new KoClipMask); const QString id = e.attribute("id"); if (id.isEmpty()) return false; clipMask->setCoordinates(KoFlake::coordinatesFromString(e.attribute("maskUnits"), KoFlake::ObjectBoundingBox)); clipMask->setContentCoordinates(KoFlake::coordinatesFromString(e.attribute("maskContentUnits"), KoFlake::UserSpaceOnUse)); QRectF maskRect; if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { maskRect.setRect( SvgUtil::fromPercentage(e.attribute("x", "-10%")), SvgUtil::fromPercentage(e.attribute("y", "-10%")), SvgUtil::fromPercentage(e.attribute("width", "120%")), SvgUtil::fromPercentage(e.attribute("height", "120%"))); } else { maskRect.setRect( parseUnitX(e.attribute("x", "-10%")), // yes, percents are insane in this case, parseUnitY(e.attribute("y", "-10%")), // but this is what SVG 1.1 tells us... parseUnitX(e.attribute("width", "120%")), parseUnitY(e.attribute("height", "120%"))); } clipMask->setMaskRect(maskRect); // ensure that the clip mask is loaded in local coordinates system m_context.pushGraphicsContext(e); m_context.currentGC()->matrix = QTransform(); m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! KoShape *clipShape = parseGroup(e); m_context.popGraphicsContext(); if (!clipShape) return false; clipMask->setShapes({clipShape}); m_clipMasks.insert(id, clipMask); return true; } void SvgParser::uploadStyleToContext(const KoXmlElement &e) { SvgStyles styles = m_context.styleParser().collectStyles(e); m_context.styleParser().parseFont(styles); m_context.styleParser().parseStyle(styles); } void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { if (!shape) return; applyCurrentBasicStyle(shape); if (KoPathShape *pathShape = dynamic_cast(shape)) { applyMarkers(pathShape); } applyFilter(shape); applyClipping(shape, shapeToOriginalUserCoordinates); applyMaskClipping(shape, shapeToOriginalUserCoordinates); } void SvgParser::applyCurrentBasicStyle(KoShape *shape) { if (!shape) return; SvgGraphicsContext *gc = m_context.currentGC(); KIS_ASSERT(gc); if (!dynamic_cast(shape)) { applyFillStyle(shape); applyStrokeStyle(shape); } if (!gc->display || !gc->visible) { /** * WARNING: here is a small inconsistency with the standard: * in the standard, 'display' is not inherited, but in * flake it is! * * NOTE: though the standard says: "A value of 'display:none' indicates * that the given element and ***its children*** shall not be * rendered directly". Therefore, using setVisible(false) is fully * legitimate here (DK 29.11.16). */ shape->setVisible(false); } shape->setTransparency(1.0 - gc->opacity); } void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e, const QPointF &shapeToOriginalUserCoordinates) { applyStyle(obj, m_context.styleParser().collectStyles(e), shapeToOriginalUserCoordinates); } void SvgParser::applyStyle(KoShape *obj, const SvgStyles &styles, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; m_context.styleParser().parseStyle(styles); if (!obj) return; if (!dynamic_cast(obj)) { applyFillStyle(obj); applyStrokeStyle(obj); } if (KoPathShape *pathShape = dynamic_cast(obj)) { applyMarkers(pathShape); } applyFilter(obj); applyClipping(obj, shapeToOriginalUserCoordinates); applyMaskClipping(obj, shapeToOriginalUserCoordinates); if (!gc->display || !gc->visible) { obj->setVisible(false); } obj->setTransparency(1.0 - gc->opacity); } QGradient* prepareGradientForShape(const SvgGradientHelper *gradient, const KoShape *shape, const SvgGraphicsContext *gc, QTransform *transform) { QGradient *resultGradient = 0; KIS_ASSERT(transform); if (gradient->gradientUnits() == KoFlake::ObjectBoundingBox) { resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform(); } else { if (gradient->gradient()->type() == QGradient::LinearGradient) { /** * Create a converted gradient that looks the same, but linked to the * bounding rect of the shape, so it would be transformed with the shape */ const QRectF boundingRect = shape->outline().boundingRect(); const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); const QTransform relativeToUser = relativeToShape * shape->transformation() * gc->matrix.inverted(); const QTransform userToRelative = relativeToUser.inverted(); const QLinearGradient *o = static_cast(gradient->gradient()); QLinearGradient *g = new QLinearGradient(); g->setStart(userToRelative.map(o->start())); g->setFinalStop(userToRelative.map(o->finalStop())); g->setCoordinateMode(QGradient::ObjectBoundingMode); g->setStops(o->stops()); g->setSpread(o->spread()); resultGradient = g; *transform = relativeToUser * gradient->transform() * userToRelative; } else if (gradient->gradient()->type() == QGradient::RadialGradient) { // For radial and conical gradients such conversion is not possible resultGradient = KoFlake::cloneGradient(gradient->gradient()); *transform = gradient->transform() * gc->matrix * shape->transformation().inverted(); const QRectF outlineRect = shape->outlineRect(); if (outlineRect.isEmpty()) return resultGradient; /** * If shape outline rect is valid, convert the gradient into OBB mode by * doing some magic conversions: we compensate non-uniform size of the shape * by applying an additional pre-transform */ QRadialGradient *rgradient = static_cast(resultGradient); const qreal maxDimension = KisAlgebra2D::maxDimension(outlineRect); const QRectF uniformSize(outlineRect.topLeft(), QSizeF(maxDimension, maxDimension)); const QTransform uniformizeTransform = QTransform::fromTranslate(-outlineRect.x(), -outlineRect.y()) * QTransform::fromScale(maxDimension / shape->outlineRect().width(), maxDimension / shape->outlineRect().height()) * QTransform::fromTranslate(outlineRect.x(), outlineRect.y()); const QPointF centerLocal = transform->map(rgradient->center()); const QPointF focalLocal = transform->map(rgradient->focalPoint()); const QPointF centerOBB = KisAlgebra2D::absoluteToRelative(centerLocal, uniformSize); const QPointF focalOBB = KisAlgebra2D::absoluteToRelative(focalLocal, uniformSize); rgradient->setCenter(centerOBB); rgradient->setFocalPoint(focalOBB); const qreal centerRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->centerRadius(), uniformSize); const qreal focalRadiusOBB = KisAlgebra2D::absoluteToRelative(rgradient->focalRadius(), uniformSize); rgradient->setCenterRadius(centerRadiusOBB); rgradient->setFocalRadius(focalRadiusOBB); rgradient->setCoordinateMode(QGradient::ObjectBoundingMode); // Warning: should it really be pre-multiplication? *transform = uniformizeTransform * gradient->transform(); } } return resultGradient; } void SvgParser::applyFillStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->fillType == SvgGraphicsContext::None) { shape->setBackground(QSharedPointer(0)); } else if (gc->fillType == SvgGraphicsContext::Solid) { shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } else if (gc->fillType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->fillId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QSharedPointer bg; bg = toQShared(new KoGradientBackground(result)); bg->setTransform(transform); shape->setBackground(bg); } } else { QSharedPointer pattern = findPattern(gc->fillId, shape); if (pattern) { shape->setBackground(pattern); } else { // no referenced fill found, use fallback color shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); } } } KoPathShape *path = dynamic_cast(shape); if (path) path->setFillRule(gc->fillRule); } void applyDashes(const KoShapeStrokeSP srcStroke, KoShapeStrokeSP dstStroke) { const double lineWidth = srcStroke->lineWidth(); QVector dashes = srcStroke->lineDashes(); // apply line width to dashes and dash offset if (dashes.count() && lineWidth > 0.0) { const double dashOffset = srcStroke->dashOffset(); QVector dashes = srcStroke->lineDashes(); for (int i = 0; i < dashes.count(); ++i) { dashes[i] /= lineWidth; } dstStroke->setLineStyle(Qt::CustomDashLine, dashes); dstStroke->setDashOffset(dashOffset / lineWidth); } else { dstStroke->setLineStyle(Qt::SolidLine, QVector()); } } void SvgParser::applyStrokeStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->strokeType == SvgGraphicsContext::None) { shape->setStroke(KoShapeStrokeModelSP()); } else if (gc->strokeType == SvgGraphicsContext::Solid) { KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } else if (gc->strokeType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->strokeId); if (gradient) { QTransform transform; QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); if (result) { QBrush brush = *result; delete result; brush.setTransform(transform); KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); stroke->setLineBrush(brush); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } else { // no referenced stroke found, use fallback color KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } } void SvgParser::applyFilter(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->filterId.isEmpty()) return; SvgFilterHelper *filter = findFilter(gc->filterId); if (! filter) return; KoXmlElement content = filter->content(); // parse filter region QRectF bound(shape->position(), shape->size()); // work on bounding box without viewbox transformation applied // so user space coordinates of bounding box and filter region match up bound = gc->viewboxTransform.inverted().mapRect(bound); QRectF filterRegion(filter->position(bound), filter->size(bound)); // convert filter region to boundingbox units QRectF objectFilterRegion; objectFilterRegion.setTopLeft(SvgUtil::userSpaceToObject(filterRegion.topLeft(), bound)); objectFilterRegion.setSize(SvgUtil::userSpaceToObject(filterRegion.size(), bound)); KoFilterEffectLoadingContext context(m_context.xmlBaseDir()); context.setShapeBoundingBox(bound); // enable units conversion context.enableFilterUnitsConversion(filter->filterUnits() == KoFlake::UserSpaceOnUse); context.enableFilterPrimitiveUnitsConversion(filter->primitiveUnits() == KoFlake::UserSpaceOnUse); KoFilterEffectRegistry *registry = KoFilterEffectRegistry::instance(); KoFilterEffectStack *filterStack = 0; QSet stdInputs; stdInputs << "SourceGraphic" << "SourceAlpha"; stdInputs << "BackgroundImage" << "BackgroundAlpha"; stdInputs << "FillPaint" << "StrokePaint"; QMap inputs; // create the filter effects and add them to the shape for (KoXmlNode n = content.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement primitive = n.toElement(); KoFilterEffect *filterEffect = registry->createFilterEffectFromXml(primitive, context); if (!filterEffect) { debugFlake << "filter effect" << primitive.tagName() << "is not implemented yet"; continue; } const QString input = primitive.attribute("in"); if (!input.isEmpty()) { filterEffect->setInput(0, input); } const QString output = primitive.attribute("result"); if (!output.isEmpty()) { filterEffect->setOutput(output); } QRectF subRegion; // parse subregion if (filter->primitiveUnits() == KoFlake::UserSpaceOnUse) { const QString xa = primitive.attribute("x"); const QString ya = primitive.attribute("y"); const QString wa = primitive.attribute("width"); const QString ha = primitive.attribute("height"); if (xa.isEmpty() || ya.isEmpty() || wa.isEmpty() || ha.isEmpty()) { bool hasStdInput = false; bool isFirstEffect = filterStack == 0; // check if one of the inputs is a standard input Q_FOREACH (const QString &input, filterEffect->inputs()) { if ((isFirstEffect && input.isEmpty()) || stdInputs.contains(input)) { hasStdInput = true; break; } } if (hasStdInput || primitive.tagName() == "feImage") { // default to 0%, 0%, 100%, 100% subRegion.setTopLeft(QPointF(0, 0)); subRegion.setSize(QSizeF(1, 1)); } else { // defaults to bounding rect of all referenced nodes Q_FOREACH (const QString &input, filterEffect->inputs()) { if (!inputs.contains(input)) continue; KoFilterEffect *inputFilter = inputs[input]; if (inputFilter) subRegion |= inputFilter->filterRect(); } } } else { const qreal x = parseUnitX(xa); const qreal y = parseUnitY(ya); const qreal w = parseUnitX(wa); const qreal h = parseUnitY(ha); subRegion.setTopLeft(SvgUtil::userSpaceToObject(QPointF(x, y), bound)); subRegion.setSize(SvgUtil::userSpaceToObject(QSizeF(w, h), bound)); } } else { // x, y, width, height are in percentages of the object referencing the filter // so we just parse the percentages const qreal x = SvgUtil::fromPercentage(primitive.attribute("x", "0")); const qreal y = SvgUtil::fromPercentage(primitive.attribute("y", "0")); const qreal w = SvgUtil::fromPercentage(primitive.attribute("width", "1")); const qreal h = SvgUtil::fromPercentage(primitive.attribute("height", "1")); subRegion = QRectF(QPointF(x, y), QSizeF(w, h)); } filterEffect->setFilterRect(subRegion); if (!filterStack) filterStack = new KoFilterEffectStack(); filterStack->appendFilterEffect(filterEffect); inputs[filterEffect->output()] = filterEffect; } if (filterStack) { filterStack->setClipRect(objectFilterRegion); shape->setFilterEffectStack(filterStack); } } void SvgParser::applyMarkers(KoPathShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (!gc->markerStartId.isEmpty() && m_markers.contains(gc->markerStartId)) { shape->setMarker(m_markers[gc->markerStartId].data(), KoFlake::StartMarker); } if (!gc->markerMidId.isEmpty() && m_markers.contains(gc->markerMidId)) { shape->setMarker(m_markers[gc->markerMidId].data(), KoFlake::MidMarker); } if (!gc->markerEndId.isEmpty() && m_markers.contains(gc->markerEndId)) { shape->setMarker(m_markers[gc->markerEndId].data(), KoFlake::EndMarker); } shape->setAutoFillMarkers(gc->autoFillMarkers); } void SvgParser::applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->clipPathId.isEmpty()) return; SvgClipPathHelper *clipPath = findClipPath(gc->clipPathId); if (!clipPath || clipPath->isEmpty()) return; QList shapes; Q_FOREACH (KoShape *item, clipPath->shapes()) { KoShape *clonedShape = item->cloneShape(); KIS_ASSERT_RECOVER(clonedShape) { continue; } shapes.append(clonedShape); } if (!shapeToOriginalUserCoordinates.isNull()) { const QTransform t = QTransform::fromTranslate(shapeToOriginalUserCoordinates.x(), shapeToOriginalUserCoordinates.y()); Q_FOREACH(KoShape *s, shapes) { s->applyAbsoluteTransformation(t); } } KoClipPath *clipPathObject = new KoClipPath(shapes, clipPath->clipPathUnits() == KoFlake::ObjectBoundingBox ? KoFlake::ObjectBoundingBox : KoFlake::UserSpaceOnUse); shape->setClipPath(clipPathObject); } void SvgParser::applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) return; if (gc->clipMaskId.isEmpty()) return; QSharedPointer originalClipMask = m_clipMasks.value(gc->clipMaskId); if (!originalClipMask || originalClipMask->isEmpty()) return; KoClipMask *clipMask = originalClipMask->clone(); clipMask->setExtraShapeOffset(shapeToOriginalUserCoordinates); shape->setClipMask(clipMask); } KoShape* SvgParser::parseUse(const KoXmlElement &e, DeferredUseStore* deferredUseStore) { QString href = e.attribute("xlink:href"); if (href.isEmpty()) return 0; QString key = href.mid(1); const bool gotDef = m_context.hasDefinition(key); if (gotDef) { return resolveUse(e, key); } else if (deferredUseStore) { deferredUseStore->add(&e, key); return 0; } qDebug() << "WARNING: Did not find reference for svg 'use' element. Skipping. Id: " << 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 1a067df347..0a2e5e5679 100644 --- a/libs/flake/svg/SvgParser.h +++ b/libs/flake/svg/SvgParser.h @@ -1,222 +1,227 @@ /* 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(); + static KoXmlDocument createDocumentFromSvg(QIODevice *device, QString *errorMsg = 0, int *errorLine = 0, int *errorColumn = 0); + static KoXmlDocument createDocumentFromSvg(const QByteArray &data, QString *errorMsg = 0, int *errorLine = 0, int *errorColumn = 0); + static KoXmlDocument createDocumentFromSvg(const QString &data, QString *errorMsg = 0, int *errorLine = 0, int *errorColumn = 0); + static KoXmlDocument createDocumentFromSvg(QXmlInputSource *source, QString *errorMsg = 0, int *errorLine = 0, int *errorColumn = 0); + /// 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 using 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 currentBoundingBox 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/tests/SvgParserTestingUtils.h b/libs/flake/tests/SvgParserTestingUtils.h index ce76f3eb90..072a7b3772 100644 --- a/libs/flake/tests/SvgParserTestingUtils.h +++ b/libs/flake/tests/SvgParserTestingUtils.h @@ -1,220 +1,221 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef SVGPARSERTESTINGUTILS_H #define SVGPARSERTESTINGUTILS_H #include #include #include #include #include #include #include #include #include #include "kis_algebra_2d.h" +#include struct SvgTester { SvgTester (const QString &data) - : parser(&resourceManager) + : parser(&resourceManager), + doc(SvgParser::createDocumentFromSvg(data)) { - QVERIFY(doc.setContent(data.toUtf8())); root = doc.documentElement(); parser.setXmlBaseDir("./"); savedData = data; //printf("%s", savedData.toUtf8().data()); } ~SvgTester () { qDeleteAll(shapes); } void run() { shapes = parser.parseSvg(root, &fragmentSize); } KoShape* findShape(const QString &name, KoShape *parent = 0) { if (parent && parent->name() == name) { return parent; } QList children; if (!parent) { children = shapes; } else { KoShapeContainer *cont = dynamic_cast(parent); if (cont) { children = cont->shapes(); } } Q_FOREACH (KoShape *shape, children) { KoShape *result = findShape(name, shape); if (result) { return result; } } return 0; } KoShapeGroup* findGroup(const QString &name) { KoShapeGroup *group = 0; KoShape *shape = findShape(name); if (shape) { group = dynamic_cast(shape); } return group; } KoDocumentResourceManager resourceManager; SvgParser parser; KoXmlDocument doc; KoXmlElement root; QSizeF fragmentSize; QList shapes; QString savedData; }; #include "../../sdk/tests/qimage_test_util.h" #ifdef USE_ROUND_TRIP #include "SvgWriter.h" #include #include #endif struct SvgRenderTester : public SvgTester { SvgRenderTester(const QString &data) : SvgTester(data), m_fuzzyThreshold(0) { } void setFuzzyThreshold(int fuzzyThreshold) { m_fuzzyThreshold = fuzzyThreshold; } static void testRender(KoShape *shape, const QString &prefix, const QString &testName, const QSize canvasSize, int fuzzyThreshold = 0) { QImage canvas(canvasSize, QImage::Format_ARGB32); canvas.fill(0); KoViewConverter converter; QPainter painter(&canvas); KoShapePainter p; p.setShapes({shape}); painter.setClipRect(canvas.rect()); p.paint(painter, converter); QVERIFY(TestUtil::checkQImage(canvas, "svg_render", prefix, testName, fuzzyThreshold)); } void test_standard_30px_72ppi(const QString &testName, bool verifyGeometry = true, const QSize &canvasSize = QSize(30,30)) { test_standard_impl(testName, verifyGeometry, canvasSize, 72.0); } void test_standard(const QString &testName, const QSize &canvasSize, qreal pixelsPerInch) { test_standard_impl(testName, false, canvasSize, pixelsPerInch); } void test_standard_impl(const QString &testName, bool verifyGeometry, const QSize &canvasSize, qreal pixelsPerInch) { QSize sizeInPx = canvasSize; QSizeF sizeInPt = QSizeF(canvasSize) * 72.0 / pixelsPerInch; Q_UNUSED(sizeInPt); // used in some definitions only! parser.setResolution(QRectF(QPointF(), sizeInPx) /* px */, pixelsPerInch /* ppi */); run(); #ifdef USE_CLONED_SHAPES { QList newShapes; Q_FOREACH (KoShape *shape, shapes) { KoShape *clonedShape = shape->cloneShape(); KIS_ASSERT(clonedShape); newShapes << clonedShape; } qDeleteAll(shapes); shapes = newShapes; } #endif /* USE_CLONED_SHAPES */ #ifdef USE_ROUND_TRIP QBuffer writeBuf; writeBuf.open(QIODevice::WriteOnly); { SvgWriter writer(shapes); writer.save(writeBuf, sizeInPt); } QDomDocument prettyDoc; prettyDoc.setContent(savedData); qDebug(); printf("\n=== Original: ===\n\n%s\n", prettyDoc.toByteArray(4).data()); printf("\n=== Saved: ===\n\n%s\n", writeBuf.data().data()); qDebug(); QVERIFY(doc.setContent(writeBuf.data())); root = doc.documentElement(); run(); #endif /* USE_ROUND_TRIP */ KoShape *shape = findShape("testRect"); KIS_ASSERT(shape); if (verifyGeometry) { QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,5)); const QPointF bottomRight= shape->absolutePosition(KoFlake::BottomRight); const QPointF expectedBottomRight(15,25); if (KisAlgebra2D::norm(bottomRight - expectedBottomRight) > 0.0001 ) { QCOMPARE(bottomRight, expectedBottomRight); } } testRender(shape, "load", testName, canvasSize, m_fuzzyThreshold); } private: int m_fuzzyThreshold; }; #endif // SVGPARSERTESTINGUTILS_H diff --git a/libs/flake/tests/TestKoDrag.cpp b/libs/flake/tests/TestKoDrag.cpp index bb6f74b6cd..285d783f11 100644 --- a/libs/flake/tests/TestKoDrag.cpp +++ b/libs/flake/tests/TestKoDrag.cpp @@ -1,87 +1,86 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TestKoDrag.h" #include #include #include #include #include #include #include #include "../../sdk/tests/qimage_test_util.h" void TestKoDrag::test() { const QString fileName = TestUtil::fetchDataFileLazy("test_svg_file.svg"); QVERIFY(!fileName.isEmpty()); QFile testShapes(fileName); testShapes.open(QIODevice::ReadOnly); - KoXmlDocument doc; - doc.setContent(testShapes.readAll()); + KoXmlDocument doc = SvgParser::createDocumentFromSvg(&testShapes); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(QRectF(0, 0, 30, 30) /* px */, 72 /* ppi */); QSizeF fragmentSize; QList shapes = parser.parseSvg(doc.documentElement(), &fragmentSize); QCOMPARE(fragmentSize, QSizeF(30,30)); { QCOMPARE(shapes.size(), 1); KoShapeGroup *layer = dynamic_cast(shapes.first()); QVERIFY(layer); QCOMPARE(layer->shapeCount(), 2); QCOMPARE(KoShape::boundingRect(shapes).toAlignedRect(), QRect(3,2,25,26)); } KoDrag drag; drag.setSvg(shapes); drag.addToClipboard(); KoSvgPaste paste; QVERIFY(paste.hasShapes()); QList newShapes = paste.fetchShapes(QRectF(0,0,15,15) /* px */, 144 /* ppi */, &fragmentSize); { QCOMPARE(newShapes.size(), 1); KoShapeGroup *layer = dynamic_cast(newShapes.first()); QVERIFY(layer); QCOMPARE(layer->shapeCount(), 2); QCOMPARE(fragmentSize.toSize(), QSize(56, 54)); QCOMPARE(KoShape::boundingRect(newShapes).toAlignedRect(), QRect(3,2,25,26)); } qDeleteAll(shapes); qDeleteAll(newShapes); } QTEST_MAIN(TestKoDrag) diff --git a/libs/flake/tests/TestSvgText.cpp b/libs/flake/tests/TestSvgText.cpp index c1d69f320d..e6e9969732 100644 --- a/libs/flake/tests/TestSvgText.cpp +++ b/libs/flake/tests/TestSvgText.cpp @@ -1,1228 +1,1292 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TestSvgText.h" #include #include "SvgParserTestingUtils.h" #include #include #include "KoSvgTextShapeMarkupConverter.h" #include #include #include void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, int newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).toInt() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } } void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, KoSvgText::AutoValue newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).value() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } QCOMPARE(props.property(id), QVariant::fromValue(newValue)); } void addProp(SvgLoadingContext &context, KoSvgTextProperties &props, const QString &attribute, const QString &value, KoSvgTextProperties::PropertyId id, qreal newValue) { props.parseSvgTextAttribute(context, attribute, value); if (props.property(id).toReal() != newValue) { qDebug() << "Failed to load the property:"; qDebug() << ppVar(attribute) << ppVar(value); qDebug() << ppVar(newValue); qDebug() << ppVar(props.property(id)); QFAIL("Fail :("); } } void TestSvgText::testTextProperties() { KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); KoSvgTextProperties props; addProp(context, props, "writing-mode", "tb-rl", KoSvgTextProperties::WritingModeId, KoSvgText::TopToBottom); addProp(context, props, "writing-mode", "rl", KoSvgTextProperties::WritingModeId, KoSvgText::RightToLeft); addProp(context, props, "glyph-orientation-vertical", "auto", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue()); addProp(context, props, "glyph-orientation-vertical", "0", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(0)); addProp(context, props, "glyph-orientation-vertical", "90", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "95", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "175", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI)); addProp(context, props, "glyph-orientation-vertical", "280", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(3 * M_PI_2)); addProp(context, props, "glyph-orientation-vertical", "350", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(0)); addProp(context, props, "glyph-orientation-vertical", "105", KoSvgTextProperties::GlyphOrientationVerticalId, KoSvgText::AutoValue(M_PI_2)); addProp(context, props, "glyph-orientation-horizontal", "0", KoSvgTextProperties::GlyphOrientationHorizontalId, 0.0); addProp(context, props, "glyph-orientation-horizontal", "90", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI_2); addProp(context, props, "glyph-orientation-horizontal", "95", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI_2); addProp(context, props, "glyph-orientation-horizontal", "175", KoSvgTextProperties::GlyphOrientationHorizontalId, M_PI); addProp(context, props, "glyph-orientation-horizontal", "280", KoSvgTextProperties::GlyphOrientationHorizontalId, 3 * M_PI_2); addProp(context, props, "direction", "rtl", KoSvgTextProperties::WritingModeId, KoSvgText::DirectionRightToLeft); addProp(context, props, "unicode-bidi", "embed", KoSvgTextProperties::UnicodeBidiId, KoSvgText::BidiEmbed); addProp(context, props, "unicode-bidi", "bidi-override", KoSvgTextProperties::UnicodeBidiId, KoSvgText::BidiOverride); addProp(context, props, "text-anchor", "middle", KoSvgTextProperties::TextAnchorId, KoSvgText::AnchorMiddle); addProp(context, props, "dominant-baseline", "ideographic", KoSvgTextProperties::DominantBaselineId, KoSvgText::DominantBaselineIdeographic); addProp(context, props, "alignment-baseline", "alphabetic", KoSvgTextProperties::AlignmentBaselineId, KoSvgText::AlignmentBaselineAlphabetic); addProp(context, props, "baseline-shift", "sub", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftSub); addProp(context, props, "baseline-shift", "super", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftSuper); addProp(context, props, "baseline-shift", "baseline", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftNone); addProp(context, props, "baseline-shift", "10%", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftPercentage); QCOMPARE(props.property(KoSvgTextProperties::BaselineShiftValueId).toDouble(), 0.1); context.currentGC()->font.setPointSizeF(180); addProp(context, props, "baseline-shift", "36", KoSvgTextProperties::BaselineShiftModeId, KoSvgText::ShiftPercentage); QCOMPARE(props.property(KoSvgTextProperties::BaselineShiftValueId).toDouble(), 0.2); addProp(context, props, "kerning", "auto", KoSvgTextProperties::KerningId, KoSvgText::AutoValue()); addProp(context, props, "kerning", "20", KoSvgTextProperties::KerningId, KoSvgText::AutoValue(20.0)); addProp(context, props, "letter-spacing", "normal", KoSvgTextProperties::LetterSpacingId, KoSvgText::AutoValue()); addProp(context, props, "letter-spacing", "20", KoSvgTextProperties::LetterSpacingId, KoSvgText::AutoValue(20.0)); addProp(context, props, "word-spacing", "normal", KoSvgTextProperties::WordSpacingId, KoSvgText::AutoValue()); addProp(context, props, "word-spacing", "20", KoSvgTextProperties::WordSpacingId, KoSvgText::AutoValue(20.0)); } void TestSvgText::testDefaultTextProperties() { KoSvgTextProperties props; QVERIFY(props.isEmpty()); QVERIFY(!props.hasProperty(KoSvgTextProperties::UnicodeBidiId)); QVERIFY(KoSvgTextProperties::defaultProperties().hasProperty(KoSvgTextProperties::UnicodeBidiId)); QCOMPARE(KoSvgTextProperties::defaultProperties().property(KoSvgTextProperties::UnicodeBidiId).toInt(), int(KoSvgText::BidiNormal)); props = KoSvgTextProperties::defaultProperties(); QVERIFY(props.hasProperty(KoSvgTextProperties::UnicodeBidiId)); QCOMPARE(props.property(KoSvgTextProperties::UnicodeBidiId).toInt(), int(KoSvgText::BidiNormal)); } void TestSvgText::testTextPropertiesDifference() { using namespace KoSvgText; KoSvgTextProperties props; props.setProperty(KoSvgTextProperties::WritingModeId, RightToLeft); props.setProperty(KoSvgTextProperties::DirectionId, DirectionRightToLeft); props.setProperty(KoSvgTextProperties::UnicodeBidiId, BidiEmbed); props.setProperty(KoSvgTextProperties::TextAnchorId, AnchorEnd); props.setProperty(KoSvgTextProperties::DominantBaselineId, DominantBaselineNoChange); props.setProperty(KoSvgTextProperties::AlignmentBaselineId, AlignmentBaselineIdeographic); props.setProperty(KoSvgTextProperties::BaselineShiftModeId, ShiftPercentage); props.setProperty(KoSvgTextProperties::BaselineShiftValueId, 0.5); props.setProperty(KoSvgTextProperties::KerningId, fromAutoValue(AutoValue(10))); props.setProperty(KoSvgTextProperties::GlyphOrientationVerticalId, fromAutoValue(AutoValue(90))); props.setProperty(KoSvgTextProperties::GlyphOrientationHorizontalId, fromAutoValue(AutoValue(180))); props.setProperty(KoSvgTextProperties::LetterSpacingId, fromAutoValue(AutoValue(20))); props.setProperty(KoSvgTextProperties::WordSpacingId, fromAutoValue(AutoValue(30))); KoSvgTextProperties newProps = props; newProps.setProperty(KoSvgTextProperties::KerningId, fromAutoValue(AutoValue(11))); newProps.setProperty(KoSvgTextProperties::LetterSpacingId, fromAutoValue(AutoValue(21))); KoSvgTextProperties diff = newProps.ownProperties(props); QVERIFY(diff.hasProperty(KoSvgTextProperties::KerningId)); QVERIFY(diff.hasProperty(KoSvgTextProperties::LetterSpacingId)); QVERIFY(!diff.hasProperty(KoSvgTextProperties::WritingModeId)); QVERIFY(!diff.hasProperty(KoSvgTextProperties::DirectionId)); } void TestSvgText::testParseFontStyles() { const QString data = "" " Hello, out there" ""; KoXmlDocument doc; QVERIFY(doc.setContent(data.toLatin1())); KoXmlElement root = doc.documentElement(); KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); SvgStyles styles = context.styleParser().collectStyles(root); context.styleParser().parseFont(styles); //QCOMPARE(styles.size(), 3); // TODO: multiple fonts! QCOMPARE(context.currentGC()->font.family(), QString("Verdana")); { QStringList expectedFonts = {"Verdana", "Times New Roman", "serif"}; QCOMPARE(context.currentGC()->fontFamiliesList, expectedFonts); } QCOMPARE(context.currentGC()->font.pointSizeF(), 15.0); QCOMPARE(context.currentGC()->font.style(), QFont::StyleOblique); QCOMPARE(context.currentGC()->font.capitalization(), QFont::SmallCaps); QCOMPARE(context.currentGC()->font.weight(), 66); { SvgStyles fontModifier; fontModifier["font-weight"] = "bolder"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.weight(), 75); } { SvgStyles fontModifier; fontModifier["font-weight"] = "lighter"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.weight(), 66); } QCOMPARE(context.currentGC()->font.stretch(), int(QFont::ExtraCondensed)); { SvgStyles fontModifier; fontModifier["font-stretch"] = "narrower"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.stretch(), int(QFont::UltraCondensed)); } { SvgStyles fontModifier; fontModifier["font-stretch"] = "wider"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.stretch(), int(QFont::ExtraCondensed)); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "underline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.underline(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "overline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.overline(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = "line-through"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.strikeOut(), true); } { SvgStyles fontModifier; fontModifier["text-decoration"] = " line-through overline"; context.styleParser().parseFont(fontModifier); QCOMPARE(context.currentGC()->font.underline(), false); QCOMPARE(context.currentGC()->font.strikeOut(), true); QCOMPARE(context.currentGC()->font.overline(), true); } } void TestSvgText::testParseTextStyles() { const QString data = "" " Hello, out there" ""; KoXmlDocument doc; QVERIFY(doc.setContent(data.toLatin1())); KoXmlElement root = doc.documentElement(); KoDocumentResourceManager resourceManager; SvgLoadingContext context(&resourceManager); context.pushGraphicsContext(); SvgStyles styles = context.styleParser().collectStyles(root); context.styleParser().parseFont(styles); QCOMPARE(context.currentGC()->font.family(), QString("Verdana")); KoSvgTextProperties &props = context.currentGC()->textProperties; QCOMPARE(props.property(KoSvgTextProperties::WritingModeId).toInt(), int(KoSvgText::TopToBottom)); QCOMPARE(props.property(KoSvgTextProperties::GlyphOrientationVerticalId).value(), KoSvgText::AutoValue(M_PI_2)); } #include #include #include void TestSvgText::testSimpleText() { const QString data = "" "" " " " " " Hello, out there!" " " "" ""; QFont testFont("DejaVu Sans"); if (!QFontInfo(testFont).exactMatch()) { QEXPECT_FAIL(0, "DejaVu Sans is *not* found! Text rendering might be broken!", Continue); } SvgRenderTester t (data); t.test_standard("text_simple", QSize(175, 40), 72.0); KoShape *shape = t.findShape("testRect"); KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); QVERIFY(chunkShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(shape)); QCOMPARE(chunkShape->shapeCount(), 0); QCOMPARE(chunkShape->layoutInterface()->isTextNode(), true); QCOMPARE(chunkShape->layoutInterface()->numChars(), 17); QCOMPARE(chunkShape->layoutInterface()->nodeText(), QString("Hello, out there!")); QVector transform = chunkShape->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 1); QVERIFY(bool(transform[0].xPos)); QVERIFY(bool(transform[0].yPos)); QVERIFY(!transform[0].dxPos); QVERIFY(!transform[0].dyPos); QVERIFY(!transform[0].rotate); QCOMPARE(*transform[0].xPos, 7.0); QCOMPARE(*transform[0].yPos, 27.0); QVector subChunks = chunkShape->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 17); //qDebug() << ppVar(subChunks[0].text); //qDebug() << ppVar(subChunks[0].transformation); //qDebug() << ppVar(subChunks[0].format); } inline KoSvgTextChunkShape* toChunkShape(KoShape *shape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_ASSERT(chunkShape); return chunkShape; } void TestSvgText::testComplexText() { const QString data = "" "" " " " " " Hello, ou" "t there cool cdata --> nice work" " " "" ""; SvgRenderTester t (data); t.test_standard("text_complex", QSize(385, 56), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); QCOMPARE(baseShape->shapeCount(), 4); QCOMPARE(baseShape->layoutInterface()->isTextNode(), false); QCOMPARE(baseShape->layoutInterface()->numChars(), 41); { // chunk 0: "Hello, " KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[0]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 7); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("Hello, ")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 7); QVERIFY(bool(transform[0].xPos)); QVERIFY(!bool(transform[1].xPos)); for (int i = 0; i < 7; i++) { QVERIFY(!i || bool(transform[i].dxPos)); if (i) { QCOMPARE(*transform[i].dxPos, qreal(i)); } } QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 7); QCOMPARE(subChunks[0].text.size(), 1); QCOMPARE(*subChunks[0].transformation.xPos, 7.0); QVERIFY(!subChunks[1].transformation.xPos); } { // chunk 1: "out" KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[1]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 3); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("out")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 2); QVERIFY(bool(transform[0].xPos)); QVERIFY(!bool(transform[1].xPos)); for (int i = 0; i < 2; i++) { QVERIFY(bool(transform[i].dxPos)); QCOMPARE(*transform[i].dxPos, qreal(i + 7)); } QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 2); QCOMPARE(subChunks[0].text.size(), 1); QCOMPARE(subChunks[1].text.size(), 2); } { // chunk 2: " there " KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[2]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 7); QCOMPARE(chunk->layoutInterface()->nodeText(), QString(" there ")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 0); QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 7); } { // chunk 3: "cool cdata --> nice work" KoSvgTextChunkShape *chunk = toChunkShape(baseShape->shapes()[3]); QCOMPARE(chunk->shapeCount(), 0); QCOMPARE(chunk->layoutInterface()->isTextNode(), true); QCOMPARE(chunk->layoutInterface()->numChars(), 24); QCOMPARE(chunk->layoutInterface()->nodeText(), QString("cool cdata --> nice work")); QVector transform = chunk->layoutInterface()->localCharTransformations(); QCOMPARE(transform.size(), 0); QVector subChunks = chunk->layoutInterface()->collectSubChunks(); QCOMPARE(subChunks.size(), 1); QCOMPARE(subChunks[0].text.size(), 24); } } void TestSvgText::testHindiText() { const QString data = "" "" " " " " "मौखिक रूप से हिंदी के काफी सामान" " " "" ""; SvgRenderTester t (data); QFont testFont("FreeSans"); if (!QFontInfo(testFont).exactMatch()) { #ifdef USE_ROUND_TRIP return; #else QEXPECT_FAIL(0, "FreeSans found is *not* found! Hindi rendering might be broken!", Continue); #endif } t.test_standard("text_hindi", QSize(260, 30), 72); } void TestSvgText::testTextBaselineShift() { const QString data = "" "" " " " " " textsuper normalsub" " " "" ""; SvgRenderTester t (data); t.test_standard("text_baseline_shift", QSize(180, 40), 72); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testTextSpacing() { const QString data = "" "" " " " " " Lorem ipsum" " Lorem ipsum (ls=4)" " Lorem ipsum (ls=-2)" " Lorem ipsum" " Lorem ipsum (ws=4)" " Lorem ipsum (ws=-2)" " Lorem ipsum" " Lorem ipsum (k=0)" " Lorem ipsum (k=2)" " Lorem ipsum (k=2,ls=2)" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_letter_word_spacing", QSize(340, 250), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testTextDecorations() { const QString data = "" "" " " " " " Lorem ipsum" " Lorem ipsum" " Lorem ipsum" " Lorem ipsum (WRONG!!!)" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_decorations", QSize(290, 135), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } void TestSvgText::testRightToLeft() { const QString data = "" "" " " " " " aa bb cc dd" " حادثتا السفينتين «بسين Bassein» و«فايبر Viper»" " *" " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " *" " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " aa bb حادثتا السفينتين بسين cc dd " " *" " الناطقون: 295 مليون - 422 مليون" " Spoken: 295 مليون - 422 مليون " " *" " aa bb c1 c2 c3 c4 dd ee" " " "" ""; SvgRenderTester t (data); t.test_standard("text_right_to_left", QSize(500,450), 72.0); KoSvgTextChunkShape *baseShape = toChunkShape(t.findShape("testRect")); QVERIFY(baseShape); // root shape is not just a chunk! QVERIFY(dynamic_cast(baseShape)); } #include #include void TestSvgText::testQtBidi() { // Arabic text sample from Wikipedia: // https://ar.wikipedia.org/wiki/%D8%A5%D9%85%D8%A7%D8%B1%D8%A7%D8%AA_%D8%A7%D9%84%D8%B3%D8%A7%D8%AD%D9%84_%D8%A7%D9%84%D9%85%D8%AA%D8%B5%D8%A7%D9%84%D8%AD QStringList ltrText; ltrText << "aa bb cc dd"; ltrText << "aa bb حادثتا السفينتين بسين cc dd"; ltrText << "aa bb \u202ec1c2 d3d4\u202C ee ff"; QStringList rtlText; rtlText << "حادثتا السفينتين «بسين Bassein» و«فايبر Viper»"; rtlText << "حادثتا السفينتين «بسين aa bb cc dd» و«فايبر Viper»"; QImage canvas(500,500,QImage::Format_ARGB32); QPainter gc(&canvas); QPointF pos(15,15); QVector textSamples; textSamples << ltrText; textSamples << rtlText; QVector textDirections; textDirections << Qt::LeftToRight; textDirections << Qt::RightToLeft; for (int i = 0; i < textSamples.size(); i++) { Q_FOREACH (const QString str, textSamples[i]) { QTextOption option; option.setTextDirection(textDirections[i]); option.setUseDesignMetrics(true); QTextLayout layout; layout.setText(str); layout.setFont(QFont("serif", 15.0)); layout.setCacheEnabled(true); layout.beginLayout(); QTextLine line = layout.createLine(); line.setPosition(pos); pos.ry() += 25; layout.endLayout(); layout.draw(&gc, QPointF()); } } canvas.save("test_bidi.png"); } void TestSvgText::testQtDxDy() { QImage canvas(500,500,QImage::Format_ARGB32); QPainter gc(&canvas); QPointF pos(15,15); QTextOption option; option.setTextDirection(Qt::LeftToRight); option.setUseDesignMetrics(true); option.setWrapMode(QTextOption::WrapAnywhere); QTextLayout layout; layout.setText("aa bb cc dd ee ff"); layout.setFont(QFont("serif", 15.0)); layout.setCacheEnabled(true); layout.beginLayout(); layout.setTextOption(option); { QTextLine line = layout.createLine(); line.setPosition(pos); line.setNumColumns(4); } pos.ry() += 25; pos.rx() += 30; { QTextLine line = layout.createLine(); line.setPosition(pos); } layout.endLayout(); layout.draw(&gc, QPointF()); canvas.save("test_dxdy.png"); } void TestSvgText::testTextOutlineSolid() { const QString data = "" "" " " " " " SA" " " "" ""; SvgRenderTester t (data); t.test_standard("text_outline_solid", QSize(30, 30), 72.0); } void TestSvgText::testNbspHandling() { const QString data = "" "" " " " " " S\u00A0A" " " "" ""; SvgRenderTester t (data); t.test_standard("text_nbsp", QSize(30, 30), 72.0); } void TestSvgText::testMulticolorText() { const QString data = "" "" " " " " " SA" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_multicolor", QSize(30, 30), 72.0); } #include void TestSvgText::testConvertToStrippedSvg() { const QString data = "" "" " " " " " SAsome stuff<><><<<>" " " "" ""; SvgRenderTester t (data); t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); KoSvgTextShape *baseShape = dynamic_cast(t.findShape("testRect")); QVERIFY(baseShape); { KoColorBackground *bg = dynamic_cast(baseShape->background().data()); QVERIFY(bg); QCOMPARE(bg->color(), QColor(Qt::blue)); } KoSvgTextShapeMarkupConverter converter(baseShape); QString svgText; QString stylesText; QVERIFY(converter.convertToSvg(&svgText, &stylesText)); QCOMPARE(stylesText, QString("")); QCOMPARE(svgText, QString("SAsome stuff<><><<<>")); // test loading svgText = "SAsome stuff<><><<<>"; QVERIFY(converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); { KoColorBackground *bg = dynamic_cast(baseShape->background().data()); QVERIFY(bg); QCOMPARE(bg->color(), QColor(Qt::green)); } { KoSvgTextProperties props = baseShape->textProperties(); QVERIFY(props.hasProperty(KoSvgTextProperties::FontSizeId)); const qreal fontSize = props.property(KoSvgTextProperties::FontSizeId).toReal(); QCOMPARE(fontSize, 19.0); } QCOMPARE(baseShape->shapeCount(), 3); } void TestSvgText::testConvertToStrippedSvgNullOrigin() { const QString data = "" "" " " " " " SAsome stuff<><><<<>" " " "" ""; SvgRenderTester t (data); t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); KoSvgTextShape *baseShape = dynamic_cast(t.findShape("testRect")); QVERIFY(baseShape); KoSvgTextShapeMarkupConverter converter(baseShape); QString svgText; QString stylesText; QVERIFY(converter.convertToSvg(&svgText, &stylesText)); QCOMPARE(stylesText, QString("")); QCOMPARE(svgText, QString("SAsome stuff<><><<<>")); } void TestSvgText::testConvertFromIncorrectStrippedSvg() { QScopedPointer baseShape(new KoSvgTextShape()); KoSvgTextShapeMarkupConverter converter(baseShape.data()); QString svgText; QString stylesText; svgText = "blah text"; QVERIFY(converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); QCOMPARE(converter.errors().size(), 0); svgText = ">><<>"; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); svgText = "blah text"; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); svgText = ""; QVERIFY(!converter.convertFromSvg(svgText, stylesText, QRectF(0,0,30,30), 72.0)); qDebug() << ppVar(converter.errors()); QCOMPARE(converter.errors().size(), 1); } void TestSvgText::testEmptyTextChunk() { const QString data = "" "" " " " " " " // no actual text! should not crash! " " "" ""; SvgRenderTester t (data); // it just shouldn't assert or fail when seeing an empty text block t.parser.setResolution(QRectF(QPointF(), QSizeF(30,30)) /* px */, 72.0/* ppi */); t.run(); } +void TestSvgText::testTrailingWhitespace() +{ + QStringList chunkA; + chunkA << "aaa"; + chunkA << " aaa"; + chunkA << "aaa "; + chunkA << " aaa "; + + QStringList chunkB; + chunkB << "bbb"; + chunkB << " bbb"; + chunkB << "bbb "; + chunkB << " bbb "; + + QStringList linkChunk; + linkChunk << ""; + linkChunk << " "; + linkChunk << ""; + linkChunk << " "; + + + const QString dataTemplate = + "" + + "" + + " " + + " " + " %1%2%3" + " " + + "" + + ""; + + for (auto itL = linkChunk.constBegin(); itL != linkChunk.constEnd(); ++itL) { + for (auto itA = chunkA.constBegin(); itA != chunkA.constEnd(); ++itA) { + for (auto itB = chunkB.constBegin(); itB != chunkB.constEnd(); ++itB) { + if (itA->rightRef(1) != " " && + itB->leftRef(1) != " " && + *itL != " " && + *itL != linkChunk.last()) continue; + + QString cleanLink = *itL; + cleanLink.replace('/', '_'); + + qDebug() << "Testcase:" << *itA << cleanLink << *itB; + + const QString data = dataTemplate.arg(*itA, *itL, *itB); + SvgRenderTester t (data); + t.setFuzzyThreshold(5); + //t.test_standard(QString("text_trailing_%1_%2_%3").arg(*itA).arg(cleanLink).arg(*itB), QSize(70, 30), 72.0); + + // all files should look exactly the same! + t.test_standard(QString("text_whitespace"), QSize(70, 30), 72.0); + } + } + } +} + void TestSvgText::testConvertHtmlToSvg() { const QString html = "" "" "" "" "" "" "" "" "

" " Lorem ipsum dolor" "

" "

sit am" "et, consectetur adipiscing

" "

" " elit. " "

" "" ""; KoSvgTextShape shape; KoSvgTextShapeMarkupConverter converter(&shape); QString svg; QString defs; converter.convertFromHtml(html, &svg, &defs); bool r = converter.convertToSvg(&svg, &defs); qDebug() << r << svg << defs; } void TestSvgText::testTextWithMultipleRelativeOffsets() { const QString data = "" "" " " " Lorem ipsum dolor sit amet" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_multiple_relative_offsets", QSize(300, 80), 72.0); } void TestSvgText::testTextWithMultipleAbsoluteOffsetsArabic() { /** * According to the standard, each **absolute** offset defines a * new text chunk, therefore, the arabic text must become * ltr reordered */ const QString data = "" "" " " " Lo rem اللغة العربية المعيارية الحديثة ip sum" " " "" ""; SvgRenderTester t (data); t.test_standard("text_multiple_absolute_offsets_arabic", QSize(530, 70), 72.0); } void TestSvgText::testTextWithMultipleRelativeOffsetsArabic() { /** * According to the standard, **relative** offsets must not define a new * text chunk, therefore, the arabic text must be written in native rtl order, * even though the individual letters are split. */ const QString data = "" "" " " " Lo rem اللغة العربية المعيارية الحديثة ip sum" " " "" ""; SvgRenderTester t (data); // we cannot expect more than one failure #ifndef USE_ROUND_TRIP QEXPECT_FAIL("", "WARNING: in Krita relative offsets also define a new text chunk, that doesn't comply with SVG standard and must be fixed", Continue); t.test_standard("text_multiple_relative_offsets_arabic", QSize(530, 70), 72.0); #endif } void TestSvgText::testTextOutline() { const QString data = "" "" " " " " " normal " " strikethrough" " overline" " underline" " " "" ""; QRect renderRect(0, 0, 450, 40); SvgRenderTester t (data); t.setFuzzyThreshold(5); t.test_standard("text_outline", renderRect.size(), 72.0); KoShape *shape = t.findShape("testRect"); KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); QVERIFY(chunkShape); KoSvgTextShape *textShape = dynamic_cast(shape); QImage canvas(renderRect.size(), QImage::Format_ARGB32); canvas.fill(0); QPainter gc(&canvas); gc.setPen(Qt::NoPen); gc.setBrush(Qt::black); gc.setRenderHint(QPainter::Antialiasing, true); gc.drawPath(textShape->textOutline()); QVERIFY(TestUtil::checkQImage(canvas, "svg_render", "load_text_outline", "converted_to_path", 3, 5)); } QTEST_MAIN(TestSvgText) diff --git a/libs/flake/tests/TestSvgText.h b/libs/flake/tests/TestSvgText.h index 0de43ce2f0..dcaf32cd98 100644 --- a/libs/flake/tests/TestSvgText.h +++ b/libs/flake/tests/TestSvgText.h @@ -1,66 +1,67 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TESTSVGTEXT_H #define TESTSVGTEXT_H #include class TestSvgText : public QObject { Q_OBJECT private Q_SLOTS: void testTextProperties(); void testDefaultTextProperties(); void testTextPropertiesDifference(); void testParseFontStyles(); void testParseTextStyles(); void testSimpleText(); void testComplexText(); void testHindiText(); void testTextBaselineShift(); void testTextSpacing(); void testTextDecorations(); void testRightToLeft(); void testQtBidi(); void testQtDxDy(); void testTextOutlineSolid(); void testNbspHandling(); void testMulticolorText(); void testConvertToStrippedSvg(); void testConvertToStrippedSvgNullOrigin(); void testConvertFromIncorrectStrippedSvg(); void testEmptyTextChunk(); + void testTrailingWhitespace(); void testConvertHtmlToSvg(); void testTextWithMultipleRelativeOffsets(); void testTextWithMultipleAbsoluteOffsetsArabic(); void testTextWithMultipleRelativeOffsetsArabic(); void testTextOutline(); }; #endif // TESTSVGTEXT_H diff --git a/libs/flake/tests/data/svg_render/load_text_outline.png b/libs/flake/tests/data/svg_render/load_text_outline.png index 799222573a..3ff7c87b60 100644 Binary files a/libs/flake/tests/data/svg_render/load_text_outline.png and b/libs/flake/tests/data/svg_render/load_text_outline.png differ diff --git a/libs/flake/tests/data/svg_render/load_text_outline_converted_to_path.png b/libs/flake/tests/data/svg_render/load_text_outline_converted_to_path.png index b1d8cdb82f..cda57efe8a 100644 Binary files a/libs/flake/tests/data/svg_render/load_text_outline_converted_to_path.png and b/libs/flake/tests/data/svg_render/load_text_outline_converted_to_path.png differ diff --git a/libs/flake/tests/data/svg_render/load_text_right_to_left.png b/libs/flake/tests/data/svg_render/load_text_right_to_left.png index b9f8e081e3..de88191036 100644 Binary files a/libs/flake/tests/data/svg_render/load_text_right_to_left.png and b/libs/flake/tests/data/svg_render/load_text_right_to_left.png differ diff --git a/libs/flake/tests/data/svg_render/load_text_whitespace.png b/libs/flake/tests/data/svg_render/load_text_whitespace.png new file mode 100644 index 0000000000..d9d143a05c Binary files /dev/null and b/libs/flake/tests/data/svg_render/load_text_whitespace.png differ diff --git a/libs/flake/text/KoSvgTextChunkShape.cpp b/libs/flake/text/KoSvgTextChunkShape.cpp index 7ed4bf7684..2830361582 100644 --- a/libs/flake/text/KoSvgTextChunkShape.cpp +++ b/libs/flake/text/KoSvgTextChunkShape.cpp @@ -1,946 +1,997 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextChunkShape.h" #include "KoSvgTextChunkShape_p.h" #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { void appendLazy(QVector *list, boost::optional value, int iteration, bool hasDefault = true, qreal defaultValue = 0.0) { if (!value) return; if (value && *value == defaultValue && hasDefault == true && list->isEmpty()) return; while (list->size() < iteration) { list->append(defaultValue); } list->append(*value); } void fillTransforms(QVector *xPos, QVector *yPos, QVector *dxPos, QVector *dyPos, QVector *rotate, QVector localTransformations) { for (int i = 0; i < localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = localTransformations[i]; appendLazy(xPos, t.xPos, i, false); appendLazy(yPos, t.yPos, i, false); appendLazy(dxPos, t.dxPos, i); appendLazy(dyPos, t.dyPos, i); appendLazy(rotate, t.rotate, i); } } QVector parseListAttributeX(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitX(context.currentGC(), str); } return result; } QVector parseListAttributeY(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitY(context.currentGC(), str); } return result; } QVector parseListAttributeAngular(const QString &value, SvgLoadingContext &context) { QVector result; QStringList list = SvgUtil::simplifyList(value); Q_FOREACH (const QString &str, list) { result << SvgUtil::parseUnitAngular(context.currentGC(), str); } return result; } QString convertListAttribute(const QVector &values) { QStringList stringValues; Q_FOREACH (qreal value, values) { stringValues.append(KisDomUtils::toString(value)); } return stringValues.join(','); } } struct KoSvgTextChunkShapePrivate::LayoutInterface : public KoSvgTextChunkShapeLayoutInterface { LayoutInterface(KoSvgTextChunkShape *_q) : q(_q) {} KoSvgText::AutoValue textLength() const { return q->d_func()->textLength; } KoSvgText::LengthAdjust lengthAdjust() const { return q->d_func()->lengthAdjust; } int numChars() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), 0); int result = 0; if (!q->shapeCount()) { result = q->d_func()->text.size(); } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); result += chunkShape->layoutInterface()->numChars(); } } return result; } int relativeCharPos(KoSvgTextChunkShape *child, int pos) const { QList childShapes = q->shapes(); int result = -1; int numCharsPassed = 0; Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(chunkShape, 0); if (chunkShape == child) { result = pos + numCharsPassed; break; } else { numCharsPassed += chunkShape->layoutInterface()->numChars(); } } return result; } bool isTextNode() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), false); return !q->shapeCount(); } QString nodeText() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!q->shapeCount() || q->d_func()->text.isEmpty(), 0); return !q->shapeCount() ? q->d_func()->text : QString(); } QVector localCharTransformations() const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(isTextNode(), QVector()); const QVector t = q->d_func()->localTransformations; return t.mid(0, qMin(t.size(), q->d_func()->text.size())); } static QString getBidiOpening(KoSvgText::Direction direction, KoSvgText::UnicodeBidi bidi) { using namespace KoSvgText; QString result; if (bidi == BidiEmbed) { result = direction == DirectionLeftToRight ? "\u202a" : "\u202b"; } else if (bidi == BidiOverride) { result = direction == DirectionLeftToRight ? "\u202d" : "\u202e"; } return result; } QVector collectSubChunks() const { QVector result; if (isTextNode()) { const QString text = q->d_func()->text; const KoSvgText::KoSvgCharChunkFormat format = q->d_func()->fetchCharFormat(); QVector transforms = q->d_func()->localTransformations; /** * Sometimes SVG can contain the X,Y offsets for the pieces of text that * do not exist, just skip them. */ if (text.size() <= transforms.size()) { transforms.resize(text.size()); } KoSvgText::UnicodeBidi bidi = KoSvgText::UnicodeBidi(q->d_func()->properties.propertyOrDefault(KoSvgTextProperties::UnicodeBidiId).toInt()); KoSvgText::Direction direction = KoSvgText::Direction(q->d_func()->properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); const QString bidiOpening = getBidiOpening(direction, bidi); if (!bidiOpening.isEmpty()) { result << SubChunk(bidiOpening, format); } if (transforms.isEmpty()) { result << SubChunk(text, format); } else { for (int i = 0; i < transforms.size(); i++) { const KoSvgText::CharTransformation baseTransform = transforms[i]; int subChunkLength = 1; for (int j = i + 1; j < transforms.size(); j++) { if (transforms[j].isNull()) { subChunkLength++; } else { break; } } if (i + subChunkLength >= transforms.size()) { subChunkLength = text.size() - i; } result << SubChunk(text.mid(i, subChunkLength), format, baseTransform); i += subChunkLength - 1; } } if (!bidiOpening.isEmpty()) { result << SubChunk("\u202c", format); } } else { Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result += chunkShape->layoutInterface()->collectSubChunks(); } } return result; } void addAssociatedOutline(const QRectF &rect) override { KIS_SAFE_ASSERT_RECOVER_RETURN(isTextNode()); QPainterPath path; path.addRect(rect); path |= q->d_func()->associatedOutline; path.setFillRule(Qt::WindingFill); path = path.simplified(); q->d_func()->associatedOutline = path; q->d_func()->size = path.boundingRect().size(); q->notifyChanged(); q->d_func()->shapeChanged(KoShape::SizeChanged); } void clearAssociatedOutline() override { q->d_func()->associatedOutline = QPainterPath(); q->d_func()->size = QSizeF(); q->notifyChanged(); q->d_func()->shapeChanged(KoShape::SizeChanged); } private: KoSvgTextChunkShape *q; }; KoSvgTextChunkShape::KoSvgTextChunkShape() : KoShapeContainer(new KoSvgTextChunkShapePrivate(this)) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::KoSvgTextChunkShape(const KoSvgTextChunkShape &rhs) : KoShapeContainer(new KoSvgTextChunkShapePrivate(*rhs.d_func(), this)) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::KoSvgTextChunkShape(KoSvgTextChunkShapePrivate *dd) : KoShapeContainer(dd) { Q_D(KoSvgTextChunkShape); d->layoutInterface.reset(new KoSvgTextChunkShapePrivate::LayoutInterface(this)); } KoSvgTextChunkShape::~KoSvgTextChunkShape() { } KoShape *KoSvgTextChunkShape::cloneShape() const { return new KoSvgTextChunkShape(*this); } QSizeF KoSvgTextChunkShape::size() const { return outlineRect().size(); } void KoSvgTextChunkShape::setSize(const QSizeF &size) { Q_UNUSED(size); // we do not support resizing! } QRectF KoSvgTextChunkShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoSvgTextChunkShape::outline() const { Q_D(const KoSvgTextChunkShape); QPainterPath result; result.setFillRule(Qt::WindingFill); if (d->layoutInterface->isTextNode()) { result = d->associatedOutline; } else { Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(chunkShape); result |= chunkShape->outline(); } } return result.simplified(); } void KoSvgTextChunkShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); } void KoSvgTextChunkShape::saveOdf(KoShapeSavingContext &context) const { Q_UNUSED(context); } bool KoSvgTextChunkShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_UNUSED(element); Q_UNUSED(context); return false; } bool KoSvgTextChunkShape::saveHtml(HtmlSavingContext &context) { Q_D(KoSvgTextChunkShape); // Should we add a newline? Check for vertical movement if we're using rtl or ltr text // XXX: if vertical text, check horizontal movement. QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); for (int i = 0; i < d->localTransformations.size(); i++) { const KoSvgText::CharTransformation &t = d->localTransformations[i]; appendLazy(&xPos, t.xPos, i, false); appendLazy(&yPos, t.yPos, i, false); appendLazy(&dxPos, t.dxPos, i); appendLazy(&dyPos, t.dyPos, i); } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); // XXX: we don't save fill, stroke, text length, length adjust or spacing and glyphs. KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); if (isRootTextNode()) { context.shapeWriter().startElement("body", false); if (layoutInterface()->isTextNode()) { context.shapeWriter().startElement("p", false); } // XXX: Save the style? } else if (parent->isRootTextNode()) { context.shapeWriter().startElement("p", false); } else { context.shapeWriter().startElement("span", false); // XXX: Save the style? } QMap attributes = ownProperties.convertToSvgTextAttributes(); if (attributes.size() > 0) { QString styleString; for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { if (QString(it.key().toLatin1().data()).contains("text-anchor")) { QString val = it.value(); if (it.value()=="middle") { val = "center"; } else if (it.value()=="end") { val = "right"; } else { val = "left"; } styleString.append("text-align") .append(": ") .append(val) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("fill")){ styleString.append("color") .append(": ") .append(it.value()) .append(";" ); } else if (QString(it.key().toLatin1().data()).contains("font-size")){ QString val = it.value(); if (QRegExp ("\\d*").exactMatch(val)) { val.append("pt"); } styleString.append(it.key().toLatin1().data()) .append(": ") .append(val) .append(";" ); } else { styleString.append(it.key().toLatin1().data()) .append(": ") .append(it.value()) .append(";" ); } } context.shapeWriter().addAttribute("style", styleString); } if (layoutInterface()->isTextNode()) { qDebug() << "saveHTML" << this << d->text << xPos << yPos << dxPos << dyPos; // After adding all the styling to the

element, add the text context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveHtml(context); } } if (isRootTextNode() && layoutInterface()->isTextNode()) { context.shapeWriter().endElement(); // body } context.shapeWriter().endElement(); // p or span return true; } void writeTextListAttribute(const QString &attribute, const QVector &values, KoXmlWriter &writer) { const QString value = convertListAttribute(values); if (!value.isEmpty()) { writer.addAttribute(attribute.toLatin1().data(), value); } } bool KoSvgTextChunkShape::saveSvg(SvgSavingContext &context) { Q_D(KoSvgTextChunkShape); if (isRootTextNode()) { context.shapeWriter().startElement("text", false); if (!context.strippedTextMode()) { context.shapeWriter().addAttribute("id", context.getID(this)); SvgUtil::writeTransformAttributeLazy("transform", transformation(), context.shapeWriter()); SvgStyleWriter::saveSvgStyle(this, context); } else { SvgStyleWriter::saveSvgFill(this, context); SvgStyleWriter::saveSvgStroke(this, context); } } else { context.shapeWriter().startElement("tspan", false); if (!context.strippedTextMode()) { SvgStyleWriter::saveSvgBasicStyle(this, context); } } if (layoutInterface()->isTextNode()) { QVector xPos; QVector yPos; QVector dxPos; QVector dyPos; QVector rotate; fillTransforms(&xPos, &yPos, &dxPos, &dyPos, &rotate, d->localTransformations); writeTextListAttribute("x", xPos, context.shapeWriter()); writeTextListAttribute("y", yPos, context.shapeWriter()); writeTextListAttribute("dx", dxPos, context.shapeWriter()); writeTextListAttribute("dy", dyPos, context.shapeWriter()); writeTextListAttribute("rotate", rotate, context.shapeWriter()); } if (!d->textLength.isAuto) { context.shapeWriter().addAttribute("textLength", KisDomUtils::toString(d->textLength.customValue)); if (d->lengthAdjust == KoSvgText::LengthAdjustSpacingAndGlyphs) { context.shapeWriter().addAttribute("lengthAdjust", "spacingAndGlyphs"); } } KoSvgTextChunkShape *parent = !isRootTextNode() ? dynamic_cast(this->parent()) : 0; KoSvgTextProperties parentProperties = parent ? parent->textProperties() : KoSvgTextProperties::defaultProperties(); KoSvgTextProperties ownProperties = textProperties().ownProperties(parentProperties); // we write down stroke/fill iff they are different from the parent's value if (!isRootTextNode()) { if (ownProperties.hasProperty(KoSvgTextProperties::FillId)) { SvgStyleWriter::saveSvgFill(this, context); } if (ownProperties.hasProperty(KoSvgTextProperties::StrokeId)) { SvgStyleWriter::saveSvgStroke(this, context); } } QMap attributes = ownProperties.convertToSvgTextAttributes(); for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { context.shapeWriter().addAttribute(it.key().toLatin1().data(), it.value()); } if (layoutInterface()->isTextNode()) { context.shapeWriter().addTextNode(d->text); } else { Q_FOREACH (KoShape *child, this->shapes()) { KoSvgTextChunkShape *childText = dynamic_cast(child); KIS_SAFE_ASSERT_RECOVER(childText) { continue; } childText->saveSvg(context); } } context.shapeWriter().endElement(); return true; } void KoSvgTextChunkShapePrivate::loadContextBasedProperties(SvgGraphicsContext *gc) { properties = gc->textProperties; font = gc->font; fontFamiliesList = gc->fontFamiliesList; } void KoSvgTextChunkShape::resetTextShape() { Q_D(KoSvgTextChunkShape); using namespace KoSvgText; d->properties = KoSvgTextProperties(); d->font = QFont(); d->fontFamiliesList = QStringList(); d->textLength = AutoValue(); d->lengthAdjust = LengthAdjustSpacing; d->localTransformations.clear(); d->text.clear(); // all the subchunks are destroyed! // (first detach, then destroy) QList shapesToReset = shapes(); Q_FOREACH (KoShape *shape, shapesToReset) { shape->setParent(0); delete shape; } } bool KoSvgTextChunkShape::loadSvg(const KoXmlElement &e, SvgLoadingContext &context) { Q_D(KoSvgTextChunkShape); SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); d->textLength = KoSvgText::parseAutoValueXY(e.attribute("textLength", ""), context, ""); d->lengthAdjust = KoSvgText::parseLengthAdjust(e.attribute("lengthAdjust", "spacing")); QVector xPos = parseListAttributeX(e.attribute("x", ""), context); QVector yPos = parseListAttributeY(e.attribute("y", ""), context); QVector dxPos = parseListAttributeX(e.attribute("dx", ""), context); QVector dyPos = parseListAttributeY(e.attribute("dy", ""), context); QVector rotate = parseListAttributeAngular(e.attribute("rotate", ""), context); const int numLocalTransformations = std::max({xPos.size(), yPos.size(), dxPos.size(), dyPos.size(), rotate.size()}); d->localTransformations.resize(numLocalTransformations); for (int i = 0; i < numLocalTransformations; i++) { if (i < xPos.size()) { d->localTransformations[i].xPos = xPos[i]; } if (i < yPos.size()) { d->localTransformations[i].yPos = yPos[i]; } if (i < dxPos.size() && dxPos[i] != 0.0) { d->localTransformations[i].dxPos = dxPos[i]; } if (i < dyPos.size() && dyPos[i] != 0.0) { d->localTransformations[i].dyPos = dyPos[i]; } if (i < rotate.size()) { d->localTransformations[i].rotate = rotate[i]; } } return true; } namespace { -bool hasNextSibling(const KoXmlNode &node) + +QString cleanUpString(QString text) { + text.replace(QRegExp("[\\r\\n]"), ""); + text.replace(QRegExp("\\s{2,}"), " "); + return text; +} + +enum Result { + FoundNothing, + FoundText, + FoundSpace +}; + +Result hasPreviousSibling(KoXmlNode node) { - if (!node.nextSibling().isNull()) return true; + while (!node.isNull()) { + if (node.isElement()) { + KoXmlElement element = node.toElement(); + if (element.tagName() == "text") break; + } + + + while (!node.previousSibling().isNull()) { + node = node.previousSibling(); + + while (!node.lastChild().isNull()) { + node = node.lastChild(); + } + + if (node.isText()) { + KoXmlText textNode = node.toText(); + const QString text = cleanUpString(textNode.data()); + + if (!text.isEmpty()) { - KoXmlNode parentNode = node.parentNode(); + // if we are the leading whitespace, we should report that + // we are the last - if (!parentNode.isNull() && - parentNode.isElement() && - parentNode.toElement().tagName() == "tspan") { + if (text == " ") { + return hasPreviousSibling(node) == FoundNothing ? FoundNothing : FoundSpace; + } - return hasNextSibling(parentNode); + return !text[text.size() - 1].isSpace() ? FoundText : FoundSpace; + } + } + } + node = node.parentNode(); } - return false; + return FoundNothing; } -bool hasPreviousSibling(const KoXmlNode &node) +Result hasNextSibling(KoXmlNode node) { - if (!node.previousSibling().isNull()) return true; + while (!node.isNull()) { + while (!node.nextSibling().isNull()) { + node = node.nextSibling(); + + while (!node.firstChild().isNull()) { + node = node.firstChild(); + } - KoXmlNode parentNode = node.parentNode(); + if (node.isText()) { + KoXmlText textNode = node.toText(); + const QString text = cleanUpString(textNode.data()); - if (!parentNode.isNull() && - parentNode.isElement() && - parentNode.toElement().tagName() == "tspan") { + // if we are the trailing whitespace, we should report that + // we are the last + if (text == " ") { + return hasNextSibling(node) == FoundNothing ? FoundNothing : FoundSpace; + } - return hasPreviousSibling(parentNode); + if (!text.isEmpty()) { + return !text[0].isSpace() ? FoundText : FoundSpace; + } + } + } + node = node.parentNode(); } - return false; + return FoundNothing; } } bool KoSvgTextChunkShape::loadSvgTextNode(const KoXmlText &text, SvgLoadingContext &context) { Q_D(KoSvgTextChunkShape); SvgGraphicsContext *gc = context.currentGC(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(gc, false); d->loadContextBasedProperties(gc); - QString data = text.data(); + QString data = cleanUpString(text.data()); - data.replace(QRegExp("[\\r\\n]"), ""); - data.replace(QRegExp("\\s{2,}"), " "); + const Result leftBorder = hasPreviousSibling(text); + const Result rightBorder = hasNextSibling(text); - if (data.startsWith(' ') && !hasPreviousSibling(text)) { + if (data.startsWith(' ') && leftBorder == FoundNothing) { data.remove(0, 1); } - if (data.endsWith(' ') && !hasNextSibling(text)) { + if (data.endsWith(' ') && rightBorder != FoundText) { data.remove(data.size() - 1, 1); } - if (data == " ") { + if (data == " " && (leftBorder == FoundNothing || rightBorder == FoundNothing)) { data = ""; } //ENTER_FUNCTION() << text.data() << "-->" << data; d->text = data; return !data.isEmpty(); } void KoSvgTextChunkShape::normalizeCharTransformations() { Q_D(KoSvgTextChunkShape); d->applyParentCharTransformations(d->localTransformations); } void KoSvgTextChunkShape::simplifyFillStrokeInheritance() { Q_D(KoSvgTextChunkShape); if (!isRootTextNode()) { KoShape *parentShape = parent(); KIS_SAFE_ASSERT_RECOVER_RETURN(parentShape); QSharedPointer bg = background(); QSharedPointer parentBg = parentShape->background(); if (!inheritBackground() && ((!bg && !parentBg) || (bg && parentBg && bg->compareTo(parentShape->background().data())))) { setInheritBackground(true); } KoShapeStrokeModelSP stroke = this->stroke(); KoShapeStrokeModelSP parentStroke= parentShape->stroke(); if (!inheritStroke() && ((!stroke && !parentStroke) || (stroke && parentStroke && stroke->compareFillTo(parentShape->stroke().data()) && stroke->compareStyleTo(parentShape->stroke().data())))) { setInheritStroke(true); } } Q_FOREACH (KoShape *shape, shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->simplifyFillStrokeInheritance(); } } KoSvgTextProperties KoSvgTextChunkShape::textProperties() const { Q_D(const KoSvgTextChunkShape); KoSvgTextProperties properties = d->properties; properties.setProperty(KoSvgTextProperties::FillId, QVariant::fromValue(KoSvgText::BackgroundProperty(background()))); properties.setProperty(KoSvgTextProperties::StrokeId, QVariant::fromValue(KoSvgText::StrokeProperty(stroke()))); return properties; } bool KoSvgTextChunkShape::isTextNode() const { Q_D(const KoSvgTextChunkShape); return d->layoutInterface->isTextNode(); } KoSvgTextChunkShapeLayoutInterface *KoSvgTextChunkShape::layoutInterface() { Q_D(KoSvgTextChunkShape); return d->layoutInterface.data(); } bool KoSvgTextChunkShape::isRootTextNode() const { return false; } /**************************************************************************************************/ /* KoSvgTextChunkShapePrivate */ /**************************************************************************************************/ #include "SimpleShapeContainerModel.h" KoSvgTextChunkShapePrivate::KoSvgTextChunkShapePrivate(KoSvgTextChunkShape *_q) : KoShapeContainerPrivate(_q) { } KoSvgTextChunkShapePrivate::KoSvgTextChunkShapePrivate(const KoSvgTextChunkShapePrivate &rhs, KoSvgTextChunkShape *q) : KoShapeContainerPrivate(rhs, q), properties(rhs.properties), font(rhs.font), fontFamiliesList(rhs.fontFamiliesList), localTransformations(rhs.localTransformations), textLength(rhs.textLength), lengthAdjust(rhs.lengthAdjust), text(rhs.text) { if (rhs.model) { SimpleShapeContainerModel *otherModel = dynamic_cast(rhs.model); KIS_ASSERT_RECOVER_RETURN(otherModel); model = new SimpleShapeContainerModel(*otherModel); } } KoSvgTextChunkShapePrivate::~KoSvgTextChunkShapePrivate() { } #include #include #include KoSvgText::KoSvgCharChunkFormat KoSvgTextChunkShapePrivate::fetchCharFormat() const { Q_Q(const KoSvgTextChunkShape); KoSvgText::KoSvgCharChunkFormat format; format.setFont(font); format.setTextAnchor(KoSvgText::TextAnchor(properties.propertyOrDefault(KoSvgTextProperties::TextAnchorId).toInt())); KoSvgText::Direction direction = KoSvgText::Direction(properties.propertyOrDefault(KoSvgTextProperties::DirectionId).toInt()); format.setLayoutDirection(direction == KoSvgText::DirectionLeftToRight ? Qt::LeftToRight : Qt::RightToLeft); KoSvgText::BaselineShiftMode shiftMode = KoSvgText::BaselineShiftMode(properties.propertyOrDefault(KoSvgTextProperties::BaselineShiftModeId).toInt()); // FIXME: we support only 'none', 'sub' and 'super' shifts at the moment. // Please implement 'percentage' as well! // WARNING!!! Qt's setVerticalAlignment() also changes the size of the font! And SVG does not(!) imply it! if (shiftMode == KoSvgText::ShiftSub) { format.setVerticalAlignment(QTextCharFormat::AlignSubScript); } else if (shiftMode == KoSvgText::ShiftSuper) { format.setVerticalAlignment(QTextCharFormat::AlignSuperScript); } KoSvgText::AutoValue letterSpacing = properties.propertyOrDefault(KoSvgTextProperties::LetterSpacingId).value(); if (!letterSpacing.isAuto) { format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(letterSpacing.customValue); } KoSvgText::AutoValue wordSpacing = properties.propertyOrDefault(KoSvgTextProperties::WordSpacingId).value(); if (!wordSpacing.isAuto) { format.setFontWordSpacing(wordSpacing.customValue); } KoSvgText::AutoValue kerning = properties.propertyOrDefault(KoSvgTextProperties::KerningId).value(); if (!kerning.isAuto) { format.setFontKerning(false); format.setFontLetterSpacingType(QFont::AbsoluteSpacing); format.setFontLetterSpacing(format.fontLetterSpacing() + kerning.customValue); } QBrush textBrush = Qt::NoBrush; if (q->background()) { KoColorBackground *colorBackground = dynamic_cast(q->background().data()); KIS_SAFE_ASSERT_RECOVER (colorBackground) { textBrush = Qt::red; } if (colorBackground) { textBrush = colorBackground->brush(); } } format.setForeground(textBrush); QPen textPen = Qt::NoPen; if (q->stroke()) { KoShapeStroke *stroke = dynamic_cast(q->stroke().data()); if (stroke) { textPen = stroke->resultLinePen(); } } format.setTextOutline(textPen); // TODO: avoid const_cast somehow... format.setAssociatedShape(const_cast(q)); return format; } void KoSvgTextChunkShapePrivate::applyParentCharTransformations(const QVector transformations) { Q_Q(KoSvgTextChunkShape); if (q->shapeCount()) { int numCharsPassed = 0; Q_FOREACH (KoShape *shape, q->shapes()) { KoSvgTextChunkShape *chunkShape = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); const int numCharsInSubtree = chunkShape->layoutInterface()->numChars(); QVector t = transformations.mid(numCharsPassed, numCharsInSubtree); if (t.isEmpty()) break; chunkShape->d_func()->applyParentCharTransformations(t); numCharsPassed += numCharsInSubtree; if (numCharsPassed >= transformations.size()) break; } } else { for (int i = 0; i < qMin(transformations.size(), text.size()); i++) { KIS_SAFE_ASSERT_RECOVER_RETURN(localTransformations.size() >= i); if (localTransformations.size() == i) { localTransformations.append(transformations[i]); } else { localTransformations[i].mergeInParentTransformation(transformations[i]); } } } } diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index 8328619dab..59bd85985c 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1239 +1,1240 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { qDebug() << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); - KoXmlDocument doc; QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); - if (!doc.setContent(fullText, &errorMessage, &errorLine, &errorColumn)) { + KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn); + if (doc.isNull()) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); qDebug() << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { qDebug() << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { qDebug() << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { qDebug() << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line qDebug() << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { qDebug() << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { qDebug() << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { qDebug() << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { qDebug() << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { qDebug() << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { qDebug() << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { qDebug() << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); - svgWriter.setAutoFormatting(true); + // disable auto-formatting to avoid axtra spaces appearing here and there + svgWriter.setAutoFormatting(false); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape * (accordign to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; boost::optional previousBlockAbsoluteXOffset = boost::none; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) boost::optional blockAbsoluteXOffset = boost::none; if (elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } blockAbsoluteXOffset = KisDomUtils::toDouble(xString); } if (previousBlockAbsoluteXOffset && blockAbsoluteXOffset && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) && svgReader.name() != "text" && elementAttributes.hasAttribute("dy")) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } previousBlockAbsoluteXOffset = blockAbsoluteXOffset; } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() == 1, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; SvgGraphicsContext *context = new SvgGraphicsContext(); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) if (it.value() == test.property(it.key())) diff.clearProperty(it.key()); return diff; } diff --git a/libs/store/KoXmlWriter.cpp b/libs/store/KoXmlWriter.cpp index d429ac000e..43db6af561 100644 --- a/libs/store/KoXmlWriter.cpp +++ b/libs/store/KoXmlWriter.cpp @@ -1,555 +1,555 @@ /* This file is part of the KDE project Copyright (C) 2004 David Faure Copyright (C) 2007 Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoXmlWriter.h" #include #include #include #include #include "../global/kis_dom_utils.h" static const int s_indentBufferLength = 100; static const int s_escapeBufferLen = 10000; class Q_DECL_HIDDEN KoXmlWriter::Private { public: Private(QIODevice* dev_, int indentLevel = 0) : dev(dev_), baseIndentLevel(indentLevel) {} ~Private() { delete[] indentBuffer; delete[] escapeBuffer; //TODO: look at if we must delete "dev". For me we must delete it otherwise we will leak it } QIODevice* dev; QStack tags; int baseIndentLevel; char* indentBuffer; // maybe make it static, but then it needs a K_GLOBAL_STATIC // and would eat 1K all the time... Maybe refcount it :) char* escapeBuffer; // can't really be static if we want to be thread-safe }; KoXmlWriter::KoXmlWriter(QIODevice* dev, int indentLevel) : d(new Private(dev, indentLevel)) { init(); } void KoXmlWriter::init() { d->indentBuffer = new char[ s_indentBufferLength ]; memset(d->indentBuffer, ' ', s_indentBufferLength); *d->indentBuffer = '\n'; // write newline before indentation, in one go d->escapeBuffer = new char[s_escapeBufferLen]; if (!d->dev->isOpen()) d->dev->open(QIODevice::WriteOnly); } KoXmlWriter::~KoXmlWriter() { delete d; } void KoXmlWriter::startDocument(const char* rootElemName, const char* publicId, const char* systemId) { Q_ASSERT(d->tags.isEmpty()); writeCString("\n"); // There isn't much point in a doctype if there's no DTD to refer to // (I'm told that files that are validated by a RelaxNG schema cannot refer to the schema) if (publicId) { writeCString("\n"); } } void KoXmlWriter::endDocument() { // just to do exactly like QDom does (newline at end of file). writeChar('\n'); Q_ASSERT(d->tags.isEmpty()); } // returns the value of indentInside of the parent -bool KoXmlWriter::prepareForChild() +bool KoXmlWriter::prepareForChild(bool indentInside) { if (!d->tags.isEmpty()) { Tag& parent = d->tags.top(); if (!parent.hasChildren) { closeStartElement(parent); parent.hasChildren = true; parent.lastChildIsText = false; } - if (parent.indentInside) { + if (parent.indentInside && indentInside) { writeIndent(); } - return parent.indentInside; + return parent.indentInside && indentInside; } - return true; + return indentInside; } void KoXmlWriter::prepareForTextNode() { if (d->tags.isEmpty()) return; Tag& parent = d->tags.top(); if (!parent.hasChildren) { closeStartElement(parent); parent.hasChildren = true; parent.lastChildIsText = true; } } void KoXmlWriter::startElement(const char* tagName, bool indentInside) { Q_ASSERT(tagName != 0); // Tell parent that it has children - bool parentIndent = prepareForChild(); + indentInside = prepareForChild(indentInside); - d->tags.push(Tag(tagName, parentIndent && indentInside)); + d->tags.push(Tag(tagName, indentInside)); writeChar('<'); writeCString(tagName); //kDebug(s_area) << tagName; } void KoXmlWriter::addCompleteElement(const char* cstr) { prepareForChild(); writeCString(cstr); } void KoXmlWriter::addCompleteElement(QIODevice* indev) { prepareForChild(); const bool wasOpen = indev->isOpen(); // Always (re)open the device in readonly mode, it might be // already open but for writing, and we need to rewind. const bool openOk = indev->open(QIODevice::ReadOnly); Q_ASSERT(openOk); if (!openOk) { warnStore << "Failed to re-open the device! wasOpen=" << wasOpen; return; } QString indentString; indentString.fill((' '), indentLevel()); QByteArray indentBuf(indentString.toUtf8()); QByteArray buffer; while (!indev->atEnd()) { buffer = indev->readLine(); d->dev->write(indentBuf); d->dev->write(buffer); } if (!wasOpen) { // Restore initial state indev->close(); } } void KoXmlWriter::endElement() { if (d->tags.isEmpty()) warnStore << "EndElement() was called more times than startElement(). " "The generated XML will be invalid! " "Please report this bug (by saving the document to another format...)" << endl; Tag tag = d->tags.pop(); if (!tag.hasChildren) { writeCString("/>"); } else { if (tag.indentInside && !tag.lastChildIsText) { writeIndent(); } writeCString("'); } } void KoXmlWriter::addTextNode(const QByteArray& cstr) { // Same as the const char* version below, but here we know the size prepareForTextNode(); char* escaped = escapeForXML(cstr.constData(), cstr.size()); writeCString(escaped); if (escaped != d->escapeBuffer) delete[] escaped; } void KoXmlWriter::addTextNode(const char* cstr) { prepareForTextNode(); char* escaped = escapeForXML(cstr, -1); writeCString(escaped); if (escaped != d->escapeBuffer) delete[] escaped; } void KoXmlWriter::addProcessingInstruction(const char* cstr) { prepareForTextNode(); writeCString(""); } void KoXmlWriter::addAttribute(const char* attrName, const QByteArray& value) { // Same as the const char* one, but here we know the size writeChar(' '); writeCString(attrName); writeCString("=\""); char* escaped = escapeForXML(value.constData(), value.size()); writeCString(escaped); if (escaped != d->escapeBuffer) delete[] escaped; writeChar('"'); } void KoXmlWriter::addAttribute(const char* attrName, const char* value) { writeChar(' '); writeCString(attrName); writeCString("=\""); char* escaped = escapeForXML(value, -1); writeCString(escaped); if (escaped != d->escapeBuffer) delete[] escaped; writeChar('"'); } void KoXmlWriter::addAttribute(const char* attrName, double value) { addAttribute(attrName, KisDomUtils::toString(value)); } void KoXmlWriter::addAttribute(const char* attrName, float value) { addAttribute(attrName, KisDomUtils::toString(value)); } void KoXmlWriter::writeIndent() { // +1 because of the leading '\n' d->dev->write(d->indentBuffer, qMin(indentLevel() + 1, s_indentBufferLength)); } void KoXmlWriter::writeString(const QString& str) { // cachegrind says .utf8() is where most of the time is spent const QByteArray cstr = str.toUtf8(); d->dev->write(cstr); } // In case of a reallocation (ret value != d->buffer), the caller owns the return value, // it must delete it (with []) char* KoXmlWriter::escapeForXML(const char* source, int length = -1) const { // we're going to be pessimistic on char length; so lets make the outputLength less // the amount one char can take: 6 char* destBoundary = d->escapeBuffer + s_escapeBufferLen - 6; char* destination = d->escapeBuffer; char* output = d->escapeBuffer; const char* src = source; // src moves, source remains for (;;) { if (destination >= destBoundary) { // When we come to realize that our escaped string is going to // be bigger than the escape buffer (this shouldn't happen very often...), // we drop the idea of using it, and we allocate a bigger buffer. // Note that this if() can only be hit once per call to the method. if (length == -1) length = qstrlen(source); // expensive... uint newLength = length * 6 + 1; // worst case. 6 is due to " and ' char* buffer = new char[ newLength ]; destBoundary = buffer + newLength; uint amountOfCharsAlreadyCopied = destination - d->escapeBuffer; memcpy(buffer, d->escapeBuffer, amountOfCharsAlreadyCopied); output = buffer; destination = buffer + amountOfCharsAlreadyCopied; } switch (*src) { case 60: // < memcpy(destination, "<", 4); destination += 4; break; case 62: // > memcpy(destination, ">", 4); destination += 4; break; case 34: // " memcpy(destination, """, 6); destination += 6; break; #if 0 // needed? case 39: // ' memcpy(destination, "'", 6); destination += 6; break; #endif case 38: // & memcpy(destination, "&", 5); destination += 5; break; case 0: *destination = '\0'; return output; // Control codes accepted in XML 1.0 documents. case 9: case 10: case 13: *destination++ = *src++; continue; default: // Don't add control codes not accepted in XML 1.0 documents. if (*src > 0 && *src < 32) { ++src; } else { *destination++ = *src++; } continue; } ++src; } // NOTREACHED (see case 0) return output; } void KoXmlWriter::addManifestEntry(const QString& fullPath, const QString& mediaType) { startElement("manifest:file-entry"); addAttribute("manifest:media-type", mediaType); addAttribute("manifest:full-path", fullPath); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, const QString& value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "string"); addTextNode(value); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, bool value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "boolean"); addTextNode(value ? "true" : "false"); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, int value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "int"); addTextNode(QString::number(value)); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, double value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "double"); addTextNode(QString::number(value)); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, float value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "double"); addTextNode(QString::number(value)); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, long value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "long"); addTextNode(QString::number(value)); endElement(); } void KoXmlWriter::addConfigItem(const QString & configName, short value) { startElement("config:config-item"); addAttribute("config:name", configName); addAttribute("config:type", "short"); addTextNode(QString::number(value)); endElement(); } void KoXmlWriter::addTextSpan(const QString& text) { QMap tabCache; addTextSpan(text, tabCache); } void KoXmlWriter::addTextSpan(const QString& text, const QMap& tabCache) { int len = text.length(); int nrSpaces = 0; // number of consecutive spaces bool leadingSpace = false; QString str; str.reserve(len); // Accumulate chars either in str or in nrSpaces (for spaces). // Flush str when writing a subelement (for spaces or for another reason) // Flush nrSpaces when encountering two or more consecutive spaces for (int i = 0; i < len ; ++i) { QChar ch = text[i]; ushort unicode = ch.unicode(); if (unicode == ' ') { if (i == 0) leadingSpace = true; ++nrSpaces; } else { if (nrSpaces > 0) { // For the first space we use ' '. // "it is good practice to use (text:s) for the second and all following SPACE // characters in a sequence." (per the ODF spec) // however, per the HTML spec, "authors should not rely on user agents to render // white space immediately after a start tag or immediately before an end tag" // (and both we and OO.o ignore leading spaces in or elements...) if (!leadingSpace) { str += ' '; --nrSpaces; } if (nrSpaces > 0) { // there are more spaces if (!str.isEmpty()) addTextNode(str); str.clear(); startElement("text:s"); if (nrSpaces > 1) // it's 1 by default addAttribute("text:c", nrSpaces); endElement(); } } nrSpaces = 0; leadingSpace = false; switch (unicode) { case '\t': if (!str.isEmpty()) addTextNode(str); str.clear(); startElement("text:tab"); if (tabCache.contains(i)) addAttribute("text:tab-ref", tabCache[i] + 1); endElement(); break; // gracefully handle \f form feed in text input. // otherwise the xml will not be valid. // \f can be added e.g. in ascii import filter. case '\f': case '\n': case QChar::LineSeparator: if (!str.isEmpty()) addTextNode(str); str.clear(); startElement("text:line-break"); endElement(); break; default: // don't add stuff that is not allowed in xml. The stuff we need we have already handled above if (ch.unicode() >= 0x20) { str += text[i]; } break; } } } // either we still have text in str or we have spaces in nrSpaces if (!str.isEmpty()) { addTextNode(str); } if (nrSpaces > 0) { // there are more spaces startElement("text:s"); if (nrSpaces > 1) // it's 1 by default addAttribute("text:c", nrSpaces); endElement(); } } QIODevice *KoXmlWriter::device() const { return d->dev; } int KoXmlWriter::indentLevel() const { return d->tags.size() + d->baseIndentLevel; } QList KoXmlWriter::tagHierarchy() const { QList answer; Q_FOREACH (const Tag & tag, d->tags) answer.append(tag.tagName); return answer; } QString KoXmlWriter::toString() const { Q_ASSERT(!d->dev->isSequential()); if (d->dev->isSequential()) return QString(); bool wasOpen = d->dev->isOpen(); qint64 oldPos = -1; if (wasOpen) { oldPos = d->dev->pos(); if (oldPos > 0) d->dev->seek(0); } else { const bool openOk = d->dev->open(QIODevice::ReadOnly); Q_ASSERT(openOk); if (!openOk) return QString(); } QString s = QString::fromUtf8(d->dev->readAll()); if (wasOpen) d->dev->seek(oldPos); else d->dev->close(); return s; } diff --git a/libs/store/KoXmlWriter.h b/libs/store/KoXmlWriter.h index bb8d532bbe..9b56cf061b 100644 --- a/libs/store/KoXmlWriter.h +++ b/libs/store/KoXmlWriter.h @@ -1,285 +1,285 @@ /* This file is part of the KDE project Copyright (C) 2004 David Faure Copyright (C) 2007 Thomas Zander Copyright (C) 2011 Lukáš Tvrdý This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef XMLWRITER_H #define XMLWRITER_H #include #include #include "kritastore_export.h" /** * A class for writing out XML (to any QIODevice), with a special attention on performance. * The XML is being written out along the way, which avoids requiring the entire * document in memory (like QDom does). */ class KRITASTORE_EXPORT KoXmlWriter { public: /** * Create a KoXmlWriter instance to write out an XML document into * the given QIODevice. */ explicit KoXmlWriter(QIODevice* dev, int indentLevel = 0); /// Destructor ~KoXmlWriter(); QIODevice *device() const; /** * Start the XML document. * This writes out the \ tag with utf8 encoding, and the DOCTYPE. * @param rootElemName the name of the root element, used in the DOCTYPE tag. * @param publicId the public identifier, e.g. "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" * @param systemId the system identifier, e.g. "office.dtd" or a full URL to it. */ void startDocument(const char* rootElemName, const char* publicId = 0, const char* systemId = 0); /// Call this to terminate an XML document. void endDocument(); /** * Start a new element, as a child of the current element. * @param tagName the name of the tag. Warning: this string must * remain alive until endElement, no copy is internally made. * Usually tagName is a string constant so this is no problem anyway. * @param indentInside if set to false, there will be no indentation inside * this tag. This is useful for elements where whitespace matters. */ void startElement(const char* tagName, bool indentInside = true); /** * Overloaded version of addAttribute( const char*, const char* ), * which is a bit slower because it needs to convert @p value to utf8 first. */ inline void addAttribute(const char* attrName, const QString& value) { addAttribute(attrName, value.toUtf8()); } /** * Add an attribute whose value is an integer */ inline void addAttribute(const char* attrName, int value) { addAttribute(attrName, QByteArray::number(value)); } /** * Add an attribute whose value is an unsigned integer */ inline void addAttribute(const char* attrName, uint value) { addAttribute(attrName, QByteArray::number(value)); } /** * Add an attribute whose value is an bool * It is written as "true" or "false" based on value */ inline void addAttribute(const char* attrName, bool value) { addAttribute(attrName, value ? "true" : "false"); } /** * Add an attribute whose value is a floating point number * The number is written out with the highest possible precision * (unlike QString::number and setNum, which default to 6 digits) */ void addAttribute(const char* attrName, double value); /** * Add an attribute whose value is a floating point number * The number is written out with the highest possible precision * (unlike QString::number and setNum, which default to 6 digits) */ void addAttribute(const char* attrName, float value); /// Overloaded version of the one taking a const char* argument, for convenience void addAttribute(const char* attrName, const QByteArray& value); /** * Add an attribute to the current element. */ void addAttribute(const char* attrName, const char* value); /** * Terminate the current element. After this you should start a new one (sibling), * add a sibling text node, or close another one (end of siblings). */ void endElement(); /** * Overloaded version of addTextNode( const char* ), * which is a bit slower because it needs to convert @p str to utf8 first. */ inline void addTextNode(const QString& str) { addTextNode(str.toUtf8()); } /// Overloaded version of the one taking a const char* argument void addTextNode(const QByteArray& cstr); /** * @brief Adds a text node as a child of the current element. * * This is appends the literal content of @p str to the contents of the element. * E.g. addTextNode( "foo" ) inside a \ element gives \foo\, * and startElement( "b" ); endElement( "b" ); addTextNode( "foo" ) gives \\foo\ */ void addTextNode(const char* cstr); /** * @brief Adds a processing instruction * * This writes a processing instruction, like , where foo * is the target, and the rest is the data. * * Processing instructions are used in XML to keep processor-specific * information in the text of the document. */ void addProcessingInstruction(const char* cstr); /** * This is quite a special-purpose method, not for everyday use. * It adds a complete element (with its attributes and child elements) * as a child of the current element. The string is supposed to be escaped * for XML already, so it will usually come from another KoXmlWriter. */ void addCompleteElement(const char* cstr); /** * This is quite a special-purpose method, not for everyday use. * It adds a complete element (with its attributes and child elements) * as a child of the current element. The iodevice is supposed to be escaped * for XML already, so it will usually come from another KoXmlWriter. * This is usually used with KTempFile. */ void addCompleteElement(QIODevice* dev); // #### Maybe we want to subclass KoXmlWriter for manifest files. /** * Special helper for writing "manifest" files * This is equivalent to startElement/2*addAttribute/endElement * This API will probably have to change (or not be used anymore) * when we add support for encrypting/signing. * @note OASIS-specific */ void addManifestEntry(const QString& fullPath, const QString& mediaType); /** * Special helper for writing config item into settings.xml * @note OASIS-specific */ void addConfigItem(const QString & configName, const QString& value); /// @note OASIS-specific void addConfigItem(const QString & configName, bool value); /// @note OASIS-specific void addConfigItem(const QString & configName, int value); /// @note OASIS-specific void addConfigItem(const QString & configName, double value); /// @note OASIS-specific void addConfigItem(const QString & configName, float value); /// @note OASIS-specific void addConfigItem(const QString & configName, long value); /// @note OASIS-specific void addConfigItem(const QString & configName, short value); // TODO addConfigItem for datetime and base64Binary /** * @brief Adds a text span as nodes of the current element. * * Unlike KoXmlWriter::addTextNode it handles tabulations, linebreaks, * and multiple spaces by using the appropriate OASIS tags. * * @param text the text to write * * @note OASIS-specific */ void addTextSpan(const QString& text); /** * Overloaded version of addTextSpan which takes an additional tabCache map. * @param text the text to write * @param tabCache optional map allowing to find a tab for a given character index * @note OASIS-specific */ void addTextSpan(const QString& text, const QMap& tabCache); /** * @return the current indentation level. * Useful when creating a sub-KoXmlWriter (see addCompleteElement) */ int indentLevel() const; /** * Return all the open tags at this time, root element first. */ QList tagHierarchy() const; /** * Return the so far written XML as string for debugging purposes. */ QString toString() const; private: struct Tag { Tag(const char* t = 0, bool ind = true) : tagName(t), hasChildren(false), lastChildIsText(false), openingTagClosed(false), indentInside(ind) {} Tag(const Tag &original) { tagName = original.tagName; hasChildren = original.hasChildren; lastChildIsText = original.lastChildIsText; openingTagClosed = original.openingTagClosed; indentInside = original.indentInside; } const char* tagName; bool hasChildren : 1; ///< element or text children bool lastChildIsText : 1; ///< last child is a text node bool openingTagClosed : 1; ///< true once the '\>' in \ is written out bool indentInside : 1; ///< whether to indent the contents of this tag }; /// Write out \n followed by the number of spaces required. void writeIndent(); // writeCString is much faster than writeString. // Try to use it as much as possible, especially with constants. void writeString(const QString& str); // TODO check return value!!! inline void writeCString(const char* cstr) { device()->write(cstr, qstrlen(cstr)); } inline void writeChar(char c) { device()->putChar(c); } inline void closeStartElement(Tag& tag) { if (!tag.openingTagClosed) { tag.openingTagClosed = true; writeChar('>'); } } char* escapeForXML(const char* source, int length) const; - bool prepareForChild(); + bool prepareForChild(bool indentInside = true); void prepareForTextNode(); void init(); class Private; Private * const d; KoXmlWriter(const KoXmlWriter &); // forbidden KoXmlWriter& operator=(const KoXmlWriter &); // forbidden }; #endif /* XMLWRITER_H */ diff --git a/libs/ui/flake/kis_shape_layer.cc b/libs/ui/flake/kis_shape_layer.cc index d6a5053b65..44d5ca5413 100644 --- a/libs/ui/flake/kis_shape_layer.cc +++ b/libs/ui/flake/kis_shape_layer.cc @@ -1,718 +1,717 @@ /* * Copyright (c) 2006-2008 Boudewijn Rempt * Copyright (c) 2007 Thomas Zander * Copyright (c) 2009 Cyrille Berger * Copyright (c) 2011 Jan Hambrecht * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_shape_layer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SvgWriter.h" #include "SvgParser.h" #include #include #include "kis_default_bounds.h" #include #include "kis_shape_layer_canvas.h" #include "kis_image_view_converter.h" #include #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_effect_mask.h" #include "commands/KoShapeReorderCommand.h" #include class ShapeLayerContainerModel : public SimpleShapeContainerModel { public: ShapeLayerContainerModel(KisShapeLayer *parent) : q(parent) {} void add(KoShape *child) override { SimpleShapeContainerModel::add(child); /** * The shape is always added with the absolute transformation set appropriately. * Here we should just squeeze it into the layer's transformation. */ KIS_SAFE_ASSERT_RECOVER_NOOP(inheritsTransform(child)); if (inheritsTransform(child)) { QTransform parentTransform = q->absoluteTransformation(0); child->applyAbsoluteTransformation(parentTransform.inverted()); } } void remove(KoShape *child) override { KIS_SAFE_ASSERT_RECOVER_NOOP(inheritsTransform(child)); if (inheritsTransform(child)) { QTransform parentTransform = q->absoluteTransformation(0); child->applyAbsoluteTransformation(parentTransform); } SimpleShapeContainerModel::remove(child); } void shapeHasBeenAddedToHierarchy(KoShape *shape, KoShapeContainer *addedToSubtree) override { q->shapeManager()->addShape(shape); SimpleShapeContainerModel::shapeHasBeenAddedToHierarchy(shape, addedToSubtree); } void shapeToBeRemovedFromHierarchy(KoShape *shape, KoShapeContainer *removedFromSubtree) override { q->shapeManager()->remove(shape); SimpleShapeContainerModel::shapeToBeRemovedFromHierarchy(shape, removedFromSubtree); } private: KisShapeLayer *q; }; struct KisShapeLayer::Private { public: Private() : canvas(0) , controller(0) , x(0) , y(0) {} KisPaintDeviceSP paintDevice; KisShapeLayerCanvasBase * canvas; KoShapeControllerBase* controller; int x; int y; }; KisShapeLayer::KisShapeLayer(KoShapeControllerBase* controller, KisImageWSP image, const QString &name, quint8 opacity) : KisExternalLayer(image, name, opacity), KoShapeLayer(new ShapeLayerContainerModel(this)), m_d(new Private()) { initShapeLayer(controller); } KisShapeLayer::KisShapeLayer(const KisShapeLayer& rhs) : KisShapeLayer(rhs, rhs.m_d->controller) { } KisShapeLayer::KisShapeLayer(const KisShapeLayer& _rhs, KoShapeControllerBase* controller, KisShapeLayerCanvasBase *canvas) : KisExternalLayer(_rhs) , KoShapeLayer(new ShapeLayerContainerModel(this)) //no _rhs here otherwise both layer have the same KoShapeContainerModel , m_d(new Private()) { // copy the projection to avoid extra round of updates! initShapeLayer(controller, _rhs.m_d->paintDevice, canvas); /** * The transformaitons of the added shapes are automatically merged into the transformation * of the layer, so we should apply this extra transform separately */ const QTransform thisInvertedTransform = this->absoluteTransformation(0).inverted(); Q_FOREACH (KoShape *shape, _rhs.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); addShape(clonedShape); } } KisShapeLayer::KisShapeLayer(const KisShapeLayer& _rhs, const KisShapeLayer &_addShapes) : KisExternalLayer(_rhs) , KoShapeLayer(new ShapeLayerContainerModel(this)) //no _merge here otherwise both layer have the same KoShapeContainerModel , m_d(new Private()) { // Make sure our new layer is visible otherwise the shapes cannot be painted. setVisible(true); initShapeLayer(_rhs.m_d->controller); /** * With current implementation this matrix will always be an identity, because * we do not copy the transformation from any of the source layers. But we should * handle this anyway, to not be caught by this in the future. */ const QTransform thisInvertedTransform = this->absoluteTransformation(0).inverted(); QList shapesAbove; QList shapesBelow; // copy in _rhs's shapes Q_FOREACH (KoShape *shape, _rhs.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); shapesBelow.append(clonedShape); } // copy in _addShapes's shapes Q_FOREACH (KoShape *shape, _addShapes.shapes()) { KoShape *clonedShape = shape->cloneShape(); KIS_SAFE_ASSERT_RECOVER(clonedShape) { continue; } clonedShape->setTransformation(shape->absoluteTransformation(0) * thisInvertedTransform); shapesAbove.append(clonedShape); } QList shapes = KoShapeReorderCommand::mergeDownShapes(shapesBelow, shapesAbove); KoShapeReorderCommand cmd(shapes); cmd.redo(); Q_FOREACH (KoShape *shape, shapesBelow + shapesAbove) { addShape(shape); } } KisShapeLayer::KisShapeLayer(KoShapeControllerBase* controller, KisImageWSP image, const QString &name, quint8 opacity, KisShapeLayerCanvasBase *canvas) : KisExternalLayer(image, name, opacity) , KoShapeLayer(new ShapeLayerContainerModel(this)) , m_d(new Private()) { initShapeLayer(controller, nullptr, canvas); } KisShapeLayer::~KisShapeLayer() { /** * Small hack alert: we should avoid updates on shape deletion */ m_d->canvas->prepareForDestroying(); Q_FOREACH (KoShape *shape, shapes()) { shape->setParent(0); delete shape; } delete m_d->canvas; delete m_d; } void KisShapeLayer::initShapeLayer(KoShapeControllerBase* controller, KisPaintDeviceSP copyFromProjection, KisShapeLayerCanvasBase *canvas) { setSupportsLodMoves(false); setShapeId(KIS_SHAPE_LAYER_ID); KIS_ASSERT_RECOVER_NOOP(this->image()); if (!copyFromProjection) { m_d->paintDevice = new KisPaintDevice(image()->colorSpace()); m_d->paintDevice->setDefaultBounds(new KisDefaultBounds(this->image())); m_d->paintDevice->setParentNode(this); } else { m_d->paintDevice = new KisPaintDevice(*copyFromProjection); } if (!canvas) { auto *slCanvas = new KisShapeLayerCanvas(this, image()); slCanvas->setProjection(m_d->paintDevice); canvas = slCanvas; } m_d->canvas = canvas; m_d->canvas->moveToThread(this->thread()); m_d->controller = controller; m_d->canvas->shapeManager()->selection()->disconnect(this); connect(m_d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); connect(m_d->canvas->selectedShapesProxy(), SIGNAL(currentLayerChanged(const KoShapeLayer*)), this, SIGNAL(currentLayerChanged(const KoShapeLayer*))); connect(this, SIGNAL(sigMoveShapes(const QPointF&)), SLOT(slotMoveShapes(const QPointF&))); } bool KisShapeLayer::allowAsChild(KisNodeSP node) const { return node->inherits("KisMask"); } void KisShapeLayer::setImage(KisImageWSP _image) { KisLayer::setImage(_image); m_d->canvas->setImage(_image); m_d->paintDevice->convertTo(_image->colorSpace()); m_d->paintDevice->setDefaultBounds(new KisDefaultBounds(_image)); } KisLayerSP KisShapeLayer::createMergedLayerTemplate(KisLayerSP prevLayer) { KisShapeLayer *prevShape = dynamic_cast(prevLayer.data()); if (prevShape) return new KisShapeLayer(*prevShape, *this); else return KisExternalLayer::createMergedLayerTemplate(prevLayer); } void KisShapeLayer::fillMergedLayerTemplate(KisLayerSP dstLayer, KisLayerSP prevLayer) { if (!dynamic_cast(dstLayer.data())) { KisLayer::fillMergedLayerTemplate(dstLayer, prevLayer); } } void KisShapeLayer::setParent(KoShapeContainer *parent) { Q_UNUSED(parent) KIS_ASSERT_RECOVER_RETURN(0) } QIcon KisShapeLayer::icon() const { return KisIconUtils::loadIcon("vectorLayer"); } KisPaintDeviceSP KisShapeLayer::original() const { return m_d->paintDevice; } KisPaintDeviceSP KisShapeLayer::paintDevice() const { return 0; } qint32 KisShapeLayer::x() const { return m_d->x; } qint32 KisShapeLayer::y() const { return m_d->y; } void KisShapeLayer::setX(qint32 x) { qint32 delta = x - this->x(); QPointF diff = QPointF(m_d->canvas->viewConverter()->viewToDocumentX(delta), 0); emit sigMoveShapes(diff); // Save new value to satisfy LSP m_d->x = x; } void KisShapeLayer::setY(qint32 y) { qint32 delta = y - this->y(); QPointF diff = QPointF(0, m_d->canvas->viewConverter()->viewToDocumentY(delta)); emit sigMoveShapes(diff); // Save new value to satisfy LSP m_d->y = y; } namespace { void filterTransformableShapes(QList &shapes) { auto it = shapes.begin(); while (it != shapes.end()) { if (shapes.size() == 1) break; if ((*it)->inheritsTransformFromAny(shapes)) { it = shapes.erase(it); } else { ++it; } } } } QList KisShapeLayer::shapesToBeTransformed() { QList shapes = shapeManager()->shapes(); // We expect that **all** the shapes inherit the transform from its parent // SANITY_CHECK: we expect all the shapes inside the // shape layer to inherit transform! Q_FOREACH (KoShape *shape, shapes) { if (shape->parent()) { KIS_SAFE_ASSERT_RECOVER(shape->parent()->inheritsTransform(shape)) { break; } } } shapes << this; filterTransformableShapes(shapes); return shapes; } void KisShapeLayer::slotMoveShapes(const QPointF &diff) { QList shapes = shapesToBeTransformed(); if (shapes.isEmpty()) return; KoShapeMoveCommand cmd(shapes, diff); cmd.redo(); } bool KisShapeLayer::accept(KisNodeVisitor& visitor) { return visitor.visit(this); } void KisShapeLayer::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } KoShapeManager* KisShapeLayer::shapeManager() const { return m_d->canvas->shapeManager(); } KoViewConverter* KisShapeLayer::converter() const { return m_d->canvas->viewConverter(); } bool KisShapeLayer::visible(bool recursive) const { return KisExternalLayer::visible(recursive); } void KisShapeLayer::setVisible(bool visible, bool isLoading) { const bool oldVisible = this->visible(false); KoShapeLayer::setVisible(visible); KisExternalLayer::setVisible(visible, isLoading); if (visible && !oldVisible && m_d->canvas->hasChangedWhileBeingInvisible()) { m_d->canvas->rerenderAfterBeingInvisible(); } } void KisShapeLayer::setUserLocked(bool value) { KoShapeLayer::setGeometryProtected(value); KisExternalLayer::setUserLocked(value); } bool KisShapeLayer::isShapeEditable(bool recursive) const { return KoShapeLayer::isShapeEditable(recursive) && isEditable(true); } // we do not override KoShape::setGeometryProtected() as we consider // the user not being able to access the layer shape from Krita UI! void KisShapeLayer::forceUpdateTimedNode() { m_d->canvas->forceRepaint(); } bool KisShapeLayer::saveShapesToStore(KoStore *store, QList shapes, const QSizeF &sizeInPt) { if (!store->open("content.svg")) { return false; } KoStoreDevice storeDev(store); storeDev.open(QIODevice::WriteOnly); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); SvgWriter writer(shapes); writer.save(storeDev, sizeInPt); if (!store->close()) { return false; } return true; } QList KisShapeLayer::createShapesFromSvg(QIODevice *device, const QString &baseXmlDir, const QRectF &rectInPixels, qreal resolutionPPI, KoDocumentResourceManager *resourceManager, QSizeF *fragmentSize) { QString errorMsg; int errorLine = 0; int errorColumn; - KoXmlDocument doc; - bool ok = doc.setContent(device, false, &errorMsg, &errorLine, &errorColumn); - if (!ok) { + KoXmlDocument doc = SvgParser::createDocumentFromSvg(device, &errorMsg, &errorLine, &errorColumn); + if (doc.isNull()) { errKrita << "Parsing error in " << "contents.svg" << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" , errorLine , errorColumn , errorMsg); } SvgParser parser(resourceManager); parser.setXmlBaseDir(baseXmlDir); parser.setResolution(rectInPixels /* px */, resolutionPPI /* ppi */); return parser.parseSvg(doc.documentElement(), fragmentSize); } bool KisShapeLayer::saveLayer(KoStore * store) const { // FIXME: we handle xRes() only! const QSizeF sizeInPx = image()->bounds().size(); const QSizeF sizeInPt(sizeInPx.width() / image()->xRes(), sizeInPx.height() / image()->yRes()); return saveShapesToStore(store, this->shapes(), sizeInPt); } bool KisShapeLayer::loadSvg(QIODevice *device, const QString &baseXmlDir) { QSizeF fragmentSize; // unused! KisImageSP image = this->image(); // FIXME: we handle xRes() only! KIS_SAFE_ASSERT_RECOVER_NOOP(qFuzzyCompare(image->xRes(), image->yRes())); const qreal resolutionPPI = 72.0 * image->xRes(); QList shapes = createShapesFromSvg(device, baseXmlDir, image->bounds(), resolutionPPI, m_d->controller->resourceManager(), &fragmentSize); Q_FOREACH (KoShape *shape, shapes) { addShape(shape); } return true; } bool KisShapeLayer::loadLayer(KoStore* store) { if (!store) { warnKrita << i18n("No store backend"); return false; } if (store->open("content.svg")) { KoStoreDevice storeDev(store); storeDev.open(QIODevice::ReadOnly); loadSvg(&storeDev, ""); store->close(); return true; } KoOdfReadStore odfStore(store); QString errorMessage; odfStore.loadAndParse(errorMessage); if (!errorMessage.isEmpty()) { warnKrita << errorMessage; return false; } KoXmlElement contents = odfStore.contentDoc().documentElement(); // dbgKrita <<"Start loading OASIS document..." << contents.text(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().localName(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().namespaceURI(); // dbgKrita <<"Start loading OASIS contents..." << contents.lastChild().isElement(); KoXmlElement body(KoXml::namedItemNS(contents, KoXmlNS::office, "body")); if (body.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No office:body tag found." ) ); return false; } body = KoXml::namedItemNS(body, KoXmlNS::office, "drawing"); if (body.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No office:drawing tag found." ) ); return false; } KoXmlElement page(KoXml::namedItemNS(body, KoXmlNS::draw, "page")); if (page.isNull()) { //setErrorMessage( i18n( "Invalid OASIS document. No draw:page tag found." ) ); return false; } KoXmlElement * master = 0; if (odfStore.styles().masterPages().contains("Standard")) master = odfStore.styles().masterPages().value("Standard"); else if (odfStore.styles().masterPages().contains("Default")) master = odfStore.styles().masterPages().value("Default"); else if (! odfStore.styles().masterPages().empty()) master = odfStore.styles().masterPages().begin().value(); if (master) { const KoXmlElement *style = odfStore.styles().findStyle( master->attributeNS(KoXmlNS::style, "page-layout-name", QString())); KoPageLayout pageLayout; pageLayout.loadOdf(*style); setSize(QSizeF(pageLayout.width, pageLayout.height)); } // We work fine without a master page KoOdfLoadingContext context(odfStore.styles(), odfStore.store()); context.setManifestFile(QString("tar:/") + odfStore.store()->currentPath() + "META-INF/manifest.xml"); KoShapeLoadingContext shapeContext(context, m_d->controller->resourceManager()); KoXmlElement layerElement; forEachElement(layerElement, context.stylesReader().layerSet()) { // FIXME: investigate what is this // KoShapeLayer * l = new KoShapeLayer(); if (!loadOdf(layerElement, shapeContext)) { dbgKrita << "Could not load vector layer!"; return false; } } KoXmlElement child; forEachElement(child, page) { KoShape * shape = KoShapeRegistry::instance()->createShapeFromOdf(child, shapeContext); if (shape) { addShape(shape); } } return true; } void KisShapeLayer::resetCache() { m_d->canvas->resetCache(); } KUndo2Command* KisShapeLayer::crop(const QRect & rect) { QPoint oldPos(x(), y()); QPoint newPos = oldPos - rect.topLeft(); return new KisNodeMoveCommand2(this, oldPos, newPos); } KUndo2Command* KisShapeLayer::transform(const QTransform &transform) { QList shapes = shapesToBeTransformed(); if (shapes.isEmpty()) return 0; KisImageViewConverter *converter = dynamic_cast(this->converter()); QTransform realTransform = converter->documentToView() * transform * converter->viewToDocument(); QList oldTransformations; QList newTransformations; QList newShadows; const qreal transformBaseScale = KoUnit::approxTransformScale(transform); Q_FOREACH (const KoShape* shape, shapes) { QTransform oldTransform = shape->transformation(); oldTransformations.append(oldTransform); QTransform globalTransform = shape->absoluteTransformation(0); QTransform localTransform = globalTransform * realTransform * globalTransform.inverted(); newTransformations.append(localTransform * oldTransform); KoShapeShadow *shadow = 0; if (shape->shadow()) { shadow = new KoShapeShadow(*shape->shadow()); shadow->setOffset(transformBaseScale * shadow->offset()); shadow->setBlur(transformBaseScale * shadow->blur()); } newShadows.append(shadow); } KUndo2Command *parentCommand = new KUndo2Command(); new KoShapeTransformCommand(shapes, oldTransformations, newTransformations, parentCommand); new KoShapeShadowCommand(shapes, newShadows, parentCommand); return parentCommand; } KoShapeControllerBase *KisShapeLayer::shapeController() const { return m_d->controller; }