diff --git a/libs/flake/CMakeLists.txt b/libs/flake/CMakeLists.txt index cbdac265d2..efc6e34e52 100644 --- a/libs/flake/CMakeLists.txt +++ b/libs/flake/CMakeLists.txt @@ -1,249 +1,254 @@ project(kritaflake) include_directories( ${CMAKE_SOURCE_DIR}/libs/flake/commands ${CMAKE_SOURCE_DIR}/libs/flake/tools ${CMAKE_SOURCE_DIR}/libs/flake/svg ${CMAKE_SOURCE_DIR}/libs/flake/text ${CMAKE_BINARY_DIR}/libs/flake ) add_subdirectory(styles) add_subdirectory(tests) add_subdirectory(resources/tests) set(kritaflake_SRCS KoGradientHelper.cpp KoFlake.cpp KoCanvasBase.cpp KoResourceManager_p.cpp KoDerivedResourceConverter.cpp KoResourceUpdateMediator.cpp KoCanvasResourceProvider.cpp KoDocumentResourceManager.cpp KoCanvasObserverBase.cpp KoCanvasSupervisor.cpp KoDockFactoryBase.cpp KoDockRegistry.cpp KoDataCenterBase.cpp KoInsets.cpp KoPathShape.cpp KoPathPoint.cpp KoPathSegment.cpp KoSelection.cpp KoSelectedShapesProxy.cpp KoSelectedShapesProxySimple.cpp KoShape.cpp KoShapeAnchor.cpp KoShapeControllerBase.cpp KoShapeApplicationData.cpp KoShapeContainer.cpp KoShapeContainerModel.cpp KoShapeGroup.cpp KoShapeManager.cpp KoShapePaintingContext.cpp KoFrameShape.cpp KoMarker.cpp KoMarkerCollection.cpp KoToolBase.cpp KoCanvasController.cpp KoCanvasControllerWidget.cpp KoCanvasControllerWidgetViewport_p.cpp KoShapeRegistry.cpp KoDeferredShapeFactoryBase.cpp KoToolFactoryBase.cpp KoPathShapeFactory.cpp KoShapeFactoryBase.cpp KoShapeUserData.cpp KoParameterShape.cpp KoPointerEvent.cpp KoShapeController.cpp KoToolSelection.cpp KoShapeLayer.cpp KoPostscriptPaintDevice.cpp KoInputDevice.cpp KoToolManager_p.cpp KoToolManager.cpp KoToolRegistry.cpp KoToolProxy.cpp KoShapeSavingContext.cpp KoShapeLoadingContext.cpp KoLoadingShapeUpdater.cpp KoPathShapeLoader.cpp KoShapeStrokeModel.cpp KoShapeStroke.cpp KoShapeBackground.cpp KoColorBackground.cpp KoGradientBackground.cpp + KoMeshGradientBackground.cpp KoOdfGradientBackground.cpp KoHatchBackground.cpp KoPatternBackground.cpp KoVectorPatternBackground.cpp KoShapeFillWrapper.cpp KoShapeFillResourceConnector.cpp KoShapeConfigWidgetBase.cpp KoDrag.cpp KoSvgPaste.cpp KoDragOdfSaveHelper.cpp KoShapeOdfSaveHelper.cpp KoConnectionPoint.cpp KoConnectionShape.cpp KoConnectionShapeLoadingUpdater.cpp KoConnectionShapeFactory.cpp KoConnectionShapeConfigWidget.cpp KoSnapGuide.cpp KoSnapProxy.cpp KoSnapStrategy.cpp KoSnapData.cpp KoShapeShadow.cpp KoSharedLoadingData.cpp KoSharedSavingData.cpp KoViewConverter.cpp KoInputDeviceHandler.cpp KoInputDeviceHandlerEvent.cpp KoInputDeviceHandlerRegistry.cpp KoImageData.cpp KoImageData_p.cpp KoImageCollection.cpp KoOdfWorkaround.cpp KoFilterEffect.cpp KoFilterEffectStack.cpp KoFilterEffectFactoryBase.cpp KoFilterEffectRegistry.cpp KoFilterEffectConfigWidgetBase.cpp KoFilterEffectRenderContext.cpp KoFilterEffectLoadingContext.cpp KoTextShapeDataBase.cpp KoTosContainer.cpp KoTosContainerModel.cpp KoClipPath.cpp KoClipMask.cpp KoClipMaskPainter.cpp KoCurveFit.cpp commands/KoShapeGroupCommand.cpp commands/KoShapeAlignCommand.cpp commands/KoShapeBackgroundCommand.cpp commands/KoShapeCreateCommand.cpp commands/KoShapeDeleteCommand.cpp commands/KoShapeDistributeCommand.cpp commands/KoShapeLockCommand.cpp commands/KoShapeMoveCommand.cpp commands/KoShapeResizeCommand.cpp commands/KoShapeShearCommand.cpp commands/KoShapeSizeCommand.cpp commands/KoShapeStrokeCommand.cpp commands/KoShapeUngroupCommand.cpp commands/KoShapeReorderCommand.cpp commands/KoShapeKeepAspectRatioCommand.cpp commands/KoPathBaseCommand.cpp commands/KoPathPointMoveCommand.cpp commands/KoPathControlPointMoveCommand.cpp commands/KoPathPointTypeCommand.cpp commands/KoPathPointRemoveCommand.cpp commands/KoPathPointInsertCommand.cpp commands/KoPathSegmentBreakCommand.cpp commands/KoPathBreakAtPointCommand.cpp commands/KoPathSegmentTypeCommand.cpp commands/KoPathCombineCommand.cpp commands/KoSubpathRemoveCommand.cpp commands/KoSubpathJoinCommand.cpp commands/KoParameterHandleMoveCommand.cpp commands/KoParameterToPathCommand.cpp commands/KoShapeTransformCommand.cpp commands/KoPathFillRuleCommand.cpp commands/KoConnectionShapeTypeCommand.cpp commands/KoShapeShadowCommand.cpp commands/KoPathReverseCommand.cpp commands/KoShapeRenameCommand.cpp commands/KoShapeRunAroundCommand.cpp commands/KoPathPointMergeCommand.cpp commands/KoShapeTransparencyCommand.cpp commands/KoShapeClipCommand.cpp commands/KoShapeUnclipCommand.cpp commands/KoPathShapeMarkerCommand.cpp commands/KoShapeConnectionChangeCommand.cpp commands/KoMultiPathPointMergeCommand.cpp commands/KoMultiPathPointJoinCommand.cpp commands/KoKeepShapesSelectedCommand.cpp commands/KoPathMergeUtils.cpp commands/KoAddRemoveShapeCommands.cpp html/HtmlSavingContext.cpp html/HtmlWriter.cpp tools/KoPathToolFactory.cpp tools/KoPathTool.cpp tools/KoPathToolSelection.cpp tools/KoPathToolHandle.cpp tools/PathToolOptionWidget.cpp tools/KoPathPointRubberSelectStrategy.cpp tools/KoPathPointMoveStrategy.cpp tools/KoPathConnectionPointStrategy.cpp tools/KoPathControlPointMoveStrategy.cpp tools/KoParameterChangeStrategy.cpp tools/KoZoomTool.cpp tools/KoZoomToolFactory.cpp tools/KoZoomToolWidget.cpp tools/KoZoomStrategy.cpp tools/KoInteractionTool.cpp tools/KoInteractionStrategy.cpp tools/KoInteractionStrategyFactory.cpp tools/KoShapeRubberSelectStrategy.cpp tools/KoPathSegmentChangeStrategy.cpp svg/KoShapePainter.cpp svg/SvgUtil.cpp svg/SvgGraphicContext.cpp svg/SvgSavingContext.cpp svg/SvgWriter.cpp svg/SvgStyleWriter.cpp svg/SvgShape.cpp svg/SvgParser.cpp svg/SvgStyleParser.cpp svg/SvgGradientHelper.cpp svg/SvgFilterHelper.cpp svg/SvgCssHelper.cpp svg/SvgClipPathHelper.cpp svg/SvgLoadingContext.cpp svg/SvgShapeFactory.cpp svg/parsers/SvgTransformParser.cpp + svg/SvgMeshGradient.cpp + svg/SvgMeshPatch.cpp + svg/SvgMeshStop.cpp + svg/SvgMeshArray.cpp text/KoSvgText.cpp text/KoSvgTextProperties.cpp text/KoSvgTextChunkShape.cpp text/KoSvgTextShape.cpp text/KoSvgTextShapeMarkupConverter.cpp resources/KoSvgSymbolCollectionResource.cpp resources/KoGamutMask.cpp FlakeDebug.cpp tests/MockShapes.cpp ) ki18n_wrap_ui(kritaflake_SRCS tools/PathToolOptionWidgetBase.ui KoConnectionShapeConfigWidget.ui tools/KoZoomToolWidget.ui ) add_library(kritaflake SHARED ${kritaflake_SRCS}) generate_export_header(kritaflake BASE_NAME kritaflake) target_include_directories(kritaflake PUBLIC $ $ $ $ ) target_link_libraries(kritaflake kritapigment kritawidgetutils kritaodf kritacommand KF5::WidgetsAddons Qt5::Svg) set_target_properties(kritaflake PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaflake ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/flake/KoMeshGradientBackground.cpp b/libs/flake/KoMeshGradientBackground.cpp new file mode 100644 index 0000000000..d8f096d329 --- /dev/null +++ b/libs/flake/KoMeshGradientBackground.cpp @@ -0,0 +1,50 @@ +#include "KoMeshGradientBackground.h" +#include +#include + +class KoMeshGradientBackground::Private : public QSharedData +{ +public: + Private() + : QSharedData() + , gradient(0) + {} + + SvgMeshGradient *gradient; + QTransform matrix; +}; + +KoMeshGradientBackground::~KoMeshGradientBackground() +{ + delete d->gradient; +} + +KoMeshGradientBackground::KoMeshGradientBackground(SvgMeshGradient *gradient, const QTransform &matrix) + : KoShapeBackground() + , d(new Private) +{ + d->gradient = gradient; + d->matrix = matrix; + Q_ASSERT(d->gradient); +} + +void KoMeshGradientBackground::paint(QPainter &painter, + KoShapePaintingContext &context, + const QPainterPath &fillPath) const +{ +} + +bool KoMeshGradientBackground::compareTo(const KoShapeBackground *other) const +{ + return false; +} + +void KoMeshGradientBackground::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) +{ + +} + +bool KoMeshGradientBackground::loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) +{ + return false; +} diff --git a/libs/flake/KoMeshGradientBackground.h b/libs/flake/KoMeshGradientBackground.h new file mode 100644 index 0000000000..711269f400 --- /dev/null +++ b/libs/flake/KoMeshGradientBackground.h @@ -0,0 +1,28 @@ +#ifndef KOMESHGRADIENTBACKGROUND_H +#define KOMESHGRADIENTBACKGROUND_H + +#include "KoShapeBackground.h" +#include +#include "SvgMeshGradient.h" + +class KRITAFLAKE_EXPORT KoMeshGradientBackground : public KoShapeBackground +{ +public: + KoMeshGradientBackground(SvgMeshGradient *gradient, const QTransform &matrix = QTransform()); + ~KoMeshGradientBackground(); + + void paint(QPainter &painter, KoShapePaintingContext &context, const QPainterPath &fillPath) const override; + + bool compareTo(const KoShapeBackground *other) const override; + + void fillStyle(KoGenStyle &style, KoShapeSavingContext &context) override; + + bool loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) override; + +private: + class Private; + QSharedDataPointer d; +}; + + +#endif // KOMESHGRADIENTBACKGROUND_H diff --git a/libs/flake/svg/SvgGradientHelper.cpp b/libs/flake/svg/SvgGradientHelper.cpp index c188324d40..5abbeec92b 100644 --- a/libs/flake/svg/SvgGradientHelper.cpp +++ b/libs/flake/svg/SvgGradientHelper.cpp @@ -1,98 +1,119 @@ /* This file is part of the KDE project * Copyright (C) 2007,2009 Jan Hambrecht * Copyright (C) 2010 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgGradientHelper.h" #include #include #include #include #include SvgGradientHelper::SvgGradientHelper() - : m_gradient(new QGradient()), m_gradientUnits(KoFlake::ObjectBoundingBox) + : m_gradient(new QGradient()) + , m_meshgradient(new SvgMeshGradient) + , m_gradientUnits(KoFlake::ObjectBoundingBox) { } SvgGradientHelper::~SvgGradientHelper() { delete m_gradient; + delete m_meshgradient; } SvgGradientHelper::SvgGradientHelper(const SvgGradientHelper &other) : m_gradient(KoFlake::cloneGradient(other.m_gradient)) + , m_meshgradient(new SvgMeshGradient(*other.m_meshgradient)) , m_gradientUnits(other.m_gradientUnits) , m_gradientTransform(other.m_gradientTransform) { } SvgGradientHelper & SvgGradientHelper::operator = (const SvgGradientHelper & rhs) { if (this == &rhs) return *this; m_gradientUnits = rhs.m_gradientUnits; m_gradientTransform = rhs.m_gradientTransform; m_gradient = KoFlake::cloneGradient(rhs.m_gradient); + m_meshgradient = new SvgMeshGradient(*rhs.m_meshgradient); return *this; } void SvgGradientHelper::setGradientUnits(KoFlake::CoordinateSystem units) { m_gradientUnits = units; } KoFlake::CoordinateSystem SvgGradientHelper::gradientUnits() const { return m_gradientUnits; } QGradient * SvgGradientHelper::gradient() const { return m_gradient; } void SvgGradientHelper::setGradient(QGradient * g) { delete m_gradient; m_gradient = g; } +void SvgGradientHelper::setMeshGradient(SvgMeshGradient *g) +{ + delete m_meshgradient; + m_meshgradient = g; +} + +SvgMeshGradient* SvgGradientHelper::meshgradient() const +{ + return m_meshgradient; +} + +bool SvgGradientHelper::isMeshGradient() const +{ + return m_meshgradient->isValid(); +} + QTransform SvgGradientHelper::transform() const { return m_gradientTransform; } void SvgGradientHelper::setTransform(const QTransform &transform) { m_gradientTransform = transform; } QGradient::Spread SvgGradientHelper::spreadMode() const { return m_gradient->spread(); } void SvgGradientHelper::setSpreadMode(const QGradient::Spread &spreadMode) { m_gradient->setSpread(spreadMode); } diff --git a/libs/flake/svg/SvgGradientHelper.h b/libs/flake/svg/SvgGradientHelper.h index 00a694f878..990708d6cd 100644 --- a/libs/flake/svg/SvgGradientHelper.h +++ b/libs/flake/svg/SvgGradientHelper.h @@ -1,69 +1,79 @@ /* This file is part of the KDE project * Copyright (C) 2007,2009 Jan Hambrecht * Copyright (C) 2010 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef SVGGRADIENTHELPER_H #define SVGGRADIENTHELPER_H #include #include #include +#include class SvgGradientHelper { public: SvgGradientHelper(); ~SvgGradientHelper(); /// Copy constructor SvgGradientHelper(const SvgGradientHelper &other); /// Sets the gradient units type void setGradientUnits(KoFlake::CoordinateSystem units); /// Returns gradient units type KoFlake::CoordinateSystem gradientUnits() const; /// Sets the gradient void setGradient(QGradient * g); /// Retrurns the gradient QGradient * gradient() const; + /// Sets the meshgradient + void setMeshGradient(SvgMeshGradient* g); + /// Returns the meshgradient + SvgMeshGradient* meshgradient() const; + + // To distinguish between SvgMeshGradient and QGradient + bool isMeshGradient() const; + /// Returns the gradient transformation QTransform transform() const; /// Sets the gradient transformation void setTransform(const QTransform &transform); /// Assignment operator SvgGradientHelper & operator = (const SvgGradientHelper & rhs); QGradient * adjustedGradient(const QRectF &bound) const; /// Converts a gradient from LogicalMode to ObjectBoundingMode static QGradient *convertGradient(const QGradient * originalGradient, const QTransform &userToRelativeTransform, const QRectF &size); QGradient::Spread spreadMode() const; void setSpreadMode(const QGradient::Spread &spreadMode); private: QGradient * m_gradient; + SvgMeshGradient *m_meshgradient; KoFlake::CoordinateSystem m_gradientUnits; QTransform m_gradientTransform; }; #endif // SVGGRADIENTHELPER_H diff --git a/libs/flake/svg/SvgMeshArray.cpp b/libs/flake/svg/SvgMeshArray.cpp new file mode 100644 index 0000000000..527513ead1 --- /dev/null +++ b/libs/flake/svg/SvgMeshArray.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 "SvgMeshArray.h" + +#include +#include + +SvgMeshArray::SvgMeshArray() +{ +} + +SvgMeshArray::SvgMeshArray(SvgMeshArray& other) +{ + // FIXME The way SvgParser works with gradients is, it frequently destroys the objects holding reference to + // SvgMeshGradients, so rather than copying we move it. This certainly needs a design refactor. + m_array = std::move(other.m_array); + +} + +SvgMeshArray::~SvgMeshArray() +{ + for (auto& row: m_array) { + for (auto& patch: row) { + delete patch; + } + } +} + +void SvgMeshArray::newRow() +{ + m_array << QVector(); +} + +bool SvgMeshArray::addPatch(QList> stops, const QPointF initialPoint) +{ + // This is function is full of edge-case landmines, please run TestMeshArray after any changes + if (stops.size() > 4 || stops.size() < 2) + return false; + + SvgMeshPatch *patch = new SvgMeshPatch(initialPoint); + + if (m_array.empty()) { + patch->addStop(stops[0].first, stops[0].second, SvgMeshPatch::Top); + stops.removeFirst(); + m_array.append(QVector() << patch); + } else { + m_array.last().append(patch); + } + + int irow = m_array.size() - 1; + int icol = m_array.last().size() - 1; + + // first stop, except for the very first in the array + if (irow != 0 || icol != 0) { + // For first row, parse patches + if (irow == 0) { + patch->addStop(stops[0].first, stops[0].second, SvgMeshPatch::Top); + stops.removeFirst(); + } else { + // path is already defined for rows >= 1 + QColor color = getStop(SvgMeshPatch::Left, irow - 1, icol).color; + + QList points = getPath(SvgMeshPatch::Bottom, irow - 1, icol); + std::reverse(points.begin(), points.end()); + + patch->addStop(points, color, SvgMeshPatch::Top); + } + } + + // Right will always be independent + patch->addStop(stops[0].first, stops[0].second, SvgMeshPatch::Right); + stops.removeFirst(); + + if (icol > 0) { + patch->addStop( + stops[0].first, + stops[0].second, + SvgMeshPatch::Bottom, + true, getStop(SvgMeshPatch::Bottom, irow, icol - 1).point); + stops.removeFirst(); + } else { + patch->addStop(stops[0].first, stops[0].second, SvgMeshPatch::Bottom); + stops.removeFirst(); + } + + // last stop + if (icol == 0) { + // if stop is in the 0th column, parse path + patch->addStop( + stops[0].first, + stops[0].second, + SvgMeshPatch::Left, + true, getStop(SvgMeshPatch::Top, irow, icol).point); + stops.removeFirst(); + } else { + QColor color = getStop(SvgMeshPatch::Bottom, irow, icol - 1).color; + + // reuse Right side of the previous patch + QList points = getPath(SvgMeshPatch::Right, irow, icol - 1); + std::reverse(points.begin(), points.end()); + + patch->addStop(points, color, SvgMeshPatch::Left); + } + return true; +} + +SvgMeshStop SvgMeshArray::getStop(const SvgMeshPatch::Type edge, const int row, const int col) const +{ + assert(row < m_array.size() && col < m_array[row].size() + && row >= 0 && col >= 0); + + SvgMeshPatch *patch = m_array[row][col]; + SvgMeshStop *node = patch->getStop(edge); + + if (node != nullptr) { + return *node; + } + + switch (patch->countPoints()) { + case 3: + case 2: + if (edge == SvgMeshPatch::Top) + return getStop(SvgMeshPatch::Left, row - 1, col); + else if (edge == SvgMeshPatch::Left) + return getStop(SvgMeshPatch::Bottom, row, col - 1); + } + assert(false); +} + +QList SvgMeshArray::getPath(const SvgMeshPatch::Type edge, const int row, const int col) const +{ + assert(row < m_array.size() && col < m_array[row].size() + && row >= 0 && col >= 0); + + return m_array[row][col]->getPath(edge).controlPoints(); +} + +int SvgMeshArray::numRows() const +{ + return m_array.size(); +} + +int SvgMeshArray::numColumns() const +{ + if (m_array.isEmpty()) + return 0; + return m_array.first().size(); +} diff --git a/libs/flake/svg/SvgMeshArray.h b/libs/flake/svg/SvgMeshArray.h new file mode 100644 index 0000000000..8e495bb956 --- /dev/null +++ b/libs/flake/svg/SvgMeshArray.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 SVGMESHARRAY_H +#define SVGMESHARRAY_H + +#include + +#include "SvgMeshPatch.h" + +class KRITAFLAKE_EXPORT SvgMeshArray +{ +public: + SvgMeshArray(); + + // MOVES the elements of m_array from other to this + SvgMeshArray(SvgMeshArray& other); + + ~SvgMeshArray(); + + void newRow(); + + bool addPatch(QList> stops, const QPointF initialPoint); + + /// Get the point of a node in mesharray + SvgMeshStop getStop(const SvgMeshPatch::Type edge, const int row, const int col) const; + + /// Get the Path Points for a segment of the meshpatch + QList getPath(const SvgMeshPatch::Type edge, const int row, const int col) const; + + + int numRows() const; + int numColumns() const; + + +private: + /// where each vector is a meshrow + QVector> m_array; +}; + +#endif // SVGMESHARRAY_H diff --git a/libs/flake/svg/SvgMeshGradient.cpp b/libs/flake/svg/SvgMeshGradient.cpp new file mode 100644 index 0000000000..6337785ce3 --- /dev/null +++ b/libs/flake/svg/SvgMeshGradient.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 "SvgMeshGradient.h" + +SvgMeshGradient::SvgMeshGradient() + : m_mesharray(new SvgMeshArray()) +{ +} + +SvgMeshGradient::SvgMeshGradient(const SvgMeshGradient& other) + : m_type(other.m_type) + , m_mesharray(new SvgMeshArray(*other.m_mesharray)) +{ +} + +SvgMeshGradient::~SvgMeshGradient() +{ + delete m_mesharray; +} + +void SvgMeshGradient::setType(SvgMeshGradient::Type type) +{ + m_type = type; +} + +SvgMeshGradient::Type SvgMeshGradient::type() const +{ + return m_type; +} + +bool SvgMeshGradient::isValid() const +{ + return m_mesharray->numRows() != 0; +} + +SvgMeshArray* SvgMeshGradient::getMeshArray() const +{ + return m_mesharray; +} diff --git a/libs/flake/svg/SvgMeshGradient.h b/libs/flake/svg/SvgMeshGradient.h new file mode 100644 index 0000000000..1b362ea53d --- /dev/null +++ b/libs/flake/svg/SvgMeshGradient.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 KISMESHGRADIENT_H +#define KISMESHGRADIENT_H + +#include + +#include "SvgMeshPatch.h" +#include "SvgMeshArray.h" + +class SvgMeshGradient +{ +public: + enum Type { + BILINEAR, + BICUBIC, + }; + + SvgMeshGradient(); + SvgMeshGradient(const SvgMeshGradient& other); + ~SvgMeshGradient(); + + void setType(Type type); + SvgMeshGradient::Type type() const; + + bool isValid() const; + + SvgMeshArray* getMeshArray() const; + +private: + Type m_type; + SvgMeshArray* m_mesharray; +}; + +#endif // KISMESHGRADIENT_H diff --git a/libs/flake/svg/SvgMeshPatch.cpp b/libs/flake/svg/SvgMeshPatch.cpp new file mode 100644 index 0000000000..052cc9bfe2 --- /dev/null +++ b/libs/flake/svg/SvgMeshPatch.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2007 Jan Hambrecht + * Copyright (c) 2020 Sharaf Zaman + * + * 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 "SvgMeshPatch.h" + +#include +#include +#include +#include + + +SvgMeshPatch::SvgMeshPatch(QPointF startingPoint) + : m_newPath(true) + , m_startingPoint(startingPoint) + , m_path(new KoPathShape) +{ +} + +SvgMeshPatch::SvgMeshPatch(const SvgMeshPatch& other) +{ +} + +SvgMeshPatch::~SvgMeshPatch() +{ + for (auto &node: m_nodes.values()) { + delete node; + } +} + +SvgMeshStop* SvgMeshPatch::getStop(SvgMeshPatch::Type type) const +{ + if (m_nodes.find(type) == m_nodes.end()) + return nullptr; + + return *m_nodes.find(type); +} + +KoPathSegment SvgMeshPatch::getPath(Type type) const +{ + KoPathPointIndex index(0, type - 1); + return m_path->segmentByIndex(index); +} + +void SvgMeshPatch::addStop(const QString& pathStr, QColor color, Type edge, bool pathIncomplete, QPointF lastPoint) +{ + SvgMeshStop *node = new SvgMeshStop(color, m_startingPoint); + m_nodes.insert(edge, node); + + m_startingPoint = parseMeshPath(pathStr, pathIncomplete, lastPoint); +} + +void SvgMeshPatch::addStop(const QList& pathPoints, QColor color, Type edge) +{ + SvgMeshStop *stop = new SvgMeshStop(color, pathPoints.first()); + m_nodes.insert(edge, stop); + + if (edge == SvgMeshPatch::Top) { + m_path->moveTo(pathPoints.first()); + m_newPath = false; + } + + // if path is a line + if (pathPoints.size() == 2) { + m_path->lineTo(pathPoints.last()); + } else if (pathPoints.size() == 4) { + // if path is a Bezier curve + m_path->curveTo(pathPoints[1], pathPoints[2], pathPoints[3]); + } + + m_startingPoint = pathPoints.last(); +} + +int SvgMeshPatch::countPoints() const +{ + return m_nodes.size(); +} + +QPointF SvgMeshPatch::parseMeshPath(const QString& s, bool pathIncomplete, const QPointF lastPoint) +{ + // bits and pieces from KoPathShapeLoader, see the copyright above + if (!s.isEmpty()) { + QString d = s; + d.replace(',', ' '); + d = d.simplified(); + + const QByteArray buffer = d.toLatin1(); + const char *ptr = buffer.constData(); + qreal curx = m_startingPoint.x(); + qreal cury = m_startingPoint.y(); + qreal tox, toy, x1, y1, x2, y2; + bool relative = false; + char command = *(ptr++); + + if (m_newPath) { + m_path->moveTo(m_startingPoint); + m_newPath = false; + } + + while (*ptr == ' ') + ++ptr; + + switch (command) { + case 'l': + relative = true; + Q_FALLTHROUGH(); + case 'L': { + ptr = getCoord(ptr, tox); + ptr = getCoord(ptr, toy); + + if (relative) { + tox = curx + tox; + toy = cury + toy; + } + + if (pathIncomplete) { + tox = lastPoint.x(); + toy = lastPoint.y(); + } + + m_path->lineTo(QPointF(tox, toy)); + break; + } + case 'c': + relative = true; + Q_FALLTHROUGH(); + case 'C': { + ptr = getCoord(ptr, x1); + ptr = getCoord(ptr, y1); + ptr = getCoord(ptr, x2); + ptr = getCoord(ptr, y2); + ptr = getCoord(ptr, tox); + ptr = getCoord(ptr, toy); + + if (relative) { + x1 = curx + x1; + y1 = cury + y1; + x2 = curx + x2; + y2 = cury + y2; + tox = curx + tox; + toy = cury + toy; + } + + if (pathIncomplete) { + tox = lastPoint.x(); + toy = lastPoint.y(); + } + + m_path->curveTo(QPointF(x1, y1), QPointF(x2, y2), QPointF(tox, toy)); + break; + } + + default: { + qWarning() << "SvgMeshPatch::parseMeshPath: Bad command \"" << command << "\""; + return QPointF(); + } + } + return {tox, toy}; + } + return QPointF(); +} + +const char* SvgMeshPatch::getCoord(const char* ptr, qreal& number) +{ + // copied from KoPathShapeLoader, see the copyright above + int integer, exponent; + qreal decimal, frac; + int sign, expsign; + + exponent = 0; + integer = 0; + frac = 1.0; + decimal = 0; + sign = 1; + expsign = 1; + + // read the sign + if (*ptr == '+') + ++ptr; + else if (*ptr == '-') { + ++ptr; + sign = -1; + } + + // read the integer part + while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9') + integer = (integer * 10) + *(ptr++) - '0'; + if (*ptr == '.') { // read the decimals + ++ptr; + while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9') + decimal += (*(ptr++) - '0') * (frac *= 0.1); + } + + if (*ptr == 'e' || *ptr == 'E') { // read the exponent part + ++ptr; + + // read the sign of the exponent + if (*ptr == '+') + ++ptr; + else if (*ptr == '-') { + ++ptr; + expsign = -1; + } + + exponent = 0; + while (*ptr != '\0' && *ptr >= '0' && *ptr <= '9') { + exponent *= 10; + exponent += *ptr - '0'; + ++ptr; + } + } + number = integer + decimal; + number *= sign * pow((qreal)10, qreal(expsign * exponent)); + + // skip the following space + if (*ptr == ' ') + ++ptr; + + return ptr; +} diff --git a/libs/flake/svg/SvgMeshPatch.h b/libs/flake/svg/SvgMeshPatch.h new file mode 100644 index 0000000000..a9f7f6ec02 --- /dev/null +++ b/libs/flake/svg/SvgMeshPatch.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 SVGMESHPATCH_H +#define SVGMESHPATCH_H + +#include +#include +#include +#include +#include + +#include + +struct SvgMeshStop { + QColor color; + QPointF point; + + SvgMeshStop() + {} + + SvgMeshStop(QColor color, QPointF point) + : color(color), point(point) + {} +}; + + +class SvgMeshPatch +{ +public: + /// Position of stop in the patch + enum Type { + Top = 1, + Right, + Bottom, + Left, + Size, + }; + + SvgMeshPatch(QPointF startingPoint); + SvgMeshPatch(const SvgMeshPatch& other); + ~SvgMeshPatch(); + + SvgMeshStop* getStop(Type type) const; + + KoPathSegment getPath(Type type) const; + + int countPoints() const; + + /* Parses raw pathstr and adds path to the shape, if the path isn't + * complete, it will have to be computed and given with pathIncomplete = true + * (Ideal case for std::optional) + */ + void addStop(const QString& pathStr, QColor color, Type edge, bool pathIncomplete = false, QPointF lastPoint = QPointF()); + + /// Adds path to the shape + void addStop(const QList& pathPoints, QColor color, Type edge); + +private: + /* Parses path and adds it to m_path and returns the last point of the curve/line + * see also: SvgMeshPatch::addStop + */ + QPointF parseMeshPath(const QString& path, bool pathIncomplete = false, const QPointF lastPoint = QPointF()); + const char* getCoord(const char* ptr, qreal& number); + +private: + bool m_newPath; + + /// This is the starting point for each path + QPointF m_startingPoint; + + QMap m_nodes; + QScopedPointer m_path; +}; + +#endif // SVGMESHPATCH_H diff --git a/libs/flake/svg/SvgMeshStop.cpp b/libs/flake/svg/SvgMeshStop.cpp new file mode 100644 index 0000000000..7c72bf4940 --- /dev/null +++ b/libs/flake/svg/SvgMeshStop.cpp @@ -0,0 +1,6 @@ +#include "SvgMeshStop.h" + +SvgMeshStop::SvgMeshStop() +{ + +} diff --git a/libs/flake/svg/SvgMeshStop.h b/libs/flake/svg/SvgMeshStop.h new file mode 100644 index 0000000000..cf75072c3e --- /dev/null +++ b/libs/flake/svg/SvgMeshStop.h @@ -0,0 +1,11 @@ +#ifndef SVGMESHSTOP_H +#define SVGMESHSTOP_H + + +class SvgMeshStop +{ +public: + SvgMeshStop(); +}; + +#endif // SVGMESHSTOP_H diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp index 04f96dcdfe..e1054c0212 100644 --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -1,1953 +1,2084 @@ /* 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 #include #include "KoFilterEffectStack.h" #include "KoFilterEffectLoadingContext.h" #include #include #include #include +#include "SvgMeshGradient.h" +#include "SvgMeshPatch.h" #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; // debugFlake << "Checking id: " << id; auto i = std::partition(m_uses.begin(), m_uses.end(), [&](const El& e) -> bool {return e.m_key != id;}); while (i != m_uses.end()) { const El& el = m_uses.back(); if (m_parse->m_context.hasDefinition(el.m_key)) { // debugFlake << "Found pending use for id: " << el.m_key; shape = m_parse->resolveUse(*(el.m_useElement), el.m_key); if (shape) { shapes.append(shape); } } m_uses.pop_back(); } } ~DeferredUseStore() { while (!m_uses.empty()) { const El& el = m_uses.back(); debugFlake << "WARNING: could not find path in m_uses; }; SvgParser::SvgParser(KoDocumentResourceManager *documentResourceManager) : m_context(documentResourceManager) , m_documentResourceManager(documentResourceManager) { } SvgParser::~SvgParser() { } 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); reader.setFeature("http://xml.org/sax/features/namespaces", false); reader.setFeature("http://xml.org/sax/features/namespace-prefixes", 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.values().toVector(); 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)); + } else if (e.tagName() == "meshgradient") { + result = parseMeshGradient(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 { debugFlake << "WARNING: Failed to parse gradient with tag" << e.tagName(); } // handle spread method QGradient::Spread spreadMethod = QGradient::PadSpread; QString spreadMethodStr = e.attribute("spreadMethod"); if (!spreadMethodStr.isEmpty()) { if (spreadMethodStr == "reflect") { spreadMethod = QGradient::ReflectSpread; } else if (spreadMethodStr == "repeat") { spreadMethod = QGradient::RepeatSpread; } } gradHelper.setSpreadMode(spreadMethod); // Parse the color stops. m_context.styleParser().parseColorStops(gradHelper.gradient(), e, gc, defaultStops); if (e.hasAttribute("gradientTransform")) { SvgTransformParser p(e.attribute("gradientTransform")); if (p.isValid()) { gradHelper.setTransform(p.transform()); } } m_context.popGraphicsContext(); m_gradients.insert(gradientId, gradHelper); return &m_gradients[gradientId]; } +SvgGradientHelper* SvgParser::parseMeshGradient(const KoXmlElement &e) +{ + SvgGradientHelper gradHelper; + QString gradientId = e.attribute("id"); + SvgMeshGradient *g = new SvgMeshGradient; + + // 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; + } + } + } + + if (e.attribute("gradientUnits") == "userSpaceOnUse") { + gradHelper.setGradientUnits(KoFlake::UserSpaceOnUse); + } + + if (e.hasAttribute("transform")) { + SvgTransformParser p(e.attribute("transform")); + if (p.isValid()) { + gradHelper.setTransform(p.transform()); + } + } + + QString type = e.attribute("type"); + g->setType(SvgMeshGradient::BILINEAR); + if (!type.isEmpty() && type == "bicubic") { + g->setType(SvgMeshGradient::BICUBIC); + } + + int irow = 0, icols; + for (int i = 0; i < e.childNodes().size(); ++i) { + KoXmlNode node = e.childNodes().at(i); + + if (node.nodeName() == "meshrow") { + + SvgMeshStop startingNode; + if (irow == 0) { + startingNode.point = QPointF( + parseUnitX(e.attribute("x")), + parseUnitY(e.attribute(("y")))); + startingNode.color = QColor(); + } + + icols = 0; + for (int j = 0; j < node.childNodes().size() ; ++j) { + KoXmlNode meshpatchNode = node.childNodes().at(j); + + if (meshpatchNode.nodeName() == "meshpatch") { + if (irow > 0) { + // Starting point for this would be the bottom (right) corner of the above patch + startingNode = g->getMeshArray()->getStop(SvgMeshPatch::Bottom, irow - 1, icols); + } else if (icols != 0) { + // Starting point for this would be the right (top) corner of the previous patch + startingNode = g->getMeshArray()->getStop(SvgMeshPatch::Right, irow, icols - 1); + } + + QList> rawStops = parseMeshPatch(meshpatchNode); + // TODO handle the false result + g->getMeshArray()->addPatch(rawStops, startingNode.point); + icols++; + } + } + g->getMeshArray()->newRow(); + irow++; + } + } + gradHelper.setMeshGradient(g); + m_gradients.insert(gradientId, gradHelper); + + return &m_gradients[gradientId]; +} + +QList> SvgParser::parseMeshPatch(const KoXmlNode& meshpatchNode) +{ + // path and its associated color + QList> rawstops; + + SvgGraphicsContext *gc = m_context.currentGC(); + if (!gc) return rawstops; + + KoXmlElement e = meshpatchNode.toElement(); + + KoXmlElement stop; + forEachElement(stop, e) { + QColor color; + + // keep the default (Invalid Color), incase none is provided + if (stop.attribute("stop-color").isNull()) { + color = QColor(); + } else { + qreal X; // don't care.. + color = m_context.styleParser().parseColorStop(stop, gc, X).second; + } + + QString pathStr = stop.attribute("path"); + + rawstops.append({pathStr, color}); + } + + return rawstops; +} + 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(); 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.insert(id, 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); + + if (gradient->isMeshGradient()) { + QSharedPointer bg; + // NOTE: this will MOVE SvgMeshPatch elements in mesharray + SvgMeshGradient *result = new SvgMeshGradient(*gradient->meshgradient()); + + // TODO handle transform + bg = toQShared(new KoMeshGradientBackground(result, transform)); shape->setBackground(bg); + } else { + 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; } debugFlake << "WARNING: Did not find reference for svg 'use' element. Skipping. Id: " << key; return 0; } KoShape* SvgParser::resolveUse(const KoXmlElement &e, const QString& key) { KoShape *result = 0; SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); // TODO: parse 'width' and 'height' as well gc->matrix.translate(parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0"))); const KoXmlElement &referencedElement = m_context.definition(key); result = parseGroup(e, referencedElement, false); 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"); qreal width = w.isEmpty() ? 666.0 : parseUnitX(w); qreal height = h.isEmpty() ? 555.0 : parseUnitY(h); if (w.isEmpty() || h.isEmpty()) { QRectF viewRect; QTransform viewTransform_unused; QRectF fakeBoundingRect(0.0, 0.0, 1.0, 1.0); if (SvgUtil::parseViewBox(e, fakeBoundingRect, &viewRect, &viewTransform_unused)) { QSizeF estimatedSize = viewRect.size(); if (estimatedSize.isValid()) { if (!w.isEmpty()) { estimatedSize = QSizeF(width, width * estimatedSize.height() / estimatedSize.width()); } else if (!h.isEmpty()) { estimatedSize = QSizeF(height * estimatedSize.width() / estimatedSize.height(), height); } width = estimatedSize.width(); height = estimatedSize.height(); } } } 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(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().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, bool createContext) { if (createContext) { 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 if (createContext) { 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 if (mergeIntoShape) { rootTextShape = mergeIntoShape; } else { rootTextShape = new KoSvgTextShape(); const QString useRichText = e.attribute("krita:useRichText", "true"); rootTextShape->setRichTextPreferred(useRichText != "false"); } } if (rootTextShape) { m_isInsideTextSubtree = true; } m_context.pushGraphicsContext(e); uploadStyleToContext(e); KoSvgTextChunkShape *textChunk = rootTextShape ? rootTextShape : new KoSvgTextChunkShape(); if (!mergeIntoShape) { 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(); 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" || b.tagName() == "symbol") { // treat svg link as group so we don't miss its child elements shapes += parseGroup(b); if (b.tagName() == "symbol") { parseSymbol(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() == "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) { if (!shape->outlineRect().isNull() || !shape->boundingRect().isNull()) { shapes.append(shape); } else { debugFlake << "WARNING: shape is totally empty!" << shape->shapeId() << ppVar(shape->outlineRect()); debugFlake << " " << shape->shapeId() << ppVar(shape->outline()); { QString string; QTextStream stream(&string); stream << b; debugFlake << " " << string; } } } } 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 48d59315d2..8b3354bc84 100644 --- a/libs/flake/svg/SvgParser.h +++ b/libs/flake/svg/SvgParser.h @@ -1,227 +1,233 @@ /* 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 { struct 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(), bool createContext = true); // 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 mesh gradient element + SvgGradientHelper* parseMeshGradient(const KoXmlElement&); + + /// Parses a single meshpatch and returns the pointer + QList> parseMeshPatch(const KoXmlNode& meshpatch); + /// 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; QMap m_symbols; QList m_toplevelShapes; QList m_defsShapes; bool m_isInsideTextSubtree = false; QString m_documentTitle; QString m_documentDescription; }; #endif diff --git a/libs/flake/svg/SvgStyleParser.cpp b/libs/flake/svg/SvgStyleParser.cpp index 93e6c57a2e..6f1a7c99a7 100644 --- a/libs/flake/svg/SvgStyleParser.cpp +++ b/libs/flake/svg/SvgStyleParser.cpp @@ -1,551 +1,557 @@ /* This file is part of the KDE project * Copyright (C) 2002-2005,2007 Rob Buis * Copyright (C) 2002-2004 Nicolas Goutte * Copyright (C) 2005-2006 Tim Beaulen * Copyright (C) 2005-2009 Jan Hambrecht * Copyright (C) 2005,2007 Thomas Zander * Copyright (C) 2006-2007 Inge Wallin * Copyright (C) 2007-2008,2010 Thorsten Zachmann * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SvgStyleParser.h" #include "SvgLoadingContext.h" #include "SvgGraphicContext.h" #include "SvgUtil.h" #include "kis_dom_utils.h" #include #include #include #include #include class Q_DECL_HIDDEN SvgStyleParser::Private { public: Private(SvgLoadingContext &loadingContext) : context(loadingContext) { textAttributes << KoSvgTextProperties::supportedXmlAttributes(); // the order of the font attributes is important, don't change without reason !!! fontAttributes << "font-family" << "font-size" << "font-weight" << "font-style" << "font-variant" << "font-stretch" << "font-size-adjust" << "font" << "text-decoration" << "letter-spacing" << "word-spacing" << "baseline-shift"; // the order of the style attributes is important, don't change without reason !!! styleAttributes << "color" << "display" << "visibility"; styleAttributes << "fill" << "fill-rule" << "fill-opacity"; styleAttributes << "stroke" << "stroke-width" << "stroke-linejoin" << "stroke-linecap"; styleAttributes << "stroke-dasharray" << "stroke-dashoffset" << "stroke-opacity" << "stroke-miterlimit"; styleAttributes << "opacity" << "filter" << "clip-path" << "clip-rule" << "mask"; styleAttributes << "marker" << "marker-start" << "marker-mid" << "marker-end" << "krita:marker-fill-method"; } SvgLoadingContext &context; QStringList textAttributes; ///< text related attributes QStringList fontAttributes; ///< font related attributes QStringList styleAttributes; ///< style related attributes }; SvgStyleParser::SvgStyleParser(SvgLoadingContext &context) : d(new Private(context)) { } SvgStyleParser::~SvgStyleParser() { delete d; } void SvgStyleParser::parseStyle(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure we parse the style attributes in the right order Q_FOREACH (const QString & command, d->styleAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } void SvgStyleParser::parseFont(const SvgStyles &styles) { SvgGraphicsContext *gc = d->context.currentGC(); if (!gc) return; // make sure to only parse font attributes here Q_FOREACH (const QString & command, d->fontAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } Q_FOREACH (const QString & command, d->textAttributes) { const QString ¶ms = styles.value(command); if (params.isEmpty()) continue; parsePA(gc, command, params); } } #include void SvgStyleParser::parsePA(SvgGraphicsContext *gc, const QString &command, const QString ¶ms) { QColor fillcolor = gc->fillColor; QColor strokecolor = gc->stroke->color(); if (params == "inherit") return; if (command == "fill") { if (params == "none") { gc->fillType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->fillId = params.mid(start, end - start); gc->fillType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(fillcolor, params.mid(end + 1).trimmed()); } else { // great we have a solid fill gc->fillType = SvgGraphicsContext::Solid; parseColor(fillcolor, params); } } else if (command == "fill-rule") { if (params == "nonzero") gc->fillRule = Qt::WindingFill; else if (params == "evenodd") gc->fillRule = Qt::OddEvenFill; } else if (command == "stroke") { if (params == "none") { gc->strokeType = SvgGraphicsContext::None; } else if (params.startsWith(QLatin1String("url("))) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->strokeId = params.mid(start, end - start); gc->strokeType = SvgGraphicsContext::Complex; // check if there is a fallback color parseColor(strokecolor, params.mid(end + 1).trimmed()); } else { // great we have a solid stroke gc->strokeType = SvgGraphicsContext::Solid; parseColor(strokecolor, params); } } else if (command == "stroke-width") { gc->stroke->setLineWidth(SvgUtil::parseUnitXY(gc, params)); } else if (command == "stroke-linejoin") { if (params == "miter") gc->stroke->setJoinStyle(Qt::MiterJoin); else if (params == "round") gc->stroke->setJoinStyle(Qt::RoundJoin); else if (params == "bevel") gc->stroke->setJoinStyle(Qt::BevelJoin); } else if (command == "stroke-linecap") { if (params == "butt") gc->stroke->setCapStyle(Qt::FlatCap); else if (params == "round") gc->stroke->setCapStyle(Qt::RoundCap); else if (params == "square") gc->stroke->setCapStyle(Qt::SquareCap); } else if (command == "stroke-miterlimit") { gc->stroke->setMiterLimit(params.toFloat()); } else if (command == "stroke-dasharray") { QVector array; if (params != "none") { QString dashString = params; QStringList dashes = dashString.replace(',', ' ').simplified().split(' '); for (QStringList::Iterator it = dashes.begin(); it != dashes.end(); ++it) { array.append(SvgUtil::parseUnitXY(gc, *it)); } // if the array is odd repeat it according to the standard if (array.size() & 1) { array << array; } } gc->stroke->setLineStyle(Qt::CustomDashLine, array); } else if (command == "stroke-dashoffset") { gc->stroke->setDashOffset(params.toFloat()); } // handle opacity else if (command == "stroke-opacity") strokecolor.setAlphaF(SvgUtil::fromPercentage(params)); else if (command == "fill-opacity") { float opacity = SvgUtil::fromPercentage(params); if (opacity < 0.0) opacity = 0.0; if (opacity > 1.0) opacity = 1.0; fillcolor.setAlphaF(opacity); } else if (command == "opacity") { gc->opacity = SvgUtil::fromPercentage(params); } else if (command == "font-family") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); QStringList familiesList = gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontFamiliesId).toStringList(); if (!familiesList.isEmpty()) { gc->font.setFamily(familiesList.first()); gc->fontFamiliesList = familiesList; } } else if (command == "font-size") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); /** * In ODF-based Krita vectors (<= 3.x) we used hardcoded font size values set to 96 ppi, * so, when loading old files, we should adjust it accordingly. */ gc->font.setPointSizeF(gc->forcedFontSizeCoeff * gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontSizeId).toReal()); } else if (command == "font-style") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); const QFont::Style style = QFont::Style(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontStyleId).toInt()); gc->font.setStyle(style); } else if (command == "font-variant") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); gc->font.setCapitalization( gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontIsSmallCapsId).toBool() ? QFont::SmallCaps : QFont::MixedCase); } else if (command == "font-stretch") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); gc->font.setStretch(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontStretchId).toInt()); } else if (command == "font-weight") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); //ENTER_FUNCTION() << ppVar(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontWeightId).toInt()); gc->font.setWeight(gc->textProperties.propertyOrDefault(KoSvgTextProperties::FontWeightId).toInt()); } else if (command == "font-size-adjust") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); warnFile << "WARNING: \'font-size-adjust\' SVG attribute is not supported!"; } else if (command == "font") { warnFile << "WARNING: \'font\' SVG attribute is not yet implemented! Please report a bug!"; } else if (command == "text-decoration") { gc->textProperties.parseSvgTextAttribute(d->context, command, params); using namespace KoSvgText; TextDecorations deco = gc->textProperties.propertyOrDefault(KoSvgTextProperties::TextDecorationId) .value(); gc->font.setStrikeOut(deco & DecorationLineThrough); gc->font.setUnderline(deco & DecorationUnderline); gc->font.setOverline(deco & DecorationOverline); } else if (command == "color") { QColor color; parseColor(color, params); gc->currentColor = color; } else if (command == "display") { if (params == "none") gc->display = false; } else if (command == "visibility") { // visible is inherited! gc->visible = params == "visible"; } else if (command == "filter") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->filterId = params.mid(start, end - start); } } else if (command == "clip-path") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipPathId = params.mid(start, end - start); } } else if (command == "clip-rule") { if (params == "nonzero") gc->clipRule = Qt::WindingFill; else if (params == "evenodd") gc->clipRule = Qt::OddEvenFill; } else if (command == "mask") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->clipMaskId = params.mid(start, end - start); } } else if (command == "marker-start") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); } } else if (command == "marker-end") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerEndId = params.mid(start, end - start); } } else if (command == "marker-mid") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerMidId = params.mid(start, end - start); } } else if (command == "marker") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; unsigned int end = params.indexOf(')', start); gc->markerStartId = params.mid(start, end - start); gc->markerMidId = gc->markerStartId; gc->markerEndId = gc->markerStartId; } } else if (command == "krita:marker-fill-method") { gc->autoFillMarkers = params == "auto"; } else if (d->textAttributes.contains(command)) { gc->textProperties.parseSvgTextAttribute(d->context, command, params); } gc->fillColor = fillcolor; gc->stroke->setColor(strokecolor); } bool SvgStyleParser::parseColor(QColor &color, const QString &s) { if (s.isEmpty() || s == "none") return false; if (s.startsWith(QLatin1String("rgb("))) { QString parse = s.trimmed(); QStringList colors = parse.split(','); QString r = colors[0].right((colors[0].length() - 4)); QString g = colors[1]; QString b = colors[2].left((colors[2].length() - 1)); if (r.contains('%')) { r = r.left(r.length() - 1); r = QString::number(int((double(255 * KisDomUtils::toDouble(r)) / 100.0))); } if (g.contains('%')) { g = g.left(g.length() - 1); g = QString::number(int((double(255 * KisDomUtils::toDouble(g)) / 100.0))); } if (b.contains('%')) { b = b.left(b.length() - 1); b = QString::number(int((double(255 * KisDomUtils::toDouble(b)) / 100.0))); } color = QColor(r.toInt(), g.toInt(), b.toInt()); } else if (s == "currentColor") { color = d->context.currentGC()->currentColor; } else { // QColor understands #RRGGBB and svg color names color.setNamedColor(s.trimmed()); } return true; } -void SvgStyleParser::parseColorStops(QGradient *gradient, - const KoXmlElement &e, - SvgGraphicsContext *context, - const QGradientStops &defaultStops) +QPair SvgStyleParser::parseColorStop(const KoXmlElement& stop, + SvgGraphicsContext *context, + qreal& previousOffset) { - QGradientStops stops; + qreal offset = 0.0; + QString offsetStr = stop.attribute("offset").trimmed(); + if (offsetStr.endsWith('%')) { + offsetStr = offsetStr.left(offsetStr.length() - 1); + offset = offsetStr.toFloat() / 100.0; + } else { + offset = offsetStr.toFloat(); + } - qreal previousOffset = 0.0; + // according to SVG the value must be within [0; 1] interval + offset = qBound(0.0, offset, 1.0); - KoXmlElement stop; - forEachElement(stop, e) { - if (stop.tagName() == "stop") { - qreal offset = 0.0; - QString offsetStr = stop.attribute("offset").trimmed(); - if (offsetStr.endsWith('%')) { - offsetStr = offsetStr.left(offsetStr.length() - 1); - offset = offsetStr.toFloat() / 100.0; - } else { - offset = offsetStr.toFloat(); - } + // according to SVG the stops' offset must be non-decreasing + offset = qMax(offset, previousOffset); + previousOffset = offset; - // according to SVG the value must be within [0; 1] interval - offset = qBound(0.0, offset, 1.0); + QColor color; - // according to SVG the stops' offset must be non-decreasing - offset = qMax(offset, previousOffset); - previousOffset = offset; + QString stopColorStr = stop.attribute("stop-color"); + QString stopOpacityStr = stop.attribute("stop-opacity"); - QColor color; + const QStringList attributes({"stop-color", "stop-opacity"}); + SvgStyles styles = parseOneCssStyle(stop.attribute("style"), attributes); - QString stopColorStr = stop.attribute("stop-color"); - QString stopOpacityStr = stop.attribute("stop-opacity"); + // SVG: CSS values have precedence over presentation attributes! + if (styles.contains("stop-color")) { + stopColorStr = styles.value("stop-color"); + } - const QStringList attributes({"stop-color", "stop-opacity"}); - SvgStyles styles = parseOneCssStyle(stop.attribute("style"), attributes); + if (styles.contains("stop-opacity")) { + stopOpacityStr = styles.value("stop-opacity"); + } - // SVG: CSS values have precedence over presentation attributes! - if (styles.contains("stop-color")) { - stopColorStr = styles.value("stop-color"); - } + if (stopColorStr.isEmpty() && stopColorStr == "inherit") { + color = context->currentColor; + } else { + parseColor(color, stopColorStr); + } - if (styles.contains("stop-opacity")) { - stopOpacityStr = styles.value("stop-opacity"); - } + if (!stopOpacityStr.isEmpty() && stopOpacityStr != "inherit") { + color.setAlphaF(qBound(0.0, KisDomUtils::toDouble(stopOpacityStr), 1.0)); + } + return QPair(offset, color); +} - if (stopColorStr.isEmpty() && stopColorStr == "inherit") { - color = context->currentColor; - } else { - parseColor(color, stopColorStr); - } +void SvgStyleParser::parseColorStops(QGradient *gradient, + const KoXmlElement &e, + SvgGraphicsContext *context, + const QGradientStops &defaultStops) +{ + QGradientStops stops; - if (!stopOpacityStr.isEmpty() && stopOpacityStr != "inherit") { - color.setAlphaF(qBound(0.0, KisDomUtils::toDouble(stopOpacityStr), 1.0)); - } + qreal previousOffset = 0.0; - stops.append(QPair(offset, color)); + KoXmlElement stop; + forEachElement(stop, e) { + if (stop.tagName() == "stop") { + stops.append(parseColorStop(stop, context, previousOffset)); } } if (!stops.isEmpty()) { gradient->setStops(stops); } else { gradient->setStops(defaultStops); } } SvgStyles SvgStyleParser::parseOneCssStyle(const QString &style, const QStringList &interestingAttributes) { SvgStyles parsedStyles; if (style.isEmpty()) return parsedStyles; QStringList substyles = style.simplified().split(';', QString::SkipEmptyParts); if (!substyles.count()) return parsedStyles; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); if (interestingAttributes.isEmpty() || interestingAttributes.contains(command)) { parsedStyles[command] = params; } } return parsedStyles; } SvgStyles SvgStyleParser::collectStyles(const KoXmlElement &e) { SvgStyles styleMap; // collect individual presentation style attributes which have the priority 0 // according to SVG standard // NOTE: font attributes should be parsed the first, because they defines 'em' and 'ex' Q_FOREACH (const QString & command, d->fontAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } Q_FOREACH (const QString &command, d->styleAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } Q_FOREACH (const QString & command, d->textAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) styleMap[command] = attribute; } // match css style rules to element QStringList cssStyles = d->context.matchingCssStyles(e); // collect all css style attributes Q_FOREACH (const QString &style, cssStyles) { QStringList substyles = style.split(';', QString::SkipEmptyParts); if (!substyles.count()) continue; for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { QStringList substyle = it->split(':'); if (substyle.count() != 2) continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); // toggle the namespace selector into the xml-like one command.replace("|", ":"); // only use style and font attributes if (d->styleAttributes.contains(command) || d->fontAttributes.contains(command) || d->textAttributes.contains(command)) { styleMap[command] = params; } } } // FIXME: if 'inherit' we should just remove the property and use the one from the context! // replace keyword "inherit" for style values QMutableMapIterator it(styleMap); while (it.hasNext()) { it.next(); if (it.value() == "inherit") { it.setValue(inheritedAttribute(it.key(), e)); } } return styleMap; } SvgStyles SvgStyleParser::mergeStyles(const SvgStyles &referencedBy, const SvgStyles &referencedStyles) { // 1. use all styles of the referencing styles SvgStyles mergedStyles = referencedBy; // 2. use all styles of the referenced style which are not in the referencing styles SvgStyles::const_iterator it = referencedStyles.constBegin(); for (; it != referencedStyles.constEnd(); ++it) { if (!referencedBy.contains(it.key())) { mergedStyles.insert(it.key(), it.value()); } } return mergedStyles; } SvgStyles SvgStyleParser::mergeStyles(const KoXmlElement &e1, const KoXmlElement &e2) { return mergeStyles(collectStyles(e1), collectStyles(e2)); } QString SvgStyleParser::inheritedAttribute(const QString &attributeName, const KoXmlElement &e) { KoXmlNode parent = e.parentNode(); while (!parent.isNull()) { KoXmlElement currentElement = parent.toElement(); if (currentElement.hasAttribute(attributeName)) { return currentElement.attribute(attributeName); } parent = currentElement.parentNode(); } return QString(); } diff --git a/libs/flake/svg/SvgStyleParser.h b/libs/flake/svg/SvgStyleParser.h index e02cdf6a28..a88ef3414c 100644 --- a/libs/flake/svg/SvgStyleParser.h +++ b/libs/flake/svg/SvgStyleParser.h @@ -1,79 +1,81 @@ /* 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 SVGSTYLEPARSER_H #define SVGSTYLEPARSER_H #include "kritaflake_export.h" #include #include #include typedef QMap SvgStyles; class SvgLoadingContext; class SvgGraphicsContext; class QColor; class QGradient; class KRITAFLAKE_EXPORT SvgStyleParser { public: explicit SvgStyleParser(SvgLoadingContext &context); ~SvgStyleParser(); /// Parses specified style attributes void parseStyle(const SvgStyles &styles); /// Parses font attributes void parseFont(const SvgStyles &styles); /// Parses a color attribute bool parseColor(QColor &, const QString &); + QPair parseColorStop(const KoXmlElement&, SvgGraphicsContext* context, qreal& previousOffset); + /// Parses gradient color stops void parseColorStops(QGradient *, const KoXmlElement &, SvgGraphicsContext *context, const QGradientStops &defaultStops); /// Creates style map from given xml element SvgStyles collectStyles(const KoXmlElement &); /// Merges two style elements, returning the merged style SvgStyles mergeStyles(const SvgStyles &, const SvgStyles &); /// Merges two style elements, returning the merged style SvgStyles mergeStyles(const KoXmlElement &, const KoXmlElement &); SvgStyles parseOneCssStyle(const QString &style, const QStringList &interestingAttributes); private: /// Parses a single style attribute void parsePA(SvgGraphicsContext *, const QString &, const QString &); /// Returns inherited attribute value for specified element QString inheritedAttribute(const QString &attributeName, const KoXmlElement &e); class Private; Private * const d; }; #endif // SVGSTYLEPARSER_H diff --git a/libs/flake/tests/CMakeLists.txt b/libs/flake/tests/CMakeLists.txt index 8aa8bb3303..26e9ffb318 100644 --- a/libs/flake/tests/CMakeLists.txt +++ b/libs/flake/tests/CMakeLists.txt @@ -1,86 +1,92 @@ set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) include(ECMAddTests) include(KritaAddBrokenUnitTest) macro_add_unittest_definitions() ecm_add_tests( TestPosition.cpp TestSelection.cpp TestPathTool.cpp TestShapeAt.cpp TestShapePainting.cpp TestKoShapeFactory.cpp TestKoShapeRegistry.cpp TestShapeContainer.cpp TestShapeGroupCommand.cpp TestShapeReorderCommand.cpp TestImageCollection.cpp TestResourceManager.cpp TestShapeBackgroundCommand.cpp TestShapeStrokeCommand.cpp TestShapeShadowCommand.cpp TestInputDevice.cpp TestSnapStrategy.cpp TestPathShape.cpp TestControlPointMoveCommand.cpp TestPointTypeCommand.cpp TestPointRemoveCommand.cpp TestRemoveSubpathCommand.cpp TestPathSegment.cpp TestSegmentTypeCommand.cpp TestKoDrag.cpp TestKoMarkerCollection.cpp LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") ecm_add_test( TestSvgParser.cpp TEST_NAME TestSvgParser LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") ecm_add_test( TestSvgParser.cpp TEST_NAME TestSvgParserCloned LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") set_property(TARGET TestSvgParserCloned PROPERTY COMPILE_DEFINITIONS USE_CLONED_SHAPES) ecm_add_test( TestSvgParser.cpp TEST_NAME TestSvgParserRoundTrip LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") set_property(TARGET TestSvgParserRoundTrip PROPERTY COMPILE_DEFINITIONS USE_ROUND_TRIP) +ecm_add_test( + TestMeshArray.cpp + TEST_NAME TestMeshArray + LINK_LIBRARIES kritaflake Qt5::Test + NAME_PREFIX "libs-flake-") + ############## broken tests ############### krita_add_broken_unit_test(TestPointMergeCommand.cpp TEST_NAME TestPointMergeCommand LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") krita_add_broken_unit_test( TestSvgText.cpp TEST_NAME TestSvgText LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") krita_add_broken_unit_test( TestSvgText.cpp TEST_NAME TestSvgTextCloned LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") set_property(TARGET TestSvgTextCloned PROPERTY COMPILE_DEFINITIONS USE_CLONED_SHAPES) krita_add_broken_unit_test( TestSvgText.cpp TEST_NAME TestSvgTextRoundTrip LINK_LIBRARIES kritaflake Qt5::Test NAME_PREFIX "libs-flake-") set_property(TARGET TestSvgTextRoundTrip PROPERTY COMPILE_DEFINITIONS USE_ROUND_TRIP) diff --git a/libs/flake/tests/TestMeshArray.cpp b/libs/flake/tests/TestMeshArray.cpp new file mode 100644 index 0000000000..e86ccb6ec5 --- /dev/null +++ b/libs/flake/tests/TestMeshArray.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 "TestMeshArray.h" + +#include +#include +#include + + +TestMeshArray::TestMeshArray() +{ +} + + +void TestMeshArray::testSinglePatch() +{ + SvgMeshArray mesharray; + QPointF point(50, 50); + mesharray.addPatch(patch0, point); + + QList path = {QPointF(50,50), QPointF(75,25), QPointF(125,75), QPointF(150,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 0, 0), path); + path = {QPointF(150,50), QPointF(175,75), QPointF(125,125), QPointF(150,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 0, 0), path); + path = {QPointF(150,150), QPointF(125,175), QPointF(75,125), QPointF(50,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 0, 0), path); + path = {QPointF(50,150), QPointF(25,125), QPointF(75,75), QPointF(50,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 0, 0), path); +} + +void TestMeshArray::test_2_by_2_Patch() +{ + SvgMeshArray mesharray; + + QPointF point(50, 50); + mesharray.addPatch(patch0, point); + + point = mesharray.getStop(SvgMeshPatch::Right, 0, 0).point; + mesharray.addPatch(patch1, point); + + mesharray.newRow(); + point = mesharray.getStop(SvgMeshPatch::Bottom, 0, 0).point; + mesharray.addPatch(patch2, point); + + point = mesharray.getStop(SvgMeshPatch::Bottom, 0, 1).point; + mesharray.addPatch(patch3, point); + + QList path = {QPointF(50,50), QPointF(75,25), QPointF(125,75), QPointF(150,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 0, 0), path); + + // compare the common side Top Bottom, col = 0 + path = {QPointF(50,150), QPointF(75,125), QPointF(125,175), QPointF(150,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 1, 0), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 0, 0), path); + + // compare the common side Top Bottom, col = 1 + path = {QPointF(150,150), QPointF(175,125), QPointF(225,175), QPointF(250,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 1, 1), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 0, 1), path); + + // compare the common side Right-Left, row = 0 + path = {QPointF(150,150), QPointF(125,125), QPointF(175,75), QPointF(150,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 0, 1), path); + path = {QPointF(150,150), QPointF(125,125), QPointF(175,75), QPointF(150,50)}; + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 0, 0), path); + + // compare the common side Right-Left, row = 1 + path = {QPointF(150,150), QPointF(175,175), QPointF(125,225), QPointF(150,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 1, 0), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 1, 1), path); + + path = {QPointF(50,250), QPointF(25,225), QPointF(75,175), QPointF(50,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 1, 0), path); + + path = {QPointF(250,250), QPointF(225,275), QPointF(175,225), QPointF(150,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 1, 1), path); + + path = {QPointF(250,150), QPointF(275,175), QPointF(225,225), QPointF(250,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 1, 1), path); +} + +void TestMeshArray::test_linear_path() +{ + SvgMeshArray mesharray; + QPointF point(50, 50); + mesharray.addPatch(linearPath0, point); + + point = mesharray.getStop(SvgMeshPatch::Right, 0, 0).point; + mesharray.addPatch(linearPath1, point); + + mesharray.newRow(); + point = mesharray.getStop(SvgMeshPatch::Bottom, 0, 0).point; + mesharray.addPatch(linearPath2, point); + + point = mesharray.getStop(SvgMeshPatch::Bottom, 0, 1).point; + mesharray.addPatch(linearPath3, point); + + QList path = {QPointF(50,50), QPointF(150,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 0, 0), path); + + // compare the common side Top Bottom, col = 0 + path = {QPointF(50,150), QPointF(150,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 1, 0), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 0, 0), path); + + // compare the common side Top Bottom, col = 1 + path = {QPointF(150,150), QPointF(250,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Top, 1, 1), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 0, 1), path); + + // compare the common side Right-Left, row = 0 + path = {QPointF(150,150), QPointF(150,50)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 0, 1), path); + path = {QPointF(150,150), QPointF(150,50)}; + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 0, 0), path); + + // compare the common side Right-Left, row = 1 + path = {QPointF(150,150), QPointF(150,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 1, 0), path); + std::reverse(path.begin(), path.end()); + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 1, 1), path); + + path = {QPointF(50,250), QPointF(50,150)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Left, 1, 0), path); + + path = {QPointF(250,250), QPointF(150,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Bottom, 1, 1), path); + + path = {QPointF(250,150), QPointF(250,250)}; + QCOMPARE(mesharray.getPath(SvgMeshPatch::Right, 1, 1), path); +} + +QTEST_MAIN(TestMeshArray) + diff --git a/libs/flake/tests/TestMeshArray.h b/libs/flake/tests/TestMeshArray.h new file mode 100644 index 0000000000..35a6a72dba --- /dev/null +++ b/libs/flake/tests/TestMeshArray.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 Sharaf Zaman + * + * 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 TESTMESHARRAY_H +#define TESTMESHARRAY_H + +#include +#include + +/* + * This is a rudimentary test to test if the path form by + * by meshpatch in mesharray are correct. This can be dropped + * later for the superior rendering based test. + */ +class TestMeshArray : public QObject +{ + Q_OBJECT +public: + TestMeshArray(); + +private Q_SLOTS: + void testSinglePatch(); + void test_2_by_2_Patch(); + void test_linear_path(); + +private: + const QList> patch0 = { + { "c 25,-25 75, 25 100,0" , QColor("#000000") }, + { "c 25, 25 -25, 75 0,100" , QColor("#800080") }, + { "c -25, 25 -75,-25 -100,0" , QColor("#ff0000") }, + { "c -25,-25, 25,-75" , QColor("#800080") }, + }; + const QList> patch1 = { + { "c 25,-25 75, 25 100,0" , QColor("#000000") }, + { "c 25, 25 -25, 75 0,100" , QColor("#000000") }, + { "c -25, 25 -75,-25" , QColor("#800080") }, + }; + const QList> patch2 = { + { "c 25, 25 -25, 75 0,100" , QColor("#000000") }, + { "c -25, 25 -75,-25 -100,0" , QColor("#ffff00") }, + { "c -25,-25, 25,-75" , QColor("#008000") }, + }; + const QList> patch3 = { + { "c 25, 25 -25, 75 0,100" , QColor("#000000") }, + { "c -25, 25 -75,-25" , QColor("#000000") }, + }; + + const QList> linearPath0 { + { "l 100,0" , QColor("#000000") }, + { "l 0,100" , QColor("#800080") }, + { "l -100,0" , QColor("#ff0000") }, + { "l" , QColor("#800080") }, + }; + const QList> linearPath1 { + { "l 100,0" , QColor("#000000") }, + { "l 0,100" , QColor("#000000") }, + { "l" , QColor("#800080") }, + }; + const QList> linearPath2 { + { "l 0,100" , QColor("#000000") }, + { "l -100,0" , QColor("#ffff00") }, + { "l" , QColor("#008000") }, + }; + const QList> linearPath3 { + { "l 0,100" , QColor("#000000") }, + { "l" , QColor("#000000") }, + }; +}; + +#endif // TESTMESHARRAY_H