Camera RAW import through LibRaw with a develop sheet (12.6a)

This commit is contained in:
雷电芽衣 committed 2026-09-27 09:19:59 -04:00
1 parent 374ba8ec73
commit 5a2fa91bc7
29 files changed
+1794 -257

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
#include "ContentView.h"
#include "IO/RawImporter.h"
#include "DialogDesk.h"
#include "UI/LayersPanel.h"
#include "UI/ToolHeaderStyle.h"
@@ -322,7 +323,7 @@ void ContentViewTests::theImporterPicksFilesAndClearsItsFlag()
QTest::qWait(50);
QCOMPARE(int(session.document().value().layers.size()), 2);
desk.typeBeforeCancel.clear();
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd)";
const QString panel = "panel|Open|open|file|||1 many Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd " + RawImporter::globs().join(" ") + ")";
QCOMPARE(desk.seen.mid(0, 2), (QStringList{panel, panel}));
QCOMPARE(desk.seen.size(), 3);
// The flag cleared from elsewhere takes the panel down.
+137
View File
@@ -0,0 +1,137 @@
#pragma once
#include <QByteArray>
#include <QSize>
#include <QFile>
#include <QString>
#include <QtEndian>
#include <array>
#include <cmath>
#include <functional>
#include <optional>
#include <stdexcept>
#include <vector>
// Tiny DNG files: an RGGB mosaic, a linear sRGB camera.
namespace DNGFixture {
struct Options {
int width = 64;
int height = 48;
// A photosite's value: 0 red, 1 green, 2 blue.
std::function<quint16(int channel, int x, int y)> value = [](int, int, int) { return quint16(20000); };
std::array<double, 3> asShotNeutral{1, 1, 1};
quint16 orientation = 1;
// A header's size past the pixels written, for budgets.
std::optional<QSize> claimed;
// Row-major: the camera's channels from sRGB's.
std::array<double, 9> mix{1, 0, 0, 0, 1, 0, 0, 0, 1};
// A fourth plane, the second green, as CMYG sensors have.
bool fourColours = false;
};
// XYZ (D65) to linear sRGB: the camera sees sRGB.
inline constexpr std::array<double, 9> colorMatrix{3.2404542, -1.5371385, -0.4985314, -0.9692660, 1.8760108, 0.0415560, 0.0556434, -0.2040259, 1.0572252};
struct Entry {
quint16 tag;
quint16 type;
quint32 count;
QByteArray data;
};
inline QByteArray le16(quint16 value)
{
QByteArray bytes(2, '\0');
qToLittleEndian(value, bytes.data());
return bytes;
}
inline QByteArray le32(quint32 value)
{
QByteArray bytes(4, '\0');
qToLittleEndian(value, bytes.data());
return bytes;
}
// Ten-thousandths; a signed numerator keeps its bits.
inline QByteArray rational(double value)
{
return le32(quint32(qint32(std::lround(value * 10000)))) + le32(10000);
}
inline QByteArray data(const Options &options)
{
QByteArray pixels;
for (int y = 0; y < options.height; ++y)
for (int x = 0; x < options.width; ++x) {
// RGGB: red on even rows and columns.
const int channel = y % 2 == 0 ? (x % 2 == 0 ? 0 : 1) : (x % 2 == 0 ? 1 : 2);
pixels += le16(options.value(channel, x, y));
}
QByteArray matrix, neutral;
const int planes = options.fourColours ? 4 : 3;
for (int row = 0; row < planes; ++row)
for (size_t column = 0; column < 3; ++column) {
const size_t from = size_t(row == 3 ? 1 : row) * 3;
matrix += rational(options.mix[from] * colorMatrix[column] + options.mix[from + 1] * colorMatrix[3 + column] + options.mix[from + 2] * colorMatrix[6 + column]);
}
for (int plane = 0; plane < planes; ++plane)
neutral += rational(options.asShotNeutral[size_t(plane == 3 ? 1 : plane)]);
const QByteArray model("OmaPhoto Fixture", 17);
std::vector<Entry> entries{
{254, 4, 1, le32(0)},
{256, 4, 1, le32(quint32(options.claimed.value_or(QSize(options.width, options.height)).width()))},
{257, 4, 1, le32(quint32(options.claimed.value_or(QSize(options.width, options.height)).height()))},
{258, 3, 1, le16(16) + le16(0)},
{259, 3, 1, le16(1) + le16(0)},
{262, 3, 1, le16(32803) + le16(0)},
{271, 2, 9, QByteArray("OmaPhoto", 9)},
{272, 2, 8, QByteArray("Fixture", 8)},
{273, 4, 1, QByteArray()},
{274, 3, 1, le16(options.orientation) + le16(0)},
{277, 3, 1, le16(1) + le16(0)},
{278, 4, 1, le32(quint32(options.height))},
{279, 4, 1, le32(quint32(pixels.size()))},
{284, 3, 1, le16(1) + le16(0)},
{33421, 3, 2, le16(2) + le16(2)},
{33422, 1, 4, options.fourColours ? QByteArray("\0\1\3\2", 4) : QByteArray("\0\1\1\2", 4)},
{50706, 1, 4, QByteArray("\1\4\0\0", 4)},
{50707, 1, 4, QByteArray("\1\1\0\0", 4)},
{50708, 2, quint32(model.size()), model},
{50710, 1, quint32(planes), QByteArray("\0\1\2\3", planes)},
{50714, 4, 1, le32(0)},
{50717, 4, 1, le32(65535)},
{50721, 10, quint32(planes * 3), matrix},
{50728, 5, quint32(planes), neutral},
{50778, 3, 1, le16(21) + le16(0)},
};
// Header, then the directory, its outside values, then the pixels.
const quint32 directory = 8, outside = directory + 2 + quint32(entries.size()) * 12 + 4;
QByteArray values;
for (Entry &entry : entries)
if (entry.data.size() > 4) {
if (values.size() % 2)
values += '\0';
const QByteArray stored = entry.data;
entry.data = le32(outside + quint32(values.size()));
values += stored;
}
if (values.size() % 2)
values += '\0';
const quint32 strip = outside + quint32(values.size());
QByteArray file = QByteArray("II*\0", 4) + le32(directory) + le16(quint16(entries.size()));
for (const Entry &entry : entries) {
const QByteArray inline_ = entry.tag == 273 ? le32(strip) : entry.data.leftJustified(4, '\0');
file += le16(entry.tag) + le16(entry.type) + le32(entry.count) + inline_;
}
file += le32(0) + values + pixels;
return file;
}
inline QString write(const QString &path, const Options &options = {})
{
QFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data(options)) <= 0)
throw std::runtime_error("could not write a DNG");
return path;
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ void DesktopIntegrationTests::theEntryOpensSwiftsDocumentTypes()
QCOMPARE(keys.value("Terminal"), QString("false"));
QVERIFY(keys.value("Categories").split(QLatin1Char(';')).contains("Graphics"));
// Swift's two document types: its projects, then four pictures.
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;"));
QCOMPARE(keys.value("MimeType"), QString("application/x-compositor-project;image/png;image/jpeg;image/heic;image/tiff;image/vnd.adobe.photoshop;image/x-dcraw;"));
}
void DesktopIntegrationTests::aProjectFolderIsAType()
+17
View File
@@ -1,5 +1,6 @@
#include "ContentView.h"
#include "IO/ImageFileDrop.h"
#include "DNGFixture.h"
#include "PSDFixture.h"
#include "UI/ProjectTabs.h"
#include "UI/ProjectWorkspaceView.h"
@@ -148,6 +149,22 @@ void ImageFileDropTests::aPictureIsCopiedToAFileFirst()
QCOMPARE(names(opened), QStringList{"Sky"});
QCOMPARE(opened.history.undoName(), QString("Import Photoshop File"));
QCOMPARE(takeCopy(before), QString("Dropped.psd"));
// A camera RAW comes after Photoshop, before any other picture.
QMimeData raw;
raw.setData("image/gif", "any picture last");
raw.setData("image/x-adobe-dng", DNGFixture::data({}));
QCOMPARE(ImageFileDrop::providers(raw).front()->formats(), QStringList{"image/x-adobe-dng"});
raw.setData("image/vnd.adobe.photoshop", "photoshop first");
QCOMPARE(ImageFileDrop::providers(raw).front()->formats(), QStringList{"image/vnd.adobe.photoshop"});
raw.removeFormat("image/vnd.adobe.photoshop");
EditorSession developed;
done = false;
ImageFileDrop::importProviders(raw, developed, std::nullopt, nullptr, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(developed.rawDevelop());
developed.finishRawDevelop(developed.rawDevelop()->settings);
QTRY_VERIFY(done);
QCOMPARE(names(developed), QStringList{"Dropped"});
QCOMPARE(takeCopy(before), QString("Dropped.dng"));
}
void ImageFileDropTests::whatCannotBeReadIsSaid()
+283
View File
@@ -0,0 +1,283 @@
#include "ContentView.h"
#include "DNGFixture.h"
#include "IO/RawImporter.h"
#include <QLabel>
#include <QProgressBar>
#include <QPushButton>
#include <QSlider>
#include <QTemporaryDir>
#include <QSignalSpy>
#include <QThreadPool>
#include <QTimer>
#include <QtTest>
// A camera RAW through the import and the develop sheet.
namespace {
QStringList names(const EditorSession &session)
{
QStringList listed;
for (const ImageLayer &each : session.document().value().layers)
listed << each.name;
return listed;
}
QUrl png(const QTemporaryDir &folder)
{
QImage image(4, 2, QImage::Format_RGBA8888);
image.fill(Qt::red);
const QString path = folder.filePath("Plain.png");
if (!image.save(path))
throw std::runtime_error("could not write a PNG");
return QUrl::fromLocalFile(path);
}
QUrl dng(const QTemporaryDir &folder, const QString &name, const DNGFixture::Options &options = {})
{
return QUrl::fromLocalFile(DNGFixture::write(folder.filePath(name), options));
}
QColor centre(const ImageLayer &layer)
{
const QImage image = layer.asset.value().image();
return image.pixelColor(image.width() / 2, image.height() / 2);
}
QColor centre(const QImage &image)
{
return image.pixelColor(image.width() / 2, image.height() / 2);
}
QString label(QWidget *sheet, const char *name)
{
return sheet->findChild<QLabel *>(name)->text();
}
}
class RawDevelopTests : public QObject {
Q_OBJECT
private slots:
void theSheetsSettingsDevelopTheLayer();
void cancelSkipsTheFileAndTheRestImport();
void whatCannotBeDevelopedIsReported();
void theLimitsLetTheirEdgeThrough();
void theSheetPreviewsAndAnswers();
};
void RawDevelopTests::theSheetsSettingsDevelopTheLayer()
{
const QTemporaryDir folder;
const QUrl raw = dng(folder, "Gray.dng");
EditorSession session;
bool done = false;
session.importImages({raw, png(folder)}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
QCOMPARE(session.rawDevelop()->path, raw.toLocalFile());
QVERIFY(session.rawDevelop()->settings == RawImporter::asShot(raw.toLocalFile()).value());
QVERIFY(session.isImporting() && !session.document());
RawDevelopSettings settings = session.rawDevelop()->settings;
settings.boost = 0;
settings.exposure = 1;
// The sheet hears at once that it has gone.
QSignalSpy changed(&session, &EditorSession::changed);
session.finishRawDevelop(settings);
QVERIFY(!session.rawDevelop() && changed.count() == 1);
QTRY_VERIFY(done);
QCOMPARE(names(session), (QStringList{"Gray", "Plain"}));
const ImageLayer &developed = session.document().value().layers.front();
QCOMPARE(developed.asset.value().size(), QSize(64, 48));
QCOMPARE(centre(developed), QColor(205, 205, 205));
QCOMPARE(developed.asset.value().thumbnail.size(), QSize(64, 48));
QCOMPARE(session.history.undoName(), QString("Import Images"));
QVERIFY(!session.importError());
}
void RawDevelopTests::cancelSkipsTheFileAndTheRestImport()
{
const QTemporaryDir folder;
EditorSession session;
bool done = false;
session.importImages({dng(folder, "Gray.dng"), png(folder)}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
session.finishRawDevelop(std::nullopt);
QTRY_VERIFY(done);
QCOMPARE(names(session), QStringList{"Plain"});
QVERIFY(!session.importError() && !session.rawDevelop());
}
void RawDevelopTests::whatCannotBeDevelopedIsReported()
{
const QTemporaryDir folder;
QFile fake(folder.filePath("Fake.dng"));
QVERIFY(fake.open(QIODevice::WriteOnly) && fake.write("not a raw") == 9);
fake.close();
DNGFixture::Options wide, tall, big;
wide.claimed = QSize(30'001, 48);
tall.claimed = QSize(64, 30'001);
big.claimed = QSize(20'000, 5'001);
DNGFixture::Options fits;
fits.claimed = QSize(20'000, 5'000);
const QUrl fitting = dng(folder, "Fits.dng", fits);
QCOMPARE(RawImporter::pixelSize(fitting.toLocalFile()), std::optional(QSize(20'000, 5'000)));
EditorSession session;
bool done = false;
const QList<QUrl> urls{QUrl::fromLocalFile(fake.fileName()), dng(folder, "Wide.dng", wide), dng(folder, "Tall.dng", tall), dng(folder, "Big.dng", big), png(folder)};
session.importImages(urls, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(done);
QVERIFY(!session.rawDevelop());
const QString unreadable = ImageImportError(ImageImportError::Kind::unreadable).what(), tooLarge = ImageImportError(ImageImportError::Kind::tooLarge).what();
QCOMPARE(session.importError().value(), QStringList({"Fake.dng: " + unreadable, "Wide.dng: " + tooLarge, "Tall.dng: " + tooLarge, "Big.dng: " + tooLarge}).join("\n\n"));
QCOMPARE(names(session), QStringList{"Plain"});
// The canvas's 4 by 2 pixels count against the budget.
session.setImportError(std::nullopt);
done = false;
session.importImages({fitting}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Fits.dng: " + tooLarge);
// Accepted, then unreadable when the full develop runs.
session.setImportError(std::nullopt);
done = false;
const QUrl gray = dng(folder, "Gray.dng");
session.importImages({gray}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
QFile broken(gray.toLocalFile());
QVERIFY(broken.open(QIODevice::WriteOnly) && broken.write("gone") == 4);
broken.close();
session.finishRawDevelop(session.rawDevelop()->settings);
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Gray.dng: " + unreadable);
QCOMPARE(names(session), QStringList{"Plain"});
// A white the camera cannot record says so.
session.setImportError(std::nullopt);
done = false;
const QUrl warm = dng(folder, "Warm.dng");
session.importImages({warm}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
RawDevelopSettings candle = session.rawDevelop()->settings;
candle.temperature = 2000;
session.finishRawDevelop(candle);
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Warm.dng: " + QString(ImageImportError(ImageImportError::Kind::whiteBalance).what()));
QCOMPARE(names(session), QStringList{"Plain"});
}
void RawDevelopTests::theLimitsLetTheirEdgeThrough()
{
const QTemporaryDir folder;
for (const QSize claimed : {QSize(30'000, 48), QSize(64, 30'000), QSize(10'000, 10'000)}) {
DNGFixture::Options edge;
edge.claimed = claimed;
EditorSession session;
bool done = false;
session.importImages({dng(folder, "Edge.dng", edge)}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
session.finishRawDevelop(std::nullopt);
QTRY_VERIFY(done);
QVERIFY(!session.importError() && !session.document());
}
}
void RawDevelopTests::theSheetPreviewsAndAnswers()
{
const QTemporaryDir folder;
const QUrl raw = dng(folder, "Gray.dng");
const QString path = raw.toLocalFile();
EditorSession session;
ContentView view(session);
view.resize(900, 600);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
bool done = false;
session.importImages({raw}, std::nullopt, [&done] { done = true; });
QDialog *sheet = view.findChild<QDialog *>("rawDevelopSheet");
QVERIFY(sheet);
// The first preview waits for nothing.
QTimer *wait = sheet->findChild<QTimer *>("rawWait");
QVERIFY(!wait->isActive());
// Another change keeps the one sheet.
session.notify();
QCOMPARE(view.findChildren<QDialog *>("rawDevelopSheet").size(), qsizetype(1));
QVERIFY(sheet->isVisible() && sheet->windowModality() == Qt::WindowModal);
QStringList words;
for (const QLabel *each : sheet->findChildren<QLabel *>())
words << each->text();
QVERIFY(words.contains("Develop “Gray.dng”"));
QCOMPARE(label(sheet, "rawExposureValue"), QString("0.00 EV"));
QCOMPARE(label(sheet, "rawTemperatureValue"), QString("6501 K"));
QCOMPARE(label(sheet, "rawTintValue"), QString("10"));
QCOMPARE(label(sheet, "rawBoostValue"), QString("1.00"));
QPushButton *reset = sheet->findChild<QPushButton *>("rawReset");
QVERIFY(!reset->isEnabled());
// The first preview lands: the boosted gray, fitted.
QWidget *preview = sheet->findChild<QWidget *>("rawPreview");
QTRY_VERIFY(sheet->findChild<QProgressBar *>("rawSpinner")->isHidden());
QCOMPARE(preview->grab().toImage().pixelColor(200, 170), QColor(139, 139, 139));
// A develop under way spins; a newer change drops it.
QSlider *exposure = sheet->findChild<QSlider *>("rawExposure");
QProgressBar *spinner = sheet->findChild<QProgressBar *>("rawSpinner");
bool spinning = false;
const QMetaObject::Connection once = connect(wait, &QTimer::timeout, wait, [&] {
spinning = !spinner->isHidden();
exposure->setValue(-100);
wait->stop();
});
exposure->setValue(100);
QTRY_VERIFY(spinning);
disconnect(once);
QThreadPool::globalInstance()->waitForDone();
QTest::qWait(50);
QCOMPARE(preview->grab().toImage().pixelColor(200, 170), QColor(139, 139, 139));
QVERIFY(!spinner->isHidden());
RawDevelopSettings dimmer = session.rawDevelop()->settings;
dimmer.exposure = -1;
wait->start();
QTRY_COMPARE_WITH_TIMEOUT(preview->grab().toImage().pixelColor(200, 170), centre(RawImporter::develop(path, dimmer)), 1000);
QVERIFY(spinner->isHidden());
// A slider changes the settings; the preview follows.
exposure->setValue(100);
QCOMPARE(label(sheet, "rawExposureValue"), QString("1.00 EV"));
QVERIFY(reset->isEnabled());
RawDevelopSettings brighter = session.rawDevelop()->settings;
brighter.exposure = 1;
const QColor expected = centre(RawImporter::develop(path, brighter));
QTRY_COMPARE(preview->grab().toImage().pixelColor(200, 170), expected);
sheet->findChild<QSlider *>("rawTemperature")->setValue(3000);
QCOMPARE(label(sheet, "rawTemperatureValue"), QString("3000 K"));
reset->click();
QCOMPARE(label(sheet, "rawExposureValue"), QString("0.00 EV"));
QCOMPARE(label(sheet, "rawTemperatureValue"), QString("6501 K"));
QCOMPARE(sheet->findChild<QSlider *>("rawExposure")->value(), 0);
QVERIFY(!reset->isEnabled());
QTRY_COMPARE(preview->grab().toImage().pixelColor(200, 170), QColor(139, 139, 139));
// Import develops the whole frame with the sheet's settings.
sheet->findChild<QSlider *>("rawExposure")->setValue(100);
sheet->findChild<QPushButton *>("rawImport")->click();
QVERIFY(!session.rawDevelop());
QTRY_VERIFY(done);
QTRY_VERIFY(!view.findChild<QDialog *>("rawDevelopSheet"));
QCOMPARE(centre(session.document().value().layers.back()), expected);
// Escape cancels; the kept decode goes with the sheet.
done = false;
session.importImages({raw}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(view.findChild<QDialog *>("rawDevelopSheet"));
sheet = view.findChild<QDialog *>("rawDevelopSheet");
QTRY_VERIFY(sheet->findChild<QProgressBar *>("rawSpinner")->isHidden());
QTest::keyClick(sheet, Qt::Key_Escape);
QTRY_VERIFY(done);
QTRY_VERIFY(!view.findChild<QDialog *>("rawDevelopSheet"));
QCOMPARE(session.document().value().layers.size(), size_t(1));
QThreadPool::globalInstance()->waitForDone();
DNGFixture::Options darker;
darker.value = [](int, int, int) { return quint16(10000); };
DNGFixture::write(path, darker);
QVERIFY(centre(RawImporter::Queue::shared().develop(path, RawImporter::asShot(path).value(), 800)).red() < 139);
// Cancel answers no as well.
done = false;
session.importImages({raw}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(view.findChild<QDialog *>("rawDevelopSheet"));
view.findChild<QDialog *>("rawDevelopSheet")->findChild<QPushButton *>("rawCancel")->click();
QTRY_VERIFY(done);
QCOMPARE(session.document().value().layers.size(), size_t(1));
}
QTEST_MAIN(RawDevelopTests)
#include "RawDevelopTests.moc"
+338
View File
@@ -0,0 +1,338 @@
#include "DNGFixture.h"
#include "IO/ImageImporter.h"
#include "IO/RawImporter.h"
#include <QColorSpace>
#include <QTemporaryDir>
#include <QtTest>
// Camera RAW through LibRaw: matching, white balance, the develop.
namespace {
QColor middle(const QImage &image)
{
return image.pixelColor(image.width() / 2, image.height() / 2);
}
std::array<int, 3> rgb(const QImage &image)
{
const QColor colour = middle(image);
return {colour.red(), colour.green(), colour.blue()};
}
// The camera's neutral under a white, through the matrix.
std::array<double, 3> neutralFor(QPointF xy)
{
const double xyz[3] = {xy.x() / xy.y(), 1, (1 - xy.x() - xy.y()) / xy.y()};
std::array<double, 3> camera{};
for (size_t row = 0; row < 3; ++row)
for (size_t column = 0; column < 3; ++column)
camera[row] += DNGFixture::colorMatrix[row * 3 + column] * xyz[column];
return {camera[0] / camera[1], 1, camera[2] / camera[1]};
}
RawDevelopSettings flat(const QString &path)
{
RawDevelopSettings settings = RawImporter::asShot(path).value();
settings.boost = 0;
return settings;
}
std::optional<ImageImportError::Kind> refusal(const std::function<void()> &run)
{
try {
run();
} catch (const ImageImportError &error) {
return error.kind;
}
return std::nullopt;
}
}
class RawImporterTests : public QObject {
Q_OBJECT
private slots:
void cameraRawIsKnownByItsExtension();
void temperatureAndTintFollowThePlanckianLocus();
void theCamerasOwnBalanceIsWhereTheSlidersStart();
void boostMixesTheFlatAndTheLibRawCurves();
void exposureScalesTheLightBeforeTheCurve();
void temperatureAndTintColourTheDevelop();
void coloursComeThroughTheCameraMatrix();
void orientationAndThePreviewsLimit();
void theQueueKeepsThePreviewsDecodeUntilReleased();
void aMixingCameraDevelopsAlike();
void aKeptDecodeDevelopsAsAFreshOne();
void fourColourSensorsAreUnsupported();
void whatIsNoRawIsRefused();
void settingsResetToTheShot();
};
void RawImporterTests::cameraRawIsKnownByItsExtension()
{
for (const char *name : {"a.dng", "b.CR2", "c.nef", "d.ARW", "e.raf", "f.orf", "g.rw2", "h.cr3"})
QVERIFY2(RawImporter::matches(name), name);
for (const char *name : {"a.jpg", "b.psd", "c.tif", "d.png", "e", "f.dng.txt"})
QVERIFY2(!RawImporter::matches(name), name);
QVERIFY(RawImporter::isRawType("image/x-adobe-dng") && RawImporter::isRawType("image/x-canon-cr2") && RawImporter::isRawType("image/x-dcraw"));
QVERIFY(!RawImporter::isRawType("image/tiff") && !RawImporter::isRawType("image/png") && !RawImporter::isRawType("nothing"));
const QStringList globs = RawImporter::globs();
for (const char *glob : {"*.dng", "*.cr2", "*.nef", "*.arw", "*.raf"})
QVERIFY2(globs.contains(glob), glob);
QVERIFY(!globs.contains("*.jpg") && !globs.contains("*.tif") && globs.count("*.dng") == 1);
QVERIFY(std::is_sorted(globs.begin(), globs.end()));
}
void RawImporterTests::temperatureAndTintFollowThePlanckianLocus()
{
const QPointF d65 = RawImporter::planckian(6500);
QVERIFY(std::abs(d65.x() - 0.313494) < 1e-6 && std::abs(d65.y() - 0.323663) < 1e-6);
// Warmer is redder: x grows as the temperature falls.
QVERIFY(RawImporter::planckian(2000).x() > RawImporter::planckian(3000).x());
QVERIFY(RawImporter::planckian(3000).x() > RawImporter::planckian(10000).x());
// Positive tint is a white above the locus: greener.
const QPointF green = RawImporter::whitePoint(5000, 30), magenta = RawImporter::whitePoint(5000, -30), on = RawImporter::whitePoint(5000, 0);
QVERIFY(std::abs(on.x() - RawImporter::planckian(5000).x()) < 1e-12);
QVERIFY(magenta.y() < on.y() && green.y() > on.y());
for (const double kelvin : {2100.0, 3200.0, 5000.0, 6500.0, 9000.0, 11900.0})
for (const double tint : {-120.0, -10.0, 0.0, 45.0, 140.0}) {
const auto [backKelvin, backTint] = RawImporter::temperatureOf(RawImporter::whitePoint(kelvin, tint));
QVERIFY2(std::abs(backKelvin - kelvin) < 0.5 && std::abs(backTint - tint) < 0.05,
qPrintable(QStringLiteral("%1 %2 → %3 %4").arg(kelvin).arg(tint).arg(backKelvin).arg(backTint)));
}
}
void RawImporterTests::theCamerasOwnBalanceIsWhereTheSlidersStart()
{
const QTemporaryDir folder;
// D65 sits a little above the Planckian locus.
const RawDevelopSettings daylight = RawImporter::asShot(DNGFixture::write(folder.filePath("d65.dng"))).value();
QVERIFY2(std::abs(daylight.temperature - 6500.7) < 1 && std::abs(daylight.tint - 9.52) < 0.05, qPrintable(QString::number(daylight.tint)));
QCOMPARE(daylight.asShotTemperature, daylight.temperature);
QCOMPARE(daylight.asShotTint, daylight.tint);
QVERIFY(daylight.isAsShot() && daylight.exposure == 0 && daylight.boost == 1);
DNGFixture::Options tungsten;
tungsten.asShotNeutral = neutralFor(RawImporter::whitePoint(3200, 20));
const RawDevelopSettings warm = RawImporter::asShot(DNGFixture::write(folder.filePath("warm.dng"), tungsten)).value();
QVERIFY2(std::abs(warm.temperature - 3200) < 5 && std::abs(warm.tint - 20) < 0.5,
qPrintable(QStringLiteral("%1 %2").arg(warm.temperature).arg(warm.tint)));
}
void RawImporterTests::boostMixesTheFlatAndTheLibRawCurves()
{
const QTemporaryDir folder;
const QString path = DNGFixture::write(folder.filePath("gray.dng"));
RawDevelopSettings settings = flat(path);
// 20000 of 65535: sRGB's curve gives 150, BT.709's 139.
QCOMPARE(rgb(RawImporter::develop(path, settings)), (std::array<int, 3>{150, 150, 150}));
settings.boost = 1;
QCOMPARE(rgb(RawImporter::develop(path, settings)), (std::array<int, 3>{139, 139, 139}));
settings.boost = 0.5;
QCOMPARE(rgb(RawImporter::develop(path, settings)), (std::array<int, 3>{145, 145, 145}));
// In the shadows BT.709 turns straight: 4.5 times the light.
DNGFixture::Options dark;
dark.value = [](int, int, int) { return quint16(500); };
const QString shadow = DNGFixture::write(folder.filePath("dark.dng"), dark);
QCOMPARE(rgb(RawImporter::develop(shadow, RawImporter::asShot(shadow).value())), (std::array<int, 3>{9, 9, 9}));
const QImage developed = RawImporter::develop(path, settings);
QCOMPARE(developed.format(), QImage::Format_RGBA8888_Premultiplied);
QVERIFY(developed.colorSpace() == QColorSpace(QColorSpace::SRgb));
QCOMPARE(middle(developed).alpha(), 255);
}
void RawImporterTests::exposureScalesTheLightBeforeTheCurve()
{
const QTemporaryDir folder;
const QString path = DNGFixture::write(folder.filePath("gray.dng"));
RawDevelopSettings settings = flat(path);
settings.exposure = 1;
QCOMPARE(rgb(RawImporter::develop(path, settings))[1], 205);
settings.exposure = -3;
QCOMPARE(rgb(RawImporter::develop(path, settings))[1], 55);
settings.exposure = 3;
QCOMPARE(rgb(RawImporter::develop(path, settings))[1], 255);
// Near white LibRaw keeps the level: no stretch.
DNGFixture::Options bright;
bright.value = [](int, int, int) { return quint16(52000); };
const QString near = DNGFixture::write(folder.filePath("bright.dng"), bright);
QCOMPARE(rgb(RawImporter::develop(near, flat(near))), (std::array<int, 3>{230, 230, 230}));
}
void RawImporterTests::temperatureAndTintColourTheDevelop()
{
const QTemporaryDir folder;
const QString path = DNGFixture::write(folder.filePath("gray.dng"));
RawDevelopSettings settings = flat(path);
// Balanced for tungsten, a daylight gray turns blue; green holds.
settings.temperature = 3200;
const std::array<int, 3> cool = rgb(RawImporter::develop(path, settings));
QVERIFY2(cool[2] > 150 && cool[1] == 150 && cool[0] < 150, qPrintable(QStringLiteral("%1 %2 %3").arg(cool[0]).arg(cool[1]).arg(cool[2])));
settings.temperature = 12000;
const std::array<int, 3> warm = rgb(RawImporter::develop(path, settings));
QVERIFY2(warm[0] > 150 && warm[1] == 150 && warm[2] < 150, qPrintable(QStringLiteral("%1 %2 %3").arg(warm[0]).arg(warm[1]).arg(warm[2])));
// Balanced for a greener white, as Adobe's positive tint, magenta.
settings.temperature = settings.asShotTemperature;
settings.tint = settings.asShotTint + 60;
const std::array<int, 3> magenta = rgb(RawImporter::develop(path, settings));
QVERIFY2(magenta[0] > 150 && magenta[1] == 150 && magenta[2] > 150, qPrintable(QStringLiteral("%1 %2 %3").arg(magenta[0]).arg(magenta[1]).arg(magenta[2])));
// A white the camera cannot see is refused, not ignored.
settings.temperature = 2000;
settings.tint = settings.asShotTint;
QCOMPARE(refusal([&] { RawImporter::develop(path, settings); }), std::optional(ImageImportError::Kind::whiteBalance));
QCOMPARE(QString(ImageImportError(ImageImportError::Kind::whiteBalance).what()),
QString("This camera can’t record that white balance. Move Temperature or Tint back toward the shot."));
}
void RawImporterTests::coloursComeThroughTheCameraMatrix()
{
const QTemporaryDir folder;
DNGFixture::Options red;
red.value = [](int channel, int, int) { return quint16(channel == 0 ? 30000 : 0); };
const QString path = DNGFixture::write(folder.filePath("red.dng"), red);
const std::array<int, 3> developed = rgb(RawImporter::develop(path, flat(path)));
QVERIFY2(developed[0] > 170 && developed[1] < 5 && developed[2] < 5, qPrintable(QStringLiteral("%1 %2 %3").arg(developed[0]).arg(developed[1]).arg(developed[2])));
}
void RawImporterTests::orientationAndThePreviewsLimit()
{
const QTemporaryDir folder;
DNGFixture::Options turned;
turned.orientation = 6;
const QString path = DNGFixture::write(folder.filePath("turned.dng"), turned);
QCOMPARE(RawImporter::pixelSize(path).value(), QSize(48, 64));
QCOMPARE(RawImporter::develop(path, flat(path)).size(), QSize(48, 64));
const QString upright = DNGFixture::write(folder.filePath("upright.dng"));
QCOMPARE(RawImporter::pixelSize(upright).value(), QSize(64, 48));
// Past the limit, a half-size decode that then fits.
QCOMPARE(RawImporter::develop(upright, flat(upright), 32).size(), QSize(32, 24));
QCOMPARE(RawImporter::develop(upright, flat(upright), 20).size(), QSize(20, 15));
QCOMPARE(RawImporter::develop(upright, flat(upright), 64).size(), QSize(64, 48));
}
void RawImporterTests::theQueueKeepsThePreviewsDecodeUntilReleased()
{
const QTemporaryDir folder;
const QString path = DNGFixture::write(folder.filePath("gray.dng"));
RawImporter::Queue &queue = RawImporter::Queue::shared();
const RawDevelopSettings settings = flat(path);
QCOMPARE(rgb(queue.develop(path, settings, 40))[1], 150);
// The file changes; the kept decode does not, until released.
DNGFixture::Options darker;
darker.value = [](int, int, int) { return quint16(10000); };
DNGFixture::write(path, darker);
QCOMPARE(rgb(queue.develop(path, settings, 40))[1], 150);
QCOMPARE(rgb(queue.develop(path, settings, std::nullopt))[1], 109);
RawDevelopSettings brighter = settings;
brighter.exposure = 1;
QCOMPARE(rgb(queue.develop(path, brighter, 40))[1], 205);
queue.release();
QCOMPARE(rgb(queue.develop(path, brighter, 40))[1], 150);
// Another limit halves the kept frame; another file decodes afresh.
QCOMPARE(queue.develop(path, settings, 20).size(), QSize(20, 15));
QCOMPARE(queue.develop(path, settings, 40).size(), QSize(32, 24));
QCOMPARE(queue.develop(path, settings, 40).size(), QSize(32, 24));
const QString other = DNGFixture::write(folder.filePath("other.dng"));
QCOMPARE(rgb(queue.develop(other, settings, 20))[1], 150);
QCOMPARE(rgb(queue.develop(path, settings, 20))[1], 109);
queue.release();
}
void RawImporterTests::aMixingCameraDevelopsAlike()
{
// Channels leak into each other and differ in gain.
const std::array<double, 9> leaky{0.35, 0.1, 0.05, 0.1, 0.8, 0.1, 0.04, 0.2, 0.56};
const auto seen = [&](std::array<double, 3> scene) {
return [leaky, scene](int channel, int, int) {
const size_t row = size_t(channel) * 3;
return quint16(std::lround(65535 * (leaky[row] * scene[0] + leaky[row + 1] * scene[1] + leaky[row + 2] * scene[2])));
};
};
const QTemporaryDir folder;
DNGFixture::Options gray;
gray.mix = leaky;
gray.asShotNeutral = {(0.35 + 0.1 + 0.05) / 1.0, 1, (0.04 + 0.2 + 0.56) / 1.0};
gray.value = seen({20000 / 65535.0, 20000 / 65535.0, 20000 / 65535.0});
const QString path = DNGFixture::write(folder.filePath("gray.dng"), gray);
const RawDevelopSettings shot = RawImporter::asShot(path).value();
QVERIFY2(std::abs(shot.temperature - 6500.7) < 2 && std::abs(shot.tint - 9.52) < 0.1, qPrintable(QStringLiteral("%1 %2").arg(shot.temperature).arg(shot.tint)));
QCOMPARE(rgb(RawImporter::develop(path, flat(path))), (std::array<int, 3>{150, 150, 150}));
DNGFixture::Options red = gray;
red.value = seen({30000 / 65535.0, 0, 0});
const QString redPath = DNGFixture::write(folder.filePath("red.dng"), red);
const std::array<int, 3> developed = rgb(RawImporter::develop(redPath, flat(path)));
QVERIFY2(developed[0] > 170 && developed[1] < 5 && developed[2] < 5, qPrintable(QStringLiteral("%1 %2 %3").arg(developed[0]).arg(developed[1]).arg(developed[2])));
// Clipped photosites stay white, whichever channels clip.
for (const double level : {1.3, 3.0}) {
DNGFixture::Options clipped = gray;
clipped.value = [leaky, level](int channel, int, int) {
const size_t row = size_t(channel) * 3;
return quint16(std::min(65535.0, 65535 * level * (leaky[row] + leaky[row + 1] + leaky[row + 2])));
};
const QString clippedPath = DNGFixture::write(folder.filePath("clipped.dng"), clipped);
for (const float boost : {0.0f, 1.0f}) {
RawDevelopSettings settings = shot;
settings.boost = boost;
QCOMPARE(rgb(RawImporter::develop(clippedPath, settings)), (std::array<int, 3>{255, 255, 255}));
}
}
}
void RawImporterTests::aKeptDecodeDevelopsAsAFreshOne()
{
const QTemporaryDir folder;
DNGFixture::Options leaky;
leaky.mix = {0.35, 0.1, 0.05, 0.1, 0.8, 0.1, 0.04, 0.2, 0.56};
leaky.value = [](int channel, int, int) { return quint16(std::array{10000, 20000, 16000}[size_t(channel)]); };
const QString path = DNGFixture::write(folder.filePath("leaky.dng"), leaky);
RawImporter::Queue &queue = RawImporter::Queue::shared();
RawDevelopSettings settings = flat(path);
for (const auto &[kelvin, tint] : std::vector<std::pair<float, float>>{{3200, 0}, {3200, 0}, {9000, 40}, {settings.asShotTemperature, settings.asShotTint}}) {
settings.temperature = kelvin;
settings.tint = tint;
QCOMPARE(rgb(queue.develop(path, settings, 40)), rgb(RawImporter::develop(path, settings, 40)));
}
queue.release();
}
void RawImporterTests::fourColourSensorsAreUnsupported()
{
const QTemporaryDir folder;
DNGFixture::Options four;
four.fourColours = true;
const QString path = DNGFixture::write(folder.filePath("four.dng"), four);
QCOMPARE(RawImporter::pixelSize(path).value(), QSize(64, 48));
QVERIFY(!RawImporter::asShot(path));
QCOMPARE(refusal([&] { RawImporter::develop(path, RawDevelopSettings()); }), std::optional(ImageImportError::Kind::unsupported));
}
void RawImporterTests::whatIsNoRawIsRefused()
{
const QTemporaryDir folder;
QFile fake(folder.filePath("fake.dng"));
QVERIFY(fake.open(QIODevice::WriteOnly) && fake.write("not a raw") == 9);
fake.close();
QCOMPARE(refusal([&] { RawImporter::develop(fake.fileName(), RawDevelopSettings()); }), std::optional(ImageImportError::Kind::unreadable));
QVERIFY(!RawImporter::asShot(fake.fileName()) && !RawImporter::pixelSize(fake.fileName()));
const QString missing = folder.filePath("missing.dng");
QCOMPARE(refusal([&] { RawImporter::develop(missing, RawDevelopSettings()); }), std::optional(ImageImportError::Kind::unreadable));
QVERIFY(!RawImporter::asShot(missing) && !RawImporter::pixelSize(missing));
}
void RawImporterTests::settingsResetToTheShot()
{
RawDevelopSettings settings;
settings.asShotTemperature = 4100;
settings.asShotTint = 7;
QVERIFY(!settings.isAsShot());
settings.reset();
QVERIFY(settings.isAsShot());
QVERIFY(settings.temperature == 4100 && settings.tint == 7 && settings.exposure == 0 && settings.boost == 1);
for (const auto &change : std::vector<std::function<void(RawDevelopSettings &)>>{
[](RawDevelopSettings &s) { s.exposure = 0.5; }, [](RawDevelopSettings &s) { s.boost = 0.9f; },
[](RawDevelopSettings &s) { s.temperature = 4000; }, [](RawDevelopSettings &s) { s.tint = 0; }}) {
RawDevelopSettings changed = settings;
change(changed);
QVERIFY(!changed.isAsShot());
}
}
QTEST_GUILESS_MAIN(RawImporterTests)
#include "RawImporterTests.moc"
+245
View File
@@ -0,0 +1,245 @@
# Every test binary, one per Swift test file.
enable_testing()
function(compositor_test name)
add_executable(${name} tests/${name}.cpp)
target_link_libraries(${name} PRIVATE compositor_core Qt6::Test)
add_test(NAME ${name} COMMAND ${name})
set_tests_properties(${name} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
endfunction()
compositor_test(AdjustmentSurfaceTests)
compositor_test(BlendModePickerTests)
compositor_test(BlendShortcutTests)
compositor_test(LayersPanelTests)
compositor_test(LayerListFocusTests)
compositor_test(LayerDragTests)
compositor_test(LayerDragGestureTests)
compositor_test(ProjectDropTests)
compositor_test(BlendModeTests)
compositor_test(BrushCanvasTests)
compositor_test(BrushGestureTests)
compositor_test(BrushKeyTests)
compositor_test(BrushCursorOverlayTests)
compositor_test(BrushControlsTests)
compositor_test(BrushPerformanceTests)
compositor_test(CanvasPerformanceTests)
compositor_test(PoolMapTests)
compositor_test(DesktopIntegrationTests)
# The install it checks needs the app built.
add_dependencies(DesktopIntegrationTests omaphoto)
# What this build installs, as CMake itself reads it.
target_compile_definitions(DesktopIntegrationTests PRIVATE OMAPHOTO_BUNDLED=$<BOOL:${OMAPHOTO_BUNDLE_ORT}>
OMAPHOTO_LIBDIR="${CMAKE_INSTALL_LIBDIR}" OMAPHOTO_MODEL_PATH="${OMAPHOTO_MODEL}")
compositor_test(BrushIntersectionTests)
compositor_test(BrushSessionTests)
compositor_test(BrushSmoothingTests)
compositor_test(BrushPreviewTests)
compositor_test(BrushRasterTests)
compositor_test(BrushTests)
compositor_test(BrushCoverageTests)
compositor_test(SpotHealingTests)
compositor_test(CloneStampTests)
compositor_test(CloneStampCanvasTests)
compositor_test(SmudgeLiquifyTests)
compositor_test(BlurToolTests)
compositor_test(SmearCanvasTests)
compositor_test(GradientTests)
compositor_test(GradientCanvasTests)
compositor_test(ShapeToolTests)
compositor_test(ShapeLayerTests)
compositor_test(ShapeCanvasTests)
compositor_test(TypeToolTests)
compositor_test(TextLayoutTests)
compositor_test(TypeSessionTests)
compositor_test(TypeStoreTests)
compositor_test(TypeControlsTests)
compositor_test(TypeFontPickerTests)
compositor_test(InlineTextEditorTests)
compositor_test(InlineTextInputTests)
compositor_test(InlineTextDrawTests)
compositor_test(InlineTextPreeditTests)
compositor_test(InlineTextPointerTests)
compositor_test(InlineTextHandleTests)
compositor_test(ColorPickerTests)
compositor_test(FloatingPanelTests)
compositor_test(ColorSwatchTests)
compositor_test(ColorPaletteControlsTests)
compositor_test(ColorPickerSheetTests)
compositor_test(EyedropperTests)
compositor_test(PickingTests)
compositor_test(LevelsTests)
compositor_test(LevelsSessionTests)
compositor_test(LevelsModelTests)
compositor_test(LevelsEdgeTests)
compositor_test(LevelsFailureTests)
compositor_test(LevelsSheetTests)
compositor_test(LevelsHandleTests)
compositor_test(LevelsCanvasTests)
compositor_test(ImageAdjustmentTests)
compositor_test(ImageAdjustmentModelTests)
compositor_test(CurvesControlsTests)
compositor_test(HueSaturationTests)
compositor_test(HueSaturationModelTests)
compositor_test(HueSaturationSessionTests)
compositor_test(HueSaturationFailureTests)
compositor_test(HueSaturationSheetTests)
compositor_test(HueSaturationSpectrumTests)
compositor_test(HueSaturationCanvasTests)
compositor_test(CanvasDisplayTests)
compositor_test(CanvasNavigationTests)
compositor_test(CanvasSizeTests)
compositor_test(CanvasThumbnailTests)
compositor_test(ClippingStackTests)
compositor_test(CompositorTests)
compositor_test(CompositorMenusTests)
compositor_test(ViewMenuTests)
compositor_test(KeyboardShortcutsTests)
compositor_test(KeyboardShortcutsSheetTests)
compositor_test(RemappedKeysTests)
compositor_test(CompositorUITestsLaunchTests)
compositor_test(ContentViewTests)
compositor_test(ContentViewStatusTests)
compositor_test(DistortTests)
compositor_test(DownsampleTests)
compositor_test(DrawCoverageTests)
compositor_test(EditorSessionSignalTests)
compositor_test(EditorSessionTests)
compositor_test(LayerDuplicateTests)
compositor_test(ExportTests)
compositor_test(FolderMaskClipTests)
compositor_test(EditorCanvasTests)
compositor_test(GroupSessionTests)
compositor_test(GroupTests)
compositor_test(GroupingSelectionTests)
compositor_test(HighDpiCanvasTests)
compositor_test(HistoryTests)
compositor_test(ImageImportSessionTests)
compositor_test(ImageImportTests)
compositor_test(ImageSizeTests)
compositor_test(JPEGExportTests)
compositor_test(LayerAppearanceControlsTests)
compositor_test(LayerAppearanceTests)
compositor_test(FolderOpacityTests)
compositor_test(LayerMaskEditTests)
compositor_test(LayerMaskSessionTests)
compositor_test(LayerMaskTests)
compositor_test(LayerRendererTests)
compositor_test(LayerTests)
compositor_test(LiveMaskRendererTests)
compositor_test(LiveMaskBakeTests)
compositor_test(LiveMaskDeleteTests)
compositor_test(LiveMaskEditTests)
compositor_test(LiveMaskSessionTests)
compositor_test(LiveMaskTests)
compositor_test(MaskPlacementTests)
compositor_test(MaskSignalTests)
compositor_test(MaskTracingTests)
compositor_test(MaskTransformTests)
compositor_test(ProjectManifestTests)
compositor_test(NativeLayerListTests)
compositor_test(NavigationToolHeaderTests)
compositor_test(NewCanvasSheetTests)
compositor_test(OmarchyThemeTests)
compositor_test(ProjectControllerTests)
compositor_test(ProjectCopyTests)
compositor_test(ProjectOpenTests)
compositor_test(ProjectReceiveTests)
compositor_test(ProjectReplaceTests)
compositor_test(ProjectWorkspaceFlowTests)
compositor_test(ProjectWorkspaceSignalTests)
compositor_test(ProjectWorkspaceTests)
compositor_test(ProjectWorkspaceViewTests)
# It starts the real app beside itself.
add_dependencies(ProjectWorkspaceViewTests omaphoto)
compositor_test(ProjectSessionTests)
compositor_test(ProjectTests)
compositor_test(RasterSnapshotSessionTests)
compositor_test(RasterSnapshotTests)
compositor_test(SessionGateTests)
compositor_test(TiledLayerTests)
compositor_test(TiledPieceTests)
compositor_test(TiledStrokeTests)
compositor_test(TransformSessionTests)
compositor_test(ToolIconTests)
compositor_test(TransformTests)
compositor_test(TransformOverlayTests)
compositor_test(TransformPressTests)
compositor_test(TransformInspectorTests)
compositor_test(TransformSnapTests)
compositor_test(CursorTests)
compositor_test(DistortCanvasTests)
compositor_test(DistortMaskTests)
compositor_test(FiltersTests)
compositor_test(FilterTests)
compositor_test(FilterModelTests)
# It checks what the filters hand Swift's C kernels.
target_include_directories(FilterModelTests PRIVATE ${OMAPHOTO_KERNELS})
compositor_test(FilterSessionTests)
compositor_test(FilterFailureTests)
compositor_test(LayerAdjustmentTests)
compositor_test(AdjustmentLayerTests)
compositor_test(AdjustmentEditorTests)
compositor_test(AdjustmentPanelTests)
compositor_test(LayerEffectsTests)
compositor_test(OuterGlowTests)
compositor_test(LayerEffectsSessionTests)
compositor_test(LayerEffectsKernelTests)
compositor_test(EffectsPreviewCacheTests)
compositor_test(LayerEffectsSurfaceTests)
compositor_test(EffectsCanvasTests)
compositor_test(EffectsFailureTests)
compositor_test(EffectsPanelTests)
compositor_test(LayerEffectRowTests)
compositor_test(GuidedMatteTests)
compositor_test(SubjectRemovalTests)
compositor_test(SubjectRemovalModelTests)
compositor_test(SubjectRemovalFailureTests)
compositor_test(RemoveBackgroundTests)
compositor_test(SelectSubjectTests)
compositor_test(ObjectSelectionTests)
compositor_test(PSDRoundTripTests)
compositor_test(PSDVectorTests)
compositor_test(PSDReaderTests)
compositor_test(PSDReaderLayerTests)
compositor_test(PSDImportTests)
compositor_test(RawImporterTests)
compositor_test(RawDevelopTests)
compositor_test(LayerEffectsRendererTests)
compositor_test(LayerFlipTests)
compositor_test(LayerMergeTests)
compositor_test(PixelEditTests)
compositor_test(PixelInvertTests)
compositor_test(PixelMoveTests)
compositor_test(SelectionTests)
compositor_test(SelectionFeatherTests)
compositor_test(SelectionSourceTests)
compositor_test(SelectionEditTests)
compositor_test(SelectionSignalTests)
compositor_test(SelectMenuTests)
compositor_test(SelectionCanvasTests)
compositor_test(SelectionOverlayTests)
compositor_test(LassoControlsTests)
compositor_test(SelectionAmountSheetTests)
compositor_test(MagicWandTests)
compositor_test(SelectionClipboardTests)
compositor_test(SmartEditTests)
compositor_test(FilterSheetTests)
compositor_test(FilterControlsTests)
compositor_test(FloatingSelectionTests)
compositor_test(CropTests)
compositor_test(GuideTests)
compositor_test(GuideCanvasTests)
compositor_test(CropSessionTests)
compositor_test(CropCanvasTests)
compositor_test(CropControlsTests)
compositor_test(CropFailureTests)
compositor_test(CanvasSizeSheetTests)
compositor_test(ImageSizeSheetTests)
compositor_test(ProjectResizeTests)
compositor_test(JPEGExportSheetTests)
compositor_test(ProjectExportTests)
compositor_test(ProjectExportFailureTests)
compositor_test(ByteCountsTests)
compositor_test(ImageFileDropTests)
compositor_test(SliderSnapTests)
compositor_test(ProjectResizeFailureTests)