14.18: Export As for PNG, JPEG and PDF, sized, over a checkerboard
This commit is contained in:
26 files changed
+1352
-688
No files matched your search
@@ -1,6 +1,6 @@
|
||||
#include "AcceptanceFixtures.h"
|
||||
#include "DialogDesk.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include <QComboBox>
|
||||
#include <map>
|
||||
#include <QLineEdit>
|
||||
@@ -164,11 +164,11 @@ void AcceptanceFinishTests::jpegPreviewAndQuitMidEdit()
|
||||
photo(app);
|
||||
// Export JPEG: View keys zoom to 100% and back.
|
||||
bool done = false;
|
||||
app.workspace.current().controller.exportJPEG([&done] { done = true; });
|
||||
QTRY_VERIFY(app.window.findChild<JPEGExportSheet *>());
|
||||
app.workspace.current().controller.exportAs(ExportFormat::jpeg, [&done] { done = true; });
|
||||
QTRY_VERIFY(app.window.findChild<ExportAsSheet *>());
|
||||
auto *preview = app.window.findChild<JPEGPreview *>();
|
||||
QVERIFY(preview && !preview->zoom());
|
||||
JPEGExportSheet *sheet = app.window.findChild<JPEGExportSheet *>();
|
||||
ExportAsSheet *sheet = app.window.findChild<ExportAsSheet *>();
|
||||
QTest::keyClick(sheet, Qt::Key_1, Qt::ControlModifier);
|
||||
QCOMPARE(preview->zoom(), std::optional<double>(1));
|
||||
QTest::keyClick(sheet, Qt::Key_Equal, Qt::ControlModifier);
|
||||
|
||||
@@ -88,10 +88,10 @@ void AcceptanceProjectTests::cropSizesAndExports()
|
||||
QCOMPARE(QImage(app.path("scene.png")).size(), QSize(600, 300));
|
||||
desk.replies = {app.path("scene.jpg")};
|
||||
done = false;
|
||||
controller.exportJPEG([&done] { done = true; });
|
||||
controller.exportAs(ExportFormat::jpeg, [&done] { done = true; });
|
||||
QTRY_VERIFY(app.window.findChild<QDialog *>());
|
||||
app.shot(QStringLiteral("11-export-jpeg"));
|
||||
QPushButton *exported = app.window.findChild<QDialog *>()->findChild<QPushButton *>(QStringLiteral("jpegExport"));
|
||||
QPushButton *exported = app.window.findChild<QDialog *>()->findChild<QPushButton *>(QStringLiteral("exportExport"));
|
||||
QVERIFY(exported);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(exported->isEnabled(), 30000);
|
||||
exported->click();
|
||||
|
||||
@@ -27,7 +27,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
Bar bar;
|
||||
const std::pair<const char *, const char *> keys[] = {
|
||||
{"newCanvas", "Ctrl+N"}, {"openProject", "Ctrl+O"}, {"importImages", ""}, {"save", "Ctrl+S"}, {"saveAs", "Ctrl+Shift+S"},
|
||||
{"exportPNG", "Ctrl+Shift+E"}, {"exportJPEG", "Ctrl+Alt+Shift+S"},
|
||||
{"exportPNG", "Ctrl+Shift+E"}, {"exportJPEG", "Ctrl+Alt+Shift+S"}, {"exportAs", "Ctrl+Alt+Shift+W"},
|
||||
{"closeProject", "Ctrl+W"}, {"undo", "Ctrl+Z"}, {"redo", "Ctrl+Shift+Z"}, {"fit", "Ctrl+0"}, {"actualPixels", "Ctrl+1"},
|
||||
{"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"showGrid", "Ctrl+'"}, {"showGuides", "Ctrl+;"}, {"gridSettings", ""}, {"showRulers", "Ctrl+R"}, {"snapEnabled", "Ctrl+Shift+;"}, {"snapToGuides", ""}, {"snapToGrid", ""}, {"snapToLayers", ""}, {"snapToDocumentBounds", ""}, {"lockGuides", "Ctrl+Alt+;"}, {"clearGuides", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"},
|
||||
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"keyboardShortcuts", ""}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
|
||||
@@ -49,7 +49,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
for (const QMenu *menu : menus)
|
||||
titles << menu->title();
|
||||
QCOMPARE(titles, (QStringList{"&File", "&Edit", "&View", "&Select", "&Image", "Fil&ter", "&Layer"}));
|
||||
QCOMPARE(menus[0]->actions().size(), 12);
|
||||
QCOMPARE(menus[0]->actions().size(), 13);
|
||||
QCOMPARE(menus[1]->actions().size(), 13);
|
||||
QCOMPARE(menus[2]->actions().size(), 19);
|
||||
QCOMPARE(menus[3]->actions().size(), 11);
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include "UI/ColorPickerSheet+Dialog.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QToolButton>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSlider>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's ExportAsSheet: the preview waits, encodes aside, then exports.
|
||||
class ExportAsSheetTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void theSheetShowsTheRasterAndWaitsForItsPreview();
|
||||
void aChangeSupersedesThePreview();
|
||||
void aSupersededEncodingNeverLands();
|
||||
void theMatteComesFromTheAppsPicker();
|
||||
void exportKeepsTheQualityAndCancelDoesNot();
|
||||
void theSavedQualityIsReadAsSwiftReadsIt();
|
||||
void anEncodingThatFailsIsShown();
|
||||
void eachFormatShowsItsRowsAndWritesItsBytes();
|
||||
void theSizeKeepsTheProportionsWithinTheLimits();
|
||||
void aClearPNGShowsTheCheckerboard();
|
||||
};
|
||||
|
||||
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<std::optional<ExportAsSheet::Chosen>> answer;
|
||||
EditorSession session;
|
||||
ExportAsSheet sheet;
|
||||
explicit Sheet(ExportRaster raster = halfRed(), ExportFormat format = ExportFormat::jpeg)
|
||||
: sheet(std::move(raster), session, format, [this](std::optional<ExportAsSheet::Chosen> chosen) { answer = std::move(chosen); })
|
||||
{
|
||||
sheet.show();
|
||||
}
|
||||
template <typename Widget> Widget &find(const char *name)
|
||||
{
|
||||
Widget *found = sheet.findChild<Widget *>(QString::fromLatin1(name));
|
||||
if (!found)
|
||||
throw std::runtime_error(std::string("no widget named ") + name);
|
||||
return *found;
|
||||
}
|
||||
bool ready() { return find<QPushButton>("exportExport").isEnabled(); }
|
||||
QString note() { return find<QLabel>("exportUpdating").text(); }
|
||||
QColor previewAt(QPoint point) { return find<QWidget>("exportPreview").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<QLabel *>())
|
||||
texts << label->text();
|
||||
return texts;
|
||||
}
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QSettings().clear();
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::theSheetShowsTheRasterAndWaitsForItsPreview()
|
||||
{
|
||||
Sheet shown(halfRed(1200, 800));
|
||||
const QStringList texts = labels(shown.sheet);
|
||||
QVERIFY(texts.contains("Export As") && texts.contains("Format") && texts.contains("Size") && texts.contains("Quality"));
|
||||
QVERIFY(texts.contains("Background") && texts.contains("sRGB"));
|
||||
QCOMPARE(shown.find<QLabel>("exportPercent").text(), QString("85%"));
|
||||
QCOMPARE(shown.find<QSlider>("exportQuality").value(), 85);
|
||||
QCOMPARE(shown.find<QWidget>("exportPreview").size(), QSize(560, 330));
|
||||
// Swift's task sleeps 200 ms before it encodes.
|
||||
auto &wait = shown.find<QTimer>("exportWait");
|
||||
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<QProgressBar>("exportSpinner").isVisible());
|
||||
QVERIFY(!shown.find<QLabel>("exportBytes").isVisible() && !shown.find<QLabel>("exportError").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<QLabel>("exportBytes").text(), ByteCounts::file(expected.data.size()));
|
||||
QVERIFY(shown.find<QLabel>("exportBytes").isVisible() && !shown.find<QLabel>("exportUpdating").isVisible());
|
||||
QVERIFY(!shown.find<QProgressBar>("exportSpinner").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 ExportAsSheetTests::aChangeSupersedesThePreview()
|
||||
{
|
||||
Sheet shown;
|
||||
QTRY_VERIFY(shown.ready());
|
||||
auto &quality = shown.find<QSlider>("exportQuality");
|
||||
// Each change waits anew; the stale preview stays beneath.
|
||||
quality.setValue(10);
|
||||
QCOMPARE(shown.find<QLabel>("exportPercent").text(), QString("10%"));
|
||||
QVERIFY(!shown.ready());
|
||||
QCOMPARE(shown.note(), QString("Updating…"));
|
||||
QVERIFY(shown.find<QTimer>("exportWait").isActive());
|
||||
QVERIFY(shown.find<QProgressBar>("exportSpinner").isVisible());
|
||||
quality.setValue(100);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
QCOMPARE(shown.find<QLabel>("exportBytes").text(), ByteCounts::file(ImageExporter::jpeg(halfRed(), {.quality = 1}).data.size()));
|
||||
shown.find<QPushButton>("exportExport").click();
|
||||
QCOMPARE(shown.answer.value().value().data, ImageExporter::jpeg(halfRed(), {.quality = 1}).data);
|
||||
QCOMPARE(shown.answer.value().value().format, ExportFormat::jpeg);
|
||||
}
|
||||
|
||||
// Slow to encode: the change comes while it runs.
|
||||
void ExportAsSheetTests::aSupersededEncodingNeverLands()
|
||||
{
|
||||
QImage noisy(6000, 4000, QImage::Format_RGBA8888_Premultiplied);
|
||||
quint32 seed = 1;
|
||||
for (int y = 0; y < noisy.height(); ++y) {
|
||||
auto *row = reinterpret_cast<quint32 *>(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<QTimer>("exportWait").isActive());
|
||||
shown.find<QSlider>("exportQuality").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<QLabel>("exportPercent").text(), QString("50%"));
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::theMatteComesFromTheAppsPicker()
|
||||
{
|
||||
QSettings().remove(ExportAsSheet::qualityKey);
|
||||
Sheet shown;
|
||||
QTRY_VERIFY(shown.ready());
|
||||
auto &matte = shown.find<DialogColorSwatch>("exportBackground");
|
||||
QVERIFY(matte.accessibleName() == QString("Export Background") && matte.toolTip() == QString("Color that fills transparent areas"));
|
||||
matte.click();
|
||||
QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Export Background)"));
|
||||
QVERIFY(shown.session.colorPicker().value().original == PaletteColor::white());
|
||||
// The same colour asks for nothing.
|
||||
shown.session.closeColorPicker(true);
|
||||
QVERIFY(shown.ready() && !shown.find<QTimer>("exportWait").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<QPushButton>("exportExport").click();
|
||||
QVERIFY(!shown.session.colorPicker());
|
||||
const QImage written = QImage::fromData(shown.answer.value().value().data, "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().data, ImageExporter::jpeg(halfRed(), {.red = 0, .green = 0, .blue = 1}).data);
|
||||
// Cancel and the sheet going close it too.
|
||||
Sheet cancelled;
|
||||
cancelled.find<DialogColorSwatch>("exportBackground").click();
|
||||
cancelled.find<QPushButton>("exportCancel").click();
|
||||
QVERIFY(!cancelled.session.colorPicker() && !cancelled.answer.value());
|
||||
EditorSession session;
|
||||
auto gone = std::make_unique<ExportAsSheet>(halfRed(), session, ExportFormat::jpeg, [](std::optional<ExportAsSheet::Chosen>) {});
|
||||
gone->findChild<DialogColorSwatch *>()->click();
|
||||
gone.reset();
|
||||
QVERIFY(!session.colorPicker());
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::exportKeepsTheQualityAndCancelDoesNot()
|
||||
{
|
||||
QSettings().remove(ExportAsSheet::qualityKey);
|
||||
{
|
||||
Sheet shown;
|
||||
shown.find<QSlider>("exportQuality").setValue(40);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("exportCancel").click();
|
||||
QVERIFY(!shown.answer.value().has_value());
|
||||
QVERIFY(!QSettings().contains(ExportAsSheet::qualityKey));
|
||||
}
|
||||
{
|
||||
Sheet shown;
|
||||
QCOMPARE(shown.find<QSlider>("exportQuality").value(), 85);
|
||||
shown.find<QSlider>("exportQuality").setValue(40);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("exportExport").click();
|
||||
QCOMPARE(QSettings().value(ExportAsSheet::qualityKey).toDouble(), 0.4);
|
||||
}
|
||||
Sheet next;
|
||||
QCOMPARE(next.find<QSlider>("exportQuality").value(), 40);
|
||||
QCOMPARE(next.find<QLabel>("exportPercent").text(), QString("40%"));
|
||||
// Return reaches Export once it is ready.
|
||||
QTRY_VERIFY(next.ready());
|
||||
QVERIFY(next.find<QPushButton>("exportExport").isDefault());
|
||||
QCOMPARE(next.find<QPushButton>("exportExport").text(), QString("Export…"));
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::theSavedQualityIsReadAsSwiftReadsIt()
|
||||
{
|
||||
const auto started = [](const QVariant &saved) {
|
||||
QSettings().setValue(ExportAsSheet::qualityKey, saved);
|
||||
Sheet shown;
|
||||
return shown.find<QLabel>("exportPercent").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(ExportAsSheet::qualityKey);
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::anEncodingThatFailsIsShown()
|
||||
{
|
||||
// JPEG holds at most 65,500 pixels a side.
|
||||
Sheet shown(halfRed(70'000, 1));
|
||||
auto &error = shown.find<QLabel>("exportError");
|
||||
QTRY_VERIFY(error.isVisible());
|
||||
QCOMPARE(error.text(), QString::fromUtf8(ExportError(ExportError::Kind::encode).what()));
|
||||
QCOMPARE(error.foregroundRole(), QPalette::BrightText);
|
||||
QVERIFY(!shown.ready() && !shown.find<QLabel>("exportUpdating").isVisible() && !shown.find<QProgressBar>("exportSpinner").isVisible());
|
||||
// A change clears the error while it waits.
|
||||
shown.find<QSlider>("exportQuality").setValue(50);
|
||||
QVERIFY(!error.isVisible());
|
||||
QCOMPARE(shown.note(), QString("Updating…"));
|
||||
QTRY_VERIFY(error.isVisible());
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::eachFormatShowsItsRowsAndWritesItsBytes()
|
||||
{
|
||||
const ExportRaster raster = halfRed();
|
||||
const auto rows = [](Sheet &shown) {
|
||||
return std::array<bool, 3>{shown.find<QSlider>("exportQuality").isVisible(), shown.find<QLabel>("exportPage").isVisible(),
|
||||
shown.find<DialogColorSwatch>("exportBackground").isVisible()};
|
||||
};
|
||||
QSettings().remove(ExportAsSheet::qualityKey);
|
||||
{
|
||||
Sheet shown(raster, ExportFormat::png);
|
||||
QVERIFY(shown.find<QAbstractButton>("exportFormatPNG").isChecked());
|
||||
QCOMPARE(rows(shown), (std::array<bool, 3>{false, false, false}));
|
||||
QCOMPARE(shown.find<QLabel>("exportNote").text(), QString("Transparency kept · sRGB"));
|
||||
QTRY_VERIFY(shown.ready());
|
||||
QCOMPARE(shown.find<QLabel>("exportBytes").text(), ByteCounts::file(ImageExporter::pngData(raster).size()));
|
||||
shown.find<QPushButton>("exportExport").click();
|
||||
QCOMPARE(shown.answer.value().value().data, ImageExporter::pngData(raster));
|
||||
QCOMPARE(shown.answer.value().value().format, ExportFormat::png);
|
||||
// Only a JPEG keeps its quality for next time.
|
||||
QVERIFY(!QSettings().contains(ExportAsSheet::qualityKey));
|
||||
}
|
||||
Sheet shown(raster, ExportFormat::png);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
// Another format waits for its own result.
|
||||
shown.find<QAbstractButton>("exportFormatPDF").click();
|
||||
QVERIFY(!shown.ready() && shown.find<QAbstractButton>("exportFormatPDF").isChecked() && !shown.find<QAbstractButton>("exportFormatPNG").isChecked());
|
||||
QCOMPARE(rows(shown), (std::array<bool, 3>{false, true, true}));
|
||||
QCOMPARE(shown.find<QLabel>("exportNote").text(), QString("sRGB"));
|
||||
// 64 by 32 at 144 per inch.
|
||||
QCOMPARE(shown.find<QLabel>("exportPage").text(), QString("0.44 × 0.22 in at 144 DPI"));
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("exportExport").click();
|
||||
QCOMPARE(shown.answer.value().value().data, ImageExporter::pdfData(raster, QColor(Qt::white)));
|
||||
QCOMPARE(shown.answer.value().value().format, ExportFormat::pdf);
|
||||
// The PDF preview lies over its page's white.
|
||||
QCOMPARE(shown.previewAt({100, 165}), QColor(Qt::white));
|
||||
shown.find<QAbstractButton>("exportFormatJPEG").click();
|
||||
QCOMPARE(rows(shown), (std::array<bool, 3>{true, false, true}));
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::theSizeKeepsTheProportionsWithinTheLimits()
|
||||
{
|
||||
const ExportRaster raster = halfRed();
|
||||
Sheet shown(raster, ExportFormat::png);
|
||||
auto &width = shown.find<QLineEdit>("exportWidth"), &height = shown.find<QLineEdit>("exportHeight");
|
||||
auto &scale = shown.find<QToolButton>("exportScale");
|
||||
QVERIFY(width.text() == "64" && height.text() == "32" && scale.text() == "100%");
|
||||
QCOMPARE(width.toolTip(), QString("Width in pixels; the height follows, keeping the proportions"));
|
||||
const auto type = [](QLineEdit &field, const QString &text) {
|
||||
field.setFocus();
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, text);
|
||||
QTest::keyClick(&field, Qt::Key_Return);
|
||||
};
|
||||
type(width, "32");
|
||||
QVERIFY(height.text() == "16" && scale.text() == "50%");
|
||||
QCOMPARE(shown.sheet.settings(), (ExportAsSheet::Settings{ExportFormat::png, shown.sheet.settings().options, 32, 16}));
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("exportExport").click();
|
||||
QCOMPARE(shown.answer.value().value().data, ImageExporter::pngData(ImageExporter::resized(raster, 32, 16)));
|
||||
type(height, "64");
|
||||
QVERIFY(width.text() == "128" && scale.text() == "200%");
|
||||
// Other text goes back; a side holds a pixel.
|
||||
type(width, "wide");
|
||||
QCOMPARE(width.text(), QString("128"));
|
||||
type(width, "0");
|
||||
QVERIFY(width.text() == "1" && height.text() == "1" && scale.text() == "2%");
|
||||
// The surface's limit caps a growth: 312.5 times here.
|
||||
type(width, "100000");
|
||||
QVERIFY(width.text() == "20000" && height.text() == "10000");
|
||||
// The menu's steps.
|
||||
QStringList steps;
|
||||
for (QAction *each : scale.menu()->actions())
|
||||
steps << each->text();
|
||||
QCOMPARE(steps, (QStringList{"25%", "50%", "75%", "100%", "200%"}));
|
||||
scale.menu()->actions().at(0)->trigger();
|
||||
QVERIFY(width.text() == "16" && height.text() == "8" && scale.text() == "25%");
|
||||
// The same size asks nothing new, as Swift's task id.
|
||||
QTRY_VERIFY(shown.ready());
|
||||
scale.menu()->actions().at(0)->trigger();
|
||||
QVERIFY(!shown.find<QTimer>("exportWait").isActive());
|
||||
QCOMPARE(scale.toolTip(), QString("Scale the export: the document stays as it is"));
|
||||
// A hundredth at least: 1120 wide stops at 11.
|
||||
Sheet large(halfRed(1120, 660), ExportFormat::png);
|
||||
auto &wide = large.find<QLineEdit>("exportWidth");
|
||||
auto &in = large.find<QPushButton>("exportZoomIn");
|
||||
QVERIFY(in.isEnabled() && in.toolTip().contains("now 50%"));
|
||||
type(wide, "-5");
|
||||
QVERIFY(wide.text() == "11" && large.find<QLineEdit>("exportHeight").text() == "7");
|
||||
// Fit follows the export's size: 11 by 7, 47 times.
|
||||
QVERIFY(!in.isEnabled() && in.toolTip().contains("now 4714%"));
|
||||
}
|
||||
|
||||
void ExportAsSheetTests::aClearPNGShowsTheCheckerboard()
|
||||
{
|
||||
// 330 square fits the frame's height, one pixel a point.
|
||||
QImage clear(330, 330, QImage::Format_RGBA8888_Premultiplied);
|
||||
clear.fill(Qt::transparent);
|
||||
Sheet shown({clear, 72}, ExportFormat::png);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
// The image starts at x 115: grays 0.35, 0.30.
|
||||
QCOMPARE(shown.previewAt({116, 1}).rgb(), QColor::fromRgbF(0.35f, 0.35f, 0.35f).rgb());
|
||||
QCOMPARE(shown.previewAt({126, 1}).rgb(), QColor::fromRgbF(0.30f, 0.30f, 0.30f).rgb());
|
||||
QCOMPARE(shown.previewAt({126, 11}).rgb(), QColor::fromRgbF(0.35f, 0.35f, 0.35f).rgb());
|
||||
// Outside the image, the frame's own gray.
|
||||
QCOMPARE(shown.previewAt({100, 165}), QColor(31, 31, 31));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ExportAsSheetTests)
|
||||
#include "ExportAsSheetTests.moc"
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include <QtEndian>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.4.8's PDF and Export As's resize, in the exporter.
|
||||
namespace {
|
||||
// Swift's fixture: 6 by 6, red left, clear right.
|
||||
ProjectSnapshot snapshot(double resolution)
|
||||
{
|
||||
QImage image = BrushRaster::context(2, 2, false);
|
||||
image.setPixel(0, 0, qRgba(255, 0, 0, 255));
|
||||
image.setPixel(0, 1, qRgba(255, 0, 0, 255));
|
||||
const QUuid id = QUuid::createUuid();
|
||||
const LayerTransform transform{.origin = {1, 1}, .size = {4, 4}, .sampling = LayerSampling::nearest};
|
||||
const ProjectLayerRecord record{.id = id, .name = "Red", .isVisible = true, .transform = transform, .imageFile = id.toString(QUuid::WithoutBraces) + ".png"};
|
||||
ProjectSnapshot made{.manifest = {.documentID = QUuid::createUuid(), .width = 6, .height = 6, .activeLayerID = id, .layers = {record}},
|
||||
.images = {{id, ImportedImage(image, image, "Red")}}};
|
||||
made.manifest.resolution = resolution;
|
||||
return made;
|
||||
}
|
||||
|
||||
// Object `number`'s text, found through the cross-reference table.
|
||||
QByteArray object(const QByteArray &pdf, int number)
|
||||
{
|
||||
const qsizetype table = pdf.lastIndexOf("startxref\n");
|
||||
const qsizetype xref = pdf.mid(table + 10).split('\n').value(0).toLongLong();
|
||||
const QList<QByteArray> lines = pdf.mid(xref).split('\n');
|
||||
const qsizetype offset = lines.value(2 + number).left(10).toLongLong();
|
||||
const QByteArray start = QByteArray::number(number) + " 0 obj\n";
|
||||
if (pdf.mid(offset, start.size()) != start)
|
||||
throw std::runtime_error("cross-reference points elsewhere");
|
||||
return pdf.mid(offset, pdf.indexOf("\nendobj\n", offset) - offset);
|
||||
}
|
||||
|
||||
// A stream's bytes, inflated; its /Length must be its size.
|
||||
QByteArray streamOf(const QByteArray &text)
|
||||
{
|
||||
const qsizetype begin = text.indexOf(">>\nstream\n") + 10, end = text.lastIndexOf("\nendstream");
|
||||
const QByteArray body = text.mid(begin, end - begin);
|
||||
const QByteArray dictionary = text.left(begin);
|
||||
const qsizetype length = dictionary.mid(dictionary.indexOf("/Length ") + 8).split(' ').value(0).toLongLong();
|
||||
if (length != body.size())
|
||||
throw std::runtime_error("stream length is wrong");
|
||||
if (!dictionary.contains("/FlateDecode"))
|
||||
return body;
|
||||
QByteArray sized(4, 0);
|
||||
qToBigEndian(quint32(1 << 20), sized.data());
|
||||
return qUncompress(sized + body);
|
||||
}
|
||||
}
|
||||
|
||||
class ExportPDFTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void pdfIsOnePageAtThePrintedSizeWithLosslessPixels();
|
||||
void aBackgroundFillsThePageUnderThePicture();
|
||||
void anOversizedCanvasIsRefused();
|
||||
void resizingKeepsTheResolutionWithinTheLimits();
|
||||
void flatteningLaysThePictureOverItsBackground();
|
||||
};
|
||||
|
||||
void ExportPDFTests::pdfIsOnePageAtThePrintedSizeWithLosslessPixels()
|
||||
{
|
||||
const QByteArray pdf = ImageExporter::pdfData(snapshot(150));
|
||||
QVERIFY(pdf.startsWith("%PDF-1.4\n") && pdf.endsWith("%%EOF\n"));
|
||||
QCOMPARE(pdf.count("/Type /Page "), qsizetype(1));
|
||||
// 6 pixels at 150 per inch: 0.04 in, 2.88 points.
|
||||
QVERIFY(object(pdf, 3).contains("/MediaBox [0 0 2.8800 2.8800]"));
|
||||
QVERIFY(pdf.contains("/FlateDecode") && !pdf.contains("/DCTDecode"));
|
||||
// The pixels as they were: red, then clear beside it.
|
||||
const QByteArray colour = streamOf(object(pdf, 5)), alpha = streamOf(object(pdf, 6));
|
||||
QVERIFY(object(pdf, 5).contains("/Width 6 /Height 6") && object(pdf, 5).contains("/SMask 6 0 R"));
|
||||
QCOMPARE(colour.size(), qsizetype(6 * 6 * 3));
|
||||
QCOMPARE(alpha.size(), qsizetype(6 * 6));
|
||||
const auto at = [&](int x, int y) { return std::array<int, 4>{uchar(colour[(y * 6 + x) * 3]), uchar(colour[(y * 6 + x) * 3 + 1]), uchar(colour[(y * 6 + x) * 3 + 2]), uchar(alpha[y * 6 + x])}; };
|
||||
QCOMPARE(at(1, 1), (std::array<int, 4>{255, 0, 0, 255}));
|
||||
QCOMPARE(at(4, 1)[3], 0);
|
||||
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"));
|
||||
// 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));
|
||||
const QByteArray edge = ImageExporter::pdfData(ExportRaster{soft, 72});
|
||||
QCOMPARE(streamOf(object(edge, 5)), QByteArray("\xff\x00\x00", 3));
|
||||
QCOMPARE(streamOf(object(edge, 6)), QByteArray("\x80", 1));
|
||||
// At 72 per inch a pixel is a point.
|
||||
QVERIFY(object(ImageExporter::pdfData(snapshot(72)), 3).contains("/MediaBox [0 0 6.0000 6.0000]"));
|
||||
QVERIFY(object(ImageExporter::pdfData(snapshot(0)), 3).contains("/MediaBox [0 0 6.0000 6.0000]"));
|
||||
}
|
||||
|
||||
void ExportPDFTests::aBackgroundFillsThePageUnderThePicture()
|
||||
{
|
||||
const ExportRaster raster = ImageExporter::render(snapshot(300));
|
||||
const QByteArray pdf = ImageExporter::pdfData(raster, QColor(255, 128, 0));
|
||||
QCOMPARE(streamOf(object(pdf, 4)), QByteArray("q\n1.0000 0.5020 0.0000 rg 0 0 1.4400 1.4400 re f\n1.4400 0 0 1.4400 0 0 cm /Im0 Do\nQ"));
|
||||
}
|
||||
|
||||
void ExportPDFTests::anOversizedCanvasIsRefused()
|
||||
{
|
||||
const ProjectSnapshot huge{.manifest = {.documentID = QUuid::createUuid(), .width = 30'000, .height = 30'000, .activeLayerID = std::nullopt, .layers = {}}, .images = {}};
|
||||
QVERIFY_THROWS_EXCEPTION(ExportError, ImageExporter::pdfData(huge));
|
||||
}
|
||||
|
||||
void ExportPDFTests::resizingKeepsTheResolutionWithinTheLimits()
|
||||
{
|
||||
QImage pair = BrushRaster::context(2, 1, false);
|
||||
pair.setPixelColor(0, 0, Qt::black);
|
||||
pair.setPixelColor(1, 0, Qt::white);
|
||||
const ExportRaster raster{pair, 300};
|
||||
// The same size is the same raster.
|
||||
QCOMPARE(ImageExporter::resized(raster, 2, 1).image.cacheKey(), pair.cacheKey());
|
||||
const ExportRaster half = ImageExporter::resized(raster, 1, 1);
|
||||
QVERIFY(half.image.size() == QSize(1, 1) && half.resolution == 300);
|
||||
QVERIFY(std::abs(half.image.pixelColor(0, 0).red() - 128) <= 1);
|
||||
QCOMPARE(ImageExporter::resized(raster, 8, 3).image.size(), QSize(8, 3));
|
||||
for (const QSize wrong : {QSize(0, 1), QSize(1, 0), QSize(30'001, 1), QSize(1, 30'001), QSize(20'000, 20'000)})
|
||||
QVERIFY_THROWS_EXCEPTION(ExportError, ImageExporter::resized(raster, wrong.width(), wrong.height()));
|
||||
}
|
||||
|
||||
void ExportPDFTests::flatteningLaysThePictureOverItsBackground()
|
||||
{
|
||||
QImage half = BrushRaster::context(1, 1, false);
|
||||
half.setPixelColor(0, 0, QColor(255, 0, 0, 128));
|
||||
const QImage flat = ImageExporter::flattened({half, 72}, QColor(0, 0, 255));
|
||||
QVERIFY(!flat.hasAlphaChannel());
|
||||
const QColor mixed = flat.pixelColor(0, 0);
|
||||
QVERIFY2(std::abs(mixed.red() - 128) <= 1 && mixed.green() == 0 && std::abs(mixed.blue() - 127) <= 1, qPrintable(mixed.name()));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ExportPDFTests)
|
||||
#include "ExportPDFTests.moc"
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "Rendering/AdjustmentSurface.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include "UI/LevelsSheet.h"
|
||||
#include <QScrollBar>
|
||||
#include <QtTest>
|
||||
@@ -178,8 +178,8 @@ void HighDpiCanvasTests::theJPEGPreviewCountsDevicePixels()
|
||||
for (int y = 0; y < 800; ++y)
|
||||
for (int x = 0; x < 1200; ++x)
|
||||
image.setPixelColor(x, y, QColor((x * 7 + y) % 256, (x + y * 3) % 256, (x * y) % 256));
|
||||
JPEGPreview preview(QSize(1200, 800), [] {}, nullptr);
|
||||
preview.setImage(image, false);
|
||||
JPEGPreview preview([] {}, nullptr);
|
||||
preview.setImage(image, QSize(1200, 800), false);
|
||||
preview.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&preview));
|
||||
// 600 by 400 points: Fit 0.825, averaged per device pixel.
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ColorPickerSheet+Dialog.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSlider>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
// 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<std::optional<QByteArray>> answer;
|
||||
EditorSession session;
|
||||
JPEGExportSheet sheet;
|
||||
explicit Sheet(ExportRaster raster = halfRed())
|
||||
: sheet(std::move(raster), session, [this](std::optional<QByteArray> data) { answer = std::move(data); })
|
||||
{
|
||||
sheet.show();
|
||||
}
|
||||
template <typename Widget> Widget &find(const char *name)
|
||||
{
|
||||
Widget *found = sheet.findChild<Widget *>(QString::fromLatin1(name));
|
||||
if (!found)
|
||||
throw std::runtime_error(std::string("no widget named ") + name);
|
||||
return *found;
|
||||
}
|
||||
bool ready() { return find<QPushButton>("jpegExport").isEnabled(); }
|
||||
QString note() { return find<QLabel>("jpegNote").text(); }
|
||||
QColor previewAt(QPoint point) { return find<QWidget>("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<QLabel *>())
|
||||
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<QLabel>("jpegPercent").text(), QString("85%"));
|
||||
QCOMPARE(shown.find<QSlider>("jpegQuality").value(), 85);
|
||||
QCOMPARE(shown.find<QWidget>("jpegPreview").size(), QSize(560, 330));
|
||||
// Swift's task sleeps 200 ms before it encodes.
|
||||
auto &wait = shown.find<QTimer>("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<QProgressBar>("jpegSpinner").isVisible());
|
||||
QVERIFY(!shown.find<QLabel>("jpegBytes").isVisible() && !shown.find<QLabel>("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<QLabel>("jpegBytes").text(), ByteCounts::file(expected.data.size()));
|
||||
QVERIFY(shown.find<QLabel>("jpegBytes").isVisible() && !shown.find<QLabel>("jpegNote").isVisible());
|
||||
QVERIFY(!shown.find<QProgressBar>("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<QSlider>("jpegQuality");
|
||||
// Each change waits anew; the stale preview stays beneath.
|
||||
quality.setValue(10);
|
||||
QCOMPARE(shown.find<QLabel>("jpegPercent").text(), QString("10%"));
|
||||
QVERIFY(!shown.ready());
|
||||
QCOMPARE(shown.note(), QString("Updating…"));
|
||||
QVERIFY(shown.find<QTimer>("jpegWait").isActive());
|
||||
QVERIFY(shown.find<QProgressBar>("jpegSpinner").isVisible());
|
||||
quality.setValue(100);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
QCOMPARE(shown.find<QLabel>("jpegBytes").text(), ByteCounts::file(ImageExporter::jpeg(halfRed(), {.quality = 1}).data.size()));
|
||||
shown.find<QPushButton>("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<quint32 *>(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<QTimer>("jpegWait").isActive());
|
||||
shown.find<QSlider>("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<QLabel>("jpegPercent").text(), QString("50%"));
|
||||
}
|
||||
|
||||
void JPEGExportSheetTests::theMatteComesFromTheAppsPicker()
|
||||
{
|
||||
QSettings().remove(JPEGExportSheet::qualityKey);
|
||||
Sheet shown;
|
||||
QTRY_VERIFY(shown.ready());
|
||||
auto &matte = shown.find<DialogColorSwatch>("jpegMatte");
|
||||
QVERIFY(matte.accessibleName() == QString("JPEG Background") && matte.toolTip() == QString("Color that fills transparent areas"));
|
||||
QCOMPARE(qobject_cast<QHBoxLayout *>(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<QTimer>("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<QPushButton>("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<DialogColorSwatch>("jpegMatte").click();
|
||||
cancelled.find<QPushButton>("jpegCancel").click();
|
||||
QVERIFY(!cancelled.session.colorPicker() && !cancelled.answer.value());
|
||||
EditorSession session;
|
||||
auto gone = std::make_unique<JPEGExportSheet>(halfRed(), session, [](std::optional<QByteArray>) {});
|
||||
gone->findChild<DialogColorSwatch *>()->click();
|
||||
gone.reset();
|
||||
QVERIFY(!session.colorPicker());
|
||||
}
|
||||
|
||||
void JPEGExportSheetTests::exportKeepsTheQualityAndCancelDoesNot()
|
||||
{
|
||||
QSettings().remove(JPEGExportSheet::qualityKey);
|
||||
{
|
||||
Sheet shown;
|
||||
shown.find<QSlider>("jpegQuality").setValue(40);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("jpegCancel").click();
|
||||
QVERIFY(!shown.answer.value().has_value());
|
||||
QVERIFY(!QSettings().contains(JPEGExportSheet::qualityKey));
|
||||
}
|
||||
{
|
||||
Sheet shown;
|
||||
QCOMPARE(shown.find<QSlider>("jpegQuality").value(), 85);
|
||||
shown.find<QSlider>("jpegQuality").setValue(40);
|
||||
QTRY_VERIFY(shown.ready());
|
||||
shown.find<QPushButton>("jpegExport").click();
|
||||
QCOMPARE(QSettings().value(JPEGExportSheet::qualityKey).toDouble(), 0.4);
|
||||
}
|
||||
Sheet next;
|
||||
QCOMPARE(next.find<QSlider>("jpegQuality").value(), 40);
|
||||
QCOMPARE(next.find<QLabel>("jpegPercent").text(), QString("40%"));
|
||||
// Return reaches Export once it is ready.
|
||||
QTRY_VERIFY(next.ready());
|
||||
QVERIFY(next.find<QPushButton>("jpegExport").isDefault());
|
||||
QCOMPARE(next.find<QPushButton>("jpegExport").text(), QString("Export…"));
|
||||
}
|
||||
|
||||
void JPEGExportSheetTests::theSavedQualityIsReadAsSwiftReadsIt()
|
||||
{
|
||||
const auto started = [](const QVariant &saved) {
|
||||
QSettings().setValue(JPEGExportSheet::qualityKey, saved);
|
||||
Sheet shown;
|
||||
return shown.find<QLabel>("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<QLabel>("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<QLabel>("jpegNote").isVisible() && !shown.find<QProgressBar>("jpegSpinner").isVisible());
|
||||
// A change clears the error while it waits.
|
||||
shown.find<QSlider>("jpegQuality").setValue(50);
|
||||
QVERIFY(!error.isVisible());
|
||||
QCOMPARE(shown.note(), QString("Updating…"));
|
||||
QTRY_VERIFY(error.isVisible());
|
||||
}
|
||||
|
||||
QTEST_MAIN(JPEGExportSheetTests)
|
||||
#include "JPEGExportSheetTests.moc"
|
||||
+21
-18
@@ -1,4 +1,4 @@
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include "Document/EditorSession.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include <QHBoxLayout>
|
||||
@@ -64,8 +64,10 @@ struct Paints : QObject {
|
||||
};
|
||||
|
||||
struct Shown {
|
||||
// The export's size, the preview's image at most that.
|
||||
QSize pixels;
|
||||
JPEGPreview preview;
|
||||
explicit Shown(QSize pixels, std::function<void()> zoomed = [] {}) : preview(pixels, std::move(zoomed), nullptr)
|
||||
explicit Shown(QSize size, std::function<void()> zoomed = [] {}) : pixels(size), preview(std::move(zoomed), nullptr)
|
||||
{
|
||||
preview.show();
|
||||
if (!QTest::qWaitForWindowExposed(&preview))
|
||||
@@ -110,14 +112,14 @@ void JPEGPreviewTests::itDrawsFittedAndBelowOneThroughAnAverage()
|
||||
QCOMPARE(drawn(preview).pixelColor(280, 165), QColor(31, 31, 31));
|
||||
Paints paints(preview.viewport());
|
||||
const QImage first = noise(1100, 660, 1);
|
||||
preview.setImage(first, false);
|
||||
preview.setImage(first, shown.pixels, false);
|
||||
QTRY_VERIFY(paints.count > 0);
|
||||
// Fitted, 550 by 330 in the middle, averaged down.
|
||||
QVERIFY(shows(preview, QRect(5, 0, 550, 330), first.scaled(550, 330, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)));
|
||||
QCOMPARE(drawn(preview).pixelColor(4, 165), QColor(31, 31, 31));
|
||||
// A new image of the same size is averaged anew.
|
||||
const QImage second = noise(1100, 660, 2);
|
||||
preview.setImage(second, false);
|
||||
preview.setImage(second, shown.pixels, false);
|
||||
QVERIFY(shows(preview, QRect(5, 0, 550, 330), second.scaled(550, 330, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)));
|
||||
// A quarter, then Fit again: each size averaged for itself.
|
||||
QCoreApplication::processEvents();
|
||||
@@ -136,7 +138,7 @@ void JPEGPreviewTests::fromOneUpEachPixelShowsAsItIs()
|
||||
Shown shown(QSize(1100, 660));
|
||||
JPEGPreview &preview = shown.preview;
|
||||
const QImage image = noise(1100, 660, 3);
|
||||
preview.setImage(image, false);
|
||||
preview.setImage(image, shown.pixels, false);
|
||||
shown.zoom(2);
|
||||
// Bars come once laid out; ranges follow the view.
|
||||
QScrollBar &across = *preview.horizontalScrollBar(), &down = *preview.verticalScrollBar();
|
||||
@@ -149,12 +151,12 @@ void JPEGPreviewTests::fromOneUpEachPixelShowsAsItIs()
|
||||
down.setValue(60);
|
||||
QVERIFY(shows(preview, QRect(0, 0, 200, 100), image.copy(50, 30, 100, 50).scaled(200, 100)));
|
||||
// A smaller decode still spans the export's pixels.
|
||||
preview.setImage(image.scaled(550, 330), false);
|
||||
preview.setImage(image.scaled(550, 330), shown.pixels, false);
|
||||
shown.zoom(1);
|
||||
QCOMPARE(down.maximum(), 660 - preview.viewport()->height());
|
||||
// A size between pixels reaches its last one.
|
||||
Shown odd(QSize(1201, 1201));
|
||||
odd.preview.setImage(noise(1201, 1201, 4), false);
|
||||
odd.preview.setImage(noise(1201, 1201, 4), odd.pixels, false);
|
||||
odd.zoom(0.5);
|
||||
QCOMPARE(odd.preview.horizontalScrollBar()->maximum(), int(std::ceil(600.5 - odd.preview.viewport()->width())));
|
||||
}
|
||||
@@ -162,7 +164,7 @@ void JPEGPreviewTests::fromOneUpEachPixelShowsAsItIs()
|
||||
void JPEGPreviewTests::theUpdatingPlateSitsInTheMiddle()
|
||||
{
|
||||
Shown shown(QSize(1100, 660));
|
||||
shown.preview.setImage(QImage(), true);
|
||||
shown.preview.setImage(QImage(), shown.pixels, true);
|
||||
const QImage image = drawn(shown.preview);
|
||||
const QColor window = shown.preview.palette().color(QPalette::Window);
|
||||
const int plate = qRound(window.red() * 0.85 + 31 * 0.15);
|
||||
@@ -181,7 +183,7 @@ void JPEGPreviewTests::zoomKeepsTheMiddleAndDragsMoveIt()
|
||||
int heard = 0;
|
||||
Shown shown(QSize(1200, 800), [&heard] { ++heard; });
|
||||
JPEGPreview &preview = shown.preview;
|
||||
preview.setImage(noise(1200, 800, 5), false);
|
||||
preview.setImage(noise(1200, 800, 5), shown.pixels, false);
|
||||
// From Fit, the middle of the image.
|
||||
Paints paints(preview.viewport());
|
||||
preview.setZoom(1);
|
||||
@@ -204,6 +206,7 @@ void JPEGPreviewTests::zoomKeepsTheMiddleAndDragsMoveIt()
|
||||
QVERIFY(across.value() == 20 && down.value() == 35);
|
||||
// A half-pixel landing rounds, as Swift scrolls to it.
|
||||
Shown odd(QSize(1201, 801));
|
||||
odd.preview.setImage(QImage(), odd.pixels, true);
|
||||
odd.preview.setZoom(1);
|
||||
QVERIFY(odd.preview.horizontalScrollBar()->value() == 321 && odd.preview.verticalScrollBar()->value() == 236);
|
||||
shown.zoom(2);
|
||||
@@ -244,26 +247,26 @@ void JPEGPreviewTests::zoomKeepsTheMiddleAndDragsMoveIt()
|
||||
|
||||
void JPEGPreviewTests::theSheetsButtonsAndKeysZoomIt()
|
||||
{
|
||||
QSettings().remove(JPEGExportSheet::qualityKey);
|
||||
QSettings().remove(ExportAsSheet::qualityKey);
|
||||
EditorSession session;
|
||||
QImage red(1200, 800, QImage::Format_RGBA8888_Premultiplied);
|
||||
red.fill(Qt::red);
|
||||
JPEGExportSheet sheet({red, 72}, session, [](std::optional<QByteArray>) {});
|
||||
ExportAsSheet sheet({red, 72}, session, ExportFormat::jpeg, [](std::optional<ExportAsSheet::Chosen>) {});
|
||||
sheet.show();
|
||||
QVERIFY(QTest::qWaitForWindowActive(&sheet));
|
||||
auto &preview = *sheet.findChild<JPEGPreview *>();
|
||||
auto &fit = *sheet.findChild<QPushButton *>(QStringLiteral("jpegFit"));
|
||||
auto &in = *sheet.findChild<QPushButton *>(QStringLiteral("jpegZoomIn"));
|
||||
auto &out = *sheet.findChild<QPushButton *>(QStringLiteral("jpegZoomOut"));
|
||||
auto &fit = *sheet.findChild<QPushButton *>(QStringLiteral("exportFit"));
|
||||
auto &in = *sheet.findChild<QPushButton *>(QStringLiteral("exportZoomIn"));
|
||||
auto &out = *sheet.findChild<QPushButton *>(QStringLiteral("exportZoomOut"));
|
||||
// Title row 8 above the preview; bottom row 12 apart.
|
||||
auto *top = qobject_cast<QVBoxLayout *>(sheet.layout()->itemAt(0)->layout());
|
||||
auto *title = qobject_cast<QHBoxLayout *>(top->itemAt(0)->layout());
|
||||
QVERIFY(top->spacing() == 8 && title->spacing() == 8 && title->itemAt(2)->widget() == &fit && title->itemAt(4)->widget() == &out);
|
||||
auto *bottom = qobject_cast<QHBoxLayout *>(sheet.layout()->itemAt(3)->layout());
|
||||
QVERIFY(bottom->spacing() == 12 && bottom->itemAt(0)->widget()->objectName() == QString("jpegSize") && bottom->itemAt(1)->spacerItem());
|
||||
auto *bottom = qobject_cast<QHBoxLayout *>(sheet.layout()->itemAt(2)->layout());
|
||||
QVERIFY(bottom->spacing() == 12 && bottom->itemAt(0)->widget()->objectName() == QString("exportNote") && bottom->itemAt(1)->spacerItem());
|
||||
QVERIFY(!fit.isEnabled() && in.isEnabled() && out.isEnabled() && in.accessibleName() == QString("Zoom in"));
|
||||
QCOMPARE(fit.toolTip(), QString("Show the whole image (Ctrl+0)"));
|
||||
QCOMPARE(in.toolTip(), QString("Zoom in (Ctrl+=), now 41%. At 100% each pixel of the JPEG is one pixel of the screen, as on the canvas"));
|
||||
QCOMPARE(in.toolTip(), QString("Zoom in (Ctrl+=), now 41%. At 100% each pixel of the export is one pixel of the screen"));
|
||||
QCOMPARE(out.toolTip(), QString("Zoom out (Ctrl+-), now 41%"));
|
||||
// The magnifiers in the button ink, redrawn with the theme.
|
||||
const QImage before = in.icon().pixmap(16).toImage();
|
||||
@@ -273,7 +276,7 @@ void JPEGPreviewTests::theSheetsButtonsAndKeysZoomIt()
|
||||
QVERIFY(in.icon().pixmap(16).toImage() != before && out.icon().pixmap(16).toImage() != in.icon().pixmap(16).toImage());
|
||||
out.click();
|
||||
QVERIFY(preview.zoom() == std::optional(0.25) && !out.isEnabled() && fit.isEnabled());
|
||||
QCOMPARE(in.toolTip(), QString("Zoom in (Ctrl+=), now 25%. At 100% each pixel of the JPEG is one pixel of the screen, as on the canvas"));
|
||||
QCOMPARE(in.toolTip(), QString("Zoom in (Ctrl+=), now 25%. At 100% each pixel of the export is one pixel of the screen"));
|
||||
in.click();
|
||||
QCOMPARE(preview.zoom(), std::optional(0.5));
|
||||
fit.click();
|
||||
|
||||
@@ -93,7 +93,7 @@ void KeyboardShortcutsTests::theListIsSwiftsWithoutHide()
|
||||
{
|
||||
const std::vector<ShortcutDefinition> &all = ShortcutDefinition::all();
|
||||
// Swift's 113 with Ungroup Layers, less Hide Compositor.
|
||||
QCOMPARE(int(all.size()), 115);
|
||||
QCOMPARE(int(all.size()), 116);
|
||||
QSet<QString> ids;
|
||||
for (const ShortcutDefinition &definition : all)
|
||||
ids.insert(definition.id());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "DialogDesk.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "IO/ProjectController.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include <QApplication>
|
||||
#include <QSemaphore>
|
||||
#include <QTemporaryDir>
|
||||
@@ -105,12 +105,12 @@ void ProjectExportFailureTests::aSnapshotThatFindsNoMemoryIsExplained()
|
||||
malloc_trim(0);
|
||||
const AddressSpaceLimit limit(512ll * 1024);
|
||||
if (jpeg)
|
||||
desk.controller.exportJPEG([&desk] { desk.done = true; });
|
||||
desk.controller.exportAs(ExportFormat::jpeg, [&desk] { desk.done = true; });
|
||||
else
|
||||
desk.controller.exportPNG([&desk] { desk.done = true; });
|
||||
}
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.alerts.seen, desk.explained(jpeg ? QStringLiteral("Couldn’t export JPEG") : QStringLiteral("Couldn’t export PNG")));
|
||||
QCOMPARE(desk.alerts.seen, desk.explained(jpeg ? QStringLiteral("Couldn’t export") : QStringLiteral("Couldn’t export PNG")));
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ void ProjectExportFailureTests::aJPEGRenderThatFindsNoMemoryIsExplained()
|
||||
release.acquire();
|
||||
});
|
||||
entered.acquire();
|
||||
desk.controller.exportJPEG([&desk] { desk.done = true; });
|
||||
desk.controller.exportAs(ExportFormat::jpeg, [&desk] { desk.done = true; });
|
||||
{
|
||||
malloc_trim(0);
|
||||
const AddressSpaceLimit limit(512ll * 1024);
|
||||
@@ -135,7 +135,7 @@ void ProjectExportFailureTests::aJPEGRenderThatFindsNoMemoryIsExplained()
|
||||
pool->waitForDone();
|
||||
}
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.alerts.seen, desk.explained(QStringLiteral("Couldn’t export JPEG")));
|
||||
QCOMPARE(desk.alerts.seen, desk.explained(QStringLiteral("Couldn’t export")));
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
}
|
||||
|
||||
@@ -200,11 +200,11 @@ void ProjectExportFailureTests::theJPEGRenderTakesTheSnapshotWithoutACopy()
|
||||
{
|
||||
malloc_trim(0);
|
||||
const AddressSpaceLimit limit(5120ll * 1024);
|
||||
desk.controller.exportJPEG([&desk] { desk.done = true; });
|
||||
desk.controller.exportAs(ExportFormat::jpeg, [&desk] { desk.done = true; });
|
||||
}
|
||||
release.release();
|
||||
QTRY_VERIFY(desk.window.findChild<JPEGExportSheet *>());
|
||||
QTest::keyClick(desk.window.findChild<JPEGExportSheet *>(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(desk.window.findChild<ExportAsSheet *>());
|
||||
QTest::keyClick(desk.window.findChild<ExportAsSheet *>(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(desk.done);
|
||||
QVERIFY(desk.alerts.seen.isEmpty() && !desk.session.isProjectBusy());
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#include "MenuFixtures.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "IO/ProjectController.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSlider>
|
||||
@@ -22,6 +23,7 @@ private slots:
|
||||
void aJPEGPassesThroughItsSheet();
|
||||
void aJPEGNeedsAWindowAndADocument();
|
||||
void theFileMenuOffersBothExports();
|
||||
void exportAsWritesEachFormatAndRemembersTheLast();
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -49,9 +51,9 @@ struct Desk {
|
||||
}
|
||||
static QString path(const QString &name) { return QDir::current().absoluteFilePath(name); }
|
||||
// The shown sheet; a closed one may await deletion.
|
||||
JPEGExportSheet *sheet()
|
||||
ExportAsSheet *sheet()
|
||||
{
|
||||
for (JPEGExportSheet *shown : window.findChildren<JPEGExportSheet *>())
|
||||
for (ExportAsSheet *shown : window.findChildren<ExportAsSheet *>())
|
||||
if (shown->isVisible())
|
||||
return shown;
|
||||
return nullptr;
|
||||
@@ -141,20 +143,20 @@ void ProjectExportTests::aCancelledExportWritesNothing()
|
||||
QCOMPARE(QDir::current().entryList(QDir::Files), QStringList());
|
||||
// A JPEG cancelled at its sheet opens no panel.
|
||||
desk.controller.window = &desk.window;
|
||||
desk.controller.exportJPEG(desk.finished());
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
QTest::keyClick(desk.sheet(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.dialogs.seen.size(), 1);
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
QVERIFY(!QSettings().contains(JPEGExportSheet::qualityKey));
|
||||
QVERIFY(!QSettings().contains(ExportAsSheet::qualityKey));
|
||||
// Cancelled at its panel, nothing is written.
|
||||
desk.dialogs.replies = {"<cancel>"};
|
||||
desk.controller.exportJPEG(desk.finished());
|
||||
QTRY_VERIFY(desk.sheet() && desk.sheet()->findChild<QPushButton *>("jpegExport")->isEnabled());
|
||||
desk.sheet()->findChild<QPushButton *>("jpegExport")->click();
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet() && desk.sheet()->findChild<QPushButton *>("exportExport")->isEnabled());
|
||||
desk.sheet()->findChild<QPushButton *>("exportExport")->click();
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.dialogs.seen.value(1), QString("panel|Export JPEG|save|file|Untitled.jpg|jpeg|busy 1"));
|
||||
QCOMPARE(desk.dialogs.seen.value(1), QString("panel|Export JPEG|save|file|Untitled.jpeg|jpeg|busy 1"));
|
||||
QCOMPARE(QDir::current().entryList(QDir::Files), QStringList());
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
}
|
||||
@@ -178,33 +180,33 @@ void ProjectExportTests::aFailedWriteIsExplained()
|
||||
void ProjectExportTests::aJPEGPassesThroughItsSheet()
|
||||
{
|
||||
Desk desk;
|
||||
QSettings().remove(JPEGExportSheet::qualityKey);
|
||||
QSettings().remove(ExportAsSheet::qualityKey);
|
||||
desk.dialogs.replies = {Desk::path("photo")};
|
||||
desk.dialogs.note = panelFilters;
|
||||
desk.controller.exportJPEG(desk.finished());
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QVERIFY(desk.session.isProjectBusy());
|
||||
// The render runs aside; then the sheet holds the window.
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
auto *dialog = qobject_cast<QDialog *>(desk.sheet()->parentWidget());
|
||||
QVERIFY(dialog && dialog->isVisible() && dialog->windowModality() == Qt::WindowModal);
|
||||
QCOMPARE(dialog->windowTitle(), QString("Export JPEG"));
|
||||
QCOMPARE(dialog->windowTitle(), QString("Export As"));
|
||||
QCOMPARE(dialog->minimumSize(), dialog->maximumSize());
|
||||
auto *exporting = desk.sheet()->findChild<QPushButton *>("jpegExport");
|
||||
desk.sheet()->findChild<QSlider *>("jpegQuality")->setValue(50);
|
||||
auto *exporting = desk.sheet()->findChild<QPushButton *>("exportExport");
|
||||
desk.sheet()->findChild<QSlider *>("exportQuality")->setValue(50);
|
||||
QTRY_VERIFY(exporting->isEnabled());
|
||||
exporting->click();
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.dialogs.seen, QStringList{"panel|Export JPEG|save|file|Untitled.jpg|jpeg|JPEG image (*.jpeg *.jpg *.jpe)"});
|
||||
QCOMPARE(desk.dialogs.seen, QStringList{"panel|Export JPEG|save|file|Untitled.jpeg|jpeg|JPEG image (*.jpeg *.jpg *.jpe)"});
|
||||
const ExportRaster raster = ImageExporter::render(desk.session.projectSnapshot().value());
|
||||
QCOMPARE(contents(Desk::path("photo.jpeg")), ImageExporter::jpeg(raster, {.quality = 0.5}).data);
|
||||
QCOMPARE(QSettings().value(JPEGExportSheet::qualityKey).toDouble(), 0.5);
|
||||
QCOMPARE(QSettings().value(ExportAsSheet::qualityKey).toDouble(), 0.5);
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
// Every JPEG suffix is one; another gains the preferred.
|
||||
for (const QString &name : {QStringLiteral("a.jpg"), QStringLiteral("b.JPE"), QStringLiteral("c.png")}) {
|
||||
desk.dialogs.replies = {Desk::path(name)};
|
||||
desk.controller.exportJPEG(desk.finished());
|
||||
QTRY_VERIFY(desk.sheet() && desk.sheet()->findChild<QPushButton *>("jpegExport")->isEnabled());
|
||||
desk.sheet()->findChild<QPushButton *>("jpegExport")->click();
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet() && desk.sheet()->findChild<QPushButton *>("exportExport")->isEnabled());
|
||||
desk.sheet()->findChild<QPushButton *>("exportExport")->click();
|
||||
QTRY_VERIFY(desk.done);
|
||||
}
|
||||
QVERIFY(QFileInfo::exists(Desk::path("a.jpg")) && QFileInfo::exists(Desk::path("b.JPE")) && QFileInfo::exists(Desk::path("c.png.jpeg")));
|
||||
@@ -214,14 +216,14 @@ void ProjectExportTests::aJPEGNeedsAWindowAndADocument()
|
||||
{
|
||||
Desk desk;
|
||||
desk.controller.window = nullptr;
|
||||
desk.controller.exportJPEG(desk.finished());
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
QTRY_VERIFY(desk.done);
|
||||
EditorSession empty;
|
||||
ProjectController idle(empty);
|
||||
idle.window = &desk.window;
|
||||
bool refused = false;
|
||||
idle.exportJPEG([&refused] { refused = true; });
|
||||
idle.exportAs(ExportFormat::jpeg, [&refused] { refused = true; });
|
||||
idle.exportPNG([&refused] { refused = true; });
|
||||
QTRY_VERIFY(refused);
|
||||
QVERIFY(desk.dialogs.seen.isEmpty() && !empty.isProjectBusy());
|
||||
@@ -239,11 +241,14 @@ void ProjectExportTests::theFileMenuOffersBothExports()
|
||||
{
|
||||
QTemporaryDir folder;
|
||||
Bar bar;
|
||||
QVERIFY(!bar.action("exportPNG").isEnabled() && !bar.action("exportJPEG").isEnabled());
|
||||
const auto all = [&](bool enabled) {
|
||||
return bar.action("exportPNG").isEnabled() == enabled && bar.action("exportJPEG").isEnabled() == enabled && bar.action("exportAs").isEnabled() == enabled;
|
||||
};
|
||||
QVERIFY(all(false));
|
||||
bar.session().createDocument(8, 8);
|
||||
QVERIFY(bar.action("exportPNG").isEnabled() && bar.action("exportJPEG").isEnabled());
|
||||
QVERIFY(all(true));
|
||||
bar.session().setIsProjectBusy(true);
|
||||
QVERIFY(!bar.action("exportPNG").isEnabled() && !bar.action("exportJPEG").isEnabled());
|
||||
QVERIFY(all(false));
|
||||
bar.session().setIsProjectBusy(false);
|
||||
DialogDesk desk;
|
||||
desk.replies = {folder.filePath("Shot.png")};
|
||||
@@ -251,21 +256,73 @@ void ProjectExportTests::theFileMenuOffersBothExports()
|
||||
QTRY_VERIFY(QFileInfo::exists(folder.filePath("Shot.png")) && !bar.session().isProjectBusy());
|
||||
bar.window.show();
|
||||
bar.action("exportJPEG").trigger();
|
||||
QTRY_VERIFY(bar.window.findChild<JPEGExportSheet *>());
|
||||
QTRY_VERIFY(bar.window.findChild<ExportAsSheet *>());
|
||||
// The menus' zoom keys reach the preview, not the canvas.
|
||||
JPEGExportSheet &sheet = *bar.window.findChild<JPEGExportSheet *>();
|
||||
ExportAsSheet &sheet = *bar.window.findChild<ExportAsSheet *>();
|
||||
QTRY_COMPARE(QApplication::activeWindow(), sheet.window());
|
||||
const double zoom = bar.session().viewport.zoom();
|
||||
QTest::keyClick(&sheet, Qt::Key_1, Qt::ControlModifier);
|
||||
QCOMPARE(sheet.findChild<JPEGPreview *>()->zoom(), std::optional(1.0));
|
||||
QCOMPARE(bar.session().viewport.zoom(), zoom);
|
||||
// In the dialog only Export takes Return.
|
||||
for (const char *name : {"jpegFit", "jpegZoomIn", "jpegZoomOut"})
|
||||
for (const char *name : {"exportFit", "exportZoomIn", "exportZoomOut"})
|
||||
QVERIFY(!sheet.findChild<QPushButton *>(QString::fromLatin1(name))->autoDefault());
|
||||
QTest::keyClick(&sheet, Qt::Key_Escape);
|
||||
QTRY_VERIFY(!bar.session().isProjectBusy());
|
||||
QCOMPARE(desk.seen.size(), 1);
|
||||
}
|
||||
|
||||
void ProjectExportTests::exportAsWritesEachFormatAndRemembersTheLast()
|
||||
{
|
||||
Desk desk;
|
||||
const auto checked = [&desk] {
|
||||
for (const ExportFormat format : allExportFormats)
|
||||
if (desk.sheet()->findChild<QAbstractButton *>(QStringLiteral("exportFormat") + rawValue(format))->isChecked())
|
||||
return format;
|
||||
throw std::runtime_error("no format checked");
|
||||
};
|
||||
// First, PNG; a PDF written then starts the next.
|
||||
desk.dialogs.replies = {Desk::path("page")};
|
||||
desk.dialogs.note = panelFilters;
|
||||
desk.controller.exportAs(std::nullopt, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
QCOMPARE(checked(), ExportFormat::png);
|
||||
desk.sheet()->findChild<QAbstractButton *>("exportFormatPDF")->click();
|
||||
auto *exporting = desk.sheet()->findChild<QPushButton *>("exportExport");
|
||||
QTRY_VERIFY(exporting->isEnabled());
|
||||
exporting->click();
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.dialogs.seen, QStringList{"panel|Export PDF|save|file|Untitled.pdf|pdf|PDF document (*.pdf)"});
|
||||
const ExportRaster raster = ImageExporter::render(desk.session.projectSnapshot().value());
|
||||
QCOMPARE(contents(Desk::path("page.pdf")), ImageExporter::pdfData(raster, QColor(Qt::white)));
|
||||
QVERIFY(!desk.session.isProjectBusy());
|
||||
desk.controller.exportAs(std::nullopt, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
QCOMPARE(checked(), ExportFormat::pdf);
|
||||
QTest::keyClick(desk.sheet(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(desk.done);
|
||||
// Export JPEG starts on JPEG, whatever came last.
|
||||
desk.controller.exportAs(ExportFormat::jpeg, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
QCOMPARE(checked(), ExportFormat::jpeg);
|
||||
QTest::keyClick(desk.sheet(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(desk.done);
|
||||
// A PNG through Export As, scaled to half.
|
||||
desk.dialogs.seen.clear();
|
||||
desk.dialogs.replies = {Desk::path("half")};
|
||||
desk.controller.exportAs(ExportFormat::png, desk.finished());
|
||||
QTRY_VERIFY(desk.sheet());
|
||||
auto *width = desk.sheet()->findChild<QLineEdit *>("exportWidth");
|
||||
width->setFocus();
|
||||
width->selectAll();
|
||||
QTest::keyClicks(width, "20");
|
||||
QTest::keyClick(width, Qt::Key_Return);
|
||||
QTRY_VERIFY(desk.sheet()->findChild<QPushButton *>("exportExport")->isEnabled());
|
||||
desk.sheet()->findChild<QPushButton *>("exportExport")->click();
|
||||
QTRY_VERIFY(desk.done);
|
||||
QCOMPARE(desk.dialogs.seen, QStringList{"panel|Export PNG|save|file|Untitled.png|png|PNG image (*.png)"});
|
||||
QCOMPARE(contents(Desk::path("half.png")), ImageExporter::pngData(ImageExporter::resized(raster, 20, 10)));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ProjectExportTests)
|
||||
#include "ProjectExportTests.moc"
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/HueSaturationSheet.h"
|
||||
#include "UI/ImageSizeSheet.h"
|
||||
#include "UI/JPEGExportSheet.h"
|
||||
#include "UI/ExportAsSheet.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/LevelsSheet.h"
|
||||
#include "UI/NewCanvasSheet.h"
|
||||
@@ -231,9 +231,9 @@ void RemappedKeysTests::everySheetBindsSwiftsKeys()
|
||||
QCOMPARE(keys(GridSettingsSheet(session, LayoutGrid(), GridAppearance(), [](const GridSettingsSheet::Settings &) {}, [](std::optional<GridSettingsSheet::Settings>) {})), both);
|
||||
QCOMPARE(keys(ImageSizeSheet(canvas, [](std::optional<ImageSizeOptions>) {})), both);
|
||||
QCOMPARE(keys(TrimSheet([](std::optional<TrimOptions>) {})), both);
|
||||
// Export JPEG also takes the View menu's four zoom keys.
|
||||
// Export As also takes the View menu's four zoom keys.
|
||||
const QSet<QString> zooms{"Ctrl+K", "Ctrl+Q", "Ctrl+0", "Ctrl+1", "Ctrl+=", "Ctrl+-"};
|
||||
QCOMPARE(keys(JPEGExportSheet(ExportRaster{red, 72}, session, [](std::optional<QByteArray>) {})), zooms);
|
||||
QCOMPARE(keys(ExportAsSheet(ExportRaster{red, 72}, session, ExportFormat::jpeg, [](std::optional<ExportAsSheet::Chosen>) {})), zooms);
|
||||
QCOMPARE(keys(ColorPickerSheet(session, [](bool) {})), both);
|
||||
QCOMPARE(keys(NewCanvasSheet(session, [](int, int, double, std::optional<QColor>) {}, [] {})), QSet<QString>{"Ctrl+K"});
|
||||
QCOMPARE(keys(TransformInspector(session)), both);
|
||||
|
||||
+2
-1
@@ -120,6 +120,7 @@ compositor_test(EditorSessionSignalTests)
|
||||
compositor_test(EditorSessionTests)
|
||||
compositor_test(LayerDuplicateTests)
|
||||
compositor_test(ExportTests)
|
||||
compositor_test(ExportPDFTests)
|
||||
compositor_test(FolderMaskClipTests)
|
||||
compositor_test(EditorCanvasTests)
|
||||
compositor_test(GroupSessionTests)
|
||||
@@ -371,7 +372,7 @@ compositor_test(CanvasSizeSheetTests)
|
||||
compositor_test(ImageSizeSheetTests)
|
||||
compositor_test(ProjectResizeTests)
|
||||
compositor_test(ImageTrimTests)
|
||||
compositor_test(JPEGExportSheetTests)
|
||||
compositor_test(ExportAsSheetTests)
|
||||
compositor_test(ProjectExportTests)
|
||||
compositor_test(ProjectExportFailureTests)
|
||||
compositor_test(ByteCountsTests)
|
||||
|
||||
Reference in new issue
Block a user