summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Rauter <matthias.rauter@qt.io>2023-07-25 13:43:11 +0200
committerVolker Hilsheimer <volker.hilsheimer@qt.io>2023-12-07 12:03:35 +0100
commite4b4153383bc9dbb4c988ba7aae93a0ee1b78328 (patch)
treee8ebf21ea231dcb113bf6169c8c499da2cf66fc8
parent6afb54cdf09da8610ce500ff384f84399c3e84d4 (diff)
Add filter attribute/element and various filter primitives
The primitive filters that can be used to build <filter> are: * feMerge * feColorMatrix * feGaussianBlur * feOffset * feComposite * feFlood [ChangeLog] Added support for the <filter> element to QtSvg. The most important but not all filter primitves are supported: feMerge, feColorMatrix, feGaussianBlur, feOffset, feComposite, feFlood. Task-number: QTBUG-115223 Task-number: QTBUG-115541 Task-number: QTBUG-115548 Task-number: QTBUG-115549 Task-number: QTBUG-115550 Task-number: QTBUG-115551 Change-Id: I01ab0477cc5fe13dd03a094fc21028c182b62f5e Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
-rw-r--r--src/svg/CMakeLists.txt1
-rw-r--r--src/svg/qsvgfilter.cpp638
-rw-r--r--src/svg/qsvgfilter_p.h179
-rw-r--r--src/svg/qsvghandler.cpp372
-rw-r--r--src/svg/qsvghelper_p.h47
-rw-r--r--src/svg/qsvgnode.cpp52
-rw-r--r--src/svg/qsvgnode_p.h7
-rw-r--r--src/svg/qsvgstructure.cpp67
-rw-r--r--src/svg/qsvgstructure_p.h15
-rw-r--r--tests/auto/qsvgrenderer/tst_qsvgrenderer.cpp197
-rw-r--r--tests/baseline/data/extended_features/blur.svg13
-rw-r--r--tests/baseline/data/extended_features/blur2.svg16
-rw-r--r--tests/baseline/data/extended_features/box.svg55
-rw-r--r--tests/baseline/data/extended_features/boxColor.svg78
-rw-r--r--tests/baseline/data/extended_features/boxGauss.svg45
-rw-r--r--tests/baseline/data/extended_features/feComposite.svg150
-rw-r--r--tests/baseline/data/extended_features/fecolormatrix.svg120
-rw-r--r--tests/baseline/data/extended_features/fecolormatrixSimple.svg65
-rw-r--r--tests/baseline/data/extended_features/femergenode.svg17
-rw-r--r--tests/baseline/data/extended_features/femergenode2.svg17
-rw-r--r--tests/baseline/data/extended_features/feoffset.svg51
-rw-r--r--tests/baseline/data/extended_features/fillThenStroke.svg63
-rw-r--r--tests/baseline/data/extended_features/filterUnits.svg179
-rw-r--r--tests/baseline/data/extended_features/filterandmask.svg59
-rw-r--r--tests/baseline/data/extended_features/textfilter.svg60
25 files changed, 2528 insertions, 35 deletions
diff --git a/src/svg/CMakeLists.txt b/src/svg/CMakeLists.txt
index b7b7424..be3edef 100644
--- a/src/svg/CMakeLists.txt
+++ b/src/svg/CMakeLists.txt
@@ -21,6 +21,7 @@ qt_internal_add_module(Svg
qsvgnode.cpp qsvgnode_p.h
qsvgrenderer.cpp qsvgrenderer.h
qsvgstructure.cpp qsvgstructure_p.h
+ qsvgfilter.cpp qsvgfilter_p.h
qsvgstyle.cpp qsvgstyle_p.h
qsvgtinydocument.cpp qsvgtinydocument_p.h
qsvghelper_p.h
diff --git a/src/svg/qsvgfilter.cpp b/src/svg/qsvgfilter.cpp
new file mode 100644
index 0000000..2c86f7d
--- /dev/null
+++ b/src/svg/qsvgfilter.cpp
@@ -0,0 +1,638 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qsvgfilter_p.h"
+
+#include "qsvgnode_p.h"
+#include "qsvgtinydocument_p.h"
+#include "qpainter.h"
+
+#include <QLoggingCategory>
+#include <QtGui/qimageiohandler.h>
+#include <QVector4D>
+
+QT_BEGIN_NAMESPACE
+
+Q_DECLARE_LOGGING_CATEGORY(lcSvgDraw);
+
+QSvgFeFilterPrimitive::QSvgFeFilterPrimitive(QSvgNode *parent, QString input, QString result,
+ const QSvgRectF &rect)
+ : QSvgStructureNode(parent)
+ , m_input(input)
+ , m_result(result)
+ , m_rect(rect)
+{
+
+}
+
+QRectF QSvgFeFilterPrimitive::localFilterBoundingBox(QSvgNode *node,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+
+ QRectF localBounds;
+ if (filterUnits != QSvg::UnitTypes::userSpaceOnUse)
+ localBounds = itemBounds;
+ else
+ localBounds = filterBounds;
+ QRectF clipRect = m_rect.combinedWithLocalRect(localBounds, node->document()->viewBox(), primitiveUnits);
+ clipRect = clipRect.intersected(filterBounds);
+
+ return clipRect;
+}
+
+QRectF QSvgFeFilterPrimitive::globalFilterBoundingBox(QSvgNode *item, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ return p->transform().mapRect(localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits));
+}
+
+void QSvgFeFilterPrimitive::clipToTransformedBounds(QImage *buffer, QPainter *p, const QRectF &localRect) const
+{
+ QPainter painter(buffer);
+ painter.setRenderHints(p->renderHints());
+ painter.translate(-buffer->offset());
+ QPainterPath clipPath;
+ clipPath.setFillRule(Qt::OddEvenFill);
+ clipPath.addRect(QRect(buffer->offset(), buffer->size()).adjusted(-10, -10, 20, 20));
+ clipPath.addPolygon(p->transform().map(QPolygonF(localRect)));
+ painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
+ painter.fillPath(clipPath, Qt::transparent);
+}
+
+QSvgFeColorMatrix::QSvgFeColorMatrix(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ ColorShiftType type, Matrix matrix)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+ , m_type(type)
+ , m_matrix(matrix)
+{
+ (void)m_type;
+ //Magic numbers see SVG 1.1(Second edition)
+ if (type == ColorShiftType::Saturate) {
+ qreal s = qBound(0., matrix.data()[0], 1.);
+
+ m_matrix.fill(0);
+
+ m_matrix.data()[0+0*5] = 0.213f + 0.787f * s;
+ m_matrix.data()[1+0*5] = 0.715f - 0.717f * s;
+ m_matrix.data()[2+0*5] = 0.072f - 0.072f * s;
+
+ m_matrix.data()[0+1*5] = 0.213f - 0.213f * s;
+ m_matrix.data()[1+1*5] = 0.715f + 0.285f * s;
+ m_matrix.data()[2+1*5] = 0.072f - 0.072f * s;
+
+ m_matrix.data()[0+2*5] = 0.213f - 0.213f * s;
+ m_matrix.data()[1+2*5] = 0.715f - 0.715f * s;
+ m_matrix.data()[2+2*5] = 0.072f + 0.928f * s;
+
+ m_matrix.data()[3+3*5] = 1;
+
+ } else if (type == ColorShiftType::HueRotate){
+ qreal angle = matrix.data()[0]/180.*M_PI;
+ qreal s = sin(angle);
+ qreal c = cos(angle);
+
+ m_matrix.fill(0);
+
+ QMatrix3x3 m1;
+ m1.data()[0+0*3] = 0.213f;
+ m1.data()[1+0*3] = 0.715f;
+ m1.data()[2+0*3] = 0.072f;
+
+ m1.data()[0+1*3] = 0.213f;
+ m1.data()[1+1*3] = 0.715f;
+ m1.data()[2+1*3] = 0.072f;
+
+ m1.data()[0+2*3] = 0.213f;
+ m1.data()[1+2*3] = 0.715f;
+ m1.data()[2+2*3] = 0.072f;
+
+ QMatrix3x3 m2;
+ m2.data()[0+0*3] = 0.787 * c;
+ m2.data()[1+0*3] = -0.715 * c;
+ m2.data()[2+0*3] = -0.072 * c;
+
+ m2.data()[0+1*3] = -0.213 * c;
+ m2.data()[1+1*3] = 0.285 * c;
+ m2.data()[2+1*3] = -0.072 * c;
+
+ m2.data()[0+2*3] = -0.213 * c;
+ m2.data()[1+2*3] = -0.715 * c;
+ m2.data()[2+2*3] = 0.928 * c;
+
+ QMatrix3x3 m3;
+ m3.data()[0+0*3] = -0.213 * s;
+ m3.data()[1+0*3] = -0.715 * s;
+ m3.data()[2+0*3] = 0.928 * s;
+
+ m3.data()[0+1*3] = 0.143 * s;
+ m3.data()[1+1*3] = 0.140 * s;
+ m3.data()[2+1*3] = -0.283 * s;
+
+ m3.data()[0+2*3] = -0.787 * s;
+ m3.data()[1+2*3] = 0.715 * s;
+ m3.data()[2+2*3] = 0.072 * s;
+
+ QMatrix3x3 m = m1 + m2 + m3;
+
+ m_matrix.data()[0+0*5] = m.data()[0+0*3];
+ m_matrix.data()[1+0*5] = m.data()[1+0*3];
+ m_matrix.data()[2+0*5] = m.data()[2+0*3];
+
+ m_matrix.data()[0+1*5] = m.data()[0+1*3];
+ m_matrix.data()[1+1*5] = m.data()[1+1*3];
+ m_matrix.data()[2+1*5] = m.data()[2+1*3];
+
+ m_matrix.data()[0+2*5] = m.data()[0+2*3];
+ m_matrix.data()[1+2*5] = m.data()[1+2*3];
+ m_matrix.data()[2+2*5] = m.data()[2+2*3];
+
+ m_matrix.data()[3+3*5] = 1;
+ } else if (type == ColorShiftType::LuminanceToAlpha){
+ m_matrix.fill(0);
+
+ m_matrix.data()[0+3*5] = 0.2125;
+ m_matrix.data()[1+3*5] = 0.7154;
+ m_matrix.data()[2+3*5] = 0.0721;
+ }
+}
+
+QSvgNode::Type QSvgFeColorMatrix::type() const
+{
+ return QSvgNode::FeColormatrix;
+}
+
+QImage QSvgFeColorMatrix::apply(QSvgNode *item, const QMap<QString, QImage> &sources, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ if (!sources.contains(m_input))
+ return QImage();
+ QImage source = sources[m_input];
+
+ QRect clipRectGlob = globalFilterBoundingBox(item, p, itemBounds, filterBounds, primitiveUnits, filterUnits).toRect();
+ QRect requiredRect = p->transform().mapRect(itemBounds).toRect();
+ clipRectGlob = clipRectGlob.intersected(requiredRect);
+ if (clipRectGlob.isEmpty())
+ return QImage();
+
+ QImage result;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888, &result)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ result.setOffset(clipRectGlob.topLeft());
+ result.fill(Qt::transparent);
+
+ Q_ASSERT(source.depth() == 32);
+
+ for (int i = 0; i < result.height(); i++) {
+ int sourceI = i - source.offset().y() + result.offset().y();
+
+ if (sourceI < 0 || sourceI >= source.height())
+ continue;
+
+ QRgb *sourceLine = reinterpret_cast<QRgb *>(source.scanLine(sourceI));
+ QRgb *resultLine = reinterpret_cast<QRgb *>(result.scanLine(i));
+
+ for (int j = 0; j < result.width(); j++) {
+ int sourceJ = j - source.offset().x() + result.offset().x();
+
+ if (sourceJ < 0 || sourceJ >= source.width())
+ continue;
+
+ qreal a = qAlpha(sourceLine[sourceJ]);
+ qreal r = qBlue(sourceLine[sourceJ]);
+ qreal g = qGreen(sourceLine[sourceJ]);
+ qreal b = qRed(sourceLine[sourceJ]);
+
+ qreal r2 = m_matrix.data()[0+0*5] * r +
+ m_matrix.data()[1+0*5] * g +
+ m_matrix.data()[2+0*5] * b +
+ m_matrix.data()[3+0*5] * a +
+ m_matrix.data()[4+0*5] * 255.;
+ qreal g2 = m_matrix.data()[0+1*5] * r +
+ m_matrix.data()[1+1*5] * g +
+ m_matrix.data()[2+1*5] * b +
+ m_matrix.data()[3+1*5] * a +
+ m_matrix.data()[4+1*5] * 255.;
+ qreal b2 = m_matrix.data()[0+2*5] * r +
+ m_matrix.data()[1+2*5] * g +
+ m_matrix.data()[2+2*5] * b +
+ m_matrix.data()[3+2*5] * a +
+ m_matrix.data()[4+2*5] * 255.;
+ qreal a2 = m_matrix.data()[0+3*5] * r +
+ m_matrix.data()[1+3*5] * g +
+ m_matrix.data()[2+3*5] * b +
+ m_matrix.data()[3+3*5] * a +
+ m_matrix.data()[4+3*5] * 255.;
+
+ resultLine[j] = qRgba(qBound(0, int(b2), 255),
+ qBound(0, int(g2), 255),
+ qBound(0, int(r2), 255),
+ qBound(0, int(a2), 255));
+ }
+ }
+
+ clipToTransformedBounds(&result, p, localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits));
+ return result;
+}
+
+QSvgFeGaussianBlur::QSvgFeGaussianBlur(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ qreal stdDeviationX, qreal stdDeviationY, EdgeMode edgemode)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+ , m_stdDeviationX(stdDeviationX)
+ , m_stdDeviationY(stdDeviationY)
+ , m_edgemode(edgemode)
+{
+ (void)m_edgemode;
+}
+
+QSvgNode::Type QSvgFeGaussianBlur::type() const
+{
+ return QSvgNode::FeGaussianblur;
+}
+
+QImage QSvgFeGaussianBlur::apply(QSvgNode *item, const QMap<QString, QImage> &sources, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ if (!sources.contains(m_input))
+ return QImage();
+ QImage source = sources[m_input];
+ Q_ASSERT(source.depth() == 32);
+
+ QPointF sigma_scaled = p->transform().map(QPointF(m_stdDeviationX, m_stdDeviationY)) -
+ p->transform().map(QPointF(0, 0));
+ qreal sigma_x = sigma_scaled.x();
+ qreal sigma_y = sigma_scaled.y();
+ if (primitiveUnits == QSvg::UnitTypes::objectBoundingBox) {
+ sigma_x *= itemBounds.width();
+ sigma_y *= itemBounds.height();
+ }
+
+ // TODO: if p->transform contains anything other than translate and scale,
+ // then the gaussian filter has to be applied in local coordinates
+ // and the resulting image has to be transformed into global
+ // coordinates
+
+ int dx = qMax(1, int(floor(sigma_x * 3. * sqrt(2. * M_PI) / 4. + 0.5)));
+ int dy = qMax(1, int(floor(sigma_y * 3. * sqrt(2. * M_PI) / 4. + 0.5)));
+
+ QRect clipRectGlob = globalFilterBoundingBox(item, p, itemBounds, filterBounds, primitiveUnits, filterUnits).toRect();
+ QRect requiredRect = p->transform().mapRect(itemBounds).toRect();
+ requiredRect.adjust(- 3 * dx, -3 * dy, 3 * dx, 3 * dy);
+ clipRectGlob = clipRectGlob.intersected(requiredRect);
+ if (clipRectGlob.isEmpty())
+ return QImage();
+
+ QImage tempSource;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888_Premultiplied, &tempSource)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ tempSource.setOffset(clipRectGlob.topLeft());
+ tempSource.fill(Qt::transparent);
+ QPainter copyPainter(&tempSource);
+ copyPainter.drawImage(source.offset() - clipRectGlob.topLeft(), source);
+ copyPainter.end();
+
+ QImage result = tempSource;
+
+ // Using the approximation of a boxblur applied 3 times. Decoupling vertical and horizontal
+ for (int m = 0; m < 6; m++) {
+ QRgb *rawSource = reinterpret_cast<QRgb *>(tempSource.bits());
+ QRgb *rawResult = reinterpret_cast<QRgb *>(result.bits());
+
+ int d = (m % 2 == 0) ? dx : dy;
+ int maxdim = (m % 2 == 0) ? tempSource.width() : tempSource.height();
+
+ if (d < 1)
+ continue;
+
+ for (int i = 0; i < tempSource.width(); i++) {
+ for (int j = 0; j < tempSource.height(); j++) {
+
+ int iipos = (m % 2 == 0) ? i : j;
+
+ QVector4D val(0, 0, 0, 0);
+ for (int k = 0; k < d; k++) {
+ int ii = iipos + k - d / 2;
+ if (ii < 0 || ii >= maxdim)
+ continue;
+ QRgb rgbVal = (m % 2 == 0) ? rawSource[ii + j * tempSource.width()] : rawSource[i + ii * tempSource.width()];
+ val += QVector4D(qBlue(rgbVal), //TODO: Why are values switched here???
+ qGreen(rgbVal),
+ qRed(rgbVal), //TODO: Why are values switched here???
+ qAlpha(rgbVal)) / d;
+ }
+ rawResult[i + j * tempSource.width()] = qRgba(qBound(0, int(val[0]), 255),
+ qBound(0, int(val[1]), 255),
+ qBound(0, int(val[2]), 255),
+ qBound(0, int(val[3]), 255));
+ }
+ }
+ tempSource = result;
+ }
+
+ clipToTransformedBounds(&result, p, localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits));
+ return result;
+}
+
+QSvgFeOffset::QSvgFeOffset(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ qreal dx, qreal dy)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+ , m_dx(dx)
+ , m_dy(dy)
+{
+
+}
+
+QSvgNode::Type QSvgFeOffset::type() const
+{
+ return QSvgNode::FeOffset;
+}
+
+QImage QSvgFeOffset::apply(QSvgNode *item, const QMap<QString, QImage> &sources, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ if (!sources.contains(m_input))
+ return QImage();
+
+ const QImage &source = sources[m_input];
+
+ QRectF clipRect = localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits);
+ QRect clipRectGlob = p->transform().mapRect(clipRect).toRect();
+
+ QPoint offset(m_dx, m_dy);
+ if (primitiveUnits == QSvg::UnitTypes::objectBoundingBox) {
+ offset = QPoint(m_dx * itemBounds.width(),
+ m_dy * itemBounds.height());
+ }
+ offset = p->transform().map(offset) - p->transform().map(QPoint(0, 0));
+
+ QRect requiredRect = QRect(source.offset(), source.size()).translated(offset);
+ clipRectGlob = clipRectGlob.intersected(requiredRect);
+
+ if (clipRectGlob.isEmpty())
+ return QImage();
+
+ QImage result;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888, &result)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ result.setOffset(clipRectGlob.topLeft());
+ result.fill(Qt::transparent);
+
+ QPainter copyPainter(&result);
+ copyPainter.drawImage(source.offset()
+ - result.offset() + offset, source);
+ copyPainter.end();
+
+ clipToTransformedBounds(&result, p, clipRect);
+ return result;
+}
+
+
+QSvgFeMerge::QSvgFeMerge(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+{
+
+}
+
+QSvgNode::Type QSvgFeMerge::type() const
+{
+ return QSvgNode::FeMerge;
+}
+
+QImage QSvgFeMerge::apply(QSvgNode *item, const QMap<QString, QImage> &sources, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ QList<QImage> mergeNodeResults;
+ QRect requiredRect;
+
+ for (int i = 0; i < renderers().size(); i++) {
+ QSvgNode *child = renderers().at(i);
+ if (child->type() == QSvgNode::FeMergenode) {
+ QSvgFeMergeNode *filter = static_cast<QSvgFeMergeNode*>(child);
+ mergeNodeResults.append(filter->apply(item, sources, p, itemBounds, filterBounds, primitiveUnits, filterUnits));
+ requiredRect = requiredRect.united(QRect(mergeNodeResults.last().offset(),
+ mergeNodeResults.last().size()));
+ }
+ }
+
+ QRectF clipRect = localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits);
+ QRect clipRectGlob = p->transform().mapRect(clipRect).toRect();
+ clipRectGlob = clipRectGlob.intersected(requiredRect);
+ if (clipRectGlob.isEmpty())
+ return QImage();
+
+ QImage result;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888, &result)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ result.setOffset(clipRectGlob.topLeft());
+ result.fill(Qt::transparent);
+
+ QPainter proxyPainter(&result);
+ for (const QImage &i : mergeNodeResults) {
+ proxyPainter.drawImage(QRect(i.offset() - result.offset(), i.size()), i);
+ }
+ proxyPainter.end();
+
+ clipToTransformedBounds(&result, p, clipRect);
+ return result;
+}
+
+QSvgFeMergeNode::QSvgFeMergeNode(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+{
+
+}
+
+QSvgNode::Type QSvgFeMergeNode::type() const
+{
+ return QSvgNode::FeMergenode;
+}
+
+QImage QSvgFeMergeNode::apply(QSvgNode *, const QMap<QString, QImage> &sources, QPainter *,
+ const QRectF &, const QRectF &, QSvg::UnitTypes, QSvg::UnitTypes) const
+{
+ return sources.value(m_input);
+}
+
+QSvgFeComposite::QSvgFeComposite(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ QString input2, Operator op, QVector4D k)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+ , m_input2(input2)
+ , m_operator(op)
+ , m_k(k)
+{
+
+}
+
+QSvgNode::Type QSvgFeComposite::type() const
+{
+ return QSvgNode::FeComposite;
+}
+
+QImage QSvgFeComposite::apply(QSvgNode *item, const QMap<QString, QImage> &sources, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+ if (!sources.contains(m_input))
+ return QImage();
+ if (!sources.contains(m_input2))
+ return QImage();
+ QImage source1 = sources[m_input];
+ QImage source2 = sources[m_input2];
+ Q_ASSERT(source1.depth() == 32);
+ Q_ASSERT(source2.depth() == 32);
+
+ QRectF clipRect = localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits);
+ QRect clipRectGlob = globalFilterBoundingBox(item, p, itemBounds, filterBounds, primitiveUnits, filterUnits).toRect();
+ QRect requiredRect = QRect(source1.offset(), source1.size()).united(
+ QRect(source2.offset(), source2.size()));
+ clipRectGlob = clipRectGlob.intersected(requiredRect);
+ if (clipRectGlob.isEmpty())
+ return QImage();
+
+ QImage result;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888, &result)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ result.setOffset(clipRectGlob.topLeft());
+ result.fill(Qt::transparent);
+
+ if (m_operator == Operator::Arithmetic) {
+ const qreal k1 = m_k.x();
+ const qreal k2 = m_k.y();
+ const qreal k3 = m_k.z();
+ const qreal k4 = m_k.w();
+
+ for (int j = 0; j < result.height(); j++) {
+ int jj1 = j - source1.offset().y() + result.offset().y();
+ int jj2 = j - source2.offset().y() + result.offset().y();
+
+ QRgb *resultLine = reinterpret_cast<QRgb *>(result.scanLine(j));
+ QRgb *source1Line = nullptr;
+ QRgb *source2Line = nullptr;
+
+ if (jj1 >= 0 && jj1 < source1.size().height())
+ source1Line = reinterpret_cast<QRgb *>(source1.scanLine(jj1));
+ if (jj2 >= 0 && jj2 < source2.size().height())
+ source2Line = reinterpret_cast<QRgb *>(source2.scanLine(jj2));
+
+ for (int i = 0; i < result.width(); i++) {
+ int ii1 = i - source1.offset().x() + result.offset().x();
+ int ii2 = i - source2.offset().x() + result.offset().x();
+
+ QVector4D s1 = QVector4D(0, 0, 0, 0);
+ QVector4D s2 = QVector4D(0, 0, 0, 0);
+
+ if (ii1 >= 0 && ii1 < source1.size().width() && source1Line) {
+ QRgb pixel1 = source1Line[ii1];
+ s1 = QVector4D(qRed(pixel1),
+ qGreen(pixel1),
+ qBlue(pixel1),
+ qAlpha(pixel1));
+ }
+
+ if (ii2 >= 0 && ii2 < source2.size().width() && source2Line) {
+ QRgb pixel2 = source2Line[ii2];
+ s2 = QVector4D(qRed(pixel2),
+ qGreen(pixel2),
+ qBlue(pixel2),
+ qAlpha(pixel2));
+ }
+
+ int r = k1 * s1.x() * s2.x() / 255. + k2 * s1.x() + k3 * s2.x() + k4 * 255.;
+ int g = k1 * s1.y() * s2.y() / 255. + k2 * s1.y() + k3 * s2.y() + k4 * 255.;
+ int b = k1 * s1.z() * s2.z() / 255. + k2 * s1.z() + k3 * s2.z() + k4 * 255.;
+ int a = k1 * s1.w() * s2.w() / 255. + k2 * s1.w() + k3 * s2.w() + k4 * 255.;
+
+ qreal alpha = qBound(0, a, 255) / 255.;
+ if (alpha == 0)
+ alpha = 1;
+ resultLine[i] = qRgba(qBound(0., r / alpha, 255.),
+ qBound(0., g / alpha, 255.),
+ qBound(0., b / alpha, 255.),
+ qBound(0, a, 255));
+ }
+ }
+ } else {
+ QPainter proxyPainter(&result);
+ proxyPainter.drawImage(QRect(source1.offset() - result.offset(), source1.size()), source1);
+
+ switch (m_operator) {
+ case Operator::In:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
+ break;
+ case Operator::Out:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_DestinationOut);
+ break;
+ case Operator::Xor:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_Xor);
+ break;
+ case Operator::Lighter:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_Lighten);
+ break;
+ case Operator::Atop:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_DestinationAtop);
+ break;
+ case Operator::Over:
+ proxyPainter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
+ break;
+ case Operator::Arithmetic: // handled above
+ Q_UNREACHABLE();
+ break;
+ }
+ proxyPainter.drawImage(QRect(source2.offset()-result.offset(), source2.size()), source2);
+ proxyPainter.end();
+ }
+
+ clipToTransformedBounds(&result, p, clipRect);
+ return result;
+}
+
+QSvgFeFlood::QSvgFeFlood(QSvgNode *parent, QString input, QString result,
+ const QSvgRectF &rect, const QColor &color)
+ : QSvgFeFilterPrimitive(parent, input, result, rect)
+ , m_color(color)
+{
+
+}
+
+QSvgNode::Type QSvgFeFlood::type() const
+{
+ return QSvgNode::FeFlood;
+}
+
+QImage QSvgFeFlood::apply(QSvgNode *item, const QMap<QString, QImage> &,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const
+{
+
+ QRectF clipRect = localFilterBoundingBox(item, itemBounds, filterBounds, primitiveUnits, filterUnits);
+ QRect clipRectGlob = p->transform().mapRect(clipRect).toRect();
+
+ QImage result;
+ if (!QImageIOHandler::allocateImage(clipRectGlob.size(), QImage::Format_RGBA8888, &result)) {
+ qCWarning(lcSvgDraw) << "The requested filter buffer is too big, ignoring";
+ return QImage();
+ }
+ result.setOffset(clipRectGlob.topLeft());
+ result.fill(m_color);
+
+ clipToTransformedBounds(&result, p, clipRect);
+ return result;
+}
+
+
+QT_END_NAMESPACE
diff --git a/src/svg/qsvgfilter_p.h b/src/svg/qsvgfilter_p.h
new file mode 100644
index 0000000..19054ab
--- /dev/null
+++ b/src/svg/qsvgfilter_p.h
@@ -0,0 +1,179 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QSVGFILTER_P_H
+#define QSVGFILTER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qsvgnode_p.h"
+#include "qtsvgglobal_p.h"
+#include "qsvgstructure_p.h"
+#include "qgenericmatrix.h"
+
+#include "QtCore/qlist.h"
+#include "QtCore/qhash.h"
+#include "QtGui/qvector4d.h"
+
+QT_BEGIN_NAMESPACE
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeFilterPrimitive : public QSvgStructureNode
+{
+public:
+ QSvgFeFilterPrimitive(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect);
+ void drawCommand(QPainter *, QSvgExtraStates &) override {};
+ QRectF fastBounds(QPainter *, QSvgExtraStates &) const override { return QRectF(); }
+ QRectF bounds(QPainter *, QSvgExtraStates &) const override { return QRectF(); }
+ QRectF localFilterBoundingBox(QSvgNode *item,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const;
+ QRectF globalFilterBoundingBox(QSvgNode *item, QPainter *p,
+ const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const;
+ void clipToTransformedBounds(QImage *buffer, QPainter *p, const QRectF &localRect) const;
+ virtual QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const = 0;
+ QString input() const {
+ return m_input;
+ }
+ QString result() const {
+ return m_result;
+ }
+protected:
+ QString m_input;
+ QString m_result;
+ QSvgRectF m_rect;
+
+
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeColorMatrix : public QSvgFeFilterPrimitive
+{
+public:
+ enum class ColorShiftType : quint8 {
+ Matrix,
+ Saturate,
+ HueRotate,
+ LuminanceToAlpha
+ };
+
+ typedef QGenericMatrix<5, 5, qreal> Matrix;
+ typedef QGenericMatrix<5, 1, qreal> Vector;
+
+ QSvgFeColorMatrix(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ ColorShiftType type, Matrix matrix);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+private:
+ ColorShiftType m_type;
+ Matrix m_matrix;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeGaussianBlur : public QSvgFeFilterPrimitive
+{
+public:
+ enum class EdgeMode : quint8 {
+ Duplicate,
+ Wrap,
+ None
+ };
+
+ QSvgFeGaussianBlur(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ qreal stdDeviationX, qreal stdDeviationY, EdgeMode edgemode);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+private:
+ qreal m_stdDeviationX;
+ qreal m_stdDeviationY;
+ EdgeMode m_edgemode;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeOffset : public QSvgFeFilterPrimitive
+{
+public:
+ QSvgFeOffset(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ qreal dx, qreal dy);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+private:
+ qreal m_dx;
+ qreal m_dy;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeMerge : public QSvgFeFilterPrimitive
+{
+public:
+ QSvgFeMerge(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeMergeNode : public QSvgFeFilterPrimitive
+{
+public:
+ QSvgFeMergeNode(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeComposite : public QSvgFeFilterPrimitive
+{
+public:
+ enum class Operator : quint8 {
+ Over,
+ In,
+ Out,
+ Atop,
+ Xor,
+ Lighter,
+ Arithmetic
+ };
+ QSvgFeComposite(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect,
+ QString input2, Operator op, QVector4D k);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+private:
+ QString m_input2;
+ Operator m_operator;
+ QVector4D m_k;
+};
+
+class Q_SVG_PRIVATE_EXPORT QSvgFeFlood : public QSvgFeFilterPrimitive
+{
+public:
+ QSvgFeFlood(QSvgNode *parent, QString input, QString result, const QSvgRectF &rect, const QColor &color);
+ Type type() const override;
+ QImage apply(QSvgNode *item, const QMap<QString, QImage> &sources,
+ QPainter *p, const QRectF &itemBounds, const QRectF &filterBounds,
+ QSvg::UnitTypes primitiveUnits, QSvg::UnitTypes filterUnits) const override;
+private:
+ QColor m_color;
+};
+
+
+
+QT_END_NAMESPACE
+
+#endif // QSVGFILTER_P_H
diff --git a/src/svg/qsvghandler.cpp b/src/svg/qsvghandler.cpp
index f6cee4c..0d351cf 100644
--- a/src/svg/qsvghandler.cpp
+++ b/src/svg/qsvghandler.cpp
@@ -8,6 +8,7 @@
#include "qsvgtinydocument_p.h"
#include "qsvgstructure_p.h"
#include "qsvggraphics_p.h"
+#include "qsvgfilter_p.h"
#include "qsvgnode_p.h"
#include "qsvgfont_p.h"
@@ -191,6 +192,7 @@ struct QSvgAttributes
QStringView markerStart;
QStringView markerMid;
QStringView markerEnd;
+ QStringView filter;
#ifndef QT_NO_CSSPARSER
@@ -240,6 +242,9 @@ QSvgAttributes::QSvgAttributes(const QXmlStreamAttributes &xmlAttributes, QSvgHa
fontWeight = value;
else if (name == QLatin1String("font-variant"))
fontVariant = value;
+ else if (name == QLatin1String("filter") &&
+ handler->featureSet() != QSvg::FeatureSet::StaticTiny1_2)
+ filter = value;
break;
case 'i':
@@ -366,6 +371,9 @@ QSvgAttributes::QSvgAttributes(const QXmlStreamAttributes &xmlAttributes, QSvgHa
fontWeight = value;
else if (name == QLatin1String("font-variant"))
fontVariant = value;
+ else if (name == QLatin1String("filter") &&
+ handler->featureSet() != QSvg::FeatureSet::StaticTiny1_2)
+ filter = value;
break;
case 'i':
@@ -2334,6 +2342,19 @@ static void parseExtendedAttributes(QSvgNode *node,
markerId.remove(0, 1);
node->setMarkerEndId(markerId);
}
+
+ if (!attributes.filter.isEmpty() &&
+ handler->featureSet() != QSvg::FeatureSet::StaticTiny1_2) {
+ QString filterStr = attributes.filter.toString().trimmed();
+
+ if (filterStr.size() > 3 && filterStr.mid(0, 3) == QLatin1String("url"))
+ filterStr = filterStr.mid(3, filterStr.size() - 3);
+ QString filterId = idFromUrl(filterStr);
+ if (filterId.startsWith(QLatin1Char('#'))) //TODO: handle urls and ids in a single place
+ filterId.remove(0, 1);
+ node->setFilterId(filterId);
+ }
+
}
static void parseRenderingHints(QSvgNode *node,
@@ -3133,6 +3154,315 @@ static QSvgNode *createMaskNode(QSvgNode *parent,
return mask;
}
+static void parseFilterBounds(QSvgNode *, const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler, QSvgRectF *rect)
+{
+ const QStringView xStr = attributes.value(QLatin1String("x"));
+ const QStringView yStr = attributes.value(QLatin1String("y"));
+ const QStringView widthStr = attributes.value(QLatin1String("width"));
+ const QStringView heightStr = attributes.value(QLatin1String("height"));
+
+ qreal x = 0;
+ if (!xStr.isEmpty()) {
+ QSvgHandler::LengthType type;
+ x = parseLength(xStr.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ x = convertToPixels(x, true, type);
+ rect->setX(x);
+ } else {
+ rect->setX(-0.1);
+ rect->setUnitX(QSvg::UnitTypes::objectBoundingBox);
+ }
+ qreal y = 0;
+ if (!yStr.isEmpty()) {
+ QSvgHandler::LengthType type;
+ y = parseLength(yStr.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ y = convertToPixels(y, false, type);
+ rect->setY(y);
+ } else {
+ rect->setY(-0.1);
+ rect->setUnitY(QSvg::UnitTypes::objectBoundingBox);
+ }
+ qreal width = 0;
+ if (!widthStr.isEmpty()) {
+ QSvgHandler::LengthType type;
+ width = parseLength(widthStr.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ width = convertToPixels(width, true, type);
+ rect->setWidth(width);
+ } else {
+ rect->setWidth(1.2);
+ rect->setUnitW(QSvg::UnitTypes::objectBoundingBox);
+ }
+ qreal height = 0;
+ if (!heightStr.isEmpty()) {
+ QSvgHandler::LengthType type;
+ height = parseLength(heightStr.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ height = convertToPixels(height, false, type);
+ rect->setHeight(height);
+ } else {
+ rect->setHeight(1.2);
+ rect->setUnitH(QSvg::UnitTypes::objectBoundingBox);
+ }
+}
+
+static QSvgNode *createFilterNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QString fU = attributes.value(QLatin1String("filterUnits")).toString();
+ QString pU = attributes.value(QLatin1String("primitiveUnits")).toString();
+
+ QSvg::UnitTypes filterUnits = fU.contains(QLatin1String("userSpaceOnUse")) ?
+ QSvg::UnitTypes::userSpaceOnUse : QSvg::UnitTypes::objectBoundingBox;
+
+ QSvg::UnitTypes primitiveUnits = pU.contains(QLatin1String("objectBoundingBox")) ?
+ QSvg::UnitTypes::objectBoundingBox : QSvg::UnitTypes::userSpaceOnUse;
+
+ QSvgRectF rect;
+ parseFilterBounds(parent, attributes, handler, &rect);
+
+ QSvgNode *filter = new QSvgFilterContainer(parent, rect, filterUnits, primitiveUnits);
+ return filter;
+}
+
+static void parseFilterAttributes(QSvgNode *parent, const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler, QString *inString, QString *outString,
+ QSvgRectF *rect)
+{
+ *inString = attributes.value(QLatin1String("in")).toString();
+ *outString = attributes.value(QLatin1String("result")).toString();
+
+ parseFilterBounds(parent, attributes, handler, rect);
+}
+
+static QSvgNode *createFeColorMatrixNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ const QString typeString = attributes.value(QLatin1String("type")).toString();
+ QString valuesString = attributes.value(QLatin1String("values")).toString();
+
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ QSvgFeColorMatrix::ColorShiftType type;
+ QSvgFeColorMatrix::Matrix values;
+ values.fill(0);
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ if (typeString.startsWith(QLatin1String("saturate")))
+ type = QSvgFeColorMatrix::ColorShiftType::Saturate;
+ else if (typeString.startsWith(QLatin1String("hueRotate")))
+ type = QSvgFeColorMatrix::ColorShiftType::HueRotate;
+ else if (typeString.startsWith(QLatin1String("luminanceToAlpha")))
+ type = QSvgFeColorMatrix::ColorShiftType::LuminanceToAlpha;
+ else
+ type = QSvgFeColorMatrix::ColorShiftType::Matrix;
+
+ if (!valuesString.isEmpty()) {
+ static QRegularExpression delimiterRE(QLatin1String("[,\\s]"));
+ const QStringList valueStringList = valuesString.split(delimiterRE, Qt::SkipEmptyParts);
+
+ for (int i = 0, j = 0; i < qMin(20, valueStringList.size()); i++) {
+ bool ok;
+ qreal v = toDouble(valueStringList.at(i), &ok);
+ if (ok) {
+ values.data()[j] = v;
+ j++;
+ }
+ }
+ } else {
+ values.setToIdentity();
+ }
+
+ QSvgNode *filter = new QSvgFeColorMatrix(parent, inputString, outputString, rect,
+ type, values);
+ return filter;
+}
+
+static QSvgNode *createFeGaussianBlurNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ const QString edgeModeString = attributes.value(QLatin1String("edgeMode")).toString();
+ QString stdDeviationString = attributes.value(QLatin1String("stdDeviation")).toString();
+
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ QSvgFeGaussianBlur::EdgeMode edgemode = QSvgFeGaussianBlur::EdgeMode::Duplicate;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+ qreal stdDeviationX = 0;
+ qreal stdDeviationY = 0;
+ if (stdDeviationString.contains(QStringLiteral(" "))){
+ stdDeviationX = qMax(0., toDouble(stdDeviationString.split(QStringLiteral(" ")).first()));
+ stdDeviationY = qMax(0., toDouble(stdDeviationString.split(QStringLiteral(" ")).last()));
+ } else {
+ stdDeviationY = stdDeviationX = qMax(0., toDouble(stdDeviationString));
+ }
+
+ if (edgeModeString.startsWith(QLatin1String("wrap")))
+ edgemode = QSvgFeGaussianBlur::EdgeMode::Wrap;
+ else if (edgeModeString.startsWith(QLatin1String("none")))
+ edgemode = QSvgFeGaussianBlur::EdgeMode::None;
+
+ QSvgNode *filter = new QSvgFeGaussianBlur(parent, inputString, outputString, rect,
+ stdDeviationX, stdDeviationY, edgemode);
+ return filter;
+}
+
+static QSvgNode *createFeOffsetNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QStringView dxString = attributes.value(QLatin1String("dx"));
+ QStringView dyString = attributes.value(QLatin1String("dy"));
+
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ qreal dx = 0;
+ if (!dxString.isEmpty()) {
+ QSvgHandler::LengthType type;
+ dx = parseLength(dxString.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ dx = convertToPixels(dx, true, type);
+ }
+
+ qreal dy = 0;
+ if (!dyString.isEmpty()) {
+ QSvgHandler::LengthType type;
+ dy = parseLength(dyString.toString(), &type, handler);
+ if (type != QSvgHandler::LT_PT)
+ dy = convertToPixels(dy, true, type);
+ }
+
+ QSvgNode *filter = new QSvgFeOffset(parent, inputString, outputString, rect,
+ dx, dy);
+ return filter;
+}
+
+static QSvgNode *createFeCompositeNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QString in2String = attributes.value(QLatin1String("in2")).toString();
+ QString operatorString = attributes.value(QLatin1String("operator")).toString();
+ QString k1String = attributes.value(QLatin1String("k1")).toString();
+ QString k2String = attributes.value(QLatin1String("k2")).toString();
+ QString k3String = attributes.value(QLatin1String("k3")).toString();
+ QString k4String = attributes.value(QLatin1String("k4")).toString();
+
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ QSvgFeComposite::Operator op = QSvgFeComposite::Operator::Over;
+ if (operatorString.startsWith(QStringLiteral("in")))
+ op = QSvgFeComposite::Operator::In;
+ else if (operatorString.startsWith(QStringLiteral("out")))
+ op = QSvgFeComposite::Operator::Out;
+ else if (operatorString.startsWith(QStringLiteral("atop")))
+ op = QSvgFeComposite::Operator::Atop;
+ else if (operatorString.startsWith(QStringLiteral("xor")))
+ op = QSvgFeComposite::Operator::Xor;
+ else if (operatorString.startsWith(QStringLiteral("lighter")))
+ op = QSvgFeComposite::Operator::Lighter;
+ else if (operatorString.startsWith(QStringLiteral("arithmetic")))
+ op = QSvgFeComposite::Operator::Arithmetic;
+
+ QVector4D k(0, 0, 0, 0);
+
+ if (op == QSvgFeComposite::Operator::Arithmetic) {
+ bool ok;
+ qreal v = toDouble(k1String, &ok);
+ if (ok)
+ k.setX(v);
+ v = toDouble(k2String, &ok);
+ if (ok)
+ k.setY(v);
+ v = toDouble(k3String, &ok);
+ if (ok)
+ k.setZ(v);
+ v = toDouble(k4String, &ok);
+ if (ok)
+ k.setW(v);
+ }
+
+ QSvgNode *filter = new QSvgFeComposite(parent, inputString, outputString, rect,
+ in2String, op, k);
+ return filter;
+}
+
+
+static QSvgNode *createFeMergeNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ QSvgNode *filter = new QSvgFeMerge(parent, inputString, outputString, rect);
+ return filter;
+}
+
+static QSvgNode *createFeFloodNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QStringView colorStr = attributes.value(QLatin1String("flood-color"));
+ const QStringView opacityStr = attributes.value(QLatin1String("flood-opacity"));
+
+ QColor color;
+ if (!constructColor(colorStr, opacityStr, color, handler))
+ color = QColor(Qt::black);
+
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ QSvgNode *filter = new QSvgFeFlood(parent, inputString, outputString, rect, color);
+ return filter;
+}
+
+static QSvgNode *createFeMergeNodeNode(QSvgNode *parent,
+ const QXmlStreamAttributes &attributes,
+ QSvgHandler *handler)
+{
+ QString inputString;
+ QString outputString;
+ QSvgRectF rect;
+
+ parseFilterAttributes(parent, attributes, handler,
+ &inputString, &outputString, &rect);
+
+ QSvgNode *filter = new QSvgFeMergeNode(parent, inputString, outputString, rect);
+ return filter;
+}
+
static bool parseSymbolLikeAttributes(const QXmlStreamAttributes &attributes, QSvgHandler *handler,
QRectF *rect, QRectF *viewBox, QPointF *refPoint,
QSvgSymbolLike::PreserveAspectRatios *aspect,
@@ -3899,6 +4229,9 @@ static FactoryMethod findGroupFactory(const QString &name, QSvg::FeatureSet feat
case 'd':
if (ref == QLatin1String("efs")) return createDefsNode;
break;
+ case 'f':
+ if (ref == QLatin1String("ilter") && featureSet != QSvg::FeatureSet::StaticTiny1_2) return createFilterNode;
+ break;
case 'g':
if (ref.isEmpty()) return createGNode;
break;
@@ -3968,6 +4301,28 @@ static FactoryMethod findGraphicsFactory(const QString &name, QSvg::FeatureSet f
return 0;
}
+static FactoryMethod findFilterFtory(const QString &name, QSvg::FeatureSet featureSet)
+{
+ if (featureSet == QSvg::FeatureSet::StaticTiny1_2)
+ return 0;
+
+ if (name.isEmpty())
+ return 0;
+
+ if (!name.startsWith(QLatin1String("fe")))
+ return 0;
+
+ if (name == QLatin1String("feMerge")) return createFeMergeNode;
+ if (name == QLatin1String("feColorMatrix")) return createFeColorMatrixNode;
+ if (name == QLatin1String("feGaussianBlur")) return createFeGaussianBlurNode;
+ if (name == QLatin1String("feOffset")) return createFeOffsetNode;
+ if (name == QLatin1String("feMergeNode")) return createFeMergeNodeNode;
+ if (name == QLatin1String("feComposite")) return createFeCompositeNode;
+ if (name == QLatin1String("feFlood")) return createFeFloodNode;
+
+ return 0;
+}
+
typedef bool (*ParseMethod)(QSvgNode *, const QXmlStreamAttributes &, QSvgHandler *);
static ParseMethod findUtilFactory(const QString &name, QSvg::FeatureSet featureSet)
@@ -4379,6 +4734,23 @@ bool QSvgHandler::startElement(const QString &localName,
}
}
}
+ } else if (FactoryMethod method = findFilterFtory(localName, featureSet())) {
+ //filter nodes to be aded to be filtercontainer
+ Q_ASSERT(!m_nodes.isEmpty());
+ node = method(m_nodes.top(), attributes, this);
+ if (node) {
+ if (m_nodes.top()->type() == QSvgNode::Filter ||
+ (m_nodes.top()->type() == QSvgNode::FeMerge && node->type() == QSvgNode::FeMergenode)) {
+ QSvgStructureNode *container =
+ static_cast<QSvgStructureNode*>(m_nodes.top());
+ container->addChild(node, someId(attributes));
+ } else {
+ const QByteArray msg = QByteArrayLiteral("Could not add child element to parent element because the types are incorrect.");
+ qCWarning(lcSvgHandler, "%s", prefixMessage(msg, xml).constData());
+ delete node;
+ node = 0;
+ }
+ }
} else if (ParseMethod method = findUtilFactory(localName, featureSet())) {
Q_ASSERT(!m_nodes.isEmpty());
if (!method(m_nodes.top(), attributes, this))
diff --git a/src/svg/qsvghelper_p.h b/src/svg/qsvghelper_p.h
index ea314b2..b11131d 100644
--- a/src/svg/qsvghelper_p.h
+++ b/src/svg/qsvghelper_p.h
@@ -36,7 +36,7 @@ public:
, m_unitH(unitH)
{}
- QRectF combineWithLocalRect(const QRectF &localRect) const {
+ QRectF combinedWithLocalRect(const QRectF &localRect) const {
QRectF result;
if (m_unitX == QSvg::UnitTypes::objectBoundingBox)
result.setX(localRect.x() + x() * localRect.width());
@@ -57,38 +57,47 @@ public:
return result;
}
- QRectF combineWithLocalRect(const QRectF &localRect, QSvg::UnitTypes units) const {
+ QPointF translationRelativeToBoundingBox(const QRectF &boundingBox) const {
+ QPointF result;
+
+ if (m_unitX == QSvg::UnitTypes::objectBoundingBox)
+ result.setX(x() * boundingBox.width());
+ else
+ result.setX(x());
+ if (m_unitY == QSvg::UnitTypes::objectBoundingBox)
+ result.setY(y() * boundingBox.height());
+ else
+ result.setY(y());
+ return result;
+ }
+
+ QRectF combinedWithLocalRect(const QRectF &localRect, const QRectF &canvasRect, QSvg::UnitTypes units) const {
QRectF result;
- if (m_unitX == QSvg::UnitTypes::objectBoundingBox || units == QSvg::UnitTypes::objectBoundingBox)
+ if (units == QSvg::UnitTypes::objectBoundingBox)
result.setX(localRect.x() + x() * localRect.width());
+ else if (m_unitX == QSvg::UnitTypes::objectBoundingBox)
+ result.setX(canvasRect.x() + x() * canvasRect.width());
else
result.setX(x());
- if (m_unitY == QSvg::UnitTypes::objectBoundingBox || units == QSvg::UnitTypes::objectBoundingBox)
+ if (units == QSvg::UnitTypes::objectBoundingBox)
result.setY(localRect.y() + y() * localRect.height());
+ else if (m_unitY == QSvg::UnitTypes::objectBoundingBox)
+ result.setY(canvasRect.y() + y() * canvasRect.height());
else
result.setY(y());
- if (m_unitW == QSvg::UnitTypes::objectBoundingBox || units == QSvg::UnitTypes::objectBoundingBox)
+ if (units == QSvg::UnitTypes::objectBoundingBox)
result.setWidth(localRect.width() * width());
+ else if (m_unitW == QSvg::UnitTypes::objectBoundingBox)
+ result.setWidth(canvasRect.width() * width());
else
result.setWidth(width());
- if (m_unitH == QSvg::UnitTypes::objectBoundingBox || units == QSvg::UnitTypes::objectBoundingBox)
+ if (units == QSvg::UnitTypes::objectBoundingBox)
result.setHeight(localRect.height() * height());
+ else if (m_unitH == QSvg::UnitTypes::objectBoundingBox)
+ result.setHeight(canvasRect.height() * height());
else
result.setHeight(height());
- return result;
- }
- QPointF translationRelativeToBoundingBox(const QRectF &boundingBox) const {
- QPointF result;
-
- if (m_unitX == QSvg::UnitTypes::objectBoundingBox)
- result.setX(x() * boundingBox.width());
- else
- result.setX(x());
- if (m_unitY == QSvg::UnitTypes::objectBoundingBox)
- result.setY(y() * boundingBox.height());
- else
- result.setY(y());
return result;
}
diff --git a/src/svg/qsvgnode.cpp b/src/svg/qsvgnode.cpp
index 56df53d..f6876c4 100644
--- a/src/svg/qsvgnode.cpp
+++ b/src/svg/qsvgnode.cpp
@@ -43,13 +43,29 @@ void QSvgNode::draw(QPainter *p, QSvgExtraStates &states)
if (shouldDrawNode(p, states)) {
applyStyle(p, states);
- if (this->hasMask()) {
- QSvgNode *maskNode = document()->namedNode(this->maskId());
+ QSvgNode *maskNode = this->hasMask() ? document()->namedNode(this->maskId()) : nullptr;
+ QSvgNode *filterNode = this->hasFilter() ? document()->namedNode(this->filterId()) : nullptr;
+ if (filterNode && filterNode->type() == QSvgNode::Filter) {
+ QTransform xf = p->transform();
+ p->resetTransform();
+ QRectF localRect = bounds(p, states);
+ QRectF boundsRect = xf.mapRect(localRect);
+ p->setTransform(xf);
+ QImage proxy = drawIntoBuffer(p, states, boundsRect.toRect());
+ proxy = static_cast<QSvgFilterContainer*>(filterNode)->applyFilter(this, proxy, p, localRect);
+
+ boundsRect = QRectF(proxy.offset(), proxy.size());
+ localRect = p->transform().inverted().mapRect(boundsRect);
if (maskNode && maskNode->type() == QSvgNode::Mask) {
- QRectF boundsRect;
- QImage mask = static_cast<QSvgMask*>(maskNode)->createMask(p, states, this, &boundsRect);
- drawWithMask(p, states, mask, boundsRect.toRect());
+ QImage mask = static_cast<QSvgMask*>(maskNode)->createMask(p, states, localRect, &boundsRect);
+ applyMaskToBuffer(&proxy, mask);
}
+ applyBufferToCanvas(p, proxy);
+
+ } else if (maskNode && maskNode->type() == QSvgNode::Mask) {
+ QRectF boundsRect;
+ QImage mask = static_cast<QSvgMask*>(maskNode)->createMask(p, states, this, &boundsRect);
+ drawWithMask(p, states, mask, boundsRect.toRect());
} else {
if (separateFillStroke())
fillThenStroke(p, states);
@@ -114,6 +130,7 @@ QImage QSvgNode::drawIntoBuffer(QPainter *p, QSvgExtraStates &states, const QRec
QPainter proxyPainter(&proxy);
proxyPainter.setPen(p->pen());
proxyPainter.setBrush(p->brush());
+ proxyPainter.setFont(p->font());
proxyPainter.translate(-boundsRect.topLeft());
proxyPainter.setTransform(p->transform(), true);
proxyPainter.setRenderHints(p->renderHints());
@@ -132,12 +149,12 @@ void QSvgNode::applyMaskToBuffer(QImage *proxy, QImage mask) const
proxyPainter.drawImage(QRect(0, 0, mask.width(), mask.height()), mask);
}
-void QSvgNode::applyBufferToCanvas(QPainter *p, QImage proxy, QRect boundsRect) const
+void QSvgNode::applyBufferToCanvas(QPainter *p, QImage proxy) const
{
- QTransform t = p->transform();
+ QTransform xf = p->transform();
p->resetTransform();
- p->drawImage(boundsRect, proxy);
- p->setTransform(t);
+ p->drawImage(QRect(proxy.offset(), proxy.size()), proxy);
+ p->setTransform(xf);
}
bool QSvgNode::isDescendantOf(const QSvgNode *parent) const
@@ -483,6 +500,23 @@ bool QSvgNode::hasMask() const
return !m_maskId.isEmpty();
}
+QString QSvgNode::filterId() const
+{
+ return m_filterId;
+}
+
+void QSvgNode::setFilterId(const QString &str)
+{
+ m_filterId = str;
+}
+
+bool QSvgNode::hasFilter() const
+{
+ if (document()->featureSet() == QSvg::FeatureSet::StaticTiny1_2)
+ return false;
+ return !m_filterId.isEmpty();
+}
+
QString QSvgNode::markerStartId() const
{
return m_markerStartId;
diff --git a/src/svg/qsvgnode_p.h b/src/svg/qsvgnode_p.h
index c15c418..92832a7 100644
--- a/src/svg/qsvgnode_p.h
+++ b/src/svg/qsvgnode_p.h
@@ -94,7 +94,7 @@ public:
QImage drawIntoBuffer(QPainter *p, QSvgExtraStates &states, const QRect &boundsRect);
void applyMaskToBuffer(QImage *proxy, QImage mask) const;
void drawWithMask(QPainter *p, QSvgExtraStates &states, const QImage &mask, const QRect &boundsRect);
- void applyBufferToCanvas(QPainter *p, QImage proxy, QRect boundsRect) const;
+ void applyBufferToCanvas(QPainter *p, QImage proxy) const;
QSvgNode *parent() const;
bool isDescendantOf(const QSvgNode *parent) const;
@@ -145,6 +145,10 @@ public:
void setMaskId(const QString &str);
bool hasMask() const;
+ QString filterId() const;
+ void setFilterId(const QString &str);
+ bool hasFilter() const;
+
QString markerStartId() const;
void setMarkerStartId(const QString &str);
bool hasMarkerStart() const;
@@ -179,6 +183,7 @@ private:
QString m_id;
QString m_class;
QString m_maskId;
+ QString m_filterId;
QString m_markerStartId;
QString m_markerMidId;
QString m_markerEndId;
diff --git a/src/svg/qsvgstructure.cpp b/src/svg/qsvgstructure.cpp
index 3c7a1e2..9234fe5 100644
--- a/src/svg/qsvgstructure.cpp
+++ b/src/svg/qsvgstructure.cpp
@@ -6,7 +6,9 @@
#include "qsvgstyle_p.h"
#include "qsvgtinydocument_p.h"
-#include <QtGui/qimageiohandler.h>
+#include "qsvggraphics_p.h"
+#include "qsvgstyle_p.h"
+#include "qsvgfilter_p.h"
#include "qpainter.h"
#include "qlocale.h"
@@ -15,7 +17,6 @@
#include <QLoggingCategory>
#include <qscopedvaluerollback.h>
#include <QtGui/qimageiohandler.h>
-#include <QLoggingCategory>
QT_BEGIN_NAMESPACE
@@ -204,6 +205,16 @@ QSvgMarker::QSvgMarker(QSvgNode *parent, QRectF bounds, QRectF viewBox, QPointF
appendStyleProperty(strokeProp, QStringLiteral(""));
}
+QSvgFilterContainer::QSvgFilterContainer(QSvgNode *parent, const QSvgRectF &bounds,
+ QSvg::UnitTypes filterUnits, QSvg::UnitTypes primitiveUnits)
+ : QSvgStructureNode(parent)
+ , m_rect(bounds)
+ , m_filterUnits(filterUnits)
+ , m_primitiveUnits(primitiveUnits)
+{
+
+}
+
void QSvgMarker::drawCommand(QPainter *p, QSvgExtraStates &states)
{
if (!states.inUse) //Symbol is only drawn in combination with another node.
@@ -365,6 +376,52 @@ QSvgNode::Type QSvgMarker::type() const
return Marker;
}
+QImage QSvgFilterContainer::applyFilter(QSvgNode *item, const QImage &buffer, QPainter *p, QRectF bounds) const
+{
+ QRectF filterBounds = m_rect.combinedWithLocalRect(bounds, document()->viewBox(), m_filterUnits);
+ QRect filterBoundsGlob = p->transform().mapRect(filterBounds).toRect();
+ QRect filterBoundsGlobRel = filterBoundsGlob.translated(-buffer.offset());
+
+ if (filterBoundsGlobRel.isEmpty())
+ return buffer;
+
+ QImage proxy = buffer.copy(filterBoundsGlobRel);
+ proxy.setOffset(filterBoundsGlob.topLeft());
+
+ QImage proxyAlpha = proxy.convertedTo(QImage::Format_Alpha8).convertedTo(proxy.format());
+ // ### TODO: allocation check
+ proxyAlpha.setOffset(proxy.offset());
+
+ QMap<QString, QImage> buffers;
+ buffers[QStringLiteral("")] = proxy;
+ buffers[QStringLiteral("SourceGraphic")] = proxy;
+ buffers[QStringLiteral("SourceAlpha")] = proxyAlpha;
+
+ QImage result;
+ for (int i = 0; i < renderers().size(); i++) {
+ QSvgNode *child = renderers().at(i);
+ if (child->type() == QSvgNode::FeMerge ||
+ child->type() == QSvgNode::FeColormatrix ||
+ child->type() == QSvgNode::FeGaussianblur ||
+ child->type() == QSvgNode::FeOffset ||
+ child->type() == QSvgNode::FeComposite ||
+ child->type() == QSvgNode::FeFlood ) {
+ QSvgFeFilterPrimitive *filter = reinterpret_cast<QSvgFeFilterPrimitive*>(child);
+ result = filter->apply(item, buffers, p, bounds, filterBounds, m_primitiveUnits, m_filterUnits);
+ if (result.size().isValid()) {
+ buffers[QStringLiteral("")] = result;
+ buffers[filter->result()] = result;
+ }
+ }
+ }
+ return result;
+}
+
+QSvgNode::Type QSvgFilterContainer::type() const
+{
+ return Filter;
+}
+
/*
Below is a lookup function based on the gperf output using the following set:
@@ -715,7 +772,7 @@ QImage QSvgMask::createMask(QPainter *p, QSvgExtraStates &states, const QRectF &
// This is required to apply a clip rectangle with transformations.
// painter.setClipRect(clipRect) sounds like the obvious thing to do but
// created artifacts due to antialiasing.
- QRectF clipRect = m_rect.combineWithLocalRect(localRect);
+ QRectF clipRect = m_rect.combinedWithLocalRect(localRect);
QPainterPath clipPath;
clipPath.setFillRule(Qt::OddEvenFill);
clipPath.addRect(mask.rect().adjusted(-10, -10, 20, 20));
@@ -789,7 +846,7 @@ QImage QSvgPattern::patternImage(QPainter *p, QSvgExtraStates &states, const QSv
}
// Calculate the pattern bounding box depending on the used UnitTypes
- QRectF patternBoundingBox = m_rect.combineWithLocalRect(peBoundingBox);
+ QRectF patternBoundingBox = m_rect.combinedWithLocalRect(peBoundingBox);
QSize imageSize;
imageSize.setWidth(qCeil(patternBoundingBox.width() * t.m11() * m_transform.m11()));
@@ -864,7 +921,7 @@ void QSvgPattern::calculateAppliedTransform(QTransform &worldTransform, QRectF p
m_appliedTransform.scale(qIsFinite(imageDownScaleFactorX) ? imageDownScaleFactorX : 1.0,
qIsFinite(imageDownScaleFactorY) ? imageDownScaleFactorY : 1.0);
- QRectF p = m_rect.combineWithLocalRect(peLocalBB);
+ QRectF p = m_rect.combinedWithLocalRect(peLocalBB);
m_appliedTransform.scale((p.width() * worldTransform.m11() * m_transform.m11()) / imageSize.width(),
(p.height() * worldTransform.m22() * m_transform.m22()) / imageSize.height());
diff --git a/src/svg/qsvgstructure_p.h b/src/svg/qsvgstructure_p.h
index 9b4d110..8f03d97 100644
--- a/src/svg/qsvgstructure_p.h
+++ b/src/svg/qsvgstructure_p.h
@@ -157,6 +157,21 @@ private:
MarkerUnits m_markerUnits;
};
+class Q_SVG_PRIVATE_EXPORT QSvgFilterContainer : public QSvgStructureNode
+{
+public:
+
+ QSvgFilterContainer(QSvgNode *parent, const QSvgRectF &bounds, QSvg::UnitTypes filterUnits, QSvg::UnitTypes primitiveUnits);
+ void drawCommand(QPainter *, QSvgExtraStates &) override {};
+ Type type() const override;
+ QImage applyFilter(QSvgNode *referenceNode, const QImage &buffer, QPainter *p, QRectF bounds) const;
+private:
+ QSvgRectF m_rect;
+ QSvg::UnitTypes m_filterUnits;
+ QSvg::UnitTypes m_primitiveUnits;
+};
+
+
class Q_SVG_PRIVATE_EXPORT QSvgSwitch : public QSvgStructureNode
{
public:
diff --git a/tests/auto/qsvgrenderer/tst_qsvgrenderer.cpp b/tests/auto/qsvgrenderer/tst_qsvgrenderer.cpp
index f078ceb..2c0955d 100644
--- a/tests/auto/qsvgrenderer/tst_qsvgrenderer.cpp
+++ b/tests/auto/qsvgrenderer/tst_qsvgrenderer.cpp
@@ -72,6 +72,12 @@ private slots:
void testMarker();
void testPatternElement();
void testCycles();
+ void testFeFlood();
+ void testFeOffset();
+ void testFeColorMatrix();
+ void testFeMerge();
+ void testFeComposite();
+ void testFeGaussian();
#ifndef QT_NO_COMPRESS
void testGzLoading();
@@ -1914,7 +1920,6 @@ void tst_QSvgRenderer::notAnimated()
QVERIFY(!renderer.isAnimationEnabled());
}
-
void tst_QSvgRenderer::testPatternElement()
{
QByteArray svgDoc("<svg viewBox=\"0 0 200 200\">"
@@ -1968,5 +1973,195 @@ void tst_QSvgRenderer::testCycles()
QVERIFY(!renderer.isValid());
}
+void tst_QSvgRenderer::testFeFlood()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<filter id=\"f1\">"
+ "<feFlood flood-color=\"red\"/>"
+ "</filter>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" fill=\"blue\" filter=\"url(#f1)\"/>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" fill=\"blue\"/>"
+ "</svg>");
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(100, 100, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+ QImage refImage(100, 100, QImage::Format_ARGB32_Premultiplied);
+ refImage.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ p.begin(&refImage);
+ p.fillRect(14, 14, 72, 72, Qt::red);
+ p.fillRect(20, 20, 60, 60, Qt::blue);
+ p.end();
+
+ QCOMPARE(refImage, image);
+}
+
+void tst_QSvgRenderer::testFeOffset()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<defs>"
+ "<filter id=\"f1\">"
+ "<feOffset in=\"SourceGraphic\" dx=\"5\" dy=\"5\"/>"
+ "</filter>"
+ "</defs>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" stroke=\"none\" fill=\"blue\"/>"
+ "</svg>"
+);
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(50, 50, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+ QImage refImage(50, 50, QImage::Format_ARGB32_Premultiplied);
+ refImage.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ p.begin(&refImage);
+ p.fillRect(10, 10, 30, 30, Qt::blue);
+ p.end();
+
+ QCOMPARE(refImage, image);
+}
+
+void tst_QSvgRenderer::testFeColorMatrix()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<defs>"
+ "<filter id=\"f1\">"
+ "<feColorMatrix in=\"SourceGraphic\" type=\"saturate\" values=\"0\"/>"
+ "</filter>"
+ "</defs>"
+ "<rect x=\"0\" y=\"0\" width=\"50\" height=\"50\" stroke=\"none\" fill=\"red\" filter=\"url(#f1)\" />"
+ "</svg>"
+);
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(50, 50, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+ QImage refImage(50, 50, QImage::Format_ARGB32_Premultiplied);
+ refImage.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ QVERIFY(image.allGray());
+}
+
+void tst_QSvgRenderer::testFeMerge()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<filter id=\"f1\">"
+ "<feOffset in=\"SourceAlpha\" dx=\"2\" dy=\"2\"/>"
+ "<feMerge>"
+ "<feMergeNode/>"
+ "<feMergeNode in=\"SourceGraphic\"/>"
+ "</feMerge>"
+ "</filter>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" fill=\"blue\" filter=\"url(#f1)\"/>"
+ "</svg>"
+);
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(50, 50, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+ QImage refImage(50, 50, QImage::Format_ARGB32_Premultiplied);
+ refImage.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ p.begin(&refImage);
+ p.fillRect(12, 12, 30, 30, Qt::black);
+ p.fillRect(10, 10, 30, 30, Qt::blue);
+ p.end();
+
+ QCOMPARE(refImage, image);
+}
+
+
+void tst_QSvgRenderer::testFeComposite()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<filter id=\"f1\">"
+ "<feOffset in=\"SourceAlpha\" dx=\"2\" dy=\"2\"/>"
+ "<feComposite in2=\"SourceGraphic\" operator=\"over\"/>"
+ "</filter>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" fill=\"blue\" filter=\"url(#f1)\"/>"
+ "</svg>"
+);
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(50, 50, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+ QImage refImage(50, 50, QImage::Format_ARGB32_Premultiplied);
+ refImage.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ p.begin(&refImage);
+ p.fillRect(10, 10, 30, 30, Qt::blue);
+ p.fillRect(12, 12, 30, 30, Qt::black);
+ p.end();
+
+ QCOMPARE(refImage, image);
+}
+
+void tst_QSvgRenderer::testFeGaussian()
+{
+ QByteArray svgDoc("<svg width=\"50\" height=\"50\">"
+ "<filter id=\"f1\">"
+ "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\"/>"
+ "</filter>"
+ "<rect x=\"10\" y=\"10\" width=\"30\" height=\"30\" fill=\"black\" filter=\"url(#f1)\"/>"
+ "</svg>"
+);
+
+ QSvgRenderer renderer(svgDoc);
+ QVERIFY(renderer.isValid());
+
+ QImage image(50, 50, QImage::Format_ARGB32_Premultiplied);
+ image.fill(Qt::white);
+
+ QPainter p;
+ p.begin(&image);
+ renderer.render(&p);
+ p.end();
+
+ QVERIFY(image.allGray());
+
+ QCOMPARE(qGray(image.pixel(QPoint(0, 25))), 255);
+ QCOMPARE(qGray(image.pixel(QPoint(5, 25))), 255);
+ QCOMPARE_LE(qGray(image.pixel(QPoint(10, 25))), 150);
+ QCOMPARE_GE(qGray(image.pixel(QPoint(10, 25))), 100);
+ QCOMPARE_LE(qGray(image.pixel(QPoint(25, 25))), 10);
+
+}
+
QTEST_MAIN(tst_QSvgRenderer)
#include "tst_qsvgrenderer.moc"
diff --git a/tests/baseline/data/extended_features/blur.svg b/tests/baseline/data/extended_features/blur.svg
new file mode 100644
index 0000000..81bc3b7
--- /dev/null
+++ b/tests/baseline/data/extended_features/blur.svg
@@ -0,0 +1,13 @@
+<svg
+ width="230"
+ height="120"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="blurMe">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
+ </filter>
+
+ <circle cx="60" cy="60" r="50" fill="green" />
+
+ <circle cx="170" cy="60" r="50" fill="green" filter="url(#blurMe)" />
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/blur2.svg b/tests/baseline/data/extended_features/blur2.svg
new file mode 100644
index 0000000..1f714d3
--- /dev/null
+++ b/tests/baseline/data/extended_features/blur2.svg
@@ -0,0 +1,16 @@
+<svg
+ width="120"
+ height="120"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="dropShadow">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="3" />
+ <feOffset dx="2" dy="4" />
+ <feMerge>
+ <feMergeNode />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+
+ <circle cx="60" cy="60" r="50" fill="green" filter="url(#dropShadow)" />
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/box.svg b/tests/baseline/data/extended_features/box.svg
new file mode 100644
index 0000000..708d1d7
--- /dev/null
+++ b/tests/baseline/data/extended_features/box.svg
@@ -0,0 +1,55 @@
+<svg width="420" height="700" viewBox="-10 0 200 350" xmlns="http://www.w3.org/2000/svg">
+ <filter id="f1" x="0" y="-2" width="5" height="5" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feFlood flood-color="orange" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f2" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feFlood flood-color="red" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" >
+ <feFlood flood-color="magenta" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f4" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox" >
+ <feFlood flood-color="blue" x="-0" y="-0" width="1" height="1"/>
+ </filter>
+
+ <filter id="f5" x="-1" y="-2" width="20" height="20" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox" >
+ <feFlood flood-color="green" x="-0.1" y="-0.1" width="0.4" height="1.2"/>
+ </filter>
+
+ <filter id="f6" >
+ <feFlood flood-color="purple" />
+ </filter>
+
+ <rect transform="translate(5 55)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f1)" />
+ <rect transform="translate(5 105)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f2)" />
+ <rect transform="translate(5 155)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f3)" />
+ <rect transform="translate(5 205)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f4)" />
+ <rect transform="translate(5 255)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#f5)" />
+ <rect transform="translate(5 305)" x="-10" y="-10" width="30" height="30" fill="blue" filter="url(#f6)" />
+
+ <rect transform="translate(5 55)" x="0" y="-2" width="5" height="5" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 105)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 155)" x="-3" y="-3" width="28" height="36" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 205)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 255)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 305)" x="-13" y="-13" width="36" height="36" fill="none" stroke="black" opacity="0.5"/>
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f1)" />
+ <rect transform="translate(105 105) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f2)" />
+ <rect transform="translate(105 155) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f3)" />
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f4)" />
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#f5)" />
+ <rect transform="translate(105 305) rotate(50 50 45)" x="-10" y="-10" width="30" height="30" fill="blue" filter="url(#f6)" />
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="-2" width="5" height="5" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 105) rotate(50 50 45)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 155) rotate(50 50 45)" x="-3" y="-3" width="28" height="36" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 305) rotate(50 50 45)" x="-13" y="-13" width="36" height="36" fill="none" stroke="black" opacity="0.5"/>
+
+</svg>
+
diff --git a/tests/baseline/data/extended_features/boxColor.svg b/tests/baseline/data/extended_features/boxColor.svg
new file mode 100644
index 0000000..ae4ee4d
--- /dev/null
+++ b/tests/baseline/data/extended_features/boxColor.svg
@@ -0,0 +1,78 @@
+<svg width="420" height="700" viewBox="-10 0 200 350" xmlns="http://www.w3.org/2000/svg">
+ <filter id="f1" x="0" y="-2" width="5" height="5" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="1 1 1 0 0
+ 0 0 0 0 0
+ 0 0 0 0 0
+ 0 0 0 1 0" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f2" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 0 0
+ 0 0 0 0 0
+ 0 0 0 1 0" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f3" x="-1" y="-2" width="4" height="4" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" >
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 0 0 0 0 0
+ 1 1 1 0 0
+ 0 0 0 1 0" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="f4" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox" >
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="1 1 1 0 0
+ 1 1 1 0 0
+ 0 0 0 0 0
+ 0 0 0 1 0" x="-0" y="-0" width="1" height="1"/>
+ </filter>
+
+ <filter id="f5" x="-1" y="-2" width="4" height="4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox" >
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="1 1 1 0 0
+ 0 0 0 0 0
+ 1 1 1 0 0
+ 0 0 0 1 0" x="-0.1" y="-0.1" width="0.4" height="1.2"/>
+ </filter>
+
+
+ <rect transform="translate(5 55)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f1)" />
+ <rect transform="translate(5 105)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f2)" />
+ <rect transform="translate(5 155)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f3)" />
+ <rect transform="translate(5 205)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f4)" />
+ <rect transform="translate(5 255)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#f5)" />
+
+ <rect transform="translate(5 55)" x="0" y="-2" width="10" height="15" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 105)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 155)" x="-10" y="-10" width="35" height="50" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 205)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 255)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f1)" />
+ <rect transform="translate(105 105) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f2)" />
+ <rect transform="translate(105 155) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f3)" />
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#f4)" />
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#f5)" />
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="-2" width="10" height="15" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 105) rotate(50 50 45)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 155) rotate(50 50 45)" x="-10" y="-10" width="35" height="50" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+</svg>
+
+
diff --git a/tests/baseline/data/extended_features/boxGauss.svg b/tests/baseline/data/extended_features/boxGauss.svg
new file mode 100644
index 0000000..0b21893
--- /dev/null
+++ b/tests/baseline/data/extended_features/boxGauss.svg
@@ -0,0 +1,45 @@
+<svg width="420" height="700" viewBox="-10 0 200 350" xmlns="http://www.w3.org/2000/svg">
+ <filter id="blur1" x="0" y="-2" width="10" height="15" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feGaussianBlur stdDeviation="5" result="blur" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="blur2" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" >
+ <feGaussianBlur stdDeviation="5" result="blur" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="blur3" x="-1" y="-2" width="20" height="20" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" >
+ <feGaussianBlur stdDeviation="5" result="blur" x="-10" y="-10" width="35" height="50"/>
+ </filter>
+
+ <filter id="blur4" x="-1" y="-2" width="20" height="20" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox" >
+ <feGaussianBlur stdDeviation="0.1" result="blur" x="-0" y="-0" width="1" height="1"/>
+ </filter>
+
+ <filter id="blur5" x="-1" y="-2" width="20" height="20" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox" >
+ <feGaussianBlur stdDeviation="0.2" result="blur" x="-0.1" y="-0.1" width="0.4" height="1.2"/>
+ </filter>
+
+ <rect transform="translate(5 55)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur1)" />
+ <rect transform="translate(5 105)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur2)" />
+ <rect transform="translate(5 155)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur3)" />
+ <rect transform="translate(5 205)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur4)" />
+ <rect transform="translate(5 255)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#blur5)" />
+
+ <rect transform="translate(5 55)" x="0" y="-2" width="10" height="15" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 105)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 155)" x="-10" y="-10" width="35" height="50" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 205)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(5 255)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur1)" />
+ <rect transform="translate(105 105) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur2)" />
+ <rect transform="translate(105 155) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur3)" />
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="30" height="30" fill="blue" filter="url(#blur4)" />
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-6" y="0" width="30" height="30" fill="blue" filter="url(#blur5)" />
+
+ <rect transform="translate(105 55) rotate(50 50 45)" x="0" y="-2" width="10" height="15" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 105) rotate(50 50 45)" x="-1" y="-2" width="20" height="20" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 155) rotate(50 50 45)" x="-10" y="-10" width="35" height="50" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 205) rotate(50 50 45)" x="0" y="0" width="19" height="18" fill="none" stroke="black" opacity="0.5"/>
+ <rect transform="translate(105 255) rotate(50 50 45)" x="-9" y="-3" width="12" height="36" fill="none" stroke="black" opacity="0.5"/>
+</svg>
diff --git a/tests/baseline/data/extended_features/feComposite.svg b/tests/baseline/data/extended_features/feComposite.svg
new file mode 100644
index 0000000..b4b709c
--- /dev/null
+++ b/tests/baseline/data/extended_features/feComposite.svg
@@ -0,0 +1,150 @@
+<svg width="1000" height="500" viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="imageOver">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="over" />
+ </filter>
+ <filter id="imageIn">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="in" />
+ </filter>
+ <filter id="imageOut">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="out" />
+ </filter>
+ <filter id="imageAtop">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="atop" />
+ </filter>
+ <filter id="imageXor">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="xor" />
+ </filter>
+ <filter id="imageArithmetic">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite
+ in2="SourceGraphic"
+ operator="arithmetic"
+ k1="0.1"
+ k2="0.2"
+ k3="0.3"
+ k4="0.4" />
+ </filter>
+ <filter id="imageLighter">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="lighter" />
+ </filter>
+ </defs>
+ <g transform="translate(0,25)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageOver)" />
+ <text x="80" y="-5">over</text>
+ </g>
+ <g transform="translate(200,25)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageIn)" />
+ <text x="80" y="-5">in</text>
+ </g>
+ <g transform="translate(400,25)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageOut)" />
+ <text x="80" y="-5">out</text>
+ </g>
+ <g transform="translate(600,25)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageAtop)" />
+ <text x="80" y="-5">atop</text>
+ </g>
+ <g transform="translate(0,240)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageXor)" />
+ <text x="80" y="-5">xor</text>
+ </g>
+ <g transform="translate(200,240)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageArithmetic)" />
+ <text x="70" y="-5">arithmetic</text>
+ </g>
+ <g transform="translate(400,240)">
+ <circle
+ cx="90"
+ cy="80"
+ r="70"
+ fill="#c00"
+ style="filter:url(#imageLighter)" />
+ <text x="80" y="-5">lighter</text>
+ </g>
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/fecolormatrix.svg b/tests/baseline/data/extended_features/fecolormatrix.svg
new file mode 100644
index 0000000..b4f51a4
--- /dev/null
+++ b/tests/baseline/data/extended_features/fecolormatrix.svg
@@ -0,0 +1,120 @@
+<svg
+ width="200%"
+ height="200%"
+ viewBox="-80 0 180 450"
+ preserveAspectRatio="xMidYMid meet"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- ref -->
+ <defs>
+ <g id="circles" color-interpolation="linearRGB" >
+ <circle cx="30" cy="30" r="20" fill="blue" fill-opacity="0.5" />
+ <circle cx="20" cy="50" r="20" fill="green" fill-opacity="0.5" />
+ <circle cx="40" cy="50" r="20" fill="red" fill-opacity="0.5" />
+ </g>
+ <g id="circles2" color-interpolation="sRGB" >
+ <circle cx="-40" cy="30" r="20" fill="blue" fill-opacity="0.5" />
+ <circle cx="-50" cy="50" r="20" fill="green" fill-opacity="0.5" />
+ <circle cx="-30" cy="50" r="20" fill="red" fill-opacity="0.5" />
+ </g>
+ </defs>
+ <use href="#circles" />
+ <use href="#circles2" />
+ <text x="70" y="50">Reference</text>
+
+ <!-- identity matrix -->
+ <filter id="colorMeTheSame">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="1 0 0 0 0
+ 0 1 0 0 0
+ 0 0 1 0 0
+ 0 0 0 1 0" />
+ </filter>
+ <use
+ href="#circles"
+ transform="translate(0 70)"
+ filter="url(#colorMeTheSame)" />
+ <use
+ href="#circles2"
+ transform="translate(0 70)"
+ filter="url(#colorMeTheSame)" />
+ <text x="70" y="120">Identity matrix</text>
+
+ <!-- Combine RGB into green matrix -->
+ <filter id="colorMeGreen">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0"
+ color-interpolation-filters="linearRGB" />
+ </filter>
+ <filter id="colorMeGreen2">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0 0
+ 0 0 0 1 0"
+ color-interpolation-filters="sRGB" />
+ </filter>
+ <use
+ href="#circles"
+ transform="translate(0 140)"
+ filter="url(#colorMeGreen)" />
+ <use
+ href="#circles2"
+ transform="translate(0 140)"
+ filter="url(#colorMeGreen2)" />
+ <text x="70" y="190">rgbToGreen</text>
+
+ <!-- saturate -->
+ <filter id="colorMeSaturate">
+ <feColorMatrix in="SourceGraphic" type="saturate" values="0.2" color-interpolation-filters="linearRGB"/>
+ </filter>
+ <filter id="colorMeSaturate2">
+ <feColorMatrix in="SourceGraphic" type="saturate" values="0.2" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use
+ href="#circles"
+ transform="translate(0 210)"
+ filter="url(#colorMeSaturate)" />
+ <use
+ href="#circles2"
+ transform="translate(0 210)"
+ filter="url(#colorMeSaturate2)" />
+ <text x="70" y="260">saturate</text>
+
+ <!-- hueRotate -->
+ <filter id="colorMeHueRotate">
+ <feColorMatrix in="SourceGraphic" type="hueRotate" values="180" color-interpolation-filters="linearRGB"/>
+ </filter>
+ <filter id="colorMeHueRotate2">
+ <feColorMatrix in="SourceGraphic" type="hueRotate" values="180" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use
+ href="#circles"
+ transform="translate(0 280)"
+ filter="url(#colorMeHueRotate)" />
+ <use
+ href="#circles2"
+ transform="translate(0 280)"
+ filter="url(#colorMeHueRotate2)" />
+ <text x="70" y="330">hueRotate</text>
+
+ <!-- luminanceToAlpha -->
+ <filter id="colorMeLTA">
+ <feColorMatrix in="SourceGraphic" type="luminanceToAlpha" color-interpolation-filters="linearRGB"/>
+ </filter>
+ <filter id="colorMeLTA2">
+ <feColorMatrix in="SourceGraphic" type="luminanceToAlpha" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use href="#circles" transform="translate(0 350)" filter="url(#colorMeLTA)" />
+ <use href="#circles2" transform="translate(0 350)" filter="url(#colorMeLTA2)" />
+ <text x="70" y="400">luminanceToAlpha</text>
+</svg>
diff --git a/tests/baseline/data/extended_features/fecolormatrixSimple.svg b/tests/baseline/data/extended_features/fecolormatrixSimple.svg
new file mode 100644
index 0000000..15c35ab
--- /dev/null
+++ b/tests/baseline/data/extended_features/fecolormatrixSimple.svg
@@ -0,0 +1,65 @@
+<svg
+ width="360"
+ height="760"
+ viewBox="0 0 180 380"
+ preserveAspectRatio="xMidYMid meet"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- ref -->
+ <defs>
+ <g id="rect1">
+ <rect id="rect11" x="10" y="30" width="20" height="20" fill="#ff0000" fill-opacity="1" />
+ <rect id="rect12" x="30" y="30" width="20" height="20" fill="#00ff00" fill-opacity="1" />
+ <rect id="rect13" x="10" y="50" width="20" height="20" fill="#0000ff" fill-opacity="1" />
+ <rect id="rect14" x="30" y="50" width="20" height="20" fill="#990099" fill-opacity="1" />
+ </g>
+ </defs>
+ <use href="#rect1" />
+ <text x="70" y="50">Reference</text>
+
+ <!-- Combine RGB into green matrix -->
+ <filter id="colorMeGreen">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ color-interpolation-filters="sRGB"
+ values="0 0 0 0 0
+ 0.5 0 0 0 0
+ 0.5 0 0 0 0
+ 0 0 0 1 0" />
+ </filter>
+ <use
+ href="#rect1"
+ transform="translate(0 70)"
+ filter="url(#colorMeGreen)" />
+ <text x="70" y="120">rgbToGreen</text>
+
+
+ <!-- saturate -->
+ <filter id="colorMeSaturate">
+ <feColorMatrix in="SourceGraphic" type="saturate" values="0" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use
+ href="#rect1"
+ transform="translate(0 140)"
+ filter="url(#colorMeSaturate)" />
+ <text x="70" y="190">saturate</text>
+
+ <!-- hueRotate -->
+ <filter id="colorMeHueRotate">
+ <feColorMatrix in="SourceGraphic" type="hueRotate" values="180" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use
+ href="#rect1"
+ transform="translate(0 210)"
+ filter="url(#colorMeHueRotate)" />
+ <text x="70" y="260">hueRotate</text>
+
+ <!-- luminanceToAlpha -->
+ <filter id="colorMeLTA">
+ <feColorMatrix in="SourceGraphic" type="luminanceToAlpha" color-interpolation-filters="sRGB"/>
+ </filter>
+ <use href="#rect1" transform="translate(0 280)" filter="url(#colorMeLTA)" />
+ <text x="70" y="330">luminanceToAlpha</text>
+
+</svg>
diff --git a/tests/baseline/data/extended_features/femergenode.svg b/tests/baseline/data/extended_features/femergenode.svg
new file mode 100644
index 0000000..3220f9d
--- /dev/null
+++ b/tests/baseline/data/extended_features/femergenode.svg
@@ -0,0 +1,17 @@
+<svg width="200" height="200" viewBox="30 30 130 130" xmlns="http://www.w3.org/2000/svg">
+ <filter id="feOffset" x="-0.1" y="-0.1" width="1.2" height="1.2">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur2" />
+ <feOffset in="blur2" dx="5" dy="5" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+
+ <rect
+ x="40"
+ y="40"
+ width="100"
+ height="100"
+ style="stroke: #000000; fill: green; filter: url(#feOffset);" />
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/femergenode2.svg b/tests/baseline/data/extended_features/femergenode2.svg
new file mode 100644
index 0000000..c1e9ff8
--- /dev/null
+++ b/tests/baseline/data/extended_features/femergenode2.svg
@@ -0,0 +1,17 @@
+<svg width="200" height="200" viewBox="30 30 130 130" xmlns="http://www.w3.org/2000/svg">
+ <filter id="feOffset" x="-10" y="-10" width="120" height="120" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur2" />
+ <feOffset in="blur2" dx="-5" dy="-5" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+
+ <rect
+ x="40"
+ y="40"
+ width="100"
+ height="100"
+ style="stroke: #000000; fill: green; filter: url(#feOffset);" />
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/feoffset.svg b/tests/baseline/data/extended_features/feoffset.svg
new file mode 100644
index 0000000..981331e
--- /dev/null
+++ b/tests/baseline/data/extended_features/feoffset.svg
@@ -0,0 +1,51 @@
+<svg width="1000" height="400" viewBox="0 0 500 200" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <filter id="offset">
+ <feOffset in="SourceGraphic" dx="5" dy="5" />
+ </filter>
+ <circle id="myCircle" cx="45" cy="45" r="25" opacity="0.5"/>
+ </defs>
+
+ <rect x="0" y="0" width="100" height="100" stroke="black" fill="green" opacity="0.5" />
+ <rect x="0" y="0" width="100" height="100" stroke="black" fill="green" opacity="0.5" filter="url(#offset)" />
+
+ <circle transform="translate(100 0)" cx="50" cy="50" r="50" stroke="black" fill="green" opacity="0.5" />
+ <circle transform="translate(100 0)" cx="50" cy="50" r="50" stroke="black" fill="green" opacity="0.5" filter="url(#offset)" />
+
+ <g transform="translate(200 0)" >
+ <circle cx="50" cy="30" r="25" stroke="black" fill="green" opacity="0.5" />
+ <circle cx="50" cy="70" r="25" stroke="black" fill="red" opacity="0.5" />
+ </g>
+ <g transform="translate(200 0)" filter="url(#offset)" >
+ <circle cx="50" cy="30" r="25" stroke="black" fill="green" opacity="0.5" />
+ <circle cx="50" cy="70" r="25" stroke="black" fill="red" opacity="0.5" />
+ </g>
+
+ <line transform="translate(300 0)" x1="0" y1="50" x2="100" y2="70" style="stroke:rgb(255,0,0);stroke-width:2" />
+ <line transform="translate(300 0)" x1="0" y1="50" x2="100" y2="70" style="stroke:rgb(255,0,0);stroke-width:2" filter="url(#offset)" />
+
+ <polygon transform="translate(400, 0)" points="30,30 30,60 50,60" fill="green" stroke="black"/>
+ <polygon transform="translate(400, 0)" points="30,30 30,60 50,60" fill="green" stroke="black" filter="url(#offset)" />
+
+ <polyline transform="translate(0, 100)" points="30,30 30,60 50,60" style="fill:none;stroke:black;stroke-width:3" />
+ <polyline transform="translate(0, 100)" points="30,30 30,60 50,60" style="fill:none;stroke:black;stroke-width:3" filter="url(#offset)" />
+
+ <path transform="translate(100, 100)" d="M10,50 L10,10 L90,50, Z" fill="green" opacity="0.5" />
+ <path transform="translate(100, 100)" d="M10,50 L10,10 L90,50, Z" fill="green" opacity="0.5" filter="url(#offset)" />
+
+ <text transform="translate(200, 100)" fill="blue" x="0" y="20" font-family="Arial" font-size="16" opacity="0.5" > Example Text! </text>
+ <text transform="translate(200, 100)" fill="red" x="0" y="20" font-family="Arial" font-size="16" filter="url(#offset)" opacity="0.5"> Example Text! </text>
+
+ <ellipse transform="translate(300, 100)" cx="50" cy="50" rx="20" ry="40" fill="green" opacity="0.5" stroke="black" />
+ <ellipse transform="translate(300, 100)" cx="50" cy="50" rx="20" ry="40" fill="green" opacity="0.5" stroke="black" filter="url(#offset)" />
+
+ <use transform="translate(400, 100)" href="#myCircle" x="20" fill="blue" stroke="black" />
+ <use transform="translate(400, 100)" href="#myCircle" x="20" fill="blue" stroke="black" filter="url(#offset)" />
+
+
+
+
+
+</svg>
+
+
diff --git a/tests/baseline/data/extended_features/fillThenStroke.svg b/tests/baseline/data/extended_features/fillThenStroke.svg
new file mode 100644
index 0000000..1a70390
--- /dev/null
+++ b/tests/baseline/data/extended_features/fillThenStroke.svg
@@ -0,0 +1,63 @@
+<svg viewBox="0 0 800 450" xmlns="http://www.w3.org/2000/svg">
+ <circle transform="translate(0, 0)" cx="50" cy="40" r="40"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <ellipse transform="translate(110, 0)" cx="50" cy="40" rx="40" ry="30"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <rect transform="translate(220, 0)" x="0" y="0" width="100" height="100"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <line transform="translate(330, 0)" x1="0" y1="0" x2="100" y2="100"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polygon transform="translate(440, 0)" points="-10,110 -10,-10 110,110"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polyline transform="translate(550, 0)" points="10,90 10,10 90,90, 10,90"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <path transform="translate(660, 0)" d="M10,90 L10,10 L90,90, Z"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+
+ <circle transform="translate(0, 110)" cx="50" cy="40" r="40"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <ellipse transform="translate(110, 110)" cx="50" cy="40" rx="40" ry="30"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <rect transform="translate(220, 110)" x="0" y="0" width="100" height="100"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <line transform="translate(330, 110)" x1="0" y1="0" x2="100" y2="100"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <polygon transform="translate(440, 110)" points="-10,110 -10,-10 110,110"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <polyline transform="translate(550, 110)" points="10,90 10,10 90,90, 10,90"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+ <path transform="translate(660, 110)" d="M10,90 L10,10 L90,90, Z"
+ fill="yellow" stroke="none" stroke-opacity="0.2" stroke-width="10" />
+
+ <circle transform="translate(0, 220)" cx="50" cy="40" r="40"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <ellipse transform="translate(110, 220)" cx="50" cy="40" rx="40" ry="30"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <rect transform="translate(220, 220)" x="0" y="0" width="100" height="100"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <line transform="translate(330, 220)" x1="0" y1="0" x2="100" y2="100"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polygon transform="translate(440, 220)" points="-10,110 -10,-10 110,110"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polyline transform="translate(550, 220)" points="10,90 10,10 90,90, 10,90"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <path transform="translate(660, 220)" d="M10,90 L10,10 L90,90, Z"
+ fill="none" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+
+ <g opacity="0.5">
+ <circle transform="translate(0, 330)" cx="50" cy="40" r="40"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <ellipse transform="translate(110, 330)" cx="50" cy="40" rx="40" ry="30"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <rect transform="translate(220, 330)" x="0" y="0" width="100" height="100"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <line transform="translate(330, 330)" x1="0" y1="0" x2="100" y2="100"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polygon transform="translate(440, 330)" points="-10,110 -10,-10 110,110"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <polyline transform="translate(550, 330)" points="10,90 10,10 90,90, 10,90"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ <path transform="translate(660, 330)" d="M10,90 L10,10 L90,90, Z"
+ fill="yellow" stroke="black" stroke-opacity="0.2" stroke-width="10" />
+ </g>
+</svg>
diff --git a/tests/baseline/data/extended_features/filterUnits.svg b/tests/baseline/data/extended_features/filterUnits.svg
new file mode 100644
index 0000000..ab79d65
--- /dev/null
+++ b/tests/baseline/data/extended_features/filterUnits.svg
@@ -0,0 +1,179 @@
+<svg
+ width="550"
+ height="700"
+ viewBox="0 0 550 700"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="f1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feOffset in="SourceGraphic" dx="20" dy="20" />
+ </filter>
+ <filter id="f2" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox">
+ <feOffset in="SourceGraphic" dx="0.2" dy="0.2" />
+ </filter>
+ <filter id="f3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feOffset in="SourceGraphic" dx="20" dy="20" />
+ </filter>
+ <filter id="f4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feOffset in="SourceGraphic" dx="0.2" dy="0.2" />
+ </filter>
+
+ <filter id="b1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="20" />
+ </filter>
+ <filter id="b2" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="0.2" />
+ </filter>
+ <filter id="b3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="20" />
+ </filter>
+ <filter id="b4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feGaussianBlur in="SourceGraphic" stdDeviation="0.2" />
+ </filter>
+
+ <filter id="c1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" />
+ </filter>
+ <filter id="c2" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" />
+ </filter>
+ <filter id="c3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" />
+ </filter>
+ <filter id="c4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 1 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" />
+ </filter>
+
+ <filter id="m1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur2" />
+ <feOffset in="blur2" dx="-5" dy="-5" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ <filter id="m2" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="0.02" result="blur2" />
+ <feOffset in="blur2" dx="-0.05" dy="-0.05" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ <filter id="m3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="2" result="blur2" />
+ <feOffset in="blur2" dx="-5" dy="-5" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ <filter id="m4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feGaussianBlur in="SourceAlpha" stdDeviation="0.02" result="blur2" />
+ <feOffset in="blur2" dx="-0.05" dy="-0.05" result="offset2" />
+ <feMerge>
+ <feMergeNode in="offset2" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
+
+ <filter id="p1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
+ <feOffset dx="15" dy="15" />
+ <feComposite in2="SourceAlpha" operator="xor" />
+ </filter>
+ <filter id="p2" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox">
+ <feOffset dx="0.2" dy="0.2" />
+ <feComposite in2="SourceAlpha" operator="xor" />
+ </filter>
+ <filter id="p3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feOffset dx="15" dy="15" />
+ <feComposite in2="SourceAlpha" operator="xor" />
+ </filter>
+ <filter id="p4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feOffset dx="0.2" dy="0.2" />
+ <feComposite in2="SourceAlpha" operator="xor" />
+ </filter>
+
+
+ <filter id="d1" filterUnits="userSpaceOnUse" x="70" y="470" width="180" height="180" primitiveUnits="userSpaceOnUse">
+ <feFlood flood-color="purple" />
+ </filter>
+ <filter id="d2" filterUnits="userSpaceOnUse" x="170" y="470" width="180" height="180" primitiveUnits="objectBoundingBox">
+ <feFlood flood-color="purple" />
+ </filter>
+ <filter id="d3" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse">
+ <feFlood flood-color="purple" />
+ </filter>
+ <filter id="d4" filterUnits="objectBoundingBox" primitiveUnits="objectBoundingBox">
+ <feFlood flood-color="purple" />
+ </filter>
+
+
+ <rect x="0" y="0" width="1000" height="1000" fill="white" />
+ <circle cx="60" cy="60" r="50" stroke="black" fill="none" />
+ <circle cx="160" cy="60" r="50" stroke="black" fill="none" />
+ <circle cx="260" cy="60" r="50" stroke="black" fill="none" />
+ <circle cx="360" cy="60" r="50" stroke="black" fill="none" />
+ <circle cx="460" cy="60" r="50" stroke="black" fill="none" />
+
+ <circle cx="60" cy="160" r="50" stroke="black" fill="none" />
+ <circle cx="160" cy="160" r="50" stroke="black" fill="none" />
+ <circle cx="260" cy="160" r="50" stroke="black" fill="none" />
+ <circle cx="360" cy="160" r="50" stroke="black" fill="none" />
+ <circle cx="460" cy="160" r="50" stroke="black" fill="none" />
+
+ <circle cx="60" cy="260" r="50" stroke="black" fill="none" />
+ <circle cx="160" cy="260" r="50" stroke="black" fill="none" />
+ <circle cx="260" cy="260" r="50" stroke="black" fill="none" />
+ <circle cx="360" cy="260" r="50" stroke="black" fill="none" />
+ <circle cx="460" cy="260" r="50" stroke="black" fill="none" />
+
+ <circle cx="60" cy="360" r="50" stroke="black" fill="none" />
+ <circle cx="160" cy="360" r="50" stroke="black" fill="none" />
+ <circle cx="260" cy="360" r="50" stroke="black" fill="none" />
+ <circle cx="360" cy="360" r="50" stroke="black" fill="none" />
+ <circle cx="460" cy="360" r="50" stroke="black" fill="none" />
+
+ <circle cx="60" cy="460" r="50" stroke="black" fill="none" />
+ <circle cx="160" cy="460" r="50" stroke="black" fill="none" />
+ <circle cx="260" cy="460" r="50" stroke="black" fill="none" />
+ <circle cx="360" cy="460" r="50" stroke="black" fill="none" />
+ <circle cx="460" cy="460" r="50" stroke="black" fill="none" />
+
+ <circle cx="60" cy="60" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="60" r="50" fill="red" opacity="0.5" filter="url(#f1)" />
+ <circle cx="260" cy="60" r="50" fill="blue" opacity="0.5" filter="url(#f2)" />
+ <circle cx="360" cy="60" r="50" fill="yellow" opacity="0.5" filter="url(#f3)" />
+ <circle cx="460" cy="60" r="50" fill="magenta" opacity="0.5" filter="url(#f4)" />
+
+ <circle cx="60" cy="160" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="160" r="50" fill="red" opacity="0.5" filter="url(#b1)" />
+ <circle cx="260" cy="160" r="50" fill="blue" opacity="0.5" filter="url(#b2)" />
+ <circle cx="360" cy="160" r="50" fill="yellow" opacity="0.5" filter="url(#b3)" />
+ <circle cx="460" cy="160" r="50" fill="magenta" opacity="0.5" filter="url(#b4)" />
+
+ <circle cx="60" cy="260" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="260" r="50" fill="red" opacity="0.5" filter="url(#c1)" />
+ <circle cx="260" cy="260" r="50" fill="blue" opacity="0.5" filter="url(#c2)" />
+ <circle cx="360" cy="260" r="50" fill="yellow" opacity="0.5" filter="url(#c3)" />
+ <circle cx="460" cy="260" r="50" fill="magenta" opacity="0.5" filter="url(#c4)" />
+
+ <circle cx="60" cy="360" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="360" r="50" fill="red" opacity="0.5" filter="url(#m1)" />
+ <circle cx="260" cy="360" r="50" fill="blue" opacity="0.5" filter="url(#m2)" />
+ <circle cx="360" cy="360" r="50" fill="yellow" opacity="0.5" filter="url(#m3)" />
+ <circle cx="460" cy="360" r="50" fill="magenta" opacity="0.5" filter="url(#m4)" />
+
+ <circle cx="60" cy="460" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="460" r="50" fill="red" opacity="0.5" filter="url(#p1)" />
+ <circle cx="260" cy="460" r="50" fill="blue" opacity="0.5" filter="url(#p2)" />
+ <circle cx="360" cy="460" r="50" fill="yellow" opacity="0.5" filter="url(#p3)" />
+ <circle cx="460" cy="460" r="50" fill="magenta" opacity="0.5" filter="url(#p4)" />
+
+ <circle cx="60" cy="560" r="50" fill="green" opacity="0.5" />
+ <circle cx="160" cy="560" r="50" fill="red" opacity="0.5" filter="url(#d1)" />
+ <circle cx="260" cy="560" r="50" fill="blue" opacity="0.5" filter="url(#d2)" />
+ <circle cx="360" cy="560" r="50" fill="yellow" opacity="0.5" filter="url(#d3)" />
+ <circle cx="460" cy="560" r="50" fill="magenta" opacity="0.5" filter="url(#d4)" />
+
+ <rect x="170" y="470" width="180" height="180" fill="none" stroke="black" />
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/filterandmask.svg b/tests/baseline/data/extended_features/filterandmask.svg
new file mode 100644
index 0000000..695720d
--- /dev/null
+++ b/tests/baseline/data/extended_features/filterandmask.svg
@@ -0,0 +1,59 @@
+<svg viewBox="-10 -10 560 230" xmlns="http://www.w3.org/2000/svg">
+ <mask id="myMask">
+ <!-- Everything under a white pixel will be visible -->
+ <g>
+ <rect x="0" y="0" width="100" height="100" fill="white" />
+
+ <!-- Everything under a black pixel will be invisible -->
+ <path
+ d="M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z"
+ fill="black" />
+ </g>
+ </mask>
+
+ <mask x="0" y="0" width="0.5" height="1" id="myMask2">
+ <!-- Everything under a white pixel will be visible -->
+ <g>
+ <rect x="0" y="0" width="100" height="100" fill="white" />
+
+ <!-- Everything under a black pixel will be invisible -->
+ <path
+ d="M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z"
+ fill="black" />
+ </g>
+ </mask>
+
+ <filter id="blur">
+ <feGaussianBlur stdDeviation="3"/>
+ </filter>
+
+ <polygon points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(110, 0)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(220, 0)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(330, 0)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(440, 0)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(0, 110)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(110, 110)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(220, 110)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(330, 110)" points="-10,110 110,110 110,-10" fill="orange" />
+ <polygon transform="translate(440, 110)" points="-10,110 110,110 110,-10" fill="orange" />
+
+ <!-- with this mask applied, we "punch" a heart shape hole into the circle -->
+ <rect filter="url(#blur)" transform="translate(0, 0)" x="0" y="0" width="100" height="100" opacity="0.5" mask="url(#myMask)" />
+ <circle filter="url(#blur)" transform="translate(110, 0)" cx="50" cy="50" r="50" mask="url(#myMask)" />
+ <ellipse filter="url(#blur)" transform="translate(220, 0)" cx="50" cy="50" rx="40" ry="70" mask="url(#myMask)" />
+ <line filter="url(#blur)" transform="translate(330, 0)" x1="0" y1="0" x2="100" y2="100" style="stroke:rgb(255,0,0);stroke-width:2" mask="url(#myMask)" />
+ <polygon filter="url(#blur)" transform="translate(440, 0)" points="-10,110 -10,-10 110,110" fill="green" mask="url(#myMask)" />
+ <polyline filter="url(#blur)" transform="translate(0, 110)" points="10,90 10,10 90,90, 10,90" style="fill:none;stroke:black;stroke-width:3" mask="url(#myMask)" />
+ <path filter="url(#blur)" transform="translate(110, 110)" d="M10,90 L10,10 L90,90, Z" fill="black" mask="url(#myMask)" />
+ <text filter="url(#blur)" transform="translate(220, 110)" fill="blue" x="0" y="20" mask="url(#myMask)" font-family="Arial" font-size="16"> Stupid SVG! </text>
+ <g filter="url(#blur)" transform="translate(330, 110)" mask="url(#myMask)">
+ <rect x="5" y="10" width="40" height="80" />
+ <ellipse cx="75" cy="50" ry="40" rx="20" />
+ </g>
+ <rect filter="url(#blur)" transform="translate(440, 110)" x="0" y="0" width="100" height="100" fill="yellow" />
+ <rect filter="url(#blur)" transform="translate(440, 110)" x="0" y="0" width="100" height="100" fill="red" mask="url(#myMask)" />
+ <rect filter="url(#blur)" transform="translate(440, 110)" x="0" y="0" width="100" height="100" fill="blue" mask="url(#myMask2)" />
+ <rect filter="url(#blur)" transform="translate(440, 110)" x="0" y="0" width="50" height="100" fill="green" mask="url(#myMask)" />
+
+</svg> \ No newline at end of file
diff --git a/tests/baseline/data/extended_features/textfilter.svg b/tests/baseline/data/extended_features/textfilter.svg
new file mode 100644
index 0000000..dd4745c
--- /dev/null
+++ b/tests/baseline/data/extended_features/textfilter.svg
@@ -0,0 +1,60 @@
+<svg width="350" height="450" viewBox="0 0 350 450" xmlns="http://www.w3.org/2000/svg">
+
+<defs>
+ <filter id="blur">
+ <feGaussianBlur stdDeviation="5 5"/>
+ </filter>
+
+ <filter id="offset">
+ <feOffset in="SourceGraphic" dx="5" dy="5" />
+ </filter>
+
+
+ <filter id="imageIn">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 0
+ 0 0 0 0 0
+ 1 1 1 1 0
+ 0 0 0 0.5 0" />
+ <feOffset dx="5" dy="5" />
+ <feComposite in2="SourceGraphic" operator="over" />
+ </filter>
+
+ <mask x="0" y="0" width="1" height="1" id="simpleMask">
+ <g>
+ <rect x="0" y="0" width="150" height="300" fill="white" />
+ <rect x="150" y="0" width="500" height="300" fill="black" />
+ </g>
+ </mask>
+
+</defs>
+
+<text font-size="50" filter="url(#blur)" font-family="Arial" fill="red" x="5" y="50"> Example Text </text>
+<text font-size="50" fill="blue" font-family="Arial" x="5" y="50" opacity="0.5"> Example Text </text>
+
+<text font-size="50" filter="url(#offset)" font-family="Arial" fill="red" x="5" y="125"> Example Text </text>
+<text font-size="50" font-family="Arial" fill="blue" x="5" y="125" opacity="0.5"> Example Text </text>
+
+<text font-size="50" filter="url(#imageIn)" font-family="Arial" fill="red" x="5" y="200"> Example Text </text>
+
+<text transform="translate(0, 225) scale(0.8 0.8) rotate(-45 0 0)" font-size="50" filter="url(#blur)" font-family="Arial" fill="red" x="5" y="50"> Example Text </text>
+<text transform="translate(0, 225) scale(0.8 0.8) rotate(-45 0 0)" font-size="50" fill="blue" font-family="Arial" x="5" y="50" opacity="0.5"> Example Text </text>
+
+<text transform="translate(5, 300)" font-size="50" font-family="Arial" fill="green" x="5" y="50"> Example Text </text>
+<text transform="translate(0, 300)" font-size="50" filter="url(#blur)" font-family="Arial" fill="red" x="5" y="50"> Example Text </text>
+<text transform="translate(0, 300)" font-size="50" fill="blue" font-family="Arial" x="5" y="50" opacity="0.5"> Example Text </text>
+
+
+<text transform="translate(5, 300)" font-size="50" font-family="Arial" fill="green" x="5" y="50"> Example Text </text>
+<text transform="translate(0, 300)" font-size="50" filter="url(#blur)" font-family="Arial" fill="red" x="5" y="50"> Example Text </text>
+<text transform="translate(0, 300)" font-size="50" fill="blue" font-family="Arial" x="5" y="50" opacity="0.5"> Example Text </text>
+
+
+<text mask="url(#simpleMask)" transform="translate(0, 375)" font-size="50" filter="url(#blur)" font-family="Arial" fill="red" x="5" y="50"> Example Text </text>
+<text mask="url(#simpleMask)" transform="translate(0, 375)" font-size="50" fill="blue" font-family="Arial" x="5" y="50" opacity="0.5"> Example Text </text>
+<rect fill="none" stroke="black" x="5" y="375" width="150" height="50"/>
+
+
+</svg> \ No newline at end of file