Files
OmaPhoto/tests/EffectsCanvasTests.cpp
T

359 lines
16 KiB
C++

#include "Document/LayerEffects+Renderer.h"
#include "IO/ImageExporter.h"
#include "Rendering/EditorCanvas.h"
#include "RenderFixtures.h"
#include "EffectsCanvasFixtures.h"
#include "SessionFixtures.h"
#include <QtTest>
// Swift's effects on the canvas: previews, the paint's surface, distortions.
namespace {
// Counts the paints a widget receives.
struct Painted : QObject {
int &count;
explicit Painted(int &count) : count(count) {}
bool eventFilter(QObject *, QEvent *event) override
{
count += event->type() == QEvent::Paint;
return false;
}
};
// A pixel only the warp makes yellow, clear of handles.
std::optional<QPoint> warpedOnly(const DistortWarp::Warped &warped, const QRectF &unwarped, const std::vector<QPointF> &handles)
{
const QImage image = warped.image.convertToFormat(QImage::Format_ARGB32);
const QTransform toImage = BrushRaster::pixelToDocument(warped.transform, image.width(), image.height()).inverted();
const auto yellowAt = [&](QPointF point) {
const QPointF mapped = toImage.map(point);
const QPoint pixel(int(std::floor(mapped.x())), int(std::floor(mapped.y())));
return image.rect().contains(pixel) && image.pixel(pixel) == qRgba(255, 255, 0, 255);
};
for (int y = 0; y < 40; ++y) {
for (int x = 0; x < 60; ++x) {
const QPointF centre(x + 0.5, y + 0.5);
const bool nearHandle = std::any_of(handles.begin(), handles.end(), [&](QPointF handle) {
return std::abs(handle.x() - centre.x()) < 7 && std::abs(handle.y() - centre.y()) < 7;
});
if (nearHandle || unwarped.adjusted(-1, -1, 1, 1).contains(centre))
continue;
if (yellowAt(centre) && yellowAt(centre + QPointF(1, 0)) && yellowAt(centre - QPointF(1, 0)) && yellowAt(centre + QPointF(0, 1))
&& yellowAt(centre - QPointF(0, 1)))
return QPoint(x, y);
}
}
return std::nullopt;
}
}
class EffectsCanvasTests : public QObject {
Q_OBJECT
private slots:
void theCanvasDrawsALayersEffectsOnceTheyLand();
void aLandedPreviewRepaintsTheView();
void aPlacedMaskClipsThePreviewAsItClipsTheExport();
void changingEffectsRepaintsTheView();
void aDeletedLayersPreviewGoesWithIt();
void aPendingDistortionWarpsTheEffectsAndApplyingKeepsThem();
void paintKeepsItsEffectsAndHandsThemOn();
void aSurfaceIsMadeAnewForOtherSettings();
void paintOnAMaskedLayerKeepsItsMask();
void paintPastTheSurfacesBudgetShowsTheLastEffects();
void invalidEffectsShowNothing();
void effectsDrawAheadOfAColourEditsPreview();
};
void EffectsCanvasTests::theCanvasDrawsALayersEffectsOnceTheyLand()
{
Scene scene;
LayerEffects effects = outline(2);
effects.shadow = ShadowEffect{.angle = 90, .distance = 6, .blur = 0, .red = 1, .green = 0, .blue = 0, .opacity = 1};
scene.session.setEffects(effects);
// Until the worker lands, the layer alone.
QCOMPARE(scene.at(QPoint(23, 20)), QColor(Qt::white));
QTRY_COMPARE(scene.at(QPoint(23, 20)), yellow);
QCOMPARE(scene.at(QPoint(30, 29)), QColor(Qt::red));
QCOMPARE(scene.at(QPoint(30, 20)), QColor(Qt::blue));
QCOMPARE(scene.gapToExport(), 0);
}
void EffectsCanvasTests::aLandedPreviewRepaintsTheView()
{
Scene scene;
QWidget window;
CanvasView &canvas = *new CanvasView(scene.session, &window);
canvas.resize(300, 200);
int paints = 0;
Painted counter(paints);
canvas.installEventFilter(&counter);
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
QTest::qWait(100);
const int shown = paints;
scene.session.setEffects(outline(2));
canvas.synchronizeDisplay();
QVERIFY(QTest::qWaitFor([&] { return paints > shown; }, 5000));
// The worker's result arrives later and asks for a paint.
const int before = paints;
QVERIFY(!scene.session.effectsPreviews.rendered(scene.square));
QTRY_VERIFY(scene.session.effectsPreviews.rendered(scene.square) && paints > before);
}
void EffectsCanvasTests::aPlacedMaskClipsThePreviewAsItClipsTheExport()
{
Scene scene;
// Left half hidden, the mask placed two pixels right.
QImage half = gray(10, 10, 255);
for (int y = 0; y < 10; ++y)
std::fill_n(half.scanLine(y), 5, uchar(0));
rewrite(scene.session, [&](ProjectSnapshot &snapshot) {
setMask(snapshot, scene.square, LayerMask::assetFrom(half));
record(snapshot, scene.square).maskPlacement = placedAt(QPointF(27, 15), QSizeF(10, 10));
record(snapshot, scene.square).maskLinked = false;
record(snapshot, scene.square).effects = outline(2);
});
scene.session.zoom(1);
scene.landed();
// Swift asks first with the mask unplaced; the twin once.
QCOMPARE(scene.gapToExport(), 0);
QCOMPARE(scene.at(QPoint(30, 20)), yellow);
QCOMPARE(scene.at(QPoint(33, 20)), QColor(Qt::blue));
}
void EffectsCanvasTests::changingEffectsRepaintsTheView()
{
Scene scene;
QVERIFY(scene.canvas.synchronizeDisplay());
QVERIFY(!scene.canvas.synchronizeDisplay());
scene.session.setEffects(outline(2));
QVERIFY(scene.canvas.synchronizeDisplay());
}
void EffectsCanvasTests::aDeletedLayersPreviewGoesWithIt()
{
Scene scene;
scene.session.setEffects(outline(2));
scene.landed();
scene.session.deleteLayerOrMask();
scene.shot();
QVERIFY(!scene.session.effectsPreviews.rendered(scene.square));
}
void EffectsCanvasTests::aPendingDistortionWarpsTheEffectsAndApplyingKeepsThem()
{
Scene scene;
scene.session.setEffects(outline(2));
const EffectsPreviewCache::Result effects = scene.landed();
EditorSession &session = scene.session;
session.beginTransform();
session.beginDistort();
// The top right corner pulled far up and out.
const Corners corners{QPointF(25, 15), QPointF(55, 5), QPointF(35, 25), QPointF(25, 25)};
session.previewCorners(corners);
const ImageLayer layer = layerWith(session, scene.square);
const std::optional<DistortWarp::Warped> warped = session.distortedEffects(layer, effects.image, effects.inset);
// The effects' grown box takes the layer's perspective.
const LayerTransform grown = LayerEffectsRenderer::placed(layer.transform, effects.image, effects.inset);
const DistortWarp::Warped expected = DistortWarp::warp(effects.image, grown, DistortWarp::carried(grown, layer.transform, corners), false, 2048);
QVERIFY(warped.value().image == expected.image && warped.value().transform == expected.transform);
QCOMPARE(session.distortedEffects(layer, effects.image, effects.inset).value().image.cacheKey(), warped.value().image.cacheKey());
const std::vector<QPointF> handles{corners[0], corners[1], corners[2], corners[3], QPointF(40, 10), QPointF(45, 15), QPointF(30, 25), QPointF(25, 20)};
const QPoint outside = warpedOnly(warped.value(), QRectF(21, 11, 18, 18), handles).value();
QCOMPARE(scene.at(outside), yellow);
// A layer outside the edit keeps its own.
QVERIFY(!session.distortedEffects(session.document().value().layers.front(), effects.image, effects.inset));
// Past 2048 pixels a side the warp is drawn smaller.
session.previewCorners({QPointF(25, 15), QPointF(2500, 15), QPointF(2500, 25), QPointF(25, 25)});
const DistortWarp::Warped far = session.distortedEffects(layer, effects.image, effects.inset).value();
QVERIFY(far.image.size() == QSize(2048, 9) && far.transform.size == QSizeF(4456, 19));
// New corners or other effects warp anew.
session.previewCorners({QPointF(25, 15), QPointF(54, 5), QPointF(35, 25), QPointF(25, 25)});
const qint64 moved = session.distortedEffects(layer, effects.image, effects.inset).value().image.cacheKey();
QVERIFY(moved != warped.value().image.cacheKey());
QVERIFY(session.distortedEffects(layer, effects.image.copy(), effects.inset).value().image.cacheKey() != moved);
session.previewCorners(corners);
const std::optional<DistortWarp::Warped> again = session.distortedEffects(layer, effects.image, effects.inset);
// Applied: the warped effects show until new ones land.
session.commitTransform();
const EffectsPreviewCache::Result seeded = session.effectsPreviews.rendered(scene.square).value();
QVERIFY(seeded.image.cacheKey() == again.value().image.cacheKey() && seeded.placement == warped.value().transform);
QCOMPARE(scene.at(outside), yellow);
QTRY_VERIFY(!session.effectsPreviews.rendered(scene.square).value().placement);
// A plain transform, or a mask's distortion, warps nothing.
session.beginTransform();
QVERIFY(!session.distortedEffects(layerWith(session, scene.square), effects.image, effects.inset));
session.cancelTransform();
session.addLayerMask(true);
session.toggleMaskLink(scene.square);
session.selectLayerTarget(scene.square, true);
session.beginTransform();
session.beginDistort();
session.previewCorners(corners);
QVERIFY(session.transformEdit().value().mask && session.transformEdit().value().corners);
QVERIFY(!session.distortedEffects(layerWith(session, scene.square), effects.image, effects.inset));
session.cancelTransform();
}
void EffectsCanvasTests::paintKeepsItsEffectsAndHandsThemOn()
{
Scene scene;
EditorSession &session = scene.session;
session.selectTool(NavigationTool::brush);
BrushSettings settings = session.brushSettings();
settings.diameter = 4;
settings.red = 1;
session.setBrushSettings(settings);
// Without effects a stroke makes no surface to hand on.
session.beginBrush(QPointF(45, 10));
QCOMPARE(scene.at(QPoint(45, 10)), QColor(Qt::red));
QVERIFY(session.finishBrushImmediately());
scene.canvas.synchronizeDisplay();
QVERIFY(!session.effectsPreviews.rendered(scene.square));
// An overlay covers the wet paint too, as it will.
LayerEffects effects = outline(2);
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 1};
session.setEffects(effects);
scene.landed();
session.beginBrush(QPointF(45, 20));
session.continueBrush(QPointF(55, 20));
QCOMPARE(scene.at(QPoint(50, 16)), yellow);
QCOMPARE(scene.at(QPoint(50, 20)), QColor(Qt::green));
// Nothing is handed on while the stroke lasts.
scene.canvas.synchronizeDisplay();
QVERIFY(!session.effectsPreviews.rendered(scene.square).value().placement);
QVERIFY(session.finishBrushImmediately());
// The surface is handed on once, so nothing blinks.
scene.canvas.synchronizeDisplay();
QVERIFY(session.effectsPreviews.rendered(scene.square).value().placement);
QCOMPARE(scene.at(QPoint(50, 16)), yellow);
QTRY_VERIFY(!session.effectsPreviews.rendered(scene.square).value().placement);
scene.canvas.synchronizeDisplay();
QVERIFY(!session.effectsPreviews.rendered(scene.square).value().placement);
QCOMPARE(scene.gapToExport(), 0);
}
void EffectsCanvasTests::aSurfaceIsMadeAnewForOtherSettings()
{
Scene scene;
EditorSession &session = scene.session;
session.setEffects(outline(2));
scene.landed();
session.selectTool(NavigationTool::brush);
BrushSettings settings = session.brushSettings();
settings.diameter = 4;
settings.red = 1;
session.setBrushSettings(settings);
session.beginBrush(QPointF(45, 20));
session.continueBrush(QPointF(55, 20));
QCOMPARE(scene.at(QPoint(50, 16)), yellow);
QVERIFY(session.finishBrushImmediately());
// A canvas alone follows nothing: the old surface stays.
LayerEffects cyan = outline(2);
cyan.stroke->red = 0;
cyan.stroke->blue = 1;
session.setEffects(cyan);
session.beginBrush(QPointF(45, 32));
session.continueBrush(QPointF(55, 32));
QCOMPARE(scene.at(QPoint(50, 28)), QColor(0, 255, 255));
session.cancelBrush();
}
void EffectsCanvasTests::paintOnAMaskedLayerKeepsItsMask()
{
Scene scene;
QImage half = gray(10, 10, 255);
for (int y = 0; y < 10; ++y)
std::fill_n(half.scanLine(y), 5, uchar(0));
rewrite(scene.session, [&](ProjectSnapshot &snapshot) {
setMask(snapshot, scene.square, LayerMask::assetFrom(half));
record(snapshot, scene.square).effects = outline(2);
});
scene.session.zoom(1);
scene.landed();
EditorSession &session = scene.session;
session.selectTool(NavigationTool::brush);
BrushSettings settings = session.brushSettings();
settings.diameter = 4;
settings.red = 1;
session.setBrushSettings(settings);
session.beginBrush(QPointF(45, 20));
session.continueBrush(QPointF(55, 20));
// Hidden half hidden, unoutlined; paint past the layer shows.
QCOMPARE(scene.at(QPoint(27, 20)), QColor(Qt::white));
QCOMPARE(scene.at(QPoint(23, 20)), QColor(Qt::white));
QCOMPARE(scene.at(QPoint(32, 20)), QColor(Qt::blue));
QCOMPARE(scene.at(QPoint(50, 16)), yellow);
session.cancelBrush();
}
void EffectsCanvasTests::paintPastTheSurfacesBudgetShowsTheLastEffects()
{
Scene scene;
// Scaled down 200 times, the stroke's grid passes 80 million.
const QImage fine = solid(2000, 2000, qRgba(0, 0, 255, 255));
rewrite(scene.session, [&](ProjectSnapshot &snapshot) {
snapshot.images.insert_or_assign(scene.square, ImportedImage(fine, fine, QStringLiteral("Square")));
record(snapshot, scene.square).transform = placedAt(QPointF(25, 15), QSizeF(10, 10));
record(snapshot, scene.square).effects = outline(400);
});
scene.session.zoom(1);
scene.landed();
EditorSession &session = scene.session;
session.selectTool(NavigationTool::brush);
BrushSettings settings = session.brushSettings();
settings.diameter = 4;
settings.red = 1;
session.setBrushSettings(settings);
session.beginBrush(QPointF(45, 20));
session.continueBrush(QPointF(55, 20));
// No surface: the last effects show, the paint without them.
QCOMPARE(scene.at(QPoint(23, 20)), yellow);
QCOMPARE(scene.at(QPoint(50, 20)), QColor(Qt::red));
QCOMPARE(scene.at(QPoint(50, 16)), QColor(Qt::white));
session.cancelBrush();
// A seed stands in where it belongs.
session.effectsPreviews.seed(scene.square, solid(8, 8, qRgba(0, 255, 0, 255)), placedAt(QPointF(2, 2), QSizeF(8, 8)));
session.beginBrush(QPointF(45, 20));
QCOMPARE(scene.at(QPoint(5, 5)), QColor(Qt::green));
QCOMPARE(scene.at(QPoint(23, 20)), QColor(Qt::white));
session.cancelBrush();
}
void EffectsCanvasTests::invalidEffectsShowNothing()
{
Scene scene;
// A stroke past Swift's 500 pixels, which files may hold.
rewrite(scene.session, [&](ProjectSnapshot &snapshot) { record(snapshot, scene.square).effects = outline(600); });
scene.session.zoom(1);
QCOMPARE(scene.at(QPoint(23, 20)), QColor(Qt::white));
QTest::qWait(150);
QVERIFY(!scene.session.effectsPreviews.rendered(scene.square));
// Painting makes no surface for them either.
EditorSession &session = scene.session;
session.selectTool(NavigationTool::brush);
BrushSettings settings = session.brushSettings();
settings.diameter = 4;
session.setBrushSettings(settings);
session.beginBrush(QPointF(45, 20));
QCOMPARE(scene.at(QPoint(45, 16)), QColor(Qt::white));
session.cancelBrush();
}
void EffectsCanvasTests::effectsDrawAheadOfAColourEditsPreview()
{
Scene scene(qRgba(100, 100, 100, 255));
scene.session.setEffects(outline(2));
scene.landed();
LevelsSettings inverted;
inverted.setCurrent(LevelRange{0, 1, 255, 255, 0});
scene.session.beginLevels();
scene.session.updateLevels(inverted, true);
QTRY_VERIFY(scene.session.levels().value().preparedPreview);
// Swift draws the effects first: the layer shows unadjusted.
QCOMPARE(scene.at(QPoint(30, 20)), QColor(100, 100, 100));
QCOMPARE(scene.at(QPoint(23, 20)), yellow);
}
QTEST_MAIN(EffectsCanvasTests)
#include "EffectsCanvasTests.moc"