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>
This commit is contained in:
commit
c864304695
564 files changed
+100804
No files matched your search
@@ -0,0 +1,116 @@
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "Document/PixelAdjust.h"
|
||||
#include "Document/PixelInvert.h"
|
||||
#include <QtTest>
|
||||
|
||||
// The invert kernel and the selection blend behind it.
|
||||
namespace {
|
||||
SelectionClip leftHalf(int width, int height)
|
||||
{
|
||||
QImage cover = BrushRaster::context(width / 2, height, true);
|
||||
cover.fill(Qt::white);
|
||||
return SelectionClip{QRectF(0, 0, width / 2, height), cover};
|
||||
}
|
||||
}
|
||||
|
||||
class PixelInvertTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void invertKeepsAlphaAndSwapsMaskGray();
|
||||
void coverageFollowsTheLayersPixelGrid();
|
||||
void blendWeighsByCoverageAndRefusesMismatchedImages();
|
||||
void aSelectionLimitsTheInvert();
|
||||
};
|
||||
|
||||
void PixelInvertTests::invertKeepsAlphaAndSwapsMaskGray()
|
||||
{
|
||||
QImage image = BrushRaster::context(2, 1, false);
|
||||
image.setPixel(0, 0, qRgba(200, 40, 0, 200));
|
||||
image.setPixel(1, 0, qRgba(0, 0, 0, 0));
|
||||
const QImage inverted = PixelInvert::run({image, false, QTransform(), std::nullopt});
|
||||
QCOMPARE(inverted.format(), QImage::Format_RGBA8888_Premultiplied);
|
||||
const uchar *first = inverted.constScanLine(0);
|
||||
QCOMPARE(int(first[0]), 0);
|
||||
QCOMPARE(int(first[1]), 160);
|
||||
QCOMPARE(int(first[2]), 200);
|
||||
QCOMPARE(int(first[3]), 200);
|
||||
// Clear stays clear.
|
||||
QCOMPARE(int(first[7]), 0);
|
||||
QImage mask(3, 1, QImage::Format_Grayscale8);
|
||||
mask.scanLine(0)[0] = 0;
|
||||
mask.scanLine(0)[1] = 100;
|
||||
mask.scanLine(0)[2] = 255;
|
||||
const QImage swapped = PixelInvert::run({mask, true, QTransform(), std::nullopt});
|
||||
QCOMPARE(swapped.format(), QImage::Format_Grayscale8);
|
||||
QCOMPARE(int(swapped.constScanLine(0)[0]), 255);
|
||||
QCOMPARE(int(swapped.constScanLine(0)[1]), 155);
|
||||
QCOMPARE(int(swapped.constScanLine(0)[2]), 0);
|
||||
// Another format is read as premultiplied first.
|
||||
QImage plain(1, 1, QImage::Format_ARGB32);
|
||||
plain.setPixel(0, 0, qRgba(255, 0, 0, 128));
|
||||
const QImage halved = PixelInvert::run({plain, false, QTransform(), std::nullopt});
|
||||
const uchar *half = halved.constScanLine(0);
|
||||
QCOMPARE(int(half[0]), 0);
|
||||
QCOMPARE(int(half[1]), 128);
|
||||
QCOMPARE(int(half[3]), 128);
|
||||
}
|
||||
|
||||
void PixelInvertTests::coverageFollowsTheLayersPixelGrid()
|
||||
{
|
||||
// A 4×2 image at 1:1: the left half selected, exactly.
|
||||
const SelectionClip half = leftHalf(4, 2);
|
||||
const QImage exact = PixelAdjust::coverage(half, 4, 2, BrushRaster::pixelToDocument({.origin = {0, 0}, .size = {4, 2}}, 4, 2));
|
||||
QCOMPARE(int(exact.constScanLine(1)[1]), 255);
|
||||
QCOMPARE(int(exact.constScanLine(1)[2]), 0);
|
||||
// The image placed 2×: the selection lands at half size.
|
||||
const QImage placed = PixelAdjust::coverage(half, 4, 2, BrushRaster::pixelToDocument({.origin = {0, 0}, .size = {8, 4}}, 4, 2));
|
||||
QCOMPARE(int(placed.constScanLine(0)[0]), 255);
|
||||
QCOMPARE(int(placed.constScanLine(0)[1]), 0);
|
||||
// A black-and-white clip enlarged 2×: its boundary turns soft.
|
||||
QImage both = BrushRaster::context(10, 2, true);
|
||||
for (int y = 0; y < 2; ++y)
|
||||
std::fill_n(both.scanLine(y), 5, uchar(255));
|
||||
const SelectionClip inside{QRectF(0, 0, 10, 2), both};
|
||||
const QImage fine = PixelAdjust::coverage(inside, 20, 4, BrushRaster::pixelToDocument({.origin = {0, 0}, .size = {10, 2}}, 20, 4));
|
||||
QCOMPARE(int(fine.constScanLine(1)[2]), 255);
|
||||
QCOMPARE(int(fine.constScanLine(1)[15]), 0);
|
||||
for (const int column : {9, 10}) {
|
||||
const int boundary = fine.constScanLine(1)[column];
|
||||
QVERIFY2(boundary > 0 && boundary < 255, qPrintable(QString::number(boundary)));
|
||||
}
|
||||
// A clip with no coverage covers nothing.
|
||||
const QImage none = PixelAdjust::coverage({QRectF(), std::nullopt}, 4, 2, QTransform());
|
||||
QCOMPARE(int(none.constScanLine(0)[0]), 0);
|
||||
}
|
||||
|
||||
void PixelInvertTests::blendWeighsByCoverageAndRefusesMismatchedImages()
|
||||
{
|
||||
QImage top = BrushRaster::context(2, 1, false), bottom = BrushRaster::context(2, 1, false);
|
||||
top.fill(QColor::fromRgba(qRgba(255, 255, 255, 255)));
|
||||
bottom.fill(QColor::fromRgba(qRgba(100, 100, 100, 255)));
|
||||
QImage cover = BrushRaster::context(2, 1, true);
|
||||
cover.scanLine(0)[0] = 255;
|
||||
cover.scanLine(0)[1] = 64;
|
||||
const SelectionClip clip{QRectF(0, 0, 2, 1), cover};
|
||||
const QTransform grid = BrushRaster::pixelToDocument({.origin = {0, 0}, .size = {2, 1}}, 2, 1);
|
||||
const QImage blended = PixelAdjust::blend(top, bottom, clip, grid, false);
|
||||
QCOMPARE(int(blended.constScanLine(0)[0]), 255);
|
||||
// 64/255 of white over gray 100, rounded up from 138.9.
|
||||
QCOMPARE(int(blended.constScanLine(0)[4]), 139);
|
||||
QCOMPARE(int(blended.constScanLine(0)[7]), 255);
|
||||
QImage gray = BrushRaster::context(2, 1, true);
|
||||
QVERIFY_THROWS_EXCEPTION(std::logic_error, PixelAdjust::blend(top, gray, clip, grid, false));
|
||||
QVERIFY_THROWS_EXCEPTION(std::logic_error, PixelAdjust::blend(top, BrushRaster::context(3, 1, false), clip, grid, false));
|
||||
}
|
||||
|
||||
void PixelInvertTests::aSelectionLimitsTheInvert()
|
||||
{
|
||||
QImage image = BrushRaster::context(4, 2, false);
|
||||
image.fill(Qt::red);
|
||||
const QImage result = PixelInvert::run({image, false, BrushRaster::pixelToDocument({.origin = {0, 0}, .size = {4, 2}}, 4, 2), leftHalf(4, 2)});
|
||||
QCOMPARE(result.pixelColor(0, 0), QColor(Qt::cyan));
|
||||
QCOMPARE(result.pixelColor(3, 1), QColor(Qt::red));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(PixelInvertTests)
|
||||
#include "PixelInvertTests.moc"
|
||||
Reference in new issue
Block a user