Camera RAW import through LibRaw with a develop sheet (12.6a)
This commit is contained in:
1 parent
374ba8ec73
commit
5a2fa91bc7
29 files changed
+1794
-257
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user