#include "UI/ByteCounts.h" #include "UI/ColorPaletteControls.h" #include "UI/JPEGExportSheet.h" #include "UI/ColorPickerSheet+Dialog.h" #include #include #include #include #include #include #include #include #include // Swift's JPEGExportSheet: the preview waits, encodes aside, then exports. class JPEGExportSheetTests : public QObject { Q_OBJECT private slots: void initTestCase(); void theSheetShowsTheRasterAndWaitsForItsPreview(); void aChangeSupersedesThePreview(); void aSupersededEncodingNeverLands(); void theMatteComesFromTheAppsPicker(); void exportKeepsTheQualityAndCancelDoesNot(); void theSavedQualityIsReadAsSwiftReadsIt(); void anEncodingThatFailsIsShown(); }; namespace { // Clear on the left, opaque red on the right. ExportRaster halfRed(int width = 64, int height = 32) { QImage image(width, height, QImage::Format_RGBA8888_Premultiplied); image.fill(Qt::transparent); for (int y = 0; y < height; ++y) for (int x = width / 2; x < width; ++x) image.setPixel(x, y, qRgba(255, 0, 0, 255)); return {image, 144}; } struct Sheet { std::optional> answer; EditorSession session; JPEGExportSheet sheet; explicit Sheet(ExportRaster raster = halfRed()) : sheet(std::move(raster), session, [this](std::optional data) { answer = std::move(data); }) { sheet.show(); } template Widget &find(const char *name) { Widget *found = sheet.findChild(QString::fromLatin1(name)); if (!found) throw std::runtime_error(std::string("no widget named ") + name); return *found; } bool ready() { return find("jpegExport").isEnabled(); } QString note() { return find("jpegNote").text(); } QColor previewAt(QPoint point) { return find("jpegPreview").grab().toImage().pixelColor(point); } }; struct PaintCount : QObject { int count = 0; explicit PaintCount(QWidget &widget) { widget.installEventFilter(this); } bool eventFilter(QObject *, QEvent *event) override { count += event->type() == QEvent::Paint; return false; } }; QStringList labels(const QWidget &widget) { QStringList texts; for (const QLabel *label : widget.findChildren()) texts << label->text(); return texts; } } void JPEGExportSheetTests::initTestCase() { QStandardPaths::setTestModeEnabled(true); QSettings().clear(); } void JPEGExportSheetTests::theSheetShowsTheRasterAndWaitsForItsPreview() { Sheet shown(halfRed(1200, 800)); const QStringList texts = labels(shown.sheet); QVERIFY(texts.contains("Export JPEG")); QVERIFY(texts.contains("1,200 × 800 px · sRGB")); QVERIFY(texts.contains("Background for transparency")); QCOMPARE(shown.find("jpegPercent").text(), QString("85%")); QCOMPARE(shown.find("jpegQuality").value(), 85); QCOMPARE(shown.find("jpegPreview").size(), QSize(560, 330)); // Swift's task sleeps 200 ms before it encodes. auto &wait = shown.find("jpegWait"); QVERIFY(wait.isActive() && wait.isSingleShot()); QCOMPARE(wait.interval(), 200); QCOMPARE(wait.timerType(), Qt::PreciseTimer); QVERIFY(!shown.ready()); QCOMPARE(shown.note(), QString("Updating…")); QVERIFY(shown.find("jpegSpinner").isVisible()); QVERIFY(!shown.find("jpegBytes").isVisible() && !shown.find("jpegError").isVisible()); // The material plate behind the spinner, over the dark gray. QCOMPARE(shown.previewAt({2, 2}), QColor(31, 31, 31)); const QColor window = shown.sheet.palette().color(QPalette::Window), plate = shown.previewAt({252, 165}); QVERIFY(qAbs(plate.red() - qRound(window.red() * 0.85 + 31 * 0.15)) <= 1); QVERIFY(qAbs(plate.blue() - qRound(window.blue() * 0.85 + 31 * 0.15)) <= 1); QTRY_VERIFY(shown.ready()); const JPEGResult expected = ImageExporter::jpeg(halfRed(1200, 800), {}); QCOMPARE(shown.find("jpegBytes").text(), ByteCounts::file(expected.data.size())); QVERIFY(shown.find("jpegBytes").isVisible() && !shown.find("jpegNote").isVisible()); QVERIFY(!shown.find("jpegSpinner").isVisible()); // Fitted to 495 × 330 and centred: white, then red. QCOMPARE(shown.previewAt({2, 2}), QColor(31, 31, 31)); QCOMPARE(shown.previewAt({10, 165}), QColor(31, 31, 31)); QVERIFY(qAbs(shown.previewAt({100, 165}).red() - 255) < 4 && qAbs(shown.previewAt({100, 165}).blue() - 255) < 4); const QColor red = shown.previewAt({460, 165}); QVERIFY(red.red() > 240 && red.green() < 16 && red.blue() < 16); } void JPEGExportSheetTests::aChangeSupersedesThePreview() { Sheet shown; QTRY_VERIFY(shown.ready()); auto &quality = shown.find("jpegQuality"); // Each change waits anew; the stale preview stays beneath. quality.setValue(10); QCOMPARE(shown.find("jpegPercent").text(), QString("10%")); QVERIFY(!shown.ready()); QCOMPARE(shown.note(), QString("Updating…")); QVERIFY(shown.find("jpegWait").isActive()); QVERIFY(shown.find("jpegSpinner").isVisible()); quality.setValue(100); QTRY_VERIFY(shown.ready()); QCOMPARE(shown.find("jpegBytes").text(), ByteCounts::file(ImageExporter::jpeg(halfRed(), {.quality = 1}).data.size())); shown.find("jpegExport").click(); QCOMPARE(shown.answer.value().value(), ImageExporter::jpeg(halfRed(), {.quality = 1}).data); } // Slow to encode: the change comes while it runs. void JPEGExportSheetTests::aSupersededEncodingNeverLands() { QImage noisy(6000, 4000, QImage::Format_RGBA8888_Premultiplied); quint32 seed = 1; for (int y = 0; y < noisy.height(); ++y) { auto *row = reinterpret_cast(noisy.scanLine(y)); for (int x = 0; x < noisy.width(); ++x) row[x] = (seed = seed * 1664525u + 1013904223u) | 0xff000000u; } Sheet shown({noisy, 72}); QTRY_VERIFY(!shown.find("jpegWait").isActive()); shown.find("jpegQuality").setValue(50); // Only the newest request's preview may ever show. bool stale = false; while (!shown.ready()) { stale = stale || shown.previewAt({100, 165}) != QColor(31, 31, 31); QCoreApplication::processEvents(QEventLoop::AllEvents, 10); } QVERIFY(!stale); QCOMPARE(shown.find("jpegPercent").text(), QString("50%")); } void JPEGExportSheetTests::theMatteComesFromTheAppsPicker() { QSettings().remove(JPEGExportSheet::qualityKey); Sheet shown; QTRY_VERIFY(shown.ready()); auto &matte = shown.find("jpegMatte"); QVERIFY(matte.accessibleName() == QString("JPEG Background") && matte.toolTip() == QString("Color that fills transparent areas")); QCOMPARE(qobject_cast(shown.sheet.layout()->itemAt(2)->layout())->spacing(), 8); matte.click(); QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (JPEG Background)")); QVERIFY(shown.session.colorPicker().value().original == PaletteColor::white()); // The same colour asks for nothing. shown.session.closeColorPicker(true); QVERIFY(shown.ready() && !shown.find("jpegWait").isActive()); // The working colour reaches the preview; Cancel takes it back. matte.click(); shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0, 0, 1})); QVERIFY(!shown.ready()); QCOMPARE(matte.grab().toImage().pixelColor(17, 9), QColor(0, 0, 255)); shown.session.closeColorPicker(false); QCOMPARE(matte.grab().toImage().pixelColor(17, 9), QColor(Qt::white)); QTRY_VERIFY(shown.ready()); // Export closes an open picker, keeping its colour. matte.click(); shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0, 0, 1})); QTRY_VERIFY(shown.ready()); shown.find("jpegExport").click(); QVERIFY(!shown.session.colorPicker()); const QImage written = QImage::fromData(shown.answer.value().value(), "jpeg"); const QColor left = written.pixelColor(8, 16), right = written.pixelColor(56, 16); QVERIFY(left.blue() > 240 && left.red() < 16 && left.green() < 16); QVERIFY(right.red() > 240 && right.blue() < 16); QCOMPARE(shown.answer.value().value(), ImageExporter::jpeg(halfRed(), {.red = 0, .green = 0, .blue = 1}).data); // Cancel and the sheet going close it too. Sheet cancelled; cancelled.find("jpegMatte").click(); cancelled.find("jpegCancel").click(); QVERIFY(!cancelled.session.colorPicker() && !cancelled.answer.value()); EditorSession session; auto gone = std::make_unique(halfRed(), session, [](std::optional) {}); gone->findChild()->click(); gone.reset(); QVERIFY(!session.colorPicker()); } void JPEGExportSheetTests::exportKeepsTheQualityAndCancelDoesNot() { QSettings().remove(JPEGExportSheet::qualityKey); { Sheet shown; shown.find("jpegQuality").setValue(40); QTRY_VERIFY(shown.ready()); shown.find("jpegCancel").click(); QVERIFY(!shown.answer.value().has_value()); QVERIFY(!QSettings().contains(JPEGExportSheet::qualityKey)); } { Sheet shown; QCOMPARE(shown.find("jpegQuality").value(), 85); shown.find("jpegQuality").setValue(40); QTRY_VERIFY(shown.ready()); shown.find("jpegExport").click(); QCOMPARE(QSettings().value(JPEGExportSheet::qualityKey).toDouble(), 0.4); } Sheet next; QCOMPARE(next.find("jpegQuality").value(), 40); QCOMPARE(next.find("jpegPercent").text(), QString("40%")); // Return reaches Export once it is ready. QTRY_VERIFY(next.ready()); QVERIFY(next.find("jpegExport").isDefault()); QCOMPARE(next.find("jpegExport").text(), QString("Export…")); } void JPEGExportSheetTests::theSavedQualityIsReadAsSwiftReadsIt() { const auto started = [](const QVariant &saved) { QSettings().setValue(JPEGExportSheet::qualityKey, saved); Sheet shown; return shown.find("jpegPercent").text(); }; QCOMPARE(started(0.333), QString("33%")); QCOMPARE(started(0.875), QString("88%")); QCOMPARE(started(7), QString("100%")); QCOMPARE(started(-2), QString("0%")); QCOMPARE(started(QStringLiteral("high")), QString("85%")); QCOMPARE(started(std::nan("")), QString("85%")); QCOMPARE(started(qInf()), QString("85%")); QSettings().remove(JPEGExportSheet::qualityKey); } void JPEGExportSheetTests::anEncodingThatFailsIsShown() { // JPEG holds at most 65,500 pixels a side. Sheet shown(halfRed(70'000, 1)); auto &error = shown.find("jpegError"); QTRY_VERIFY(error.isVisible()); QCOMPARE(error.text(), QString::fromUtf8(ExportError(ExportError::Kind::encode).what())); QCOMPARE(error.foregroundRole(), QPalette::BrightText); QVERIFY(!shown.ready() && !shown.find("jpegNote").isVisible() && !shown.find("jpegSpinner").isVisible()); // A change clears the error while it waits. shown.find("jpegQuality").setValue(50); QVERIFY(!error.isVisible()); QCOMPARE(shown.note(), QString("Updating…")); QTRY_VERIFY(error.isVisible()); } QTEST_MAIN(JPEGExportSheetTests) #include "JPEGExportSheetTests.moc"