14.18 round 1: a starved PDF throws, an early zoom centres, inches round to even, tests tightened

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 09:38:52 -04:00
1 parent 9be01073df
commit cd31454815
13 files changed
+210 -15

No files matched your search

+16
View File
@@ -30,6 +30,7 @@ private slots:
void eachFormatShowsItsRowsAndWritesItsBytes();
void theSizeKeepsTheProportionsWithinTheLimits();
void aClearPNGShowsTheCheckerboard();
void thePageRoundsAsSwiftsFormatter();
};
namespace {
@@ -382,5 +383,20 @@ void ExportAsSheetTests::aClearPNGShowsTheCheckerboard()
QCOMPARE(shown.previewAt({100, 165}), QColor(31, 31, 31));
}
// Swift 6.0.3's formatter, measured: halves of its digits to even.
void ExportAsSheetTests::thePageRoundsAsSwiftsFormatter()
{
const auto page = [](int width, int height, double dpi) {
Sheet shown({halfRed(width, height).image, dpi}, ExportFormat::pdf);
return shown.find<QLabel>("exportPage").text();
};
QCOMPARE(page(45, 9, 72), QString("0.62 × 0.12 in at 72 DPI"));
QCOMPARE(page(27, 1, 72), QString("0.38 × 0.01 in at 72 DPI"));
QCOMPARE(page(1, 3, 200), QString("0 × 0.02 in at 200 DPI"));
QCOMPARE(page(27, 29, 200), QString("0.14 × 0.14 in at 200 DPI"));
QCOMPARE(page(100, 2999, 300), QString("0.33 × 10 in at 300 DPI"));
QCOMPARE(page(30000, 1, 1), QString("30,000 × 1 in at 1 DPI"));
}
QTEST_MAIN(ExportAsSheetTests)
#include "ExportAsSheetTests.moc"
+73
View File
@@ -0,0 +1,73 @@
#include "AddressSpaceLimit.h"
#include "IO/ImageExporter.h"
#include <QThreadPool>
#include <QtEndian>
#include <QtTest>
#include <malloc.h>
// A starved PDF throws; it never comes back broken.
class ExportPDFFailureTests : public QObject {
Q_OBJECT
private slots:
void aStarvedPDFThrowsOrIsWhole();
};
namespace {
// Each Flate stream's bytes, inflated.
QList<QByteArray> inflated(const QByteArray &pdf)
{
QList<QByteArray> streams;
for (qsizetype at = pdf.indexOf("/FlateDecode"); at >= 0; at = pdf.indexOf("/FlateDecode", at + 1)) {
const qsizetype begin = pdf.indexOf(">>\nstream\n", at) + 10, end = pdf.indexOf("\nendstream", begin);
QByteArray sized(4, 0);
qToBigEndian(quint32(64 << 20), sized.data());
streams << qUncompress(sized + pdf.mid(begin, end - begin));
}
return streams;
}
}
void ExportPDFFailureTests::aStarvedPDFThrowsOrIsWhole()
{
// Noise, so the compressed streams stay near their size.
QImage noise(1000, 1000, QImage::Format_RGBA8888_Premultiplied);
quint32 seed = 7;
for (int y = 0; y < noise.height(); ++y) {
auto *row = reinterpret_cast<quint32 *>(noise.scanLine(y));
for (int x = 0; x < noise.width(); ++x)
row[x] = (seed = seed * 1664525u + 1013904223u) | 0xff000000u;
}
const ExportRaster raster{noise, 72};
// Qt converts on pool threads: start them before any limit.
QThreadPool::globalInstance()->setExpiryTimeout(-1);
ImageExporter::pdfData(raster);
int refused = 0, whole = 0;
for (qint64 megabytes = 2; megabytes <= 48; ++megabytes) {
QByteArray pdf;
try {
const AddressSpaceLimit limit(megabytes << 20);
pdf = ImageExporter::pdfData(raster);
} catch (const ExportError &error) {
QCOMPARE(error.kind, ExportError::Kind::render);
++refused;
continue;
}
const QList<QByteArray> streams = inflated(pdf);
QCOMPARE(streams.size(), qsizetype(2));
QVERIFY2(streams[0].size() == 1000 * 1000 * 3 && streams[1].size() == 1000 * 1000, qPrintable(QString::number(megabytes)));
++whole;
}
// The sweep reaches both sides of the limit.
QVERIFY(refused > 0 && whole > 0);
}
int main(int argc, char **argv)
{
// Large asks map memory the limit counts.
mallopt(M_MMAP_THRESHOLD, 64 * 1024);
QCoreApplication app(argc, argv);
ExportPDFFailureTests tests;
return QTest::qExec(&tests, argc, argv);
}
#include "ExportPDFFailureTests.moc"
+8
View File
@@ -79,6 +79,14 @@ void ExportPDFTests::pdfIsOnePageAtThePrintedSizeWithLosslessPixels()
QCOMPARE(at(0, 0)[3], 0);
// The page draws the image over it, nothing beneath.
QCOMPARE(streamOf(object(pdf, 4)), QByteArray("q\n2.8800 0 0 2.8800 0 0 cm /Im0 Do\nQ"));
// Rows top first, each channel its own, as drawn.
QImage grid = BrushRaster::context(2, 3, false);
for (int y = 0; y < 3; ++y)
for (int x = 0; x < 2; ++x)
grid.setPixel(x, y, qRgba(10 + 40 * y + 20 * x, 100 + 30 * y - 10 * x, 200 - 50 * y + 5 * x, 255));
const QByteArray rows = ImageExporter::pdfData(ExportRaster{grid, 72});
QCOMPARE(streamOf(object(rows, 5)), QByteArray("\x0a\x64\xc8\x1e\x5a\xcd\x32\x82\x96\x46\x78\x9b\x5a\xa0\x64\x6e\x96\x69", 18));
QVERIFY(object(rows, 5).contains("/Width 2 /Height 3"));
// A soft edge goes straight: colour whole, alpha apart.
QImage soft = BrushRaster::context(1, 1, false);
soft.setPixelColor(0, 0, QColor(255, 0, 0, 128));
+30
View File
@@ -21,6 +21,7 @@ private slots:
void theUpdatingPlateSitsInTheMiddle();
void zoomKeepsTheMiddleAndDragsMoveIt();
void theSheetsButtonsAndKeysZoomIt();
void aZoomBeforeTheFirstImageCentresIt();
};
namespace {
@@ -303,5 +304,34 @@ void JPEGPreviewTests::theSheetsButtonsAndKeysZoomIt()
ShortcutSettings::shared().reload();
}
// Swift makes the preview with its first result, centred.
void JPEGPreviewTests::aZoomBeforeTheFirstImageCentresIt()
{
EditorSession session;
QImage red(1200, 800, QImage::Format_RGBA8888_Premultiplied);
red.fill(Qt::red);
const auto offsets = [&](bool early) {
ExportAsSheet sheet({red, 72}, session, ExportFormat::png, [](std::optional<ExportAsSheet::Chosen>) {});
sheet.show();
auto &exporting = *sheet.findChild<QPushButton *>(QStringLiteral("exportExport"));
if (early) {
if (!QTest::qWaitForWindowActive(&sheet) || exporting.isEnabled())
throw std::runtime_error("ready before the key");
QTest::keyClick(&sheet, Qt::Key_1, Qt::ControlModifier);
}
if (!QTest::qWaitFor([&] { return exporting.isEnabled(); }))
throw std::runtime_error("never ready");
auto &preview = *sheet.findChild<JPEGPreview *>();
if (!early)
preview.setZoom(1);
const QSize view = preview.viewport()->size();
return std::array<int, 4>{preview.horizontalScrollBar()->value(), preview.verticalScrollBar()->value(), view.width(), view.height()};
};
const std::array<int, 4> early = offsets(true), late = offsets(false);
QCOMPARE(early, late);
QVERIFY(early[0] == int(std::lround(600 - early[2] / 2.0)) && early[1] == int(std::lround(400 - early[3] / 2.0)));
QVERIFY(early[0] > 300 && early[1] > 200);
}
QTEST_MAIN(JPEGPreviewTests)
#include "JPEGPreviewTests.moc"
+7
View File
@@ -101,6 +101,13 @@ void KeyboardShortcutsTests::theListIsSwiftsWithoutHide()
QVERIFY(!ids.contains("Menus:Hide Compositor"));
QCOMPARE(named("Export JPEG").original, ShortcutChord("s", 11));
QVERIFY(named("Export JPEG").isMenu());
// Swift 1.4.8's Export As follows Export JPEG.
QCOMPARE(named("Export As").original, ShortcutChord("w", 11));
QVERIFY(named("Export As").isMenu() && named("Export As").group == named("Export JPEG").group);
const auto position = [&all](const char *title) {
return std::find_if(all.begin(), all.end(), [title](const ShortcutDefinition &each) { return each.id() == QStringLiteral("Menus:") + QLatin1String(title); }) - all.begin();
};
QCOMPARE(position("Export As"), position("Export JPEG") + 1);
QCOMPARE(named("Content-Aware Fill").original, ShortcutChord("\x7f", 8));
QCOMPARE(named("Temporary Hand tool (hold)").original, ShortcutChord(" "));
QCOMPARE(named("Opacity digit 7 (type two for exact %)").original, ShortcutChord("7"));
+38
View File
@@ -24,6 +24,7 @@ private slots:
void aJPEGNeedsAWindowAndADocument();
void theFileMenuOffersBothExports();
void exportAsWritesEachFormatAndRemembersTheLast();
void theMenuOpensExportAsOnTheLastFormat();
};
namespace {
@@ -324,5 +325,42 @@ void ProjectExportTests::exportAsWritesEachFormatAndRemembersTheLast()
QCOMPARE(contents(Desk::path("half.png")), ImageExporter::pngData(ImageExporter::resized(raster, 20, 10)));
}
void ProjectExportTests::theMenuOpensExportAsOnTheLastFormat()
{
QTemporaryDir folder;
Bar bar;
bar.session().createDocument(8, 8);
bar.window.show();
DialogDesk desk;
const auto shown = [&bar]() -> ExportAsSheet & {
if (!QTest::qWaitFor([&bar] { return bar.window.findChild<ExportAsSheet *>() && bar.window.findChild<ExportAsSheet *>()->isVisible(); }))
throw std::runtime_error("no sheet");
return *bar.window.findChild<ExportAsSheet *>();
};
const auto checked = [](ExportAsSheet &sheet, const char *name) { return sheet.findChild<QAbstractButton *>(QString::fromLatin1(name))->isChecked(); };
const auto closed = [&bar] { return !bar.session().isProjectBusy() && !bar.window.findChild<ExportAsSheet *>(); };
// PNG first; a PDF written through the menu comes next.
bar.action("exportAs").trigger();
QVERIFY(checked(shown(), "exportFormatPNG"));
shown().findChild<QAbstractButton *>("exportFormatPDF")->click();
desk.replies = {folder.filePath("Page")};
QTRY_VERIFY(shown().findChild<QPushButton *>("exportExport")->isEnabled());
shown().findChild<QPushButton *>("exportExport")->click();
QTRY_VERIFY(QFileInfo::exists(folder.filePath("Page.pdf")) && closed());
bar.action("exportAs").trigger();
QVERIFY(checked(shown(), "exportFormatPDF"));
QTest::keyClick(&shown(), Qt::Key_Escape);
QTRY_VERIFY(closed());
// Export JPEG starts on JPEG; Export As stays on PDF.
bar.action("exportJPEG").trigger();
QVERIFY(checked(shown(), "exportFormatJPEG"));
QTest::keyClick(&shown(), Qt::Key_Escape);
QTRY_VERIFY(closed());
bar.action("exportAs").trigger();
QVERIFY(checked(shown(), "exportFormatPDF"));
QTest::keyClick(&shown(), Qt::Key_Escape);
QTRY_VERIFY(closed());
}
QTEST_MAIN(ProjectExportTests)
#include "ProjectExportTests.moc"
+1
View File
@@ -121,6 +121,7 @@ compositor_test(EditorSessionTests)
compositor_test(LayerDuplicateTests)
compositor_test(ExportTests)
compositor_test(ExportPDFTests)
compositor_test(ExportPDFFailureTests)
compositor_test(FolderMaskClipTests)
compositor_test(EditorCanvasTests)
compositor_test(GroupSessionTests)