Files
OmaPhoto/tests/MaskPlacementTests.cpp
雷电芽衣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

223 lines
8.6 KiB
C++

#include "Document/LayerMask.h"
#include "IO/ImageExporter.h"
#include "Rendering/LayerRenderer.h"
#include "RenderFixtures.h"
#include "AddressSpaceLimit.h"
#include <QtTest>
namespace {
int value(const QImage &image, int x, int y)
{
return image.constScanLine(y)[x];
}
// White rim, black middle: reveals beyond its own pixels.
QImage ringMask(int side)
{
QImage mask = gray(side, side, 255);
for (int y = 1; y < side - 1; ++y) {
for (int x = 1; x < side - 1; ++x)
mask.scanLine(y)[x] = 0;
}
return mask;
}
const LayerTransform layer{.origin = {0, 0}, .size = {100, 100}};
const LayerTransform rightHalf{.origin = {50, 0}, .size = {50, 100}};
std::optional<QImage> tiny()
{
return gray(1, 1, 7);
}
}
class MaskPlacementTests : public QObject {
Q_OBJECT
private slots:
void aMaskCoveringItsLayerClipsAsItIs();
void aPlacedMaskIsResampledIntoTheLayerGrid();
void theBackgroundComesFromTheThumbnail();
void aLimitShrinksThePlacedGrid();
void placedMasksAreCached();
void aPlacedMaskThatCannotBeRenderedIsLoggedAndNil();
void theCacheKeysOnEveryPartOfThePlacement();
void aFailedBuildIsNeverCached();
void hugeGridsAreReturnedButNotKept();
void theNinthEntryEvictsTheLeastRecentlyUsed();
void thePixelBudgetEvictsToo();
void fineMasksArePlacedFromHalvings();
};
void MaskPlacementTests::aMaskCoveringItsLayerClipsAsItIs()
{
const QImage pixels = ringMask(8);
LayerMask mask(LayerMask::assetFrom(pixels));
QCOMPARE(mask.clipImage(std::nullopt, layer, 100, 100).value().cacheKey(), pixels.cacheKey());
LayerTransform sameSpot = layer;
sameSpot.sampling = LayerSampling::nearest;
QCOMPARE(mask.clipImage(sameSpot, layer, 100, 100).value().cacheKey(), pixels.cacheKey());
QCOMPARE(mask.clipImage(rightHalf, layer, 0, 100).value().cacheKey(), pixels.cacheKey());
QCOMPARE(mask.clipImage(rightHalf, layer, 100, 0).value().cacheKey(), pixels.cacheKey());
mask.isEnabled = false;
QVERIFY(!mask.clipImage(rightHalf, layer, 100, 100).has_value());
}
void MaskPlacementTests::aPlacedMaskIsResampledIntoTheLayerGrid()
{
const LayerMask mask(LayerMask::assetFrom(ringMask(10)));
const QImage placed = mask.clipImage(rightHalf, layer, 200, 100).value();
QCOMPARE(placed.format(), QImage::Format_Grayscale8);
QCOMPARE(placed.size(), QSize(200, 100));
QCOMPARE(value(placed, 20, 50), 255);
QCOMPARE(value(placed, 150, 50), 0);
QCOMPARE(value(placed, 102, 50), 255);
QCOMPARE(value(placed, 150, 2), 255);
const LayerMask hiding(LayerMask::assetFrom(gray(10, 10, 0)));
QCOMPARE(value(hiding.clipImage(rightHalf, layer, 200, 100).value(), 20, 50), 0);
}
void MaskPlacementTests::theBackgroundComesFromTheThumbnail()
{
const LayerMask whiteRimBlackThumbnail(ImportedImage(ringMask(10), gray(4, 4, 0), "Layer Mask"));
QCOMPARE(value(whiteRimBlackThumbnail.clipImage(rightHalf, layer, 200, 100).value(), 20, 50), 0);
const LayerMask blackMaskWhiteThumbnail(ImportedImage(gray(10, 10, 0), gray(4, 4, 255), "Layer Mask"));
QCOMPARE(value(blackMaskWhiteThumbnail.clipImage(rightHalf, layer, 200, 100).value(), 20, 50), 255);
}
void MaskPlacementTests::aLimitShrinksThePlacedGrid()
{
const LayerMask mask(LayerMask::assetFrom(ringMask(10)));
QCOMPARE(mask.clipImage(rightHalf, layer, 4000, 2000, 100).value().size(), QSize(100, 50));
QCOMPARE(mask.clipImage(rightHalf, layer, 4001, 2000, 100).value().size(), QSize(100, 50));
QCOMPARE(mask.clipImage(rightHalf, layer, 300, 90, 100).value().size(), QSize(100, 30));
QCOMPARE(mask.clipImage(rightHalf, layer, 80, 40, 100).value().size(), QSize(80, 40));
QCOMPARE(mask.clipImage(rightHalf, layer, 80, 40, 0.5).value().size(), QSize(1, 1));
const QImage shrunk = mask.clipImage(rightHalf, layer, 4000, 2000, 100).value();
QCOMPARE(value(shrunk, 10, 25), 255);
QCOMPARE(value(shrunk, 75, 25), 0);
}
void MaskPlacementTests::placedMasksAreCached()
{
const LayerMask mask(LayerMask::assetFrom(ringMask(10)));
const QImage first = mask.clipImage(rightHalf, layer, 64, 64).value();
QCOMPARE(mask.clipImage(rightHalf, layer, 64, 64).value().cacheKey(), first.cacheKey());
LayerTransform nudged = rightHalf;
nudged.origin.rx() += 1;
QVERIFY(mask.clipImage(nudged, layer, 64, 64).value().cacheKey() != first.cacheKey());
}
void MaskPlacementTests::aPlacedMaskThatCannotBeRenderedIsLoggedAndNil()
{
const LayerMask mask(LayerMask::assetFrom(ringMask(10)));
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("a placed mask could not be rendered: .*"));
std::optional<AddressSpaceLimit> limit(std::in_place, 16 * 1024 * 1024);
const bool rendered = mask.clipImage(rightHalf, layer, 20000, 20000).has_value();
limit.reset();
QVERIFY(!rendered);
}
void MaskPlacementTests::theCacheKeysOnEveryPartOfThePlacement()
{
MaskPlacementCache cache;
const QImage mask = gray(2, 2, 9);
int builds = 0;
const auto build = [&] { builds += 1; return tiny(); };
QCOMPARE(cache.image(mask, rightHalf, layer, 10, 20, build).value().size(), QSize(1, 1));
cache.image(mask, rightHalf, layer, 10, 20, build);
QCOMPARE(builds, 1);
cache.image(mask.copy(), rightHalf, layer, 10, 20, build);
QCOMPARE(builds, 2);
cache.image(mask, layer, layer, 10, 20, build);
QCOMPARE(builds, 3);
cache.image(mask, rightHalf, rightHalf, 10, 20, build);
QCOMPARE(builds, 4);
cache.image(mask, rightHalf, layer, 11, 20, build);
QCOMPARE(builds, 5);
cache.image(mask, rightHalf, layer, 10, 21, build);
QCOMPARE(builds, 6);
cache.image(mask, rightHalf, layer, 10, 20, build);
QCOMPARE(builds, 6);
}
void MaskPlacementTests::aFailedBuildIsNeverCached()
{
MaskPlacementCache cache;
int builds = 0;
const auto failing = [&]() -> std::optional<QImage> { builds += 1; return std::nullopt; };
QVERIFY(!cache.image(gray(2, 2, 9), rightHalf, layer, 10, 20, failing).has_value());
QVERIFY(!cache.image(gray(2, 2, 9), rightHalf, layer, 10, 20, failing).has_value());
QCOMPARE(builds, 2);
}
void MaskPlacementTests::hugeGridsAreReturnedButNotKept()
{
MaskPlacementCache cache;
const QImage mask = gray(2, 2, 9);
int builds = 0;
const auto build = [&] { builds += 1; return tiny(); };
cache.image(mask, rightHalf, layer, 10, 10, build);
QVERIFY(cache.image(mask, rightHalf, layer, 8001, 8000, build).has_value());
cache.image(mask, rightHalf, layer, 8001, 8000, build);
QCOMPARE(builds, 3);
cache.image(mask, rightHalf, layer, 10, 10, build);
QCOMPARE(builds, 3);
cache.image(mask, rightHalf, layer, 7000, 8000, build);
cache.image(mask, rightHalf, layer, 7000, 8000, build);
QCOMPARE(builds, 4);
}
void MaskPlacementTests::theNinthEntryEvictsTheLeastRecentlyUsed()
{
MaskPlacementCache cache;
const QImage mask = gray(2, 2, 9);
int builds = 0;
const auto build = [&] { builds += 1; return tiny(); };
for (int width = 1; width <= 8; ++width)
cache.image(mask, rightHalf, layer, width, 1, build);
cache.image(mask, rightHalf, layer, 1, 1, build);
QCOMPARE(builds, 8);
cache.image(mask, rightHalf, layer, 9, 1, build);
QCOMPARE(builds, 9);
cache.image(mask, rightHalf, layer, 1, 1, build);
QCOMPARE(builds, 9);
cache.image(mask, rightHalf, layer, 2, 1, build);
QCOMPARE(builds, 10);
}
void MaskPlacementTests::thePixelBudgetEvictsToo()
{
MaskPlacementCache cache;
const QImage mask = gray(2, 2, 9);
int builds = 0;
const auto build = [&] { builds += 1; return tiny(); };
for (int height = 4000; height < 4003; ++height)
cache.image(mask, rightHalf, layer, 4000, height, build);
cache.image(mask, rightHalf, layer, 4000, 4000, build);
QCOMPARE(builds, 3);
cache.image(mask, rightHalf, layer, 4000, 4003, build);
QCOMPARE(builds, 4);
cache.image(mask, rightHalf, layer, 4000, 4000, build);
QCOMPARE(builds, 4);
cache.image(mask, rightHalf, layer, 4000, 4001, build);
QCOMPARE(builds, 5);
}
void MaskPlacementTests::fineMasksArePlacedFromHalvings()
{
QImage stripes = gray(1024, 16, 0);
for (int y = 0; y < 16; ++y) {
for (int x = 0; x < 1024; ++x)
stripes.scanLine(y)[x] = x % 4 == 1 || x % 4 == 2 ? 255 : 0;
}
const LayerMask mask(LayerMask::assetFrom(stripes));
const LayerTransform wide{.origin = {0, 0}, .size = {256, 16}};
const LayerTransform narrow{.origin = {64, 0}, .size = {128, 16}};
const QImage placed = mask.clipImage(narrow, wide, 256, 16).value();
for (int x = 72; x < 184; ++x)
QVERIFY(std::abs(value(placed, x, 8) - 127) <= 12);
}
QTEST_MAIN(MaskPlacementTests)
#include "MaskPlacementTests.moc"