diff options
author | Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> | 2022-11-07 14:41:45 +0100 |
---|---|---|
committer | Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> | 2022-11-29 10:10:47 +0100 |
commit | 77ad7934057b056bdb0c15d15a01ef83190dafaa (patch) | |
tree | d7b6e5c609c3eaf52445a8c0e08e47bc9483a867 | |
parent | 5019037adf38f7b7fd299949af3f8f46c07e574d (diff) | |
download | qtsvg-77ad7934057b056bdb0c15d15a01ef83190dafaa.tar.gz |
QSvgGenerator: add support for clip paths
SVG 1.1 allows to specify clipping paths. Before they were silently
discarded, but now we can support them.
The SVG generator code is very simple at its core -- at *any* state
change of the painter, a new <g> tag is emitted with the new state
(brush, pen, transform, ...).
Clipping is slightly more complicated because:
1) it needs its own element (<clipPath>), which needs to be referenced
by a shape/group by using a clip-path attribute (specifying a url).
2) in QPainter clipping happens in the logical coordinates when the clip
was set. Then the coordinates may get transformed again, but the drawn
shapes still have to honor the original clipping. In SVG, if one
specifies both the clip-path and the transform attributes on a shape,
the transformation also affects the clip-path (!). This is the
'clipPathUnits' attribute [1], that however doesn't match QPainter
semantics. As a workaround:
a) store clip paths already transformed (using the transform existing
when the clip path got set)
b) when clipping is active, emit an untransformed group, clip that
group, then open another inner group with the current painter
transformation. This ensures that the clip path is unaffected by any
further modification of the painter's transform.
Add a manual test.
[1] https://www.w3.org/TR/SVG11/masking.html#EstablishingANewClippingPath
Change-Id: I78161091925dc09c86e35ed042e31cece2618b9d
Reviewed-by: Albert Astals Cid <aacid@kde.org>
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
-rw-r--r-- | src/svg/qsvggenerator.cpp | 70 | ||||
-rw-r--r-- | tests/manual/CMakeLists.txt | 2 | ||||
-rw-r--r-- | tests/manual/cliptests/CMakeLists.txt | 9 | ||||
-rw-r--r-- | tests/manual/cliptests/svgcliptest.cpp | 132 |
4 files changed, 213 insertions, 0 deletions
diff --git a/src/svg/qsvggenerator.cpp b/src/svg/qsvggenerator.cpp index 7514275..7ab882d 100644 --- a/src/svg/qsvggenerator.cpp +++ b/src/svg/qsvggenerator.cpp @@ -20,6 +20,8 @@ #include "qdebug.h" +#include <optional> + QT_BEGIN_NAMESPACE static void translate_color(const QColor &color, QString *color_string, @@ -109,6 +111,21 @@ public: QString dashPattern, dashOffset; QString fill, fillOpacity; } attributes; + + QString generateClipPathName() { + ++numClipPaths; + currentClipPathName = QStringLiteral("clipPath%1").arg(numClipPaths); + return currentClipPathName; + } + + std::optional<QPainterPath> clipPath; + bool clipEnabled = false; + bool isClippingEffective() const { + return clipEnabled && clipPath.has_value(); + } + QString currentClipPathName; + int numClipPaths = 0; + bool hasEmittedClipGroup = false; }; static inline QPaintEngine::PaintEngineFeatures svgEngineFeatures() @@ -137,6 +154,7 @@ public: bool end() override; void updateState(const QPaintEngineState &state) override; + void updateClipState(const QPaintEngineState &state); void popGroup(); void drawEllipse(const QRectF &r) override; @@ -937,6 +955,8 @@ bool QSvgPaintEngine::end() *d->stream << d->header; *d->stream << d->defs; *d->stream << d->body; + if (d->hasEmittedClipGroup) + *d->stream << "</g>"; if (d->afterFirstUpdate) *d->stream << "</g>" << Qt::endl; // close the updateState @@ -992,9 +1012,20 @@ void QSvgPaintEngine::updateState(const QPaintEngineState &state) // always stream full gstate, which is not required, but... // close old state and start a new one... + if (d->hasEmittedClipGroup) + *d->stream << "</g>\n"; if (d->afterFirstUpdate) *d->stream << "</g>\n\n"; + updateClipState(state); + + if (d->isClippingEffective()) { + *d->stream << QStringLiteral("<g clip-path=\"url(#%1)\">").arg(d->currentClipPathName); + d->hasEmittedClipGroup = true; + } else { + d->hasEmittedClipGroup = false; + } + *d->stream << "<g "; qbrushToSvg(state.brush()); @@ -1018,6 +1049,45 @@ void QSvgPaintEngine::updateState(const QPaintEngineState &state) d->afterFirstUpdate = true; } +void QSvgPaintEngine::updateClipState(const QPaintEngineState &state) +{ + Q_D(QSvgPaintEngine); + switch (d->svgVersion) { + case QSvgGenerator::SvgVersion::SvgTiny12: + // no clip handling in Tiny 1.2 + return; + case QSvgGenerator::SvgVersion::Svg11: + break; + } + + const QPaintEngine::DirtyFlags flags = state.state(); + + const bool clippingChanged = flags.testAnyFlags(DirtyClipPath | DirtyClipRegion); + if (clippingChanged) { + switch (state.clipOperation()) { + case Qt::NoClip: + d->clipEnabled = false; + d->clipPath.reset(); + break; + case Qt::ReplaceClip: + case Qt::IntersectClip: + d->clipPath = painter()->transform().map(painter()->clipPath()); + break; + } + } + + if (flags & DirtyClipEnabled) + d->clipEnabled = state.isClipEnabled(); + + if (d->isClippingEffective() && clippingChanged) { + d->stream->setString(&d->defs); + *d->stream << QLatin1String("<clipPath id=\"%1\">\n").arg(d->generateClipPathName()); + drawPath(*d->clipPath); + *d->stream << "</clipPath>\n"; + d->stream->setString(&d->body); + } +} + void QSvgPaintEngine::drawEllipse(const QRectF &r) { Q_D(QSvgPaintEngine); diff --git a/tests/manual/CMakeLists.txt b/tests/manual/CMakeLists.txt new file mode 100644 index 0000000..2a050a0 --- /dev/null +++ b/tests/manual/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(cliptests) + diff --git a/tests/manual/cliptests/CMakeLists.txt b/tests/manual/cliptests/CMakeLists.txt new file mode 100644 index 0000000..0d568d0 --- /dev/null +++ b/tests/manual/cliptests/CMakeLists.txt @@ -0,0 +1,9 @@ +qt_internal_add_manual_test(svgcliptest + SOURCES + svgcliptest.cpp + INCLUDE_DIRECTORIES + . + LIBRARIES + Qt::Svg +) + diff --git a/tests/manual/cliptests/svgcliptest.cpp b/tests/manual/cliptests/svgcliptest.cpp new file mode 100644 index 0000000..b150a18 --- /dev/null +++ b/tests/manual/cliptests/svgcliptest.cpp @@ -0,0 +1,132 @@ +// Copyright (C) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Giuseppe D'Angelo <giuseppe.dangelo@kdab.com> +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QGuiApplication> + +#include <QFile> + +#include <QSvgGenerator> +#include <QPainter> +#include <QBrush> +#include <QPen> +#include <QPainterPath> +#include <QRect> +#include <QSize> + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + + const QStringList arguments = app.arguments(); + if (arguments.size() < 2) { + qWarning("Missing file name"); + return 0; + } + + QFile output(arguments[1]); + if (!output.open(QIODevice::WriteOnly)) + qFatal("Cannot open output file name"); + + QSvgGenerator generator(QSvgGenerator::SvgVersion::Svg11); + generator.setOutputDevice(&output); + generator.setSize(QSize(1000, 500)); + generator.setViewBox(QRect(0, 0, 1000, 500)); + + { + QPainter painter(&generator); + QFont f = painter.font(); + f.setPointSize(48); + painter.setFont(f); + + { + painter.save(); + // clipped rectangle + painter.setClipRect(QRect(100, 100, 250, 200)); + + painter.setBrush(QColorConstants::Blue); + painter.drawEllipse(QRect(0, 100, 400, 200)); + + { + painter.save(); + + // transformed element within clip + painter.setBrush(QColorConstants::Green); + painter.translate(300, 150); + painter.rotate(45); + painter.drawEllipse(QPointF(0, 0), 100, 50); + + painter.restore(); + } + + painter.drawText(200, 200, "A very long clipped text"); + + painter.restore(); + } + { + // unclipped + painter.setBrush(QColorConstants::Red); + painter.drawEllipse(0, 0, 200, 150); + } + { + painter.save(); + + // transformed clip (by transforming the painter before setting the clip); + painter.translate(500, 0); + painter.rotate(45); + + painter.setClipRect(QRect(50, 50, 150, 200)); + + painter.setBrush(QColorConstants::Green); + QPainterPath path; + path.addRect(50, 50, 100, 100); + path.moveTo(0, 0); + path.cubicTo(300, 0, 150, 150, 300, 300); + path.cubicTo(0, 300, 150, 150, 0, 0); + painter.drawPath(path); + + painter.setBrush(QColorConstants::Blue); + painter.drawEllipse(QPointF(125, 125), 100, 50); + + painter.restore(); + } + + { + painter.save(); + + // transformed clip + painter.translate(700, 50); + + // clip by path + QPainterPath path; + path.moveTo(0, 0); + path.cubicTo(300, 0, 150, 150, 300, 300); + path.cubicTo(0, 300, 150, 150, 0, 0); + + painter.setBrush(QColorConstants::Svg::red); + painter.drawPath(path); + + painter.setClipPath(path); + + painter.setBrush(QColorConstants::Svg::purple); + painter.drawEllipse(QPointF(150, 50), 150, 50); + + // transform and remove clipping + painter.translate(0, 100); + painter.setClipping(false); + painter.setBrush(QColorConstants::Svg::darkblue); + painter.drawEllipse(QPointF(150, 50), 150, 50); + + // transform and clip again + painter.translate(0, 100); + painter.setClipping(true); + painter.setBrush(QColorConstants::Svg::green); + painter.drawEllipse(QPointF(150, 50), 150, 50); + + + painter.restore(); + } + + } + + output.close(); +} |