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,228 @@
|
||||
#include "LevelsFixtures.h"
|
||||
#include "Rendering/RasterSnapshot.h"
|
||||
#include <QSignalSpy>
|
||||
|
||||
// The session around an open Levels edit: gates, queue, sampling.
|
||||
class LevelsSessionTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void anOpenEditHoldsTheSession();
|
||||
void aCommitHoldsTheProjectBusy();
|
||||
void aPendingGradientLandsFirst();
|
||||
void bigLayersPreviewAScaledCopy();
|
||||
void theRebuildDropsShapeAndText();
|
||||
void previewsRunOneAtATime();
|
||||
void autoWaitsForTheHistogramAndEndsSampling();
|
||||
void sampleModeRedrawsAndAnnounces();
|
||||
void samplesComeFromTheLayersOwnPixel();
|
||||
};
|
||||
|
||||
void LevelsSessionTests::anOpenEditHoldsTheSession()
|
||||
{
|
||||
const std::unique_ptr<EditorSession> session = rampSession();
|
||||
session->beginLevels();
|
||||
QVERIFY(!session->canAdjustColors());
|
||||
// A second Levels waits for the first.
|
||||
const QUuid first = session->levels().value().id;
|
||||
session->beginLevels();
|
||||
QCOMPARE(session->levels().value().id, first);
|
||||
// No other tool or layer while it is open.
|
||||
const NavigationTool tool = session->tool();
|
||||
session->selectTool(NavigationTool::brush);
|
||||
QCOMPARE(session->tool(), tool);
|
||||
const std::optional<QUuid> layer = session->activeLayerID();
|
||||
session->selectLayer(std::nullopt);
|
||||
QCOMPARE(session->activeLayerID(), layer);
|
||||
// A file request waits, then runs once it closes.
|
||||
bool asked = false;
|
||||
session->waitForFileRequest([&asked] { asked = true; });
|
||||
QTest::qWait(20);
|
||||
QVERIFY(!asked);
|
||||
session->cancelLevels();
|
||||
QTRY_VERIFY(asked);
|
||||
QVERIFY(session->canAdjustColors());
|
||||
}
|
||||
|
||||
void LevelsSessionTests::aCommitHoldsTheProjectBusy()
|
||||
{
|
||||
const std::unique_ptr<EditorSession> session = rampSession();
|
||||
session->beginLevels();
|
||||
LevelsSettings settings;
|
||||
settings.setCurrent(LevelRange{0, 1, 255, 255, 0});
|
||||
session->updateLevels(settings, false);
|
||||
bool done = false;
|
||||
session->commitLevels([&done] { done = true; });
|
||||
QVERIFY(session->isProjectBusy() && session->levels().value().committing);
|
||||
// Meanwhile every change is refused; a second commit returns.
|
||||
session->updateLevels(LevelsSettings(), true);
|
||||
QVERIFY(session->levels().value().settings == settings);
|
||||
session->cancelLevels();
|
||||
QVERIFY(session->levels());
|
||||
bool again = false;
|
||||
session->commitLevels([&again] { again = true; });
|
||||
QTRY_VERIFY(done && again);
|
||||
QVERIFY(!session->isProjectBusy() && !session->levels());
|
||||
QCOMPARE(bytes(session->activeLayer().value().asset.value().image())[0], uchar(255));
|
||||
}
|
||||
|
||||
void LevelsSessionTests::aPendingGradientLandsFirst()
|
||||
{
|
||||
const std::unique_ptr<EditorSession> session = rampSession();
|
||||
session->selectTool(NavigationTool::gradient);
|
||||
session->beginGradient(QPointF(0, 0.5));
|
||||
session->moveGradient(std::nullopt, QPointF(6, 0.5));
|
||||
QVERIFY(session->gradientEdit());
|
||||
const int count = session->history.undoCount();
|
||||
session->beginLevels();
|
||||
QTRY_VERIFY(session->levels());
|
||||
QVERIFY(!session->gradientEdit());
|
||||
QCOMPARE(session->history.undoCount(), count + 1);
|
||||
QCOMPARE(session->history.undoName(), QString("Gradient"));
|
||||
session->cancelLevels();
|
||||
}
|
||||
|
||||
void LevelsSessionTests::bigLayersPreviewAScaledCopy()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(9000, 2);
|
||||
QImage wide(9000, 2, QImage::Format_RGBA8888_Premultiplied);
|
||||
wide.fill(Qt::red);
|
||||
session.insert(ImportedImage(wide, wide, QStringLiteral("Wide")));
|
||||
session.beginLevels();
|
||||
// Past 8000 a side: 8000 across, the height truncated.
|
||||
const LevelsEdit &edit = session.levels().value();
|
||||
QCOMPARE(edit.previewSource.size(), QSize(8000, 1));
|
||||
QCOMPARE(edit.previewSource.pixelColor(4000, 0), QColor(Qt::red));
|
||||
QCOMPARE(edit.previewMapping.map(QPointF(8000, 1)), QPointF(9000, 2));
|
||||
QCOMPARE(edit.mapping.map(QPointF(9000, 2)), QPointF(9000, 2));
|
||||
session.cancelLevels();
|
||||
// A painted layer previews from its tiles, never flattened.
|
||||
session.selectTool(NavigationTool::brush);
|
||||
session.beginBrush(QPointF(100, 1));
|
||||
session.finishBrush();
|
||||
const ImportedImage painted = session.activeLayer().value().asset.value();
|
||||
QVERIFY(painted.raster && !painted.raster->hasMaterializedPixels());
|
||||
session.beginLevels();
|
||||
QVERIFY(!painted.raster->hasMaterializedPixels());
|
||||
QCOMPARE(session.levels().value().previewSource.pixelColor(89, 0), QColor(Qt::black));
|
||||
session.cancelLevels();
|
||||
// One pixel tall stays one pixel tall.
|
||||
EditorSession thin;
|
||||
thin.createDocument(9000, 1);
|
||||
QImage line(9000, 1, QImage::Format_RGBA8888_Premultiplied);
|
||||
line.fill(Qt::red);
|
||||
thin.insert(ImportedImage(line, line, QStringLiteral("Line")));
|
||||
thin.beginLevels();
|
||||
QCOMPARE(thin.levels().value().previewSource.size(), QSize(8000, 1));
|
||||
thin.cancelLevels();
|
||||
// Smaller layers preview their own pixels.
|
||||
const std::unique_ptr<EditorSession> small = rampSession();
|
||||
small->beginLevels();
|
||||
QCOMPARE(small->levels().value().previewSource.cacheKey(), small->activeLayer().value().asset.value().image().cacheKey());
|
||||
QCOMPARE(small->levels().value().previewMapping, small->levels().value().mapping);
|
||||
}
|
||||
|
||||
void LevelsSessionTests::theRebuildDropsShapeAndText()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(20, 20);
|
||||
session.selectTool(NavigationTool::shape);
|
||||
session.beginShape(QPointF(2, 2));
|
||||
session.dragShape(QPointF(12, 12), false, false);
|
||||
session.finishShape();
|
||||
QVERIFY(session.activeLayer().value().shape);
|
||||
session.beginLevels();
|
||||
LevelsSettings settings;
|
||||
settings.setCurrent(LevelRange{0, 1, 255, 255, 0});
|
||||
session.updateLevels(settings, false);
|
||||
commit(session);
|
||||
QVERIFY(!session.activeLayer().value().shape);
|
||||
QCOMPARE(session.history.undoName(), QString("Levels"));
|
||||
}
|
||||
|
||||
void LevelsSessionTests::previewsRunOneAtATime()
|
||||
{
|
||||
const std::unique_ptr<EditorSession> session = bigSession();
|
||||
session->beginLevels();
|
||||
LevelsSettings brighter, inverted;
|
||||
brighter.setCurrent(LevelRange{0, 2, 255, 0, 255});
|
||||
inverted.setCurrent(LevelRange{0, 1, 255, 255, 0});
|
||||
// The second waits for the first; the newest lands last.
|
||||
session->updateLevels(brighter, true);
|
||||
session->updateLevels(inverted, true);
|
||||
QVERIFY(session->levels().value().pending);
|
||||
QTRY_VERIFY(!session->levels().value().pending && session->levels().value().preparedPreview
|
||||
&& session->levels().value().preparedPreview.value().pixelColor(0, 0) == QColor(Qt::cyan));
|
||||
// Identity settings show the layer itself.
|
||||
const int revision = session->brushRevision();
|
||||
session->updateLevels(LevelsSettings(), true);
|
||||
QVERIFY(!session->levels().value().preparedPreview && session->brushRevision() > revision);
|
||||
session->cancelLevels();
|
||||
}
|
||||
|
||||
void LevelsSessionTests::autoWaitsForTheHistogramAndEndsSampling()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(2, 1);
|
||||
const QImage source = image({{50, 50, 50, 255}, {200, 200, 200, 255}});
|
||||
session.insert(ImportedImage(source, source, QStringLiteral("Gray")));
|
||||
session.beginLevels();
|
||||
session.setLevelsSampleMode(LevelsSample::white);
|
||||
// Its count arrives from the event loop: not yet.
|
||||
session.autoLevels(LevelsAuto::contrast);
|
||||
QVERIFY(session.levels().value().settings.isIdentity() && session.levels().value().sampleMode);
|
||||
QTRY_VERIFY(session.levels().value().histogramReady);
|
||||
session.autoLevels(LevelsAuto::contrast);
|
||||
QVERIFY(!session.levels().value().sampleMode);
|
||||
QVERIFY(session.levels().value().settings.ranges[0].black == 50 && session.levels().value().settings.ranges[0].white == 200);
|
||||
session.cancelLevels();
|
||||
}
|
||||
|
||||
void LevelsSessionTests::sampleModeRedrawsAndAnnounces()
|
||||
{
|
||||
const std::unique_ptr<EditorSession> session = rampSession();
|
||||
QSignalSpy changed(session.get(), &EditorSession::changed);
|
||||
// Without an edit it is refused, silently.
|
||||
session->setLevelsSampleMode(LevelsSample::black);
|
||||
QCOMPARE(changed.count(), 0);
|
||||
session->beginLevels();
|
||||
changed.clear();
|
||||
const int revision = session->brushRevision();
|
||||
session->setLevelsSampleMode(LevelsSample::black);
|
||||
QCOMPARE(changed.count(), 1);
|
||||
QVERIFY(session->brushRevision() > revision);
|
||||
QCOMPARE(session->levels().value().sampleMode, std::optional(LevelsSample::black));
|
||||
session->setLevelsSampleMode(std::nullopt);
|
||||
QVERIFY(!session->levels().value().sampleMode);
|
||||
// No mode, no sample.
|
||||
session->sampleLevels(QPointF(2.5, 0.5));
|
||||
QVERIFY(session->levels().value().settings.isIdentity());
|
||||
session->cancelLevels();
|
||||
}
|
||||
|
||||
void LevelsSessionTests::samplesComeFromTheLayersOwnPixel()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(8, 4);
|
||||
// Black then white, scaled twice and moved by one.
|
||||
const QImage source = image({{0, 0, 0, 255}, {255, 255, 255, 255}});
|
||||
session.insert(ImportedImage(source, source, QStringLiteral("Pair")));
|
||||
LayerTransform placed = session.activeLayer().value().transform;
|
||||
placed.origin = QPointF(1, 1);
|
||||
placed.size = QSizeF(4, 2);
|
||||
session.beginTransform();
|
||||
session.previewTransform(placed);
|
||||
session.commitTransform();
|
||||
session.beginLevels();
|
||||
session.setLevelsSampleMode(LevelsSample::white);
|
||||
// Outside the layer, inside the document: nothing.
|
||||
session.sampleLevels(QPointF(0.5, 0.5));
|
||||
QVERIFY(session.levels().value().settings.isIdentity());
|
||||
// Document point (2, 2) is black: white stops at 1.
|
||||
session.sampleLevels(QPointF(2, 2));
|
||||
QCOMPARE(session.levels().value().settings.ranges[1].white, 1.0);
|
||||
session.cancelLevels();
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LevelsSessionTests)
|
||||
#include "LevelsSessionTests.moc"
|
||||
Reference in new issue
Block a user