diff options
author | Matthias Rauter <matthias.rauter@qt.io> | 2023-07-25 13:43:11 +0200 |
---|---|---|
committer | Volker Hilsheimer <volker.hilsheimer@qt.io> | 2023-12-07 12:03:35 +0100 |
commit | e4b4153383bc9dbb4c988ba7aae93a0ee1b78328 (patch) | |
tree | e8ebf21ea231dcb113bf6169c8c499da2cf66fc8 | |
parent | 6afb54cdf09da8610ce500ff384f84399c3e84d4 (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>
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 |