#include "BudgetFixtures.h" #include "Document/EditorSession.h" #include "IO/ImageImporter.h" #include "IO/PSD/PSDReader.h" #include #include #include #include // 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 ""; } 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(const_cast(bytes.constData())); stream.avail_in = uInt(bytes.size()); stream.next_out = reinterpret_cast(out.data()); stream.avail_out = uInt(out.size()); deflate(&stream, Z_FINISH); out.resize(qsizetype(stream.total_out)); deflateEnd(&stream); return out; } std::optional refusal(const std::function &run) { try { run(); } catch (const ImageImportError &error) { return error.kind; } return std::nullopt; } bool imported(EditorSession &session, const QList &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"), "" ""); 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"), ""); 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(""); 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"