Files
OmaPhoto/tests/LayerEffectsSurfaceTests.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

193 lines
8.6 KiB
C++

#include "Document/LayerEffects+Renderer.h"
#include "Rendering/LayerEffectsSurface.h"
#include "RenderFixtures.h"
#include <QPainter>
#include <QtTest>
// Swift's LayerEffectsSurface: a painted layer's effects, kept up.
namespace {
LayerEffects everyKind()
{
LayerEffects effects;
effects.stroke = StrokeEffect{.size = 5, .red = 1, .green = 1, .blue = 0, .opacity = 1};
effects.shadow = ShadowEffect{.angle = 45, .distance = 4, .blur = 16, .red = 1, .green = 0, .blue = 0, .opacity = 0.8};
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 0.5};
effects.innerShadow = InnerShadowEffect{.angle = 120, .distance = 3, .blur = 60, .red = 0, .green = 0, .blue = 0, .opacity = 0.7};
return effects;
}
void addRows()
{
QTest::addColumn<LayerEffects>("effects");
const LayerEffects all = everyKind();
LayerEffects outside, inside, cast, overlay, inner;
outside.stroke = all.stroke;
inside.stroke = StrokeEffect{.size = 10, .red = 1, .green = 0, .blue = 1, .opacity = 0.9, .inside = true};
cast.shadow = all.shadow;
overlay.colorOverlay = all.colorOverlay;
inner.innerShadow = all.innerShadow;
QTest::newRow("outside stroke") << outside;
QTest::newRow("inside stroke") << inside;
QTest::newRow("shadow") << cast;
QTest::newRow("overlay") << overlay;
QTest::newRow("inner shadow") << inner;
QTest::newRow("every kind") << all;
}
// A soft disc well inside a clear layer.
QImage disc()
{
QImage image = BrushRaster::context(80, 60, false);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(40, 90, 200));
painter.drawEllipse(QPointF(40, 30), 15.5, 13);
return image;
}
// Opaque and translucent cells: a window cut short shows.
QImage checker(int width, int height)
{
QImage image = BrushRaster::context(width, height, false);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const int alpha = (x / 8 + y / 8) % 2 ? 255 : 96;
image.setPixel(x, y, qRgba(40 * alpha / 255, 90 * alpha / 255, 200 * alpha / 255, alpha));
}
}
return image;
}
// A tile of `base` with a clear hole in it.
BrushPatch holed(const QImage &base, const QRect &tile, const QRect &hole)
{
QImage image = base.copy(tile);
QPainter painter(&image);
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.fillRect(hole.translated(-tile.topLeft()), Qt::transparent);
return {QRectF(tile), image};
}
std::unique_ptr<LayerEffectsSurface> surfaceFor(const QImage &image, const LayerEffects &effects)
{
return LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(image.size()), QRectF(image.rect()));
}
ImportedImage asset(const QImage &image)
{
return ImportedImage(image, QImage(), QStringLiteral("Layer"));
}
}
class LayerEffectsSurfaceTests : public QObject {
Q_OBJECT
private slots:
void theFirstUpdateDrawsWhatTheRendererDraws_data() { addRows(); }
void theFirstUpdateDrawsWhatTheRendererDraws();
void newPaintIsRedoneAsFarAsItReaches_data() { addRows(); }
void newPaintIsRedoneAsFarAsItReaches();
void tilesAlreadyTakenRedoNothing();
void paintPastTheLayerShowsUnmasked();
void aSmallerMaskScalesSmoothlyAsTheRenderersDoes();
void aSurfaceHoldsEightyMillionPixelsAtMost();
void aSurfaceFitsTheStrokeItWasMadeFor();
};
void LayerEffectsSurfaceTests::theFirstUpdateDrawsWhatTheRendererDraws()
{
QFETCH(LayerEffects, effects);
const QImage image = disc();
const std::unique_ptr<LayerEffectsSurface> surface = surfaceFor(image, effects);
QVERIFY(surface && !surface->image());
surface->update(asset(image), {}, std::nullopt);
const LayerEffectsRenderer::Rendered expected = LayerEffectsRenderer::render(image, std::nullopt, effects);
QCOMPARE(surface->margin, expected.inset);
QVERIFY(surface->image().value() == expected.image);
// A painted base draws from its tiles, alike, never flattened.
const std::unique_ptr<LayerEffectsSurface> tiled = surfaceFor(image, effects);
const std::shared_ptr<const RasterSnapshot> raster = painted(image, {});
tiled->update(ImportedImage(raster, QImage(), QStringLiteral("Painted")), {}, std::nullopt);
QVERIFY(tiled->image().value() == expected.image && !raster->hasMaterializedPixels());
}
void LayerEffectsSurfaceTests::newPaintIsRedoneAsFarAsItReaches()
{
QFETCH(LayerEffects, effects);
// Two tiles painted at once, each with a hole.
const QImage base = checker(900, 900);
const std::vector<BrushPatch> patches{holed(base, QRect(0, 0, 256, 256), QRect(200, 180, 56, 76)),
holed(base, QRect(256, 0, 256, 256), QRect(266, 40, 20, 30))};
const std::unique_ptr<LayerEffectsSurface> surface = surfaceFor(base, effects);
surface->update(asset(base), {}, std::nullopt);
surface->update(asset(base), patches, std::nullopt);
// Everything the hole reaches, past its tile too, is redone.
QVERIFY(surface->image().value() == LayerEffectsRenderer::render(composite(base, patches), std::nullopt, effects).image);
}
void LayerEffectsSurfaceTests::tilesAlreadyTakenRedoNothing()
{
const QImage base = solid(300, 300, qRgba(40, 90, 200, 255));
const std::unique_ptr<LayerEffectsSurface> surface = surfaceFor(base, everyKind());
const BrushPatch red{QRectF(0, 0, 256, 256), solid(256, 256, qRgba(255, 0, 0, 255))};
surface->update(asset(base), {red}, std::nullopt);
const qint64 first = surface->image().value().cacheKey();
surface->update(asset(base), {red}, std::nullopt);
QCOMPARE(surface->image().value().cacheKey(), first);
// A tile published anew is taken in again.
const BrushPatch green{red.rect, solid(256, 256, qRgba(0, 255, 0, 255))};
surface->update(asset(base), {green}, std::nullopt);
QVERIFY(surface->image().value() == LayerEffectsRenderer::render(composite(base, {green}), std::nullopt, everyKind()).image);
}
void LayerEffectsSurfaceTests::paintPastTheLayerShowsUnmasked()
{
// The layer, wholly masked, sits inside a larger grid.
LayerEffects effects;
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 1};
const std::unique_ptr<LayerEffectsSurface> surface = LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(40, 30), QRectF(10, 10, 20, 10));
const BrushPatch paint{QRectF(0, 0, 40, 30), solid(40, 30, qRgba(255, 0, 0, 255))};
surface->update(asset(solid(20, 10, qRgba(0, 0, 255, 255))), {paint}, gray(20, 10, 0));
const QImage image = surface->image().value();
QVERIFY(surface->margin == 2 && image.size() == QSize(44, 34));
QCOMPARE(image.pixel(4, 4), qRgba(0, 255, 0, 255));
QCOMPARE(image.pixel(29, 29), qRgba(0, 255, 0, 255));
QCOMPARE(qAlpha(image.pixel(12, 12)), 0);
QCOMPARE(qAlpha(image.pixel(31, 21)), 0);
}
void LayerEffectsSurfaceTests::aSmallerMaskScalesSmoothlyAsTheRenderersDoes()
{
const QImage image = solid(64, 16, qRgba(40, 90, 200, 255));
QImage mask(2, 1, QImage::Format_Grayscale8);
mask.scanLine(0)[0] = 0;
mask.scanLine(0)[1] = 255;
const std::unique_ptr<LayerEffectsSurface> surface = surfaceFor(image, everyKind());
surface->update(asset(image), {}, mask);
QVERIFY(surface->image().value() == LayerEffectsRenderer::render(image, mask, everyKind()).image);
}
void LayerEffectsSurfaceTests::aSurfaceHoldsEightyMillionPixelsAtMost()
{
LayerEffects effects;
effects.colorOverlay = ColorOverlayEffect();
// A margin of two: 8,000 by 10,000 fits exactly.
QVERIFY(LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(7996, 9996), QRectF(0, 0, 7996, 9996)));
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("an effects surface passes its pixel budget"));
QVERIFY(!LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(7997, 9996), QRectF(0, 0, 7997, 9996)));
}
void LayerEffectsSurfaceTests::aSurfaceFitsTheStrokeItWasMadeFor()
{
const QUuid id = QUuid::createUuid();
const std::unique_ptr<LayerEffectsSurface> surface = LayerEffectsSurface::make(id, everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10));
QVERIFY(surface->matches(id, everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10)));
QVERIFY(!surface->matches(QUuid::createUuid(), everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10)));
QVERIFY(!surface->matches(id, LayerEffects(), QSizeF(40, 30), QRectF(5, 5, 20, 10)));
QVERIFY(!surface->matches(id, everyKind(), QSizeF(41, 30), QRectF(5, 5, 20, 10)));
QVERIFY(!surface->matches(id, everyKind(), QSizeF(40, 30), QRectF(6, 5, 20, 10)));
}
QTEST_GUILESS_MAIN(LayerEffectsSurfaceTests)
#include "LayerEffectsSurfaceTests.moc"