Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QTest>
#include <qguiapplication.h>
#include <qdebug.h>
#include <qsvgrenderer.h>
#include <qsvggenerator.h>
#include <QBuffer>
#include <QByteArray>
#include <QFile>
#include <QImage>
#include <QPainter>
#include <QPen>
#include <QPicture>
#include <QRgb>
#include <QRectF>
#include <QTimer>
#include <QXmlStreamReader>
#include <QImageReader>
using namespace Qt::Literals::StringLiterals;
class tst_QSvgRenderer : public QObject
{
Q_OBJECT
public:
tst_QSvgRenderer();
virtual ~tst_QSvgRenderer();
private slots:
void getSetCheck();
void emptyRect_data();
void emptyRect();
void invalidUrl_data();
void invalidUrl();
void testStrokeWidth();
#if QT_CONFIG(picture)
void constructorQXmlStreamReader() const;
void loadQXmlStreamReader() const;
void nestedQXmlStreamReader() const;
void testMapViewBoxToTarget();
void testRenderElement();
#endif
void testRenderElementToBounds();
void testRenderDocumentWithSizeToBounds();
void stylePropagation() const;
void transformForElement() const;
void boundsOnElement() const;
void gradientStops() const;
void gradientRefs();
void recursiveRefs_data();
void recursiveRefs();
void fillRule_data();
void fillRule();
void opacity_data();
void opacity();
void groupOpacity();
void paths();
void paths2();
void emptyPath_data();
void emptyPath();
void displayMode();
void strokeInherit();
void testFillInheritance();
void testStopOffsetOpacity();
void testUseElement();
void smallFont();
void glyphWarnings_data();
void glyphWarnings();
void styleSheet();
void duplicateStyleId();
void ossFuzzRender_data();
void ossFuzzRender();
void ossFuzzLoad_data();
void ossFuzzLoad();
void imageRendering();
void imageMalformedDataUrl();
void illegalAnimateTransform_data();
void illegalAnimateTransform();
void tSpanLineBreak();
void tSpanMultiline_data();
void tSpanMultiline();
void tSpanPosition();
void animated();
void notAnimated();
void notAnimatedOption();
void testMaskElement();
void testSymbol();
void testMarker();
void testPatternElement();
void testMisplacedElement();
void testCycles_data();
void testCycles();
void testFeFlood();
void testFeOffset();
void testFeColorMatrix();
void testFeMerge();
void testFeComposite();
void testFeGaussian();
void testFeGaussianBigDimensions_data();
void testFeGaussianBigDimensions();
void testFeBlend();
void testOption_data();
void testOption();
void testUseInsideContainerElement_data();
void testUseInsideContainerElement();
void testDeeplyNested_data();
void testDeeplyNested();
#ifndef QT_NO_COMPRESS
void testGzLoading();
# ifdef QT_BUILD_INTERNAL
void testGzHelper_data();
void testGzHelper();
# endif
#endif
private:
static const char *const src;
};
const char *const tst_QSvgRenderer::src = "<svg><g><rect x='250' y='250' width='500' height='500'/>"
"<rect id='foo' x='400' y='400' width='100' height='100'/></g></svg>";
tst_QSvgRenderer::tst_QSvgRenderer()
{
}
tst_QSvgRenderer::~tst_QSvgRenderer()
{
}
// Testing get/set functions
void tst_QSvgRenderer::getSetCheck()
{
QSvgRenderer obj1;
// int QSvgRenderer::framesPerSecond()
// void QSvgRenderer::setFramesPerSecond(int)
obj1.setFramesPerSecond(20);
QCOMPARE(20, obj1.framesPerSecond());
obj1.setFramesPerSecond(0);
QCOMPARE(0, obj1.framesPerSecond());
QTest::ignoreMessage(QtWarningMsg,
"QSvgRenderer::setFramesPerSecond: Cannot set negative value -2147483648");
obj1.setFramesPerSecond(INT_MIN);
QCOMPARE(0, obj1.framesPerSecond()); // Can't have a negative framerate
obj1.setFramesPerSecond(INT_MAX);
QCOMPARE(INT_MAX, obj1.framesPerSecond());
// bool QSvgRenderer::isAnimationEnabled()
// void QSvgRenderer::setAnimationEnabled()
QVERIFY(obj1.isAnimationEnabled());
obj1.setAnimationEnabled(false);
QVERIFY(!obj1.isAnimationEnabled());
obj1.setAnimationEnabled(true);
QVERIFY(obj1.isAnimationEnabled());
}
void tst_QSvgRenderer::emptyRect_data()
{
// Testing rects with zero width or height or no given value
// Those caused divisions by zero, e.g. QTBUG-49160 and oss-fuzz issue 23588
// UBSAN is required to see those divisions by zero
QTest::addColumn<QByteArray>("svg");
QTest::newRow("nothing") << R"(<svg><rect/></svg>)"_ba;
QTest::newRow("no width zero height") << R"(<svg><rect height="0"/></svg>)"_ba;
QTest::newRow("zero width no height") << R"(<svg><rect width="0"/></svg>)"_ba;
QTest::newRow("no width") << R"(<svg><rect height="1"/></svg>)"_ba;
QTest::newRow("no height") << R"(<svg><rect width="1"/></svg>)"_ba;
QTest::newRow("both zero") << R"(<svg><rect width="0" height="0"/></svg>)"_ba;
QTest::newRow("zero width") << R"(<svg><rect width="0" height="1"/></svg>)"_ba;
QTest::newRow("zero heigth") << R"(<svg><rect width="1" height="0"/></svg>)"_ba;
}
void tst_QSvgRenderer::emptyRect()
{
QFETCH(QByteArray, svg);
QSvgRenderer renderer(svg);
QVERIFY(renderer.isValid());
}
void tst_QSvgRenderer::invalidUrl_data()
{
QTest::addColumn<QByteArray>("svg");
QTest::newRow("url0")
<< R"(<svg><linearGradient id="0"/><circle fill="url0" /></svg>)"_ba;
QTest::newRow("url(0")
<< R"(<svg><linearGradient id="0"/><circle fill="url(0" /></svg>)"_ba;
QTest::newRow("url (0")
<< R"(<svg><linearGradient id="0"/><circle fill="url (0" /></svg>)"_ba;
QTest::newRow("url ( 0")
<< R"(<svg><linearGradient id="0"/><circle fill="url ( 0" /></svg>)"_ba;
QTest::newRow("url#")
<< R"(<svg><linearGradient id="0"/><circle fill="url#" /></svg>)"_ba;
QTest::newRow("url#(")
<< R"(<svg><linearGradient id="0"/><circle fill="url#(" /></svg>)"_ba;
QTest::newRow("url(#")
<< R"(<svg><linearGradient id="0"/><circle fill="url(#" /></svg>)"_ba;
QTest::newRow("url(# ")
<< R"(<svg><linearGradient id="0"/><circle fill="url(# " /></svg>)"_ba;
QTest::newRow("url(# 0")
<< R"(<svg><linearGradient id="0"/><circle fill="url(# 0" /></svg>)"_ba;
QTest::newRow("urlblabla")
<< R"(<svg><linearGradient id="blabla"/><circle fill="urlblabla" /></svg>)"_ba;
QTest::newRow("url(blabla")
<< R"(<svg><linearGradient id="blabla"/><circle fill="url(blabla" /></svg>)"_ba;
QTest::newRow("url(blabla) ")
<< R"(<svg><linearGradient id="blabla"/><circle fill="url(blabla) "/></svg>)"_ba;
QTest::newRow("url(#blabla")
<< R"(<svg><linearGradient id="blabla"/><circle fill="url(#blabla" /></svg>)"_ba;
QTest::newRow("inexistent")
<< R"svg(<svg><g><path d="M0 0" style="stroke:url(#inexistent)"/></g></svg>)svg"_ba;
QTest::newRow("emptyUrl")
<< R"svg(<svg><text fill="url()"/></svg>)svg"_ba;
}
void tst_QSvgRenderer::invalidUrl()
{
QFETCH(const QByteArray, svg);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Could not resolve property"));
QSvgRenderer renderer(svg);
QVERIFY(renderer.isValid());
}
void tst_QSvgRenderer::testStrokeWidth()
{
qreal squareSize = 30.0;
qreal strokeWidth = 1.0;
qreal topLeft = 100.0;
QSvgGenerator generator;
QBuffer buffer;
QByteArray byteArray;
buffer.setBuffer(&byteArray);
generator.setOutputDevice(&buffer);
QPainter painter(&generator);
painter.setBrush(Qt::blue);
// Draw a rect with stroke
painter.setPen(QPen(Qt::black, strokeWidth));
painter.drawRect(topLeft, topLeft, squareSize, squareSize);
// Draw a rect without stroke
painter.setPen(Qt::NoPen);
painter.drawRect(topLeft, topLeft, squareSize, squareSize);
painter.end();
// Insert ID tags into the document
byteArray.insert(byteArray.indexOf("stroke=\"#000000\""), "id=\"SquareStroke\" ");
byteArray.insert(byteArray.indexOf("stroke=\"none\""), "id=\"SquareNoStroke\" ");
QSvgRenderer renderer(byteArray);
QRectF noStrokeRect = renderer.boundsOnElement("SquareNoStroke");
QCOMPARE(noStrokeRect.width(), squareSize);
QCOMPARE(noStrokeRect.height(), squareSize);
QCOMPARE(noStrokeRect.x(), topLeft);
QCOMPARE(noStrokeRect.y(), topLeft);
QRectF strokeRect = renderer.boundsOnElement("SquareStroke");
QCOMPARE(strokeRect.width(), squareSize + strokeWidth);
QCOMPARE(strokeRect.height(), squareSize + strokeWidth);
QCOMPARE(strokeRect.x(), topLeft - (strokeWidth / 2));
QCOMPARE(strokeRect.y(), topLeft - (strokeWidth / 2));
}
#if QT_CONFIG(picture)
void tst_QSvgRenderer::constructorQXmlStreamReader() const
{
const QByteArray data(src);
QXmlStreamReader reader(data);
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(&reader);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
}
void tst_QSvgRenderer::loadQXmlStreamReader() const
{
const QByteArray data(src);
QXmlStreamReader reader(data);
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend;
rend.load(&reader);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
}
void tst_QSvgRenderer::nestedQXmlStreamReader() const
{
const QByteArray data(QByteArray("<bar>") + QByteArray(src) + QByteArray("</bar>"));
QXmlStreamReader reader(data);
QCOMPARE(reader.readNext(), QXmlStreamReader::StartDocument);
QCOMPARE(reader.readNext(), QXmlStreamReader::StartElement);
QCOMPARE(reader.name().toString(), "bar"_L1);
QPicture picture;
QPainter painter(&picture);
QSvgRenderer renderer(&reader);
renderer.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
QCOMPARE(reader.readNext(), QXmlStreamReader::EndElement);
QCOMPARE(reader.name().toString(), "bar"_L1);
QCOMPARE(reader.readNext(), QXmlStreamReader::EndDocument);
QVERIFY(reader.atEnd());
QVERIFY(!reader.hasError());
}
void tst_QSvgRenderer::testMapViewBoxToTarget()
{
const char *src = R"(<svg><g><rect x="250" y="250" width="500" height="500" /></g></svg>)";
QByteArray data(src);
{ // No viewport, viewBox, targetRect, or deviceRect -> boundingRect
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 500, 500));
}
{ // No viewport, viewBox, targetRect -> deviceRect
QPicture picture;
picture.setBoundingRect(QRect(100, 100, 200, 200));
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(100, 100, 200, 200));
}
{ // No viewport, viewBox -> targetRect
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, QRectF(50, 50, 250, 250));
painter.end();
QCOMPARE(picture.boundingRect(), QRect(50, 50, 250, 250));
}
data.replace("<svg>", "<svg viewBox=\"0 0 1000 1000\">");
{ // No viewport, no targetRect -> viewBox
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(250, 250, 500, 500));
}
data.replace("<svg", "<svg width=\"500\" height=\"500\"");
{ // Viewport
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(125, 125, 250, 250));
}
// Requires keep-aspectratio feature
{ // Viewport and viewBox specified -> scale 500x500 square to 1000x750 while preserving aspect ratio gives 750x750
data = R"(<svg width="1000" height="750" viewBox="-250 -250 500 500"><g><rect x="0" y="0" width="500" height="500" /></g></svg>)";
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.setAspectRatioMode(Qt::KeepAspectRatio);
rend.render(&painter);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(500, 375, 750, 750));
}
}
void tst_QSvgRenderer::testRenderElement()
{
QByteArray data(src);
{ // No viewport, viewBox, targetRect, or deviceRect -> boundingRect
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
}
{ // No viewport, viewBox, targetRect -> deviceRect
QPicture picture;
picture.setBoundingRect(QRect(100, 100, 200, 200));
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(100, 100, 200, 200));
}
{ // No viewport, viewBox -> targetRect
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, "foo"_L1, QRectF(50, 50, 250, 250));
painter.end();
QCOMPARE(picture.boundingRect(), QRect(50, 50, 250, 250));
}
data.replace("<svg>", "<svg viewBox=\"0 0 1000 1000\">");
{ // No viewport, no targetRect -> view box size
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
}
data.replace("<svg", "<svg width=\"500\" height=\"500\"");
{ // Viewport
QPicture picture;
QPainter painter(&picture);
QSvgRenderer rend(data);
rend.render(&painter, "foo"_L1);
painter.end();
QCOMPARE(picture.boundingRect(), QRect(0, 0, 100, 100));
}
}
#endif
void tst_QSvgRenderer::testRenderElementToBounds()
{
// QTBUG-79933
QImage reference(400, 200, QImage::Format_ARGB32);
{
reference.fill(Qt::transparent);
QPainter p(&reference);
p.fillRect( 0, 0, 200, 100, Qt::blue);
p.fillRect(200, 100, 200, 100, Qt::blue);
p.fillRect( 0, 0, 100, 50, Qt::red);
p.fillRect(100, 50, 100, 50, Qt::red);
p.fillRect(200, 100, 100, 50, Qt::red);
p.fillRect(300, 150, 100, 50, Qt::red);
}
QImage rendering(400, 200, QImage::Format_ARGB32);
{
const char *const src =
"<svg viewBox=\"0 0 100 100\">" // Presence of a viewBox triggered QTBUG-79933
"<path id=\"el1\" d=\"M 80,10 H 0 V 0 h 40 v 20 h 40 z\" fill=\"red\" />"
"<path id=\"el2\" d=\"M 90,100 V 20 h 10 V 60 H 80 v 40 z\" fill=\"blue\" />"
"</svg>";
const QByteArray data(src);
QSvgRenderer rend(data);
rendering.fill(Qt::transparent);
QPainter p(&rendering);
rend.render(&p, "el1", QRectF( 0, 0, 200, 100));
rend.render(&p, "el2", QRectF( 0, 0, 200, 100));
rend.render(&p, "el1", QRectF(200, 100, 200, 100));
rend.render(&p, "el2", QRectF(200, 100, 200, 100));
}
QCOMPARE(reference, rendering);
}
void tst_QSvgRenderer::testRenderDocumentWithSizeToBounds()
{
// QTBUG-80888
QImage reference(400, 200, QImage::Format_ARGB32);
{
reference.fill(Qt::transparent);
QPainter p(&reference);
p.fillRect(100, 100, 100, 50, Qt::blue);
p.fillRect(200, 50, 100, 50, Qt::blue);
}
QImage rendering(400, 200, QImage::Format_ARGB32);
{
const char *const src = R"src(
<svg width="20" height="80">
<g transform="translate(-100,-100)">
<path d="m 110,180 v -80 h 10 v 40 h -20 v 40 z" fill="blue" />
</g>
</svg>
)src";
const QByteArray data(src);
QSvgRenderer rend(data);
rendering.fill(Qt::transparent);
QPainter p(&rendering);
rend.render(&p, QRectF(100, 50, 200, 100));
}
QCOMPARE(reference, rendering);
}
void tst_QSvgRenderer::stylePropagation() const
{
#if !QT_CONFIG(cssparser)
QSKIP("cssparser feature disabled, skipping test");
#else
QByteArray data(R"(<svg>
<g id="foo" style="fill:#ffff00;">
<g id="bar" style="fill:#ff00ff;">
<g id="baz" style="fill:#00ffff;">
<rect id="alpha" x="0" y="0" width="100" height="100"/>
</g>
<rect id="beta" x="100" y="0" width="100" height="100"/>
</g>
<rect id="gamma" x="0" y="100" width="100" height="100"/>
</g>
<rect id="delta" x="100" y="100" width="100" height="100"/>
</svg>)"); // alpha=cyan, beta=magenta, gamma=yellow, delta=black
QImage image1(200, 200, QImage::Format_RGB32);
QImage image2(200, 200, QImage::Format_RGB32);
QImage image3(200, 200, QImage::Format_RGB32);
QPainter painter;
QSvgRenderer renderer(data);
QLatin1StringView parts[4] = {"alpha"_L1, "beta"_L1, "gamma"_L1, "delta"_L1};
QVERIFY(painter.begin(&image1));
for (int i = 0; i < 4; ++i)
renderer.render(&painter, parts[i], QRectF(renderer.boundsOnElement(parts[i])));
painter.end();
QVERIFY(painter.begin(&image2));
renderer.render(&painter, renderer.viewBoxF());
painter.end();
QVERIFY(painter.begin(&image3));
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(Qt::cyan));
painter.drawRect(0, 0, 100, 100);
painter.setBrush(QBrush(Qt::magenta));
painter.drawRect(100, 0, 100, 100);
painter.setBrush(QBrush(Qt::yellow));
painter.drawRect(0, 100, 100, 100);
painter.setBrush(QBrush(Qt::black));
painter.drawRect(100, 100, 100, 100);
painter.end();
QCOMPARE(image1, image2);
QCOMPARE(image1, image3);
#endif // QT_CONFIG(cssparser)
}
static qreal transformNorm(const QTransform &m)
{
return qSqrt(m.m11() * m.m11()
+ m.m12() * m.m12()
+ m.m13() * m.m13()
+ m.m21() * m.m21()
+ m.m22() * m.m22()
+ m.m23() * m.m23()
+ m.m31() * m.m31()
+ m.m32() * m.m32()
+ m.m33() * m.m33());
}
template<typename T>
static inline bool diffIsSmallEnough(T diff, T norm)
{
static_assert(std::is_same_v<T, double> || std::is_same_v<T, float>);
T sigma = []{
if constexpr (std::is_same_v<T, double>)
return 1e-12;
else
return 1e-5;
}();
return diff <= sigma * norm;
}
static void compareTransforms(const QTransform &m1, const QTransform &m2)
{
qreal norm1 = transformNorm(m1);
qreal norm2 = transformNorm(m2);
qreal diffNorm = transformNorm(QTransform(m1.m11() - m2.m11(),
m1.m12() - m2.m12(),
m1.m13() - m2.m13(),
m1.m21() - m2.m21(),
m1.m22() - m2.m22(),
m1.m23() - m2.m23(),
m1.m31() - m2.m31(),
m1.m32() - m2.m32(),
m1.m33() - m2.m33()));
QVERIFY(diffIsSmallEnough(diffNorm, qMin(norm1, norm2)));
}
void tst_QSvgRenderer::transformForElement() const
{
QByteArray data(R"(<svg>
<g id="ichi" transform="translate(-3,1) ">
<g id="ni" transform="rotate(45) ">
<g id="san" transform="scale(4,2) ">
<g id="yon" transform="matrix(1,2,3,4,5,6) ">
<rect id="firkant" x="-1" y="-1" width="2" height="2"/>
</g>
</g>
</g>
</g>
</svg>)");
QImage image(13, 37, QImage::Format_RGB32);
QPainter painter(&image);
QSvgRenderer renderer(data);
compareTransforms(painter.worldTransform(), renderer.transformForElement("ichi"_L1));
painter.translate(-3, 1);
compareTransforms(painter.worldTransform(), renderer.transformForElement("ni"_L1));
painter.rotate(45);
compareTransforms(painter.worldTransform(), renderer.transformForElement("san"_L1));
painter.scale(4, 2);
compareTransforms(painter.worldTransform(), renderer.transformForElement("yon"_L1));
painter.setWorldTransform(QTransform(1, 2, 3, 4, 5, 6), true);
compareTransforms(painter.worldTransform(), renderer.transformForElement("firkant"_L1));
}
void tst_QSvgRenderer::boundsOnElement() const
{
QByteArray data(R"(<svg>
<g id="prim" transform="translate(-3,1) ">
<g id="sjokade" stroke="none" stroke-width="10">
<rect id="kaviar" transform="rotate(45) " x="-10" y="-10" width="20" height="20"/>
</g>
<g id="nugatti" stroke="black" stroke-width="2">
<use x="0" y="0" transform="rotate(45) " xlink:href="#kaviar"/>
</g>
<g id="nutella" stroke="none" stroke-width="10">
<path id="baconost" transform="rotate(45) " d="M-10 -10 L10 -10 L10 10 L-10 10 Z"/>
</g>
<g id="hapaa" transform="translate(-2,2) " stroke="black" stroke-width="2">
<use x="0" y="0" transform="rotate(45) " xlink:href="#baconost"/>
</g>
</g>
<text id="textA" x="50" y="100">Lorem ipsum</text>
<text id="textB" transform="matrix(1 0 0 1 50 100) ">Lorem ipsum</text>
<g id="textGroup">
<text id="textC" transform="matrix(1 0 0 2 20 10) ">Lorem ipsum</text>
<text id="textD" transform="matrix(1 0 0 2 30 40) ">Lorem ipsum</text>
</g>
</svg>)");
qreal sqrt2 = qSqrt(2);
QSvgRenderer renderer(data);
QCOMPARE(renderer.boundsOnElement("sjokade"_L1),
QRectF(-10 * sqrt2, -10 * sqrt2, 20 * sqrt2, 20 * sqrt2));
QCOMPARE(renderer.boundsOnElement("kaviar"_L1),
QRectF(-10 * sqrt2, -10 * sqrt2, 20 * sqrt2, 20 * sqrt2));
QCOMPARE(renderer.boundsOnElement("nugatti"_L1),
QRectF(-11, -11, 22, 22));
QCOMPARE(renderer.boundsOnElement("nutella"_L1),
QRectF(-10 * sqrt2, -10 * sqrt2, 20 * sqrt2, 20 * sqrt2));
QCOMPARE(renderer.boundsOnElement("baconost"_L1),
QRectF(-10 * sqrt2, -10 * sqrt2, 20 * sqrt2, 20 * sqrt2));
QCOMPARE(renderer.boundsOnElement("hapaa"_L1),
QRectF(-13, -9, 22, 22));
QCOMPARE(renderer.boundsOnElement("prim"_L1),
QRectF(-10 * sqrt2 - 3, -10 * sqrt2 + 1, 20 * sqrt2, 20 * sqrt2));
QRectF textBoundsA = renderer.boundsOnElement("textA"_L1);
QVERIFY(!textBoundsA.isEmpty());
QCOMPARE(renderer.boundsOnElement("textB"_L1), textBoundsA);
QRectF cBounds = renderer.boundsOnElement("textC"_L1);
QRectF dBounds = renderer.boundsOnElement("textD"_L1);
QVERIFY(!cBounds.isEmpty());
QCOMPARE(cBounds.size(), dBounds.size());
QRectF groupBounds = renderer.boundsOnElement("textGroup"_L1);
QCOMPARE(groupBounds, cBounds | dBounds);
}
void tst_QSvgRenderer::gradientStops() const
{
{
QByteArray data(R"(<svg>
<defs>
<linearGradient id="gradient">
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="64" width="64" x="0" y="0"/>
</svg>)");
QSvgRenderer renderer(data);
QImage image(64, 64, QImage::Format_ARGB32_Premultiplied);
QImage refImage(64, 64, QImage::Format_ARGB32_Premultiplied);
image.fill(0x87654321);
refImage.fill(0x87654321);
QPainter painter(&image);
renderer.render(&painter);
QCOMPARE(image, refImage);
}
{
QByteArray data(R"(<svg>
<defs>
<linearGradient id="gradient">
<stop offset="1" stop-color="cyan"/>
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="64" width="64" x="0" y="0"/>
</svg>)");
QSvgRenderer renderer(data);
QImage image(64, 64, QImage::Format_ARGB32_Premultiplied);
QImage refImage(64, 64, QImage::Format_ARGB32_Premultiplied);
refImage.fill(0xff00ffff);
QPainter painter(&image);
renderer.render(&painter);
QCOMPARE(image, refImage);
}
{
QByteArray data(R"(<svg>
<defs>
<linearGradient id="gradient">
<stop offset="0" stop-color="red"/>
<stop offset="0" stop-color="cyan"/>
<stop offset="0.5" stop-color="cyan"/>
<stop offset="0.5" stop-color="magenta"/>
<stop offset="0.5" stop-color="yellow"/>
<stop offset="1" stop-color="yellow"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="64" width="64" x="0" y="0"/>
</svg>)");
QSvgRenderer renderer(data);
QImage image(64, 64, QImage::Format_ARGB32_Premultiplied);
QImage refImage(64, 64, QImage::Format_ARGB32_Premultiplied);
QPainter painter;
painter.begin(&refImage);
painter.fillRect(QRectF(0, 0, 32, 64), Qt::cyan);
painter.fillRect(QRectF(32, 0, 32, 64), Qt::yellow);
painter.end();
painter.begin(&image);
renderer.render(&painter);
painter.end();
QCOMPARE(image, refImage);
}
}
void tst_QSvgRenderer::gradientRefs()
{
const char *svgs[] = {
R"(<svg>
<defs>
<linearGradient id="gradient">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="8" width="256" x="0" y="0"/>
</svg>)",
R"(<svg>
<defs>
<linearGradient id="gradient" xlink:href="#gradient0">
</linearGradient>
<linearGradient id="gradient0">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="8" width="256" x="0" y="0"/>
</svg>)",
R"(<svg>
<defs>
<linearGradient id="gradient0">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
<linearGradient id="gradient" xlink:href="#gradient0">
</linearGradient>
</defs>
<rect fill="url(#gradient) " height="8" width="256" x="0" y="0"/>
</svg>)",
R"(<svg>
<rect fill="url(#gradient) " height="8" width="256" x="0" y="0"/>
<defs>
<linearGradient id="gradient0">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
<linearGradient id="gradient" xlink:href="#gradient0">
</linearGradient>
</defs>
</svg>)",
R"(<svg>
<defs>
<linearGradient xlink:href="#0" id="0">
<stop offset="0" stop-color="red" stop-opacity="0"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
</defs>
<rect fill="url(#0) " height="8" width="256" x="0" y="0"/>
</svg>)"
};
for (size_t i = 0 ; i < sizeof(svgs) / sizeof(svgs[0]) ; ++i)
{
QByteArray data = svgs[i];
QSvgRenderer renderer(data);
QImage image(256, 8, QImage::Format_ARGB32_Premultiplied);
image.fill(0);
QPainter painter(&image);
renderer.render(&painter);
const QRgb *line = reinterpret_cast<QRgb *>(image.scanLine(3));
QRgb left = line[0]; // transparent black
QRgb mid = line[127]; // semi transparent magenta
QRgb right = line[255]; // opaque blue
QVERIFY(qAlpha(left) < 3 && qRed(left) < 3 && qGreen(left) == 0 && qBlue(left) < 3);
QVERIFY(qAbs(qAlpha(mid) - 127) < 3 && qAbs(qRed(mid) - 63) < 4
&& qGreen(mid) == 0 && qAbs(qBlue(mid) - 63) < 4);
QVERIFY(qAlpha(right) > 253 && qRed(right) < 3 && qGreen(right) == 0
&& qBlue(right) > 251);
}
}
void tst_QSvgRenderer::recursiveRefs_data()
{
QTest::addColumn<QByteArray>("svg");
QTest::addColumn<QByteArray>("expectedWarning");
QTest::newRow("single") << R"(<svg><linearGradient id="0" xlink:href="#0"/>
<rect x="0" y="0" width="20" height="20" fill="url(#0) "/>
</svg>)"_ba
<< "Could not resolve property : 0"_ba;
QTest::newRow("double") << R"(<svg><linearGradient id="0" xlink:href="#1"/>
<linearGradient id="1" xlink:href="#0"/>
<rect x="0" y="0" width="20" height="20" fill="url(#0) "/>
</svg>)"_ba
<< "Could not resolve property : 1"_ba;
QTest::newRow("triple") << R"(<svg>
<linearGradient id="0" xlink:href="#1"/>
<linearGradient id="1" xlink:href="#2"/>
<linearGradient id="2" xlink:href="#0"/>
<rect x="0" y="0" width="20" height="20" fill="url(#0) "/>
</svg>)"_ba
<< "Could not resolve property : 1"_ba;
QTest::newRow("pattern") << R"(<svg><pattern id="pattern" width="4" height="4"
fill="url(#pattern) ">
<rect width="2" height="2" fill=" "/></pattern>
<rect width="2" height="2" fill="url(#pattern) "/>
</svg>)"_ba
<< "The pattern is trying to render itself recursively. "
"Returning a transparent QImage of the right size."_ba;
// lead to division by zero in QSvgPattern::patternImage while loading document
QTest::newRow("pattern-no-elements") << R"(<svg><pattern id="pattern" width="4" height="4"
fill="url(#pattern) "/>
</svg>)"_ba
<< QByteArray();
QTest::newRow("markers") << R"(<svg><marker id="mark" markerWidth="10" markerHeight="10"
viewBox="0 0 1 1" refX="0" refY="0.5">
<rect x="0" y="0" width="1" height="1" fill="red"
marker-end="url(#mark)/></marker>
</svg>)"_ba
<< QByteArray();
QTest::newRow("symbol") << R"(<svg><symbol id="dot" width="100" height="100" viewBox="0 0 1 1">
<use href="#dot" x="0" y="0"/></symbol>
</svg>)"_ba
<< "link dot is recursive!"_ba;
}
void tst_QSvgRenderer::recursiveRefs()
{
QFETCH(QByteArray, svg);
QFETCH(QByteArray, expectedWarning);
QImage image(20, 20, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::green);
QImage refImage = image.copy();
if (!expectedWarning.isEmpty())
QTest::ignoreMessage(QtWarningMsg, expectedWarning.data());
QSvgRenderer renderer(svg);
QPainter painter(&image);
renderer.render(&painter);
QCOMPARE(image, refImage);
}
#ifndef QT_NO_COMPRESS
void tst_QSvgRenderer::testGzLoading()
{
QSvgRenderer renderer(QFINDTESTDATA("heart.svgz"));
QVERIFY(renderer.isValid());
QSvgRenderer resourceRenderer(":/heart.svgz"_L1);
QVERIFY(resourceRenderer.isValid());
QFile largeFileGz(QFINDTESTDATA("large.svgz"));
QVERIFY(largeFileGz.open(QIODevice::ReadOnly));
QByteArray data = largeFileGz.readAll();
QSvgRenderer autoDetectGzData(data);
QVERIFY(autoDetectGzData.isValid());
}
# ifdef QT_BUILD_INTERNAL
QT_BEGIN_NAMESPACE
QByteArray qt_inflateGZipDataFrom(QIODevice *device);
QT_END_NAMESPACE
void tst_QSvgRenderer::testGzHelper_data()
{
QTest::addColumn<QByteArray>("in");
QTest::addColumn<QByteArray>("out");
QTest::newRow("empty") << QByteArray() << QByteArray();
QTest::newRow("small") << QByteArray::fromHex(QByteArray("1f8b08005819934800034b"
"cbcfe70200a865327e04000000")) << QByteArray("foo\n");
QFile largeFileGz(QFINDTESTDATA("large.svgz"));
QVERIFY(largeFileGz.open(QIODevice::ReadOnly));
QFile largeFile(QFINDTESTDATA("large.svg"));
QVERIFY(largeFile.open(QIODevice::ReadOnly));
QTest::newRow("large") << largeFileGz.readAll() << largeFile.readAll();
QTest::newRow("zeroes") << QByteArray::fromHex(QByteArray("1f8b0800131f9348000333"
"301805a360148c54c00500d266708601040000")) << QByteArray(1024, '0').append('\n');
QTest::newRow("twoMembers") << QByteArray::fromHex(QByteArray("1f8b08001c2a934800034b"
"cbcfe70200a865327e040000001f8b08001c2a934800034b4a2ce20200e9b3a20404000000"))
<< QByteArray("foo\nbar\n");
QTest::newRow("corruptedSecondMember") << QByteArray::fromHex(QByteArray("1f8b08001c2a934800034b"
"cbcfe70200a865327e040000001f8c08001c2a934800034b4a2ce20200e9b3a20404000000"))
<< QByteArray();
}
void tst_QSvgRenderer::testGzHelper()
{
QFETCH(QByteArray, in);
QFETCH(QByteArray, out);
QBuffer buffer(&in);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QVERIFY(buffer.isReadable());
QByteArray result = qt_inflateGZipDataFrom(&buffer);
QCOMPARE(result, out);
}
# endif // #ifdef QT_BUILD_INTERNAL
#endif // #ifndef QT_NO_COMPRESS
void tst_QSvgRenderer::fillRule_data()
{
QTest::addColumn<const QByteArray>("svg");
QTest::addColumn<bool>("nonzero");
QTest::newRow("paths-default")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<path d="M10 0 L30 0 L30 30 L0 30 L0 10 L20 10 L20 20 L10 20 Z" )"_ba
R"(fill="red"/></svg>)"_ba << true;
QTest::newRow("paths-nonzero")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<path d="M10 0 L30 0 L30 30 L0 30 L0 10 L20 10 L20 20 L10 20 Z" )"_ba
R"(fill="red" fill-rule="nonzero"/></svg>)"_ba << true;
QTest::newRow("paths-evenodd")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<path d="M10 0 L30 0 L30 30 L0 30 L0 10 L20 10 L20 20 L10 20 Z" )"_ba
R"(fill="red" fill-rule="evenodd"/></svg>)"_ba << false;
QTest::newRow("polygons-default")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<polygon points="10 0 30 0 30 30 0 30 0 10 20 10 20 20 10 20" )"_ba
R"(fill="red"/></svg>)"_ba << true;
QTest::newRow("polygons-nonzero")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<polygon points="10 0 30 0 30 30 0 30 0 10 20 10 20 20 10 20" )"_ba
R"(fill="red" fill-rule="nonzero"/></svg>)"_ba << true;
QTest::newRow("polygons-evenodd")
<< R"(<svg><rect x="0" y="0" height="30" width="30" fill="blue"/>)"_ba
R"(<polygon points="10 0 30 0 30 30 0 30 0 10 20 10 20 20 10 20" )"_ba
R"(fill="red" fill-rule="evenodd"/></svg>)"_ba << false;
}
void tst_QSvgRenderer::fillRule()
{
QFETCH(const QByteArray, svg);
QFETCH(bool, nonzero);
QImage refImage(30, 30, QImage::Format_ARGB32_Premultiplied);
{
refImage.fill(0xffff0000);
QPainter p(&refImage);
if (nonzero) {
p.fillRect(QRectF(0, 0, 10, 10), Qt::blue);
} else {
p.fillRect(QRectF(0, 0, 10, 10), Qt::blue);
p.fillRect(QRectF(10, 10, 10, 10), Qt::blue);
}
}
QByteArray data(svg);
QSvgRenderer renderer(data);
QImage image(30, 30, QImage::Format_ARGB32_Premultiplied);
image.fill(0);
QPainter p(&image);
renderer.render(&p);
QCOMPARE(image, refImage);
}
void tst_QSvgRenderer::opacity_data()
{
QTest::addColumn<const QByteArray>("svg");
constexpr uint count = 5;
constexpr std::array<QByteArrayView, count> opacities{"-1.4641"_L1, "0"_L1, "0.5"_L1, "1"_L1, "1.337"_L1};
constexpr std::array<QByteArrayView, count> colors1{"#7f7f7f"_L1, "#7f7f7f"_L1, "#402051"_L1, "blue"_L1, "#123456"_L1};
constexpr std::array<QByteArrayView, count> colors2{"red"_L1, "#bad"_L1, "#bedead"_L1, "#7f7f7f"_L1, "#7f7f7f"_L1};
for (uint i = 0; i < count; i++) {
QByteArray data = R"(<svg><rect x="0" y="0" height="10" width="10" )"_ba
R"(fill=")"_ba + colors1[i] + R"("/>)"_ba
R"(<rect x="0" y="0" height="10" width="10" )"_ba
R"(fill=")"_ba + colors2[i] + R"(" )"_ba
R"(fill-opacity=")"_ba + opacities[i] + R"("/></svg>)"_ba;
QTest::newRow(("fill-opacity" + QString::number(i + 1)).toStdString().c_str()) << data;
}
for (uint i = 0; i < count; i++) {
QByteArray data = R"(<svg viewBox="0 0 10 10"><polyline points="0 5 10 5" fill="none" )"_ba
R"(stroke=")"_ba + colors1[i] + R"(" stroke-width="10"/>)"_ba
R"(<line x1="5" y1="0" x2="5" y2="10" fill="none" )"_ba
R"(stroke=")"_ba + colors2[i] + R"(" stroke-width="10" )"_ba
R"(stroke-opacity=")"_ba + opacities[i] + R"("/></svg>)"_ba;
QTest::newRow(("stroke-opacity" + QString::number(i + 1)).toStdString().c_str()) << data;
}
for (uint i = 0; i < count; i++) {
QByteArray data = R"(<svg><defs><linearGradient id="gradient">)"_ba
R"(<stop offset="0" stop-color=")"_ba + colors2[i] + R"("/>)"_ba
R"(<stop offset="1" stop-color=")"_ba + colors2[i] + R"("/>)"_ba
R"(</linearGradient></defs><rect x="0" y="0" height="10" width="10" )"_ba
R"(fill=")"_ba + colors1[i] + R"("/>)"_ba
R"(<rect x="0" y="0" height="10" width="10" fill="url(#gradient) " )"_ba
R"(fill-opacity=")"_ba + opacities[i] + R"("/></svg>)"_ba;
QTest::newRow(("grads-fill-opacity" + QString::number(i + 1)).toStdString().c_str()) << data;
}
for (uint i = 0; i < count; i++) {
QByteArray data = R"(<svg viewBox="0 0 10 10"><defs><linearGradient id="grad">)"_ba
R"(<stop offset="0" stop-color=")"_ba + colors2[i] + R"("/>)"_ba
R"(<stop offset="1" stop-color=")"_ba + colors2[i] + R"("/>)"_ba
R"(</linearGradient></defs><polyline points="0 5 10 5" fill="none" )"_ba
R"(stroke=")"_ba + colors1[i] + R"(" stroke-width="10"/>)"_ba
R"(<line x1="5" y1="0" x2="5" y2="10" fill="none" )"_ba
R"(stroke="url(#grad) " stroke-width="10" )"_ba
R"(stroke-opacity=")"_ba + opacities[i] + R"("/></svg>)"_ba;
QTest::newRow(("grads-stroke-opacity" + QString::number(i + 1)).toStdString().c_str()) << data;
}
}
void tst_QSvgRenderer::opacity()
{
QFETCH(const QByteArray, svg);
QSvgRenderer renderer(svg);
QVERIFY(renderer.isValid());
QImage image(10, 10, QImage::Format_ARGB32_Premultiplied);
image.fill(0xffff00ff);
QPainter painter(&image);
renderer.render(&painter);
painter.end();
QCOMPARE(image.pixel(5, 5), 0xff7f7f7f);
}
void tst_QSvgRenderer::groupOpacity()
{
// group opacity QTBUG-122310
const QByteArray data = R"svg(
<g transform="translate(0, 0)">
<rect fill="#808080" x="0" y="0" width="10" height="10" fill-opacity="0.5"/>
<rect fill="#808080" x="5" y="5" width="10" height="10" fill-opacity="0.5"/>
</g>
<g transform="translate(20, 0)" fill-opacity="0.5">
<rect fill="#808080" x="0" y="0" width="10" height="10"/>
<rect fill="#808080" x="5" y="5" width="10" height="10"/>
</g>
<g transform="translate(0, 20)">
<rect fill="#808080" x="0" y="0" width="10" height="10" opacity="0.5"/>
<rect fill="#808080" x="5" y="5" width="10" height="10" opacity="0.5"/>
</g>
<g transform="translate(20, 20)" opacity="0.5">
<rect fill="#808080" x="0" y="0" width="10" height="10"/>
<rect fill="#808080" x="5" y="5" width="10" height="10"/>
</g>
</svg>
)svg"_ba;
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
QImage image(140, 140, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::white);
QPainter p;
p.begin(&image);
renderer.render(&p);
p.end();
const QRgb lightGray(0xffc0c0c0);
const QRgb gray(0xffa0a0a0);
QCOMPARE(image.pixel(QPoint(10, 10)), lightGray);
QCOMPARE(image.pixel(QPoint(30, 30)), gray);
QCOMPARE(image.pixel(QPoint(90, 10)), lightGray);
QCOMPARE(image.pixel(QPoint(110, 30)), gray);
QCOMPARE(image.pixel(QPoint(10, 90)), lightGray);
QCOMPARE(image.pixel(QPoint(30, 110)), gray);
QCOMPARE(image.pixel(QPoint(90, 90)), lightGray);
QCOMPARE(image.pixel(QPoint(110, 110)), lightGray);
}
void tst_QSvgRenderer::paths()
{
static const char *svgs[] = {
// Absolute coordinates, explicit commands.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\"M50 0 V50 H0 Q0 25 25 25 T50 0 C25 0 50 50 25 50 S25 0 0 0 Z\" fill=\"red\" fill-rule=\"evenodd\"/>"
"</svg>",
// Absolute coordinates, implicit commands.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\"M50 0 50 50 0 50 Q0 25 25 25 Q50 25 50 0 C25 0 50 50 25 50 C0 50 25 0 0 0 Z\" fill=\"red\" fill-rule=\"evenodd\" />"
"</svg>",
// Relative coordinates, explicit commands.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\"m50 0 v50 h-50 q0 -25 25 -25 t25 -25 c-25 0 0 50 -25 50 s0 -50 -25 -50 z\" fill=\"red\" fill-rule=\"evenodd\" />"
"</svg>",
// Relative coordinates, implicit commands.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\"m50 0 0 50 -50 0 q0 -25 25 -25 25 0 25 -25 c-25 0 0 50 -25 50 -25 0 0 -50 -25 -50 z\" fill=\"red\" fill-rule=\"evenodd\" />"
"</svg>",
// Absolute coordinates, explicit commands, minimal whitespace.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\"m50 0v50h-50q0-25 25-25t25-25c-25 0 0 50-25 50s0-50-25-50z\" fill=\"red\" fill-rule=\"evenodd\" />"
"</svg>",
// Absolute coordinates, explicit commands, extra whitespace.
"<svg>"
" <rect x=\"0\" y=\"0\" height=\"50\" width=\"50\" fill=\"blue\" />"
" <path d=\" M 50 0 V 50 H 0 Q 0 25 25 25 T 50 0 C 25 0 50 50 25 50 S 25 0 0 0 Z \" fill=\"red\" fill-rule=\"evenodd\" />"
"</svg>"
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
images[i] = QImage(50, 50, QImage::Format_ARGB32_Premultiplied);
images[i].fill(0);
p.begin(&images[i]);
renderer.render(&p);
p.end();
if (i != 0) {
QCOMPARE(images[i], images[0]);
}
}
}
void tst_QSvgRenderer::paths2()
{
const char *svg =
<path d="M 3 8 A 5 5 0 1013 8" id="path1"/>
</svg>)";
QByteArray data(svg);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
QCOMPARE(renderer.boundsOnElement("path1"_L1).toRect(), QRect(3, 8, 10, 5));
}
void tst_QSvgRenderer::emptyPath_data()
{
QTest::addColumn<QByteArray>("svg");
// This caused a division by zero in in QPainterPath::angleAtPercent, reported by UBSAN.
QTest::newRow("empty")
<< (R"(<svg><path marker-end="url(#w) "/></svg>)"_ba);
QTest::newRow("zero-movements")
<< (R"(<svg><path d="M1 2L1 2l0 0H1h0V2v0Z" marker-end="url(#w) "/></svg>)"_ba);
}
void tst_QSvgRenderer::emptyPath()
{
QFETCH(QByteArray, svg);
QImage image(377, 233, QImage::Format_RGB32);
QPainter painter(&image);
QSvgRenderer renderer(svg);
renderer.render(&painter);
}
void tst_QSvgRenderer::displayMode()
{
static const char *svgs[] = {
// All visible.
R"(<svg>
<g>
<rect x="0" y="0" height="10" width="10" fill="red" />
<rect x="0" y="0" height="10" width="10" fill="blue" />
</g>
</svg>)",
// Don't display svg element.
R"(<svg display="none">
<g>
<rect x="0" y="0" height="10" width="10" fill="red" />
<rect x="0" y="0" height="10" width="10" fill="blue" />
</g>
</svg>)",
// Don't display g element.
R"(<svg>
<g display="none">
<rect x="0" y="0" height="10" width="10" fill="red" />
<rect x="0" y="0" height="10" width="10" fill="blue" />
</g>
</svg>)",
// Don't display first rect element.
R"(<svg>
<g>
<rect x="0" y="0" height="10" width="10" fill="red" display="none" />
<rect x="0" y="0" height="10" width="10" fill="blue" />
</g>
</svg>)",
// Don't display second rect element.
R"(<svg>
<g>
<rect x="0" y="0" height="10" width="10" fill="red" />
<rect x="0" y="0" height="10" width="10" fill="blue" display="none" />
</g>
</svg>)",
// Don't display svg element, but set display mode to "inline" for other elements.
R"(<svg display="none">
<g display="inline">
<rect x="0" y="0" height="10" width="10" fill="red" display="inline" />
<rect x="0" y="0" height="10" width="10" fill="blue" display="inline" />
</g>
</svg>)"
};
QRgb expectedColors[] = {0xff0000ff, 0xff00ff00, 0xff00ff00, 0xff0000ff, 0xffff0000, 0xff00ff00};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
QImage image(10, 10, QImage::Format_ARGB32_Premultiplied);
image.fill(0xff00ff00);
p.begin(&image);
renderer.render(&p);
p.end();
QCOMPARE(image.pixel(5, 5), expectedColors[i]);
}
}
void tst_QSvgRenderer::strokeInherit()
{
static const char *svgs[] = {
// Reference.
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25"/>
</g>
</svg>)",
// stroke
R"(<svg viewBox="0 0 200 30">
<g stroke="none" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke="blue"/>
</g>
<g stroke="yellow" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25" stroke="green"/>
</g>
</svg>)",
// stroke-width
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="0" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-width="20"/>
</g>
<g stroke="green" stroke-width="10" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25" stroke-width="0"/>
</g>
</svg>)",
// stroke-linecap
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="round"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-linecap="butt"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25"/>
</g>
</svg>)",
// stroke-linejoin
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="round" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-linejoin="miter"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25"/>
</g>
</svg>)",
// stroke-miterlimit
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="2" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-miterlimit="1"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25"/>
</g>
</svg>)",
// stroke-dasharray
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="1,1,1,1,1,1,3,1,3,1,3,1,1,1,1,1,1,3"
stroke-dashoffset="10" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-dasharray="20,10"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="none" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25" stroke-dasharray="3,3,1"/>
</g>
</svg>)",
// stroke-dashoffset
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="0" stroke-opacity="0.5">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-dashoffset="10"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="0">
<polyline fill="none" points="10 25 80 25" stroke-dashoffset="4.5"/>
</g>
</svg>)",
// stroke-opacity
R"(<svg viewBox="0 0 200 30">
<g stroke="blue" stroke-width="20" stroke-linecap="butt"
stroke-linejoin="miter" stroke-miterlimit="1" stroke-dasharray="20,10"
stroke-dashoffset="10" stroke-opacity="0">
<polyline fill="none" points="10 10 100 10 100 20 190 20" stroke-opacity="0.5"/>
</g>
<g stroke="green" stroke-width="0" stroke-dasharray="3,3,1" stroke-dashoffset="4.5">
<polyline fill="none" points="10 25 80 25"/>
</g>
</svg>)"
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
images[i] = QImage(200, 30, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
if (i != 0) {
QCOMPARE(images[0], images[i]);
}
}
}
void tst_QSvgRenderer::testFillInheritance()
{
static const char *svgs[] = {
//reference
R"(<svg viewBox = "0 0 200 200">
<polygon points="20,20 50,120 100,10 40,80 50,80" fill= "red" stroke = "blue" fill-opacity = "0.5" fill-rule = "evenodd"/>
</svg>)",
R"(<svg viewBox = "0 0 200 200">
<polygon points="20,20 50,120 100,10 40,80 50,80" fill= "red" stroke = "blue" fill-opacity = "0.5" fill-rule = "evenodd"/>
<rect x = "40" y = "40" width = "70" height ="20" fill = "green" fill-opacity = "0"/>
</svg>)",
R"(<svg viewBox = "0 0 200 200">
<g fill = "red" fill-opacity = "0.5" fill-rule = "evenodd">
<polygon points="20,20 50,120 100,10 40,80 50,80" stroke = "blue"/>
</g>
<rect x = "40" y = "40" width = "70" height ="20" fill = "green" fill-opacity = "0"/>
</svg>)",
R"(<svg viewBox = "0 0 200 200">
<g fill = "green" fill-rule = "nonzero">
<polygon points="20,20 50,120 100,10 40,80 50,80" stroke = "blue" fill = "red" fill-opacity = "0.5" fill-rule = "evenodd"/>
</g>
<g fill-opacity = "0.8" fill = "red">
<rect x = "40" y = "40" width = "70" height ="20" fill = "green" fill-opacity = "0"/>
</g>
</svg>)",
R"(<svg viewBox = "0 0 200 200">
<g fill = "red" >
<g fill-opacity = "0.5">
<g fill-rule = "evenodd">
<g>
<polygon points="20,20 50,120 100,10 40,80 50,80" stroke = "blue"/>
</g>
</g>
</g>
</g>
<g fill-opacity = "0.8" >
<rect x = "40" y = "40" width = "70" height ="20" fill = "none"/>
</g>
</svg>)",
R"(<svg viewBox = "0 0 200 200">
<g fill = "none" fill-opacity = "0">
<polygon points="20,20 50,120 100,10 40,80 50,80" stroke = "blue" fill = "red" fill-opacity = "0.5" fill-rule = "evenodd"/>
</g>
<g fill-opacity = "0" >
<rect x = "40" y = "40" width = "70" height ="20" fill = "green"/>
</g>
</svg>)"
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
images[i] = QImage(200, 200, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
if (i != 0) {
QCOMPARE(images[0], images[i]);
}
}
}
void tst_QSvgRenderer::testStopOffsetOpacity()
{
static const char *svgs[] = {
//reference
R"(<svg viewBox="0 0 64 64">
<radialGradient id="MyGradient1" gradientUnits="userSpaceOnUse" cx="50" cy="50" r="30" fx="20" fy="20">
<stop offset="0.0" style="stop-color:red" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:green" stop-opacity="1"/>
<stop offset="1" style="stop-color:yellow" stop-opacity="1"/>
</radialGradient>
<radialGradient id="MyGradient2" gradientUnits="userSpaceOnUse" cx="50" cy="70" r="70" fx="20" fy="20">
<stop offset="0.0" style="stop-color:blue" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:violet" stop-opacity="1"/>
<stop offset="1" style="stop-color:orange" stop-opacity="1"/>
</radialGradient>
<rect x="5" y="5" width="55" height="55" fill="url(#MyGradient1) " stroke="black" />
<rect x="20" y="20" width="35" height="35" fill="url(#MyGradient2) "/>
</svg>)",
//Stop Offset
R"(<svg viewBox="0 0 64 64">
<radialGradient id="MyGradient1" gradientUnits="userSpaceOnUse" cx="50" cy="50" r="30" fx="20" fy="20">
<stop offset="abc" style="stop-color:red" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:green" stop-opacity="1"/>
<stop offset="1" style="stop-color:yellow" stop-opacity="1"/>
</radialGradient>
<radialGradient id="MyGradient2" gradientUnits="userSpaceOnUse" cx="50" cy="70" r="70" fx="20" fy="20">
<stop offset="-3.bc" style="stop-color:blue" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:violet" stop-opacity="1"/>
<stop offset="1" style="stop-color:orange" stop-opacity="1"/>
</radialGradient>
<rect x="5" y="5" width="55" height="55" fill="url(#MyGradient1) " stroke="black" />
<rect x="20" y="20" width="35" height="35" fill="url(#MyGradient2) "/>
</svg>)",
//Stop Opacity
R"(<svg viewBox="0 0 64 64">
<radialGradient id="MyGradient1" gradientUnits="userSpaceOnUse" cx="50" cy="50" r="30" fx="20" fy="20">
<stop offset="0.0" style="stop-color:red" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:green" stop-opacity="x.45"/>
<stop offset="1" style="stop-color:yellow" stop-opacity="-3.abc"/>
</radialGradient>
<radialGradient id="MyGradient2" gradientUnits="userSpaceOnUse" cx="50" cy="70" r="70" fx="20" fy="20">
<stop offset="0.0" style="stop-color:blue" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:violet" stop-opacity="-0.xy"/>
<stop offset="1" style="stop-color:orange" stop-opacity="z.5"/>
</radialGradient>
<rect x="5" y="5" width="55" height="55" fill="url(#MyGradient1) " stroke="black" />
<rect x="20" y="20" width="35" height="35" fill="url(#MyGradient2) "/>
</svg>)",
//Stop offset and Stop opacity
R"(<svg viewBox="0 0 64 64">
<radialGradient id="MyGradient1" gradientUnits="userSpaceOnUse" cx="50" cy="50" r="30" fx="20" fy="20">
<stop offset="abc" style="stop-color:red" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:green" stop-opacity="x.45"/>
<stop offset="1" style="stop-color:yellow" stop-opacity="-3.abc"/>
</radialGradient>
<radialGradient id="MyGradient2" gradientUnits="userSpaceOnUse" cx="50" cy="70" r="70" fx="20" fy="20">
<stop offset="-3.bc" style="stop-color:blue" stop-opacity="0.3"/>
<stop offset="0.5" style="stop-color:violet" stop-opacity="-0.xy"/>
<stop offset="1" style="stop-color:orange" stop-opacity="z.5"/>
</radialGradient>
<rect x="5" y="5" width="55" height="55" fill="url(#MyGradient1) " stroke="black" />
<rect x="20" y="20" width="35" height="35" fill="url(#MyGradient2) "/>
</svg>)"
};
QImage images[4];
QPainter p;
for (int i = 0; i < 4; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
QVERIFY(renderer.isValid());
images[i] = QImage(64, 64, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
}
QCOMPARE(images[0], images[1]);
QCOMPARE(images[0], images[2]);
QCOMPARE(images[0], images[3]);
}
void tst_QSvgRenderer::testUseElement()
{
static const char *svgs[] = {
// 0 - Use referring to non group node (1)
R"(<svg viewBox = "0 0 200 200">"
<polygon points="20,20 50,120 100,10 40,80 50,80"/>"
<polygon points="20,80 50,180 100,70 40,140 50,140" fill= "red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" stroke-width = "3"/>"
</svg>)",
// 1
R"(<svg viewBox = "0 0 200 200">"
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80"/>"
<use y = "60" xlink:href = "#usedPolyline" fill= "red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" stroke-width = "3"/>"
</svg>)",
// 2
R"(<svg viewBox = "0 0 200 200">"
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80"/>"
<g fill = " red" fill-opacity ="0.2">"
<use y = "60" xlink:href = "#usedPolyline" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" stroke-width = "3"/>"
</g>"
</svg>)",
// 3
R"(<svg viewBox = "0 0 200 200">
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80"/>
<g stroke-width = "3" stroke = "yellow">
<use y = "60" xlink:href = "#usedPolyline" fill = " red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd"/>
</g>
</svg>)",
// 4 - Use referring to non group node (2)
R"(<svg viewBox = "0 0 200 200">
<polygon points="20,20 50,120 100,10 40,80 50,80" fill = "green" fill-rule = "nonzero" stroke = "purple" stroke-width = "4" stroke-dasharray = "1,1,3,1" stroke-offset = "3" stroke-miterlimit = "6" stroke-linecap = "butt" stroke-linejoin = "round"/>
<polygon points="20,80 50,180 100,70 40,140 50,140" fill= "red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" stroke-width = "3" stroke-dasharray = "1,1,1,1" stroke-offset = "5" stroke-miterlimit = "3" stroke-linecap = "butt" stroke-linejoin = "square"/>
</svg>)",
// 5
R"(<svg viewBox = "0 0 200 200">
<g fill = "green" fill-rule = "nonzero" stroke = "purple" stroke-width = "4" stroke-dasharray = "1,1,3,1" stroke-offset = "3" stroke-miterlimit = "6" stroke-linecap = "butt" stroke-linejoin = "round">
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80" />
</g>
<g stroke = "blue" stroke-width = "3" stroke-dasharray = "1,1,1,1" stroke-offset = "5" stroke-miterlimit = "3" stroke-linecap = "butt" stroke-linejoin = "square">
<use y = "60" xlink:href = "#usedPolyline" fill-opacity = "0.7" fill= "red" stroke = "blue" fill-rule = "evenodd"/>
</g>
</svg>)",
// 6
R"(<svg viewBox = "0 0 200 200">
<g fill = "green" fill-rule = "nonzero" stroke = "purple" stroke-width = "4" stroke-dasharray = "1,1,3,1" stroke-offset = "3" stroke-miterlimit = "6" stroke-linecap = "butt" stroke-linejoin = "round">
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80" />
</g>
<g stroke-width = "3" stroke-dasharray = "1,1,1,1" stroke-offset = "5" stroke-miterlimit = "3" stroke-linecap = "butt" stroke-linejoin = "square" >
<use y = "60" xlink:href = "#usedPolyline" fill= "red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" />
</g>
</svg>)",
// 7 - Use referring to group node
R"(<svg viewBox = "0 0 200 200">
<g>
<circle cx="0" cy="0" r="100" fill = "red" fill-opacity = "0.6"/>
<rect x = "10" y = "10" width = "30" height = "30" fill = "red" fill-opacity = "0.5"/>
<circle fill="#a6ce39" cx="0" cy="0" r="33" fill-opacity = "0.5"/>
</g>
</svg>)",
// 8
R"(<svg viewBox = "0 0 200 200">
<defs>
<g id="usedG">
<circle cx="0" cy="0" r="100" fill-opacity = "0.6"/>
<rect x = "10" y = "10" width = "30" height = "30"/>
<circle fill="#a6ce39" cx="0" cy="0" r="33" />
</g>
</defs>
<use xlink:href ="#usedG" fill = "red" fill-opacity ="0.5"/>
</svg>)",
// 9
R"(<svg viewBox = "0 0 200 200">
<defs>
<g fill = "blue" fill-opacity = "0.3">
<g id="usedG">
<circle cx="0" cy="0" r="100" fill-opacity = "0.6"/>
<rect x = "10" y = "10" width = "30" height = "30"/>
<circle fill="#a6ce39" cx="0" cy="0" r="33" />
</g>
</g>
</defs>
<g fill = "red" fill-opacity ="0.5">
<use xlink:href ="#usedG" />
</g>
</svg>)",
// 10 - Self referral, should be ignored
R"(<svg><g id="0"><use xlink:href="#0" /></g></svg>)",
// 11
R"(<svg width="200" height="200">
<rect width="100" height="50"/>
</svg>)",
// 12
R"(<svg width="200" height="200">
<g id="0"><use xlink:href="#0" /><rect width="100" height="50"/></g>
</svg>)",
// 13
R"(<svg width="200" height="200">
<g id="0"><g><use xlink:href="#0" /><rect width="100" height="50"/></g></g>
</svg>)",
// 14 (undefined)
R"(<svg width="200" height="200">
<rect width="100" height="50"/>
<use x="100" y="100" opacity="0.5" xlink:href="#nosuch" />
</svg>)",
// 15 - Forward references
R"(<svg viewBox = "0 0 200 200">
<use y = "60" xlink:href = "#usedPolyline" fill= "red" stroke = "blue" fill-opacity = "0.7" fill-rule = "evenodd" stroke-width = "3"/>
<polygon id = "usedPolyline" points="20,20 50,120 100,10 40,80 50,80"/>
</svg>)",
// 16
R"(<svg viewBox = "0 0 200 200">
<use xlink:href ="#usedG" fill = "red" fill-opacity ="0.5"/>
<defs>
<g id="usedG">
<circle cx="0" cy="0" r="100" fill-opacity = "0.6"/>
<rect x = "10" y = "10" width = "30" height = "30"/>
<circle fill="#a6ce39" cx="0" cy="0" r="33" />
</g>
</defs>
</svg>)",
// 17 - Indirect self referral
R"(<svg>
<defs>
<g id="g0">
<g id="g1"><use href="#g2"/></g>
<g id="g2"><use href="#g1"/></g>
</g>
</defs>
<use xlink:href="#g0" fill="black"/>
</svg>)"
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
images[i] = QImage(200, 200, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
if (i < 4 && i != 0) {
QCOMPARE(images[0], images[i]);
} else if (i > 4 && i < 7) {
if (sizeof(qreal) != sizeof(float))
{
// These images use blending functions which due to numerical
// issues on Windows CE and likes differ in very few pixels.
// For this reason an exact comparison will fail.
QCOMPARE(images[4], images[i]);
}
} else if (i > 7 && i < 10) {
QCOMPARE(images[8], images[i]);
} else if (i == 12 || i == 13 || i == 17) {
QCOMPARE(images[10], images[i]);
} else if (i > 11 && i < 15) {
QCOMPARE(images[11], images[i]);
} else if (i == 15) {
QCOMPARE(images[0], images[i]);
} else if (i == 16) {
QCOMPARE(images[8], images[i]);
}
}
}
void tst_QSvgRenderer::smallFont()
{
static const char *svgs[] = { R"(<svg width="50px" height="50px"><text x="10" y="10" font-size="0">Hello world</text></svg>)",
R"(<svg width="50px" height="50px"><text x="10" y="10" font-size="0.5">Hello world</text></svg>)"
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
if (i == 0)
QTest::ignoreMessage(QtWarningMsg, "QFont::setPointSizeF: Point size <= 0 (0.000000), must be greater than 0");
QTest::ignoreMessage(QtWarningMsg, "QFont::setPixelSize: Pixel size <= 0 (0)");
QSvgRenderer renderer(data);
images[i] = QImage(50, 50, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
}
QVERIFY(images[0] != images[1]);
}
void tst_QSvgRenderer::glyphWarnings_data()
{
QTest::addColumn<QString>("missingGlyphAttributes");
QTest::addColumn<QString>("secondElement");
QTest::addColumn<QByteArrayList>("warnings");
const QString glyph = R"(<glyph %1 d="M0 0L9 9z"/>)";
QTest::newRow("glyph-without-unicode")
<< "" << glyph.arg("")
<< QByteArrayList{"glyph does not define a non-empty 'unicode' attribute and will be ignored",
"<input>:1:120: Problem parsing glyph"};
QTest::newRow("glyph-empty-unicode")
<< "" << glyph.arg(R"(unicode="")")
<< QByteArrayList{"glyph does not define a non-empty 'unicode' attribute and will be ignored",
"<input>:1:130: Problem parsing glyph"};
QTest::newRow("missing-glyph-with-unicode")
<< R"(unicode="a")" << glyph.arg(R"(unicode="a")")
<< QByteArrayList{"Ignoring missing-glyph's 'unicode' attribute"};
QTest::newRow("double-missing-glyph")
<< "" << R"(<missing-glyph d="M0 0L9 9z"/>)"
<< QByteArrayList{"The font already has a 'missing-glyph' element.",
"<input>:1:127: Problem parsing missing-glyph"};
}
void tst_QSvgRenderer::glyphWarnings()
{
// This test validates that questionable attributes in a font's glyph and missing-glyph elements
// will trigger suitable warnings while parsing of the entire document finishes sucessfully.
const QString svgTemplate = R"(<svg><defs>)"
R"(<font xml:id="tstFnt"><font-face font-family="tstFnt"/>)"
R"(<missing-glyph %1 d="M0 0L9 9z"/>)"
R"(%2)"
R"(</font></defs>)"
R"(<text font-family="tstFnt">a</text></svg>)";
QFETCH(QString, missingGlyphAttributes);
QFETCH(QString, secondElement);
QFETCH(const QByteArrayList, warnings);
for (const auto &w: warnings)
QTest::ignoreMessage(QtWarningMsg, w.constData());
QSvgRenderer renderer(svgTemplate.arg(missingGlyphAttributes).arg(secondElement).toUtf8());
QVERIFY(renderer.isValid());
}
void tst_QSvgRenderer::styleSheet()
{
static const char *svgs[] = { R"(<svg><style type="text/css">.cls {fill:#ff0000;}</style><rect class="cls" x = "10" y = "10" width = "30" height = "30"/></svg>)",
R"(<svg><style>.cls {fill:#ff0000;}</style><rect class="cls" x = "10" y = "10" width = "30" height = "30"/></svg>)",
};
const int COUNT = sizeof(svgs) / sizeof(svgs[0]);
QImage images[COUNT];
QPainter p;
for (int i = 0; i < COUNT; ++i) {
QByteArray data(svgs[i]);
QSvgRenderer renderer(data);
images[i] = QImage(50, 50, QImage::Format_ARGB32_Premultiplied);
images[i].fill(-1);
p.begin(&images[i]);
renderer.render(&p);
p.end();
}
QCOMPARE(images[0], images[1]);
}
void tst_QSvgRenderer::duplicateStyleId()
{
QByteArray svg = QByteArrayLiteral(R"(<svg><linearGradient id="a"/>
<rect style="fill:url(#a) "/>
<linearGradient id="a"/></svg>)");
QTest::ignoreMessage(QtWarningMsg, R"(Duplicate unique style id: "a")");
QImage image(200, 200, QImage::Format_RGB32);
QPainter painter(&image);
QSvgRenderer renderer(svg);
renderer.render(&painter);
}
void tst_QSvgRenderer::ossFuzzRender_data()
{
QTest::addColumn<QByteArray>("svg");
// oss-fuzz's findings can be looked up by their respective id:
// or, respectively:
// when configured with "-sanitize undefined", this resulted in:
// "runtime error: -nan is outside the range of representable values of type 'int'"
// runtime error: signed integer overflow: -2147483648 + -2147483648 cannot be represented in type 'int'
QTest::newRow("NaN-in-path") // id=24131
<< R"(<svg><path d="M- 4 44044404444E-334-"/></svg>)"_ba;
QTest::newRow("empty polygon") // id=399769595
<< R"-(<svg><linearGradient id="c"/><polygon stroke="url(#c)"/><polygon points="-- 7-" stroke="url(#c)"/></svg>)-"_ba;
// runtime error: signed integer overflow: -2147483648 + -1 cannot be represented in type 'int'
QTest::newRow("excessive moveto in path") // id=406541912
<< R"(<svg><path stroke="#000" d="M- 7e8t9 ."/><marker id="c"/><use href=" c"/></svg>)"_ba;
// Bad-cast to QSvgMarker from QSvgLine -> Heap-buffer-overflow
QTest::newRow("line-as-marker") // id=496327371
<< R"-(<svg><line x1="4" id="lledr" marker-end="url(#lledr)" stroke="#00f"/></svg>)-"_ba;
QTest::newRow("line-as-mask") // modeled after 496327371 to test similar problem, needs UBSAN
<< R"-(<svg>
<defs>
<line x1="4" id="line"/>
<mask id="mask" width="2" height="2" mask="url(#line)"/>
</defs>
<rect width="2" height="2" mask="url(#mask)"/>
</svg>)-"_ba;
}
void tst_QSvgRenderer::ossFuzzRender()
{
QFETCH(QByteArray, svg);
QImage image(377, 233, QImage::Format_RGB32);
QPainter painter(&image);
QSvgRenderer renderer(svg);
renderer.render(&painter);
}
void tst_QSvgRenderer::ossFuzzLoad_data()
{
QTest::addColumn<QByteArray>("svg");
// oss-fuzz's findings can be looked up by their respective id:
// or, respectively:
// when configured with "-sanitize undefined", these resulted in:
// "runtime error: division by zero"
QTest::newRow("missing-arc-data") // id=23731
<< R"(<svg><path d="A4------">)"_ba;
QTest::newRow("subnormal-radius") // id=24738
<< R"(<svg><path d="a 2 1e-212.....">)"_ba;
// resulted in null pointer deref
QTest::newRow("badly-nested") // id=61586
<< R"(<svg><style>*{font-family:q}<linearGradient><stop>)"_ba;
// resulted in stack overflow
QTest::newRow("cyclic-reference") // id=42532991
<< R"-(<svg><pattern height="3" width="9" id="c"><path d="v4T1-" stroke="url(#c)"><symbol>)-"_ba;
// resulted in stack overflow
QTest::newRow("cyclic-reference-from-parent") // id=390467765
<< R"-(<svg stroke="url(#c)"><pattern height="2" width="4" id="c"/><path stroke="#F00" d="v2"/></svg>)-"_ba;
// resulted in memory leak, reported when configured with "-sanitize address"
QTest::newRow("animation-without-target") // id=456050169
<< R"-(<svg><animateTransform type="rotate" from=" " to=" " href="#X">)-"_ba;
// resulted in signed integer overflow, reported when configured with "-sanitize undefined"
QTest::newRow("extreme-font-weights") // id=510580899
<< R"(<svg><style>*{font-weight:2147483647}<g>*{font-weight:1}<g><symbol>)"_ba;
// resulted in failed assert when assigning to int in qCeil()
QTest::newRow("excessive-pattern-size") // id=553133837
<< R"-(<svg><pattern height="2" width="7149" id="c"/><path d="l496795-" stroke="url(#c)"/></svg>)-"_ba;
QTest::newRow("excessive-pattern-size-oom") // found by local fuzzing after partially fixing 553133837
<< R"-(<svg><pattern height="2" width="7149" id="c"/><path d="t3 633333" stroke="url(#c)"/></svg>)-"_ba;
}
void tst_QSvgRenderer::ossFuzzLoad()
{
QFETCH(QByteArray, svg);
QSvgRenderer().load(svg);
}
QByteArray image_data_url(QImage &image) {
QByteArray data;
QBuffer buffer(&data);
QTEST_ASSERT(buffer.open(QBuffer::ReadWrite));
image.save(&buffer, "PNG");
buffer.close();
QByteArray url("data:image/png;base64,");
url.append(data.toBase64());
return url;
}
void tst_QSvgRenderer::imageRendering() {
QImage img(2, 2, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::green);
img.setPixel(0, 0, qRgb(255, 0, 0));
img.setPixel(1, 1, qRgb(255, 0, 0));
QByteArray imgurl(image_data_url(img));
QString svgtemplate(
"<svg><g transform='scale(2, 2)'>"
"<image image-rendering='%1' xlink:href='%2' width='2' height='2' />"
"</g></svg>"
);
const char *cases[] = {"optimizeQuality", "optimizeSpeed"};
for (auto ir: cases) {
QString svg = svgtemplate.arg(QLatin1String(ir)).arg(QLatin1String(imgurl));
QImage img1(4, 4, QImage::Format_ARGB32);
QPainter p1;
p1.begin(&img1);
QSvgRenderer renderer(svg.toLatin1());
Q_ASSERT(renderer.isValid());
renderer.render(&p1);
p1.end();
QImage img2(4, 4, QImage::Format_ARGB32);
QPainter p2(&img2);
p2.scale(2, 2);
if (QLatin1String(ir) == "optimizeSpeed"_L1)
p2.setRenderHint(QPainter::SmoothPixmapTransform, false);
else if (QLatin1String(ir) == "optimizeQuality"_L1)
p2.setRenderHint(QPainter::SmoothPixmapTransform, true);
p2.drawImage(0, 0, img);
p2.end();
QCOMPARE(img1, img2);
}
}
void tst_QSvgRenderer::imageMalformedDataUrl()
{
// The input below triggered an assert in qDecodeDataUrl() which is used when creating svg
// nodes. That assert is fixed and tested in qtbase. Still, the input is invalid and should be
// treated as such. The test makes sure that QSvgRenderer properly warns about it.
QTest::ignoreMessage(QtWarningMsg, R"(Could not create image from "data:charset,")");
QVERIFY(QSvgRenderer().load(
R"(<svg><image width="1" height="1" xlink:href="data:charset,"/></svg>)"_ba));
}
void tst_QSvgRenderer::illegalAnimateTransform_data()
{
QTest::addColumn<QByteArray>("svg");
QTest::newRow("surplus-values")
<< R"(<svg><animateTransform type="rotate" begin="1" dur="2" values="8,0,5,0">)"_ba;
QTest::newRow("missing-value")
<< R"(<svg><animateTransform type="rotate" begin="1" dur="2" values="1,2">)"_ba;
QTest::newRow("from:..+to:f")
<< R"(<svg><animateTransform type="rotate" begin="1" dur="2" from=".. 5 2" to="f">)"_ba;
QTest::newRow("by:--,..")
<< R"(<svg><animateTransform type="scale" begin="1" dur="2" by="--,..">)"_ba;
}
void tst_QSvgRenderer::illegalAnimateTransform()
{
QFETCH(QByteArray, svg);
QSvgRenderer renderer;
QVERIFY(!renderer.load(svg)); // also shouldn't assert
}
void tst_QSvgRenderer::testMaskElement()
{
QByteArray svgDoc(R"(<svg width="240" height="240">
<defs>
<radialGradient id="myGradient">
<stop offset="0" stop-color="black"/>
<stop offset="1" stop-color="white"/>
</radialGradient>
<mask id="mask" width="240" height="240">
<rect width="240" height="240" fill="white"/>
<circle cx="120" cy="120" r="120" fill="url(#myGradient) "/>
</mask>
</defs>
<rect width="240" height="240" fill="red" mask="url(#mask) "/>
</svg>)");
QSvgRenderer renderer(svgDoc);
QVERIFY(renderer.isValid());
QImage image(240, 240, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QImage refImage(240, 240, QImage::Format_ARGB32_Premultiplied);
refImage.fill(Qt::transparent);
QImage refMask(240, 240, QImage::QImage::Format_RGBA8888);
QPainter p;
p.begin(&image);
renderer.render(&p);
p.end();
p.begin(&refMask);
p.fillRect(0, 0, 240, 240, QColorConstants::Svg::white);
QRadialGradient radialGradient(0.5, 0.5, 0.5, 0.5, 0.5, 0);
radialGradient.setCoordinateMode(QGradient::ObjectMode);
radialGradient.setInterpolationMode(QGradient::ComponentInterpolation);
QBrush gradientBrush(radialGradient);
p.setBrush(gradientBrush);
p.setPen(Qt::NoPen);
p.drawEllipse(QPointF(120, 120), 120, 120);
p.end();
for (int i=0; i < refMask.height(); i++) {
QRgb *line = reinterpret_cast<QRgb *>(refMask.scanLine(i));
for (int j=0; j < refMask.width(); j++) {
const qreal rC = 0.2125, gC = 0.7154, bC = 0.0721; //luminanceToAlpha following SVG 1.1
int alpha = 255 - (qRed(line[j]) * rC + qGreen(line[j]) * gC + qBlue(line[j]) * bC) * qAlpha(line[j])/255.;
line[j] = qRgba(0, 0, 0, alpha);
}
}
p.begin(&refImage);
p.fillRect(0, 0, 240, 240, QColorConstants::Svg::red);
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
p.drawImage(QRect(0, 0, 240, 240), refMask);
p.end();
QCOMPARE(refImage, image);
}
void tst_QSvgRenderer::testSymbol()
{
QByteArray svgDoc(R"(<svg width="100" height="100">
<symbol id="dot" width="100" height="100" viewBox="0 0 1 1">
<rect x="0.25" y="0.25" width="0.5" height="0.5" fill="red"/>
</symbol>
<use href="#dot" x="0" y="0"/>
</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.setBrush(Qt::red);
p.setPen(Qt::NoPen);
p.drawRect(25, 25, 50, 50);
p.end();
QCOMPARE(refImage, image);
}
void tst_QSvgRenderer::testMarker()
{
QByteArray svgDoc(R"(<svg width="100" height="100">
<marker id="mark" markerWidth="10" markerHeight="10" viewBox="0 0 1 1" refX="0" refY="0.5">
<rect x="0" y="0" width="1" height="1" fill="red"/>
</marker>
<line x1="10" y1="50" x2="90" y2="50" stroke="white" marker-end="url(#mark) "/>
</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.setPen(Qt::white);
p.drawLine(10, 50, 90, 50);
p.setBrush(Qt::red);
p.setPen(Qt::NoPen);
p.drawRect(90, 45, 10, 10);
p.end();
QCOMPARE(refImage, image);
}
void tst_QSvgRenderer::tSpanLineBreak()
{
QSvgRenderer renderer;
QVERIFY(renderer.load("<svg><textArea>Foo<tbreak/>Bar</textArea></svg>"_ba));
QImage img(50, 50, QImage::Format_ARGB32);
{
QPainter p(&img);
renderer.render(&p); // Don't crash
}
}
void tst_QSvgRenderer::tSpanMultiline_data() // QTBUG-149803
{
// A <tspan> takes its absolute x/y at face value, in the same user coordinate system
// as the enclosing <text> element. This is how tools such as Inkscape lay out text:
// one <tspan> per line, each with its own y. We render two spans and count the number
// of distinct vertical bands of ink: spans placed far apart stay on separate lines,
// while spans placed a pixel apart overlap onto (effectively) a single line.
QTest::addColumn<QByteArray>("svg");
QTest::addColumn<int>("expectedBands");
auto doc = [](const char *x1, const char *y1, const char *x2, const char *y2) {
return QByteArray("<svg viewBox=\"0 0 100 100\">"
"<text x=\"10\" y=\"20\" font-size=\"10\" fill=\"black\">"
"<tspan x=\"") + x1 + "\" y=\"" + y1 + "\">one</tspan>"
"<tspan x=\"" + x2 + "\" y=\"" + y2 + "\">two</tspan>"
"</text></svg>";
};
// A large y delta is a genuine line break: two separate bands.
QTest::newRow("line break") << doc("10", "20", "10", "50") << 2;
// A one-pixel y delta must not become a line break: the spans overlap into one band.
QTest::newRow("overlap") << doc("10", "20", "11", "21") << 1;
// Same baseline, different x: two spans side by side on one line.
QTest::newRow("same line") << doc("10", "20", "40", "20") << 1;
}
void tst_QSvgRenderer::tSpanMultiline()
{
QFETCH(QByteArray, svg);
QFETCH(int, expectedBands);
QSvgRenderer renderer;
QVERIFY(renderer.load(svg));
QImage img(100, 100, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::white);
{
QPainter p(&img);
renderer.render(&p);
}
// Collect the rows that contain any painted (non-white) pixel and count how many
// distinct vertical bands they form. Two lines of text separated by a gap yield two bands;
// spans on the same (or an overlapping) baseline collapse into one.
int bands = 0;
bool inBand = false;
for (int y = 0; y < img.height(); ++y) {
bool rowHasInk = false;
for (int x = 0; x < img.width(); ++x) {
if (qGray(img.pixel(x, y)) < 128) {
rowHasInk = true;
break;
}
}
if (rowHasInk && !inBand)
++bands;
inBand = rowHasInk;
}
QCOMPARE(bands, expectedBands);
}
void tst_QSvgRenderer::tSpanPosition() // QTBUG-122294
{
// The <text> element carries no x/y of its own; it is positioned entirely by the
// absolute x/y of its child <tspan> (as produced by Inkscape). Before tspan x/y was
// honored, the baseline collapsed to (0, 0) and the glyphs landed in the corner.
const char *const svg =
"<svg width=\"400\" height=\"200\" viewBox=\"0 0 400 200\">"
"<text id=\"t\" font-size=\"40\" fill=\"black\">"
"<tspan x=\"100\" y=\"150\">FOOBAR</tspan></text></svg>";
QSvgRenderer renderer;
QVERIFY(renderer.load(QByteArray(svg)));
// boundsOnElement must report the tspan's position, not the origin.
const QRectF bounds = renderer.boundsOnElement(QLatin1String("t"));
QVERIFY(!bounds.isEmpty());
QVERIFY2(qAbs(bounds.left() - 100.0) < 5.0, qPrintable(QString::number(bounds.left())));
QVERIFY2(bounds.top() > 90.0 && bounds.bottom() < 170.0,
qPrintable(QString("top=%1 bottom=%2").arg(bounds.top()).arg(bounds.bottom())));
// The image is the same size as the viewBox, so user units map 1:1 to pixels.
QImage img(400, 200, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::white);
{
QPainter p(&img);
renderer.render(&p);
}
int minX = img.width(), minY = img.height(), maxX = -1, maxY = -1;
for (int y = 0; y < img.height(); ++y) {
for (int x = 0; x < img.width(); ++x) {
if (qGray(img.pixel(x, y)) < 128) {
minX = qMin(minX, x); maxX = qMax(maxX, x);
minY = qMin(minY, y); maxY = qMax(maxY, y);
}
}
}
QVERIFY2(maxX >= 0, "nothing was painted");
// Glyphs start at x ~= 100 and rest on the baseline at y ~= 150; crucially they are
// nowhere near the (0, 0) corner the bug collapsed them to.
QVERIFY2(minX >= 95 && minX <= 120, qPrintable(QString("minX=%1").arg(minX)));
QVERIFY2(minY > 100, qPrintable(QString("minY=%1").arg(minY)));
QVERIFY2(maxY >= 140 && maxY <= 158, qPrintable(QString("maxY=%1").arg(maxY)));
}
static const char *const animatedSvgContents = R"(<svg>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/>
</path></svg>)";
void tst_QSvgRenderer::animated()
{
QSvgRenderer renderer;
QVERIFY(renderer.load(QByteArray(animatedSvgContents)));
QVERIFY(renderer.isAnimationEnabled());
QCOMPARE(renderer.framesPerSecond(), 30);
QTimer *timer = renderer.findChild<QTimer *>();
QVERIFY(timer);
QVERIFY(timer->isActive());
// Toggling animationEnabled
renderer.setAnimationEnabled(false);
QVERIFY(!renderer.isAnimationEnabled());
QVERIFY(!timer->isActive());
renderer.setAnimationEnabled(true);
QVERIFY(renderer.isAnimationEnabled());
QVERIFY(timer->isActive());
// Adjusting the FPS
renderer.setFramesPerSecond(0);
QVERIFY(renderer.isAnimationEnabled());
QVERIFY(!timer->isActive());
renderer.setFramesPerSecond(30);
QVERIFY(renderer.isAnimationEnabled());
QVERIFY(timer->isActive());
// Mixing both
renderer.setFramesPerSecond(0);
QVERIFY(!timer->isActive());
renderer.setAnimationEnabled(true); // this isn't enough to restart the animation, we are still at FPS 0
QVERIFY(!timer->isActive());
renderer.setFramesPerSecond(30);
QVERIFY(timer->isActive());
// Load non-animated SVG
QVERIFY(renderer.load(QByteArray(src)));
QVERIFY(renderer.isAnimationEnabled()); // property didn't change
QVERIFY(!timer->isActive()); // but timer stopped
}
void tst_QSvgRenderer::notAnimated()
{
// Start with animations disabled
QSvgRenderer renderer;
renderer.setAnimationEnabled(false);
QVERIFY(renderer.load(QByteArray(animatedSvgContents)));
QVERIFY(!renderer.isAnimationEnabled());
}
void tst_QSvgRenderer::notAnimatedOption()
{
QSvgRenderer renderer;
renderer.setOptions(QtSvg::Option::DisableAnimations);
// Shouldn't crash accessing the animator when it's disabled.
QVERIFY(renderer.load(QByteArray(animatedSvgContents)));
}
void tst_QSvgRenderer::testPatternElement()
{
QByteArray svgDoc(R"(<svg viewBox="0 0 200 200">
<pattern id="pattern" patternUnits="userSpaceOnUse" width="20" height="20">
<rect x="0" y="0" width="10" height="10" fill="red"/>
<rect x="10" y="0" width="10" height="10" fill="green"/>
<rect x="0" y="10" width="10" height="10" fill="blue"/>
<rect x="10" y="10" width="10" height="10" fill="yellow"/>
</pattern>
<rect width="200" height="200" fill="url(#pattern) "/>
</svg>)");
QSvgRenderer renderer(svgDoc);
QVERIFY(renderer.isValid());
QImage image(200, 200, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::white);
QImage refImage(200, 200, QImage::Format_ARGB32_Premultiplied);
refImage.fill(Qt::white);
QImage refPattern(20, 20, QImage::Format_ARGB32);
refPattern.fill(Qt::transparent);
QPainter p;
p.begin(&image);
renderer.render(&p);
p.end();
p.begin(&refPattern);
p.fillRect(0, 0, 10, 10, QColorConstants::Svg::red);
p.fillRect(10, 0, 10, 10, QColorConstants::Svg::green);
p.fillRect(0, 10, 10, 10, QColorConstants::Svg::blue);
p.fillRect(10, 10, 10, 10, QColorConstants::Svg::yellow);
p.end();
p.begin(&refImage);
p.fillRect(0, 0, 200, 200, QBrush(refPattern));
p.end();
QCOMPARE(refImage, image);
}
void tst_QSvgRenderer::testMisplacedElement()
{
// This input caused a QSvgPattern node to be created with a QSvgPatternStyle referencing to it.
// The code then detected that the <pattern> element is misplaced in the <text> element and
// deleted it. That left behind the QSvgPatternStyle pointing to the deleted QSvgPattern. That
// was reported when running the test with ASAN or UBSAN.
QByteArray svg(R"(<svg>
<text><pattern id="ptn" width="4" height="4"/></text>
<g fill="url(#ptn) "/>
</svg>)");
QImage image(20, 20, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::green);
QImage refImage = image.copy();
QTest::ignoreMessage(QtWarningMsg, "<input>:2:68: Could not add child element to parent "
"element because the types are incorrect.");
QTest::ignoreMessage(QtWarningMsg, "<input>:4:28: Could not resolve property: ptn");
QSvgRenderer renderer(svg);
QPainter painter(&image);
renderer.render(&painter);
QCOMPARE(image, refImage);
}
void tst_QSvgRenderer::testCycles_data()
{
QTest::addColumn<QByteArray>("svgDoc");
QTest::newRow("fill-pattern") << R"(<svg viewBox="0 0 200 200">
<pattern id="pattern" patternUnits="userSpaceOnUse" width="20" height="20">
<rect x="0" y="0" width="10" height="10" fill="url(#pattern) "/>
</pattern>
</svg>)"_ba;
QTest::newRow("use") << R"(<svg viewBox="0 0 200 200">
<g xml:id="group-1"><use xml:id="use-1" xlink:href="#group-2"/></g>
<g xml:id="group-2"><use xml:id="use-2" xlink:href="#group-1"/></g>
</svg>)"_ba;
}
void tst_QSvgRenderer::testCycles()
{
QFETCH(QByteArray, svgDoc);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Cycles detected in SVG"));
QSvgRenderer renderer(svgDoc);
QVERIFY(!renderer.isValid());
}
void tst_QSvgRenderer::testFeFlood()
{
QByteArray svgDoc(R"(<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(R"(<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(R"(<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(R"(<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(R"(<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(R"(<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);
}
void tst_QSvgRenderer::testFeGaussianBigDimensions_data()
{
QTest::addColumn<int>("allocationLimit");
QTest::newRow("1MB") << 1;
QTest::newRow("2MB") << 2;
QTest::newRow("4MB") << 4;
QTest::newRow("8MB") << 8;
}
void tst_QSvgRenderer::testFeGaussianBigDimensions()
{
QFETCH(const int, allocationLimit);
// At 512 by 512, the buffer's size is 8MiB, because the buffer uses
// 32 bytes per pixel for intermediate calculations.
// The test is expected to log a warning when the allocation limit is below 8MB.
QByteArray svgDoc(R"-(<svg width="50" height="50">
<defs>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0"
width="512" height="512">
<feGaussianBlur stdDeviation="1"/>
</filter>
</defs>
<rect x="10" y="10" width="10" height="10" fill="blue" filter="url(#f)"/>
</svg>)-");
QImageReader::setAllocationLimit(allocationLimit);
if (allocationLimit < 8) { // adjust limit when filter's actual memory consumption changes
QTest::ignoreMessage(QtWarningMsg,
"\"The requested filter buffer is too big, ignoring\"");
}
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();
}
void tst_QSvgRenderer::testFeBlend()
{
QByteArray svgDoc(R"(<svg width="50" height="50">
<filter id="f1">
<feOffset in="SourceAlpha" dx="2" dy="2"/>
<feBlend in2="SourceGraphic" mode="normal"/>
</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::transparent);
QImage refImage(50, 50, QImage::Format_ARGB32_Premultiplied);
refImage.fill(Qt::transparent);
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::testOption_data()
{
QTest::addColumn<QtSvg::Option>("option");
QTest::newRow("No Option") << QtSvg::Option::NoOption;
QTest::newRow("Tiny 1.2 Features") << QtSvg::Option::Tiny12FeaturesOnly;
QTest::newRow("Assume Trusted Source") << QtSvg::Option::AssumeTrustedSource;
QTest::newRow("Disable SMIL") << QtSvg::Option::DisableSMILAnimations;
QTest::newRow("Disable Animations") << QtSvg::Option::DisableAnimations;
}
void tst_QSvgRenderer::testOption()
{
QFETCH(QtSvg::Option, option);
QSvgRenderer renderer;
renderer.setOptions(option);
QVERIFY(renderer.options().testFlag(option));
}
void tst_QSvgRenderer::testUseInsideContainerElement_data()
{
QTest::addColumn<QByteArray>("svgDoc");
// <use> inside <symbol>
QTest::newRow("use-in-symbol")
<< R"(<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="dot" overflow="visible">
<circle cx="50" cy="50" r="50" fill="red"/>
</symbol>
<symbol id="dots" overflow="visible">
<use xlink:href="#dot"/>
</symbol>
</defs>
<use xlink:href="#dots" x="0" y="0"/>
</svg>)"_ba;
// <use> inside <marker>
QTest::newRow("use-in-marker")
<< R"-(<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<rect id="box" x="0" y="0" width="100" height="100" fill="red"/>
<marker id="m" viewBox="0 0 100 100" markerWidth="100" markerHeight="100" refX="50" refY="50">
<use xlink:href="#box"/>
</marker>
</defs>
<line x1="50" y1="50" x2="50" y2="50" stroke="white" stroke-width="1" marker-start="url(#m)"/>
</svg>)-"_ba;
// <use> inside <pattern>
QTest::newRow("use-in-pattern")
<< R"-(<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<rect id="tile" width="100" height="100" fill="red"/>
<pattern id="p" patternUnits="userSpaceOnUse" width="100" height="100">
<use xlink:href="#tile"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#p)"/>
</svg>)-"_ba;
}
void tst_QSvgRenderer::testUseInsideContainerElement()
{
QFETCH(QByteArray, svgDoc);
QSvgRenderer renderer(svgDoc);
QVERIFY(renderer.isValid());
QImage image(100, 100, QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::white);
QPainter p(&image);
renderer.render(&p);
p.end();
// The center pixel must be red — that confirms <use> was resolved correctly
QVERIFY(image.pixelColor(50, 50) == QColor(Qt::red));
}
void tst_QSvgRenderer::testDeeplyNested_data()
{
auto generateNests = [](uint count) -> QByteArray
{
QByteArray svgDoc = R"(<svg width="50" height="50">)"_ba;
for (uint i = 0; i < count; i++) {
svgDoc += R"(<g id="g)"_ba;
svgDoc += QByteArray::number(i);
svgDoc += R"(">)"_ba;
}
svgDoc += R"(<line id="line1" x1="10" y1="10" x2="40" y2="10" stroke="black" stroke-width="4"/>)"_ba
R"(<line id="line1" x1="10" y1="20" x2="40" y2="20" stroke="black" stroke-width="4"/>)"_ba
R"(<line id="line1" x1="10" y1="30" x2="40" y2="30" stroke="black" stroke-width="4"/>)"_ba;
for (uint i = 0; i < count; i++)
svgDoc += R"(</g>)"_ba;
svgDoc += R"(</svg>)"_ba;
return svgDoc;
};
QTest::addColumn<QByteArray>("deeplyNested");
QTest::addColumn<bool>("trusted");
for (int i = 8; i <= 65536; i *= 2) {
QTest::newRow((QString::number(i) + " groups").toStdString().c_str())
<< generateNests(i) << (i <= 32);
}
}
void tst_QSvgRenderer::testDeeplyNested()
{
QFETCH(QByteArray, deeplyNested);
QFETCH(bool, trusted);
QSvgRenderer renderer;
if (trusted)
renderer.setOptions(QtSvg::AssumeTrustedSource);
renderer.load(deeplyNested);
// Remove after reintroducing "Removing Parsing Limit in QSvgHandler" and no risk for regressions.
QByteArrayView failMessage = "This test is expected to fail due to reverting \"Removing Parsing"
"Limit in QSvgHandler\" commit. "_ba;
QEXPECT_FAIL("2048 groups", failMessage.constData(), Abort);
QEXPECT_FAIL("4096 groups", failMessage.constData(), Abort);
QEXPECT_FAIL("8192 groups", failMessage.constData(), Abort);
QEXPECT_FAIL("16384 groups",failMessage.constData(), Abort);
QEXPECT_FAIL("32768 groups",failMessage.constData(), Abort);
QEXPECT_FAIL("65536 groups",failMessage.constData(), Abort);
QVERIFY(renderer.isValid());
if (!trusted) {
QTest::ignoreMessage(QtWarningMsg,
"Too many nested nodes at g exceeding max nested limit of 32 . "
"Enable AssumeTrustedSource in QSvgHandler or set "
"QT_SVG_DEFAULT_OPTIONS=2 to disable this check.");
}
QImage image(QSize(50, 50), QImage::Format_ARGB32_Premultiplied);
image.fill(Qt::transparent);
QPainter p(&image);
renderer.render(&p);
}
QTEST_MAIN(tst_QSvgRenderer)
#include "tst_qsvgrenderer.moc"