Files
OmaPhoto/tests/SVGImportTests.cpp
T

175 lines
9.0 KiB
C++

#include "BudgetFixtures.h"
#include "Document/EditorSession.h"
#include "IO/ImageImporter.h"
#include "IO/PSD/PSDReader.h"
#include <QColorSpace>
#include <QTemporaryDir>
#include <QtTest>
#include <zlib.h>
// Swift 1.2.10 (0534e00): SVG drawn once into an image layer.
namespace {
// A red rectangle filling a document of the given size.
QByteArray svg(const QByteArray &width, const QByteArray &height)
{
return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + width + "\" height=\"" + height + "\"><rect width=\"100%\" height=\"100%\" fill=\"red\"/></svg>";
}
QString written(const QTemporaryDir &folder, const QString &name, const QByteArray &bytes)
{
QFile file(folder.filePath(name));
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size())
throw std::runtime_error("cannot write a fixture");
return file.fileName();
}
// The same bytes in gzip, as `.svgz` holds them.
QByteArray gzipped(const QByteArray &bytes)
{
z_stream stream{};
if (deflateInit2(&stream, 9, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) != Z_OK)
throw std::runtime_error("no zlib");
QByteArray out(int(deflateBound(&stream, uLong(bytes.size()))), '\0');
stream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(bytes.constData()));
stream.avail_in = uInt(bytes.size());
stream.next_out = reinterpret_cast<Bytef *>(out.data());
stream.avail_out = uInt(out.size());
deflate(&stream, Z_FINISH);
out.resize(qsizetype(stream.total_out));
deflateEnd(&stream);
return out;
}
std::optional<ImageImportError::Kind> refusal(const std::function<void()> &run)
{
try {
run();
} catch (const ImageImportError &error) {
return error.kind;
}
return std::nullopt;
}
bool imported(EditorSession &session, const QList<QUrl> &urls)
{
bool done = false;
session.importImages(urls, std::nullopt, [&] { done = true; });
return QTest::qWaitFor([&] { return done && !session.isImporting(); }, 10'000);
}
}
class SVGImportTests : public QObject {
Q_OBJECT
private slots:
void anSVGFitsTheCanvasOrKeepsItsSize();
void brokenOrOversizedSVGsRefuse();
void theSessionImportsSVGBySuffix();
};
void SVGImportTests::anSVGFitsTheCanvasOrKeepsItsSize()
{
const QTemporaryDir folder;
const QString path = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
const ImportedImage alone = ImageImporter::decodeSVG(path, std::nullopt);
QCOMPARE(alone.size(), QSize(40, 20));
QCOMPARE(alone.name, QString("icon"));
QCOMPARE(alone.image().pixelColor(20, 10), QColor(255, 0, 0));
QCOMPARE(alone.image().colorSpace(), QColorSpace(QColorSpace::SRgb));
QVERIFY(!alone.thumbnail.isNull());
// Fitted, the smaller side's ratio rules and sides round.
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(400, 300)).size(), QSize(400, 200));
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(10, 10)).size(), QSize(10, 5));
const QString thin = written(folder, QStringLiteral("thin.svg"), svg("3", "1"));
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(10, 10)).size(), QSize(10, 3));
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(1, 1)).size(), QSize(1, 1));
QCOMPARE(ImageImporter::decodeSVG(thin, QSizeF(5, 5)).size(), QSize(5, 2));
QCOMPARE(ImageImporter::decodeSVG(path, QSizeF(400, 100)).size(), QSize(200, 100));
const QString narrow = written(folder, QStringLiteral("narrow.svg"), svg("1", "3"));
QCOMPARE(ImageImporter::decodeSVG(narrow, QSizeF(1, 1)).size(), QSize(1, 1));
QCOMPARE(ImageImporter::decodeSVG(narrow, QSizeF(5, 5)).size(), QSize(2, 5));
// Red top left, blue bottom right, clear between: upright.
const QString corners = written(folder, QStringLiteral("corners.svg"),
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"20\"><rect width=\"5\" height=\"10\" fill=\"red\"/>"
"<rect x=\"5\" y=\"10\" width=\"5\" height=\"10\" fill=\"blue\"/></svg>");
const QImage fitted = ImageImporter::decodeSVG(corners, QSizeF(100, 100)).image();
QCOMPARE(fitted.size(), QSize(50, 100));
QCOMPARE(fitted.pixelColor(12, 25), QColor(255, 0, 0));
QCOMPARE(fitted.pixelColor(37, 75), QColor(0, 0, 255));
QCOMPARE(fitted.pixelColor(37, 25).alpha(), 0);
QCOMPARE(fitted.pixelColor(12, 75).alpha(), 0);
// A large drawing's thumbnail is 96 on its long side.
const QString large = written(folder, QStringLiteral("large.svg"), svg("200", "400"));
QCOMPARE(ImageImporter::decodeSVG(large, std::nullopt).thumbnail.size(), QSize(48, 96));
// Compressed and upper-case names read as SVG.
const QString compressed = written(folder, QStringLiteral("icon.SVGZ"), gzipped(svg("40", "20")));
QVERIFY(ImageImporter::isSVG(compressed));
QCOMPARE(ImageImporter::decodeSVG(compressed, std::nullopt).size(), QSize(40, 20));
QVERIFY(ImageImporter::isSVG(QStringLiteral("/a/b.Svg")));
QVERIFY(!ImageImporter::isSVG(QStringLiteral("/a/b.png")));
QVERIFY(!ImageImporter::isSVG(QStringLiteral("/a/svg")));
}
void SVGImportTests::brokenOrOversizedSVGsRefuse()
{
const QTemporaryDir folder;
using Kind = ImageImportError::Kind;
const QString broken = written(folder, QStringLiteral("broken.svg"), "<svg");
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(broken, std::nullopt); }), std::optional(Kind::unreadable));
// An empty drawing with a zero side has no size.
const QString empty = written(folder, QStringLiteral("empty.svg"), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"0\" height=\"20\"/>");
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(empty, std::nullopt); }), std::optional(Kind::unreadable));
for (const char *box : {"0 0 0 10", "0 0 10 0"}) {
const QString flat = written(folder, QStringLiteral("flat.svg"), QByteArray("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"") + box + "\"/>");
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(flat, std::nullopt); }), std::optional(Kind::unreadable));
}
const QString wide = written(folder, QStringLiteral("wide.svg"), svg("30001", "1"));
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(wide, std::nullopt); }), std::optional(Kind::tooLarge));
QCOMPARE(ImageImporter::decodeSVG(wide, QSizeF(30'000, 30'000)).size(), QSize(30'000, 1));
const QString tall = written(folder, QStringLiteral("tall.svg"), svg("1", "30001"));
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(tall, std::nullopt); }), std::optional(Kind::tooLarge));
QCOMPARE(ImageImporter::decodeSVG(tall, QSizeF(30'000, 30'000)).size(), QSize(1, 30'000));
// The budget counts the pixels drawn.
const QString icon = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
QCOMPARE(ImageImporter::decodeSVG(icon, std::nullopt, 800).size(), QSize(40, 20));
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(icon, std::nullopt, 799); }), std::optional(Kind::tooLarge));
// Fitted, the budget counts the drawn size, not the declared.
QCOMPARE(ImageImporter::decodeSVG(icon, QSizeF(400, 300), 80'000).size(), QSize(400, 200));
QCOMPARE(refusal([&] { ImageImporter::decodeSVG(icon, QSizeF(400, 300), 79'999); }), std::optional(Kind::tooLarge));
QCOMPARE(ImageImporter::decodeSVG(icon, QSizeF(10, 10), 50).size(), QSize(10, 5));
}
void SVGImportTests::theSessionImportsSVGBySuffix()
{
const QTemporaryDir folder;
const QString icon = written(folder, QStringLiteral("icon.svg"), svg("40", "20"));
// Without a document, its declared size makes the canvas.
EditorSession session;
QVERIFY(imported(session, {QUrl::fromLocalFile(icon)}));
QCOMPARE(session.document().value().size(), QSizeF(40, 20));
QCOMPARE(session.activeLayer().value().name, QString("icon"));
// Over a canvas, it is drawn to fit it.
EditorSession canvas;
canvas.createDocument(400, 300, true);
QVERIFY(imported(canvas, {QUrl::fromLocalFile(icon)}));
QCOMPARE(canvas.activeLayer().value().asset.value().size(), QSize(400, 200));
QCOMPARE(canvas.activeLayer().value().transform.origin, QPointF(0, 50));
// The suffix rules before Photoshop's bytes.
const QString disguised = written(folder, QStringLiteral("disguised.svg"), QByteArray("8BPS\0\1", 6) + QByteArray(40, '\0'));
QVERIFY(PSDReader::matches(disguised));
QVERIFY(imported(canvas, {QUrl::fromLocalFile(disguised)}));
QCOMPARE(canvas.importError().value(), QString("disguised.svg: The image could not be read. It may be damaged or unavailable."));
// The drawing counts against what the canvas already holds.
EditorSession full;
full.createDocument(40, 20, true);
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget() - 800))
full.insert(layer);
QVERIFY(imported(full, {QUrl::fromLocalFile(icon)}));
QCOMPARE(full.importError(), std::nullopt);
QCOMPARE(full.activeLayer().value().asset.value().size(), QSize(40, 20));
QVERIFY(imported(full, {QUrl::fromLocalFile(icon)}));
QVERIFY(full.importError().value().startsWith("icon.svg: This import exceeds"));
}
QTEST_MAIN(SVGImportTests)
#include "SVGImportTests.moc"