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

+29 -1
View File
@@ -4,6 +4,7 @@
#include "UI/FilterSheet.h"
#include "UI/HueSaturationSheet.h"
#include "UI/LevelsSheet.h"
#include "UI/RawDevelopSheet.h"
#include <QFileDialog>
#include <QVBoxLayout>
#include <QMessageBox>
@@ -85,7 +86,7 @@ void ContentView::showImporter()
m_importer = new QFileDialog(this);
m_importer->setAttribute(Qt::WA_DeleteOnClose);
m_importer->setFileMode(QFileDialog::ExistingFiles);
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd)"));
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd %1)").arg(RawImporter::globs().join(' ')));
connect(m_importer, &QDialog::finished, this, [this, panel = m_importer.data()](int result) {
// Ours no longer: the flag must not reject it again.
if (m_importer != panel)
@@ -160,3 +161,30 @@ void ContentView::showConversionSheet()
m_conversionSheet->layout()->addWidget(new PSDConversionSheet(*request, [this](bool confirmed) { m_session.finishConversion(confirmed); }, m_conversionSheet));
m_conversionSheet->open();
}
void ContentView::showRawDevelopSheet()
{
const std::optional<RawDevelopRequest> &request = m_session.rawDevelop();
if (!request) {
// Ours no longer, so the close answers nothing.
if (QDialog *shown = std::exchange(m_rawSheet, nullptr))
shown->reject();
return;
}
if (m_rawSheet)
return;
m_rawSheet = new QDialog(this);
m_rawSheet->setObjectName(QStringLiteral("rawDevelopSheet"));
m_rawSheet->setAttribute(Qt::WA_DeleteOnClose);
m_rawSheet->setWindowTitle(QStringLiteral("Develop RAW"));
auto *layout = new QVBoxLayout(m_rawSheet);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSizeConstraint(QLayout::SetFixedSize);
// Escape and the close button cancel, as Swift's dismissal.
connect(m_rawSheet, &QDialog::finished, this, [this, shown = m_rawSheet.data()] {
if (m_rawSheet == shown)
m_session.finishRawDevelop(std::nullopt);
});
layout->addWidget(new RawDevelopSheet(*request, [this](std::optional<RawDevelopSettings> settings) { m_session.finishRawDevelop(settings); }, m_rawSheet));
m_rawSheet->open();
}
+1
View File
@@ -399,6 +399,7 @@ void ContentView::synchronize()
: hint(m_session.tool(), iconKind(m_session.tool()), m_session.blurMode(), m_session.shapeKind()));
showImporter();
showConversionSheet();
showRawDevelopSheet();
showAlert(m_importAlert, QStringLiteral("Import couldn’t finish"), m_session.importError(), [this] { m_session.setImportError(std::nullopt); });
showAlert(m_brushAlert, QStringLiteral("Couldn’t paint"), m_session.brushError(), [this] { m_session.setBrushError(std::nullopt); });
showAlert(m_cropAlert, QStringLiteral("Couldn’t crop"), m_session.cropError(), [this] { m_session.setCropError(std::nullopt); });
+2
View File
@@ -53,6 +53,7 @@ private:
// Swift's onChange of levels: its panel opens and closes.
void synchronizePanels();
void showConversionSheet();
void showRawDevelopSheet();
ToolIconKind iconKind(NavigationTool tool) const;
void showHeader(NavigationTool tool);
void showWelcome(bool shown);
@@ -86,6 +87,7 @@ private:
// The Photoshop sheet and the request it shows.
QPointer<QDialog> m_conversionSheet;
std::optional<PSDConversionRequest> m_conversionShown;
QPointer<QDialog> m_rawSheet;
QPointer<QMessageBox> m_importAlert;
QPointer<QMessageBox> m_brushAlert;
QPointer<QMessageBox> m_cropAlert;
+57
View File
@@ -1,6 +1,8 @@
#include "Document/EditorSession.h"
#include "Document/ProjectWorkspace.h"
#include "Document/PixelAdjust.h"
#include "IO/PSD/PSDReader.h"
#include "IO/RawImporter.h"
#include "Logging.h"
#include <QtConcurrent>
@@ -105,6 +107,10 @@ void EditorSession::decodeNext()
}
}
const QUrl url = request.urls[m_importIndex];
if (url.isLocalFile() && RawImporter::matches(url.toLocalFile())) {
decodeRaw(url.toLocalFile(), 100'000'000 - usedPixels);
return;
}
if (url.isLocalFile() && PSDReader::matches(url.toLocalFile())) {
beginPSDReading(QStringLiteral("Open “%1”?").arg(url.fileName()), QStringLiteral("Import"));
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = 100'000'000 - usedPixels]() -> PhotoshopRead {
@@ -133,6 +139,57 @@ void EditorSession::decodeNext()
}));
}
void EditorSession::decodeRaw(const QString &path, qint64 remaining)
{
const QString name = QFileInfo(path).fileName();
const std::optional<QSize> size = RawImporter::pixelSize(path);
const auto refuse = [&](ImageImportError::Kind kind) {
m_importFailures << name + ": " + ImageImportError(kind).what();
m_importIndex += 1;
decodeNext();
};
if (!size)
return refuse(ImageImportError::Kind::unreadable);
if (size->width() > 30'000 || size->height() > 30'000 || qint64(size->width()) * size->height() > remaining)
return refuse(ImageImportError::Kind::tooLarge);
developRaw(path, [this, path, name](std::optional<RawDevelopSettings> settings) {
if (!settings) {
m_importIndex += 1;
decodeNext();
return;
}
// Seconds of work: a worker, so the window keeps drawing.
m_decoder.setFuture(QtConcurrent::run([path, name, settings = *settings]() -> Decoded {
try {
const QImage developed = RawImporter::Queue::shared().develop(path, settings, std::nullopt);
return {ImportedImage(developed, PixelAdjust::thumbnail(developed), ProjectTab::nameWithoutSuffix(path)), QString()};
} catch (const ImageImportError &error) {
return {std::nullopt, name + ": " + error.what()};
} catch (const ExportError &error) {
return {std::nullopt, name + ": " + error.what()};
}
}));
});
}
void EditorSession::developRaw(const QString &path, std::function<void(std::optional<RawDevelopSettings>)> answer)
{
m_rawAnswer = std::move(answer);
m_rawDevelop = RawDevelopRequest{path, RawImporter::asShot(path).value_or(RawDevelopSettings())};
notify();
}
void EditorSession::finishRawDevelop(std::optional<RawDevelopSettings> settings)
{
m_rawDevelop = std::nullopt;
notify();
// Frees the preview's decode on a worker, behind any preview.
(void)QtConcurrent::run([] { RawImporter::Queue::shared().release(); });
// The import resumes from the event loop, as a task.
if (const auto answer = std::exchange(m_rawAnswer, {}))
QMetaObject::invokeMethod(this, [answer, settings] { answer(settings); }, Qt::QueuedConnection);
}
void EditorSession::finishDecode()
{
const Decoded decoded = m_decoder.result();
+8
View File
@@ -17,6 +17,7 @@
#include "Document/EditorSession+Model.h"
#include "IO/ProjectStore.h"
#include "IO/PSD/PSDDocumentBuilder.h"
#include "IO/RawImporter.h"
#include "UI/PSDConversionSheet.h"
#include "Rendering/CanvasViewport.h"
#include "Rendering/EffectsPreviewCache.h"
@@ -70,6 +71,9 @@ public:
void setImportError(std::optional<QString> error);
bool showsConversionSheet() const { return m_showsConversionSheet; }
const std::optional<PSDConversionRequest> &conversionRequest() const { return m_conversionRequest; }
// The RAW file the develop sheet shows, while it shows.
const std::optional<RawDevelopRequest> &rawDevelop() const { return m_rawDevelop; }
void finishRawDevelop(std::optional<RawDevelopSettings> settings);
std::optional<QUuid> renamingLayerID() const { return m_renamingLayerID; }
void setRenamingLayerID(std::optional<QUuid> id);
@@ -242,6 +246,8 @@ public:
void finishPSDReading(const std::vector<PSDConversion> &conversions, std::function<void(bool)> answer);
void endPSDReading();
void finishConversion(bool confirmed);
void decodeRaw(const QString &path, qint64 remaining);
void developRaw(const QString &path, std::function<void(std::optional<RawDevelopSettings>)> answer);
void insertPhotoshop(const PSDImport &imported, const QString &named, std::optional<QPointF> centeredAt = std::nullopt);
void createDocument(int width, int height, bool emptyLayer = false);
void fit();
@@ -386,6 +392,8 @@ private:
bool m_showsConversionSheet = false;
std::optional<PSDConversionRequest> m_conversionRequest;
std::function<void(bool)> m_conversionAnswer;
std::optional<RawDevelopRequest> m_rawDevelop;
std::function<void(std::optional<RawDevelopSettings>)> m_rawAnswer;
// Cancel pressed while the file was still read.
bool m_conversionCancelled = false;
std::optional<QString> m_brushError;
+5 -1
View File
@@ -1,5 +1,6 @@
#include "IO/ImageFileDrop.h"
#include "Document/ProjectWorkspace.h"
#include "IO/RawImporter.h"
#include "Logging.h"
#include <QDir>
#include <QFile>
@@ -7,7 +8,7 @@
#include <algorithm>
namespace {
// Swift's order: PNG, JPEG, HEIC, TIFF, Photoshop, then any picture.
// Swift's order: PNG, JPEG, HEIC, TIFF, Photoshop, RAW, then any.
QStringList pictureFormats(const QMimeData &data)
{
QStringList formats;
@@ -15,6 +16,9 @@ QStringList pictureFormats(const QMimeData &data)
QStringLiteral("image/vnd.adobe.photoshop")})
if (data.hasFormat(format))
formats << format;
for (const QString &format : data.formats())
if (RawImporter::isRawType(format) && !formats.contains(format))
formats << format;
for (const QString &format : data.formats())
if (format.startsWith(QLatin1String("image/")) && !formats.contains(format))
formats << format;
+2
View File
@@ -21,6 +21,8 @@ QString description(ImageImportError::Kind kind)
return QStringLiteral("Choose a JPEG, PNG, HEIC, TIFF, or Photoshop (PSD) file.");
case ImageImportError::Kind::tooLarge:
return QStringLiteral("This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit.");
case ImageImportError::Kind::whiteBalance:
return QStringLiteral("This camera can’t record that white balance. Move Temperature or Tint back toward the shot.");
}
Q_UNREACHABLE();
}
+2 -1
View File
@@ -38,7 +38,8 @@ private:
class ImageImportError : public std::runtime_error {
public:
enum class Kind { unreadable, unsupported, tooLarge };
// whiteBalance is Linux's: LibRaw cannot take every white.
enum class Kind { unreadable, unsupported, tooLarge, whiteBalance };
explicit ImageImportError(Kind kind);
Kind kind;
};
+336
View File
@@ -0,0 +1,336 @@
#include "IO/RawImporter.h"
#include "IO/ImageExporter.h"
#include "IO/ImageImporter.h"
#include "Logging.h"
#include "Rendering/PoolMap.h"
#include <QColorSpace>
#include <QFile>
#include <QMimeDatabase>
#include <algorithm>
#include <array>
#include <cmath>
#include <numeric>
#include <libraw.h>
namespace {
[[noreturn]] void refuse(const QString &path, const char *step, int code)
{
qCWarning(lcIO).noquote() << "cannot develop" << path << step << libraw_strerror(code);
throw ImageImportError(ImageImportError::Kind::unreadable);
}
// The file opened and identified; unpacked when asked.
std::shared_ptr<LibRaw> opened(const QString &path, bool unpack)
{
auto raw = std::make_shared<LibRaw>();
if (const int code = raw->open_file(QFile::encodeName(path).constData()); code != LIBRAW_SUCCESS)
refuse(path, "open", code);
if (unpack)
if (const int code = raw->unpack(); code != LIBRAW_SUCCESS)
refuse(path, "unpack", code);
return raw;
}
QPointF uv(QPointF xy)
{
const double d = -2 * xy.x() + 12 * xy.y() + 3;
return QPointF(4 * xy.x() / d, 6 * xy.y() / d);
}
QPointF xyOf(QPointF uv)
{
const double d = 2 * uv.x() - 8 * uv.y() + 4;
return QPointF(3 * uv.x() / d, 2 * uv.y() / d);
}
// The locus's unit normal in uv, pointing greener.
QPointF normal(double kelvin)
{
const QPointF tangent = uv(RawImporter::planckian(kelvin + 1)) - uv(RawImporter::planckian(kelvin - 1));
const double length = std::hypot(tangent.x(), tangent.y());
QPointF up(-tangent.y() / length, tangent.x() / length);
return up.y() < 0 ? -up : up;
}
using Matrix = std::array<std::array<double, 3>, 3>;
// XYZ and linear sRGB under D65, both ways.
constexpr Matrix xyzToSrgb{{{3.2404542, -1.5371385, -0.4985314}, {-0.9692660, 1.8760108, 0.0415560}, {0.0556434, -0.2040259, 1.0572252}}};
constexpr Matrix srgbToXyz{{{0.4124564, 0.3575761, 0.1804375}, {0.2126729, 0.7151522, 0.0721750}, {0.0193339, 0.1191920, 0.9503041}}};
std::array<double, 3> times(const Matrix &m, const std::array<double, 3> &v)
{
std::array<double, 3> out{};
for (size_t row = 0; row < 3; ++row)
out[row] = m[row][0] * v[0] + m[row][1] * v[1] + m[row][2] * v[2];
return out;
}
Matrix inverse(const Matrix &m)
{
const double det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
Matrix out;
for (size_t i = 0; i < 3; ++i)
for (size_t j = 0; j < 3; ++j) {
const size_t r0 = (j + 1) % 3, r1 = (j + 2) % 3, c0 = (i + 1) % 3, c1 = (i + 2) % 3;
out[i][j] = (m[r0][c0] * m[r1][c1] - m[r0][c1] * m[r1][c0]) / det;
}
return out;
}
// LibRaw's camera → sRGB matrix, filled for every camera.
Matrix rgbCam(const libraw_colordata_t &color)
{
Matrix m;
for (size_t row = 0; row < 3; ++row)
for (size_t column = 0; column < 3; ++column)
m[row][column] = color.rgb_cam[row][column];
return m;
}
// The raw camera response to a white: dcraw's matrices undone.
std::array<double, 3> response(const libraw_colordata_t &color, QPointF xy)
{
const std::array<double, 3> xyz{xy.x() / xy.y(), 1, (1 - xy.x() - xy.y()) / xy.y()};
std::array<double, 3> camera = times(inverse(rgbCam(color)), times(xyzToSrgb, xyz));
for (size_t channel = 0; channel < 3; ++channel)
camera[channel] /= color.pre_mul[channel];
return camera;
}
// The white a raw camera neutral stands for.
QPointF whiteOf(const libraw_colordata_t &color, std::array<double, 3> neutral)
{
for (size_t channel = 0; channel < 3; ++channel)
neutral[channel] *= color.pre_mul[channel];
const std::array<double, 3> xyz = times(srgbToXyz, times(rgbCam(color), neutral));
const double sum = xyz[0] + xyz[1] + xyz[2];
return QPointF(xyz[0] / sum, xyz[1] / sum);
}
// Three colours only: dcraw's matrices hold no fourth to invert.
void requireThreeColours(const LibRaw &raw, const QString &path)
{
if (raw.imgdata.idata.colors == 3)
return;
qCWarning(lcIO).noquote() << "cannot develop" << path << "a sensor of" << raw.imgdata.idata.colors << "colours";
throw ImageImportError(ImageImportError::Kind::unsupported);
}
// Green-anchored multipliers; the smallest, which LibRaw divides out.
double setWhite(LibRaw &raw, const QString &path, const RawDevelopSettings &settings)
{
// A process overwrites `color`; unpack's copy stays the camera's.
const std::array<double, 3> camera = response(raw.imgdata.rawdata.color, RawImporter::whitePoint(settings.temperature, settings.tint));
if (*std::min_element(camera.begin(), camera.end()) <= 0) {
qCWarning(lcIO).noquote() << "cannot develop" << path << "at" << settings.temperature << "K, tint" << settings.tint << ": the camera sees no such white";
throw ImageImportError(ImageImportError::Kind::whiteBalance);
}
float *const multipliers = raw.imgdata.params.user_mul;
for (size_t channel = 0; channel < 3; ++channel)
multipliers[channel] = float(camera[1] / camera[channel]);
return *std::min_element(multipliers, multipliers + 3);
}
double srgb(double linear)
{
return linear <= 0.0031308 ? 12.92 * linear : 1.055 * std::pow(linear, 1 / 2.4) - 0.055;
}
// LibRaw's default curve: BT.709, gamma 0.45 and slope 4.5.
double bt709(double linear)
{
return linear < 0.018 ? 4.5 * linear : 1.099 * std::pow(linear, 0.45) - 0.099;
}
QImage render(LibRaw &raw, const QString &path, const RawDevelopSettings &settings, std::optional<double> limit)
{
requireThreeColours(raw, path);
libraw_output_params_t &params = raw.imgdata.params;
// Linear sRGB in 16 bits: a power of one.
params.output_color = 1;
params.output_bps = 16;
params.gamm[0] = 1;
params.no_auto_bright = 1;
params.adjust_maximum_thr = 0;
params.use_camera_wb = 0;
params.use_auto_wb = 0;
// LibRaw clips at white in camera space: highlights stay neutral.
const double smallest = setWhite(raw, path, settings);
// A half-size process leaves `sizes` halved; unpack's copy stays.
const int longest = std::max(raw.imgdata.rawdata.sizes.width, raw.imgdata.rawdata.sizes.height);
params.half_size = limit && longest > *limit ? 1 : 0;
if (const int code = raw.dcraw_process(); code != LIBRAW_SUCCESS)
refuse(path, "process", code);
int code = LIBRAW_SUCCESS;
const std::unique_ptr<libraw_processed_image_t, void (*)(libraw_processed_image_t *)> made(raw.dcraw_make_mem_image(&code), LibRaw::dcraw_clear_mem);
if (!made)
refuse(path, "output", code);
QImage image(made->width, made->height, QImage::Format_RGBA8888_Premultiplied);
if (image.isNull())
throw ExportError(ExportError::Kind::render);
image.setColorSpace(QColorSpace::SRgb);
const double gain = std::pow(2.0, double(settings.exposure)) * smallest;
const auto *samples = reinterpret_cast<const quint16 *>(made->data);
uchar *const bits = image.bits();
const qsizetype stride = image.bytesPerLine();
std::vector<int> rows(size_t(image.height()));
std::iota(rows.begin(), rows.end(), 0);
PoolMap::blocking(rows, [&](int y) {
for (int x = 0; x < image.width(); ++x) {
uchar *out = bits + y * stride + x * 4;
for (int channel = 0; channel < 3; ++channel) {
const double linear = std::min(samples[(qsizetype(y) * image.width() + x) * 3 + channel] / 65535.0 * gain, 1.0);
const double flat = srgb(linear);
out[channel] = uchar(std::lround(255 * (flat + settings.boost * (bt709(linear) - flat))));
}
out[3] = 255;
}
});
if (!limit || std::max(image.width(), image.height()) <= *limit)
return image;
const QImage fitted = image.scaled(QSize(int(*limit), int(*limit)), Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (fitted.isNull())
throw ExportError(ExportError::Kind::render);
return fitted;
}
}
bool RawDevelopSettings::isAsShot() const
{
return exposure == 0 && boost == 1 && temperature == asShotTemperature && tint == asShotTint;
}
void RawDevelopSettings::reset()
{
exposure = 0;
boost = 1;
temperature = asShotTemperature;
tint = asShotTint;
}
bool RawImporter::matches(const QString &path)
{
return QMimeDatabase().mimeTypeForFile(path, QMimeDatabase::MatchExtension).inherits(QStringLiteral("image/x-dcraw"));
}
bool RawImporter::isRawType(const QString &mimeName)
{
return QMimeDatabase().mimeTypeForName(mimeName).inherits(QStringLiteral("image/x-dcraw"));
}
QStringList RawImporter::globs()
{
QStringList globs;
for (const QMimeType &type : QMimeDatabase().allMimeTypes())
if (type.inherits(QStringLiteral("image/x-dcraw")))
globs << type.globPatterns();
globs.sort();
return globs;
}
QPointF RawImporter::planckian(double kelvin)
{
const double t = std::clamp(kelvin, 1667.0, 25000.0);
const double x = t <= 4000 ? -0.2661239e9 / (t * t * t) - 0.2343589e6 / (t * t) + 0.8776956e3 / t + 0.179910
: -3.0258469e9 / (t * t * t) + 2.1070379e6 / (t * t) + 0.2226347e3 / t + 0.240390;
const double y = t <= 2222 ? -1.1063814 * x * x * x - 1.34811020 * x * x + 2.18555832 * x - 0.20219683
: t <= 4000 ? -0.9549476 * x * x * x - 1.37418593 * x * x + 2.09137015 * x - 0.16748867
: 3.0817580 * x * x * x - 5.87338670 * x * x + 3.75112997 * x - 0.37001483;
return QPointF(x, y);
}
QPointF RawImporter::whitePoint(double kelvin, double tint)
{
// Adobe's tint: 3000 per Duv, positive a greener white.
return xyOf(uv(planckian(kelvin)) + normal(kelvin) * (tint / 3000));
}
std::pair<double, double> RawImporter::temperatureOf(QPointF xy)
{
const QPointF target = uv(xy);
const auto distance = [&](double mired) {
const QPointF on = uv(planckian(1e6 / mired));
return std::hypot(target.x() - on.x(), target.y() - on.y());
};
// Coarse steps in mireds, then golden sections round the best.
double best = 40;
for (double mired = 40; mired <= 600; mired += 1)
if (distance(mired) < distance(best))
best = mired;
double low = best - 1, high = best + 1;
const double ratio = (std::sqrt(5.0) - 1) / 2;
for (int step = 0; step < 60; ++step) {
const double a = high - ratio * (high - low), b = low + ratio * (high - low);
if (distance(a) < distance(b))
high = b;
else
low = a;
}
const double kelvin = 1e6 / ((low + high) / 2);
const QPointF offset = target - uv(planckian(kelvin));
const QPointF up = normal(kelvin);
return {kelvin, 3000 * (offset.x() * up.x() + offset.y() * up.y())};
}
std::optional<RawDevelopSettings> RawImporter::asShot(const QString &path)
{
try {
const std::shared_ptr<LibRaw> raw = opened(path, false);
const libraw_colordata_t &color = raw->imgdata.color;
requireThreeColours(*raw, path);
// Without the camera's reading, dcraw's daylight.
const float *shot = color.cam_mul[0] > 0 ? color.cam_mul : color.pre_mul;
const auto [kelvin, tint] = temperatureOf(whiteOf(color, {1.0 / shot[0], 1.0 / shot[1], 1.0 / shot[2]}));
RawDevelopSettings settings;
settings.temperature = settings.asShotTemperature = float(kelvin);
settings.tint = settings.asShotTint = float(tint);
return settings;
} catch (const ImageImportError &) {
return std::nullopt;
}
}
QImage RawImporter::develop(const QString &path, const RawDevelopSettings &settings, std::optional<double> limit)
{
const std::shared_ptr<LibRaw> raw = opened(path, true);
return render(*raw, path, settings, limit);
}
std::optional<QSize> RawImporter::pixelSize(const QString &path)
{
try {
const std::shared_ptr<LibRaw> raw = opened(path, false);
// It fails only when called before the file is identified.
raw->adjust_sizes_info_only();
return QSize(raw->imgdata.sizes.iwidth, raw->imgdata.sizes.iheight);
} catch (const ImageImportError &) {
return std::nullopt;
}
}
RawImporter::Queue &RawImporter::Queue::shared()
{
static Queue queue;
return queue;
}
QImage RawImporter::Queue::develop(const QString &path, const RawDevelopSettings &settings, std::optional<double> limit)
{
const std::lock_guard lock(m_mutex);
if (!limit)
return RawImporter::develop(path, settings, std::nullopt);
// One file's unpacked frame is kept; a move only processes.
if (!m_cached || m_path != path) {
m_cached = opened(path, true);
m_path = path;
}
return render(*m_cached, path, settings, limit);
}
void RawImporter::Queue::release()
{
const std::lock_guard lock(m_mutex);
m_cached.reset();
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <QImage>
#include <QPointF>
#include <QSize>
#include <QString>
#include <QStringList>
#include <memory>
#include <mutex>
#include <optional>
// What the camera recorded, before the look was decided.
struct RawDevelopSettings {
// Stops of exposure either side of what was recorded.
float exposure = 0;
// White balance in Kelvin, from the camera's own reading.
float temperature = 5000;
// Green–magenta balance, from the camera's own reading.
float tint = 0;
// The tone curve: 1 LibRaw's full curve, 0 flat.
float boost = 1;
// The camera's own choice, where Reset returns.
float asShotTemperature = 5000;
float asShotTint = 0;
bool isAsShot() const;
void reset();
friend bool operator==(const RawDevelopSettings &, const RawDevelopSettings &) = default;
};
// Swift's tuple: the file under the sheet, where it starts.
struct RawDevelopRequest {
QString path;
RawDevelopSettings settings;
};
class LibRaw;
// Swift's RawImporter: camera RAW developed through LibRaw.
namespace RawImporter {
// Every camera RAW the system names, by extension.
bool matches(const QString &path);
// The same by MIME name, for dropped pictures.
bool isRawType(const QString &mimeName);
// Their file patterns, for the importer's filter.
QStringList globs();
// The camera's white balance, where the sliders start.
std::optional<RawDevelopSettings> asShot(const QString &path);
// The developed image; `limit` caps the preview's long edge.
QImage develop(const QString &path, const RawDevelopSettings &settings, std::optional<double> limit = std::nullopt);
// The frame's size, oriented, without developing it.
std::optional<QSize> pixelSize(const QString &path);
// Planckian chromaticity, Kim et al., 1667–25000 K.
QPointF planckian(double kelvin);
// A white of this temperature and tint, CIE xy.
QPointF whitePoint(double kelvin, double tint);
// The temperature and tint of a white, the inverse.
std::pair<double, double> temperatureOf(QPointF xy);
// Swift's actor: one develop at a time, decode kept.
class Queue {
public:
static Queue &shared();
QImage develop(const QString &path, const RawDevelopSettings &settings, std::optional<double> limit);
// Lets go of the decoded frame when the sheet closes.
void release();
private:
std::mutex m_mutex;
QString m_path;
std::shared_ptr<LibRaw> m_cached;
};
}
+188
View File
@@ -0,0 +1,188 @@
#include "UI/RawDevelopSheet.h"
#include "IO/ImageExporter.h"
#include "IO/ImageImporter.h"
#include "Logging.h"
#include <QFileInfo>
#include <QFutureWatcher>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QProgressBar>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSlider>
#include <QTimer>
#include <QVBoxLayout>
#include <QtConcurrent>
#include <cmath>
// Swift's ZStack: 35% black, the fitted preview, rounded 6.
class RawDevelopSheet::Preview : public QWidget {
public:
using QWidget::QWidget;
QImage image;
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
QPainterPath plate;
plate.addRoundedRect(QRectF(rect()), 6, 6);
painter.fillPath(plate, QColor::fromRgbF(0, 0, 0, 0.35f));
if (image.isNull())
return;
QRectF target(QPointF(), QSizeF(image.size()).scaled(QSizeF(size()), Qt::KeepAspectRatio));
target.moveCenter(QRectF(rect()).center());
QPainterPath clip;
clip.addRoundedRect(target, 6, 6);
painter.setClipPath(clip);
painter.drawImage(target, image);
}
};
RawDevelopSheet::RawDevelopSheet(RawDevelopRequest request, std::function<void(std::optional<RawDevelopSettings>)> finish, QWidget *parent)
: QWidget(parent), m_path(std::move(request.path)), m_finish(std::move(finish)), m_settings(request.settings), m_wait(new QTimer(this)),
m_preview(new Preview(this)), m_spinner(new QProgressBar(m_preview)), m_reset(new QPushButton(QStringLiteral("Reset"), this))
{
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(16);
column->setSizeConstraint(QLayout::SetFixedSize);
auto *title = new QLabel(QStringLiteral("Develop “%1”").arg(QFileInfo(m_path).fileName()), this);
title->setTextFormat(Qt::PlainText);
QFont font = title->font();
font.setPixelSize(17);
font.setBold(true);
title->setFont(font);
column->addWidget(title);
m_preview->setObjectName(QStringLiteral("rawPreview"));
m_preview->setFixedSize(560, 340);
m_spinner->setObjectName(QStringLiteral("rawSpinner"));
m_spinner->setRange(0, 0);
m_spinner->setTextVisible(false);
m_spinner->setGeometry(272, 162, 16, 16);
column->addWidget(m_preview);
// Swift's sliders are continuous; these step finer than shown.
addRow(QStringLiteral("Exposure"), &RawDevelopSettings::exposure, -3, 3, 100, 2, QStringLiteral(" EV"));
addRow(QStringLiteral("Temperature"), &RawDevelopSettings::temperature, 2000, 12000, 1, 0, QStringLiteral(" K"));
addRow(QStringLiteral("Tint"), &RawDevelopSettings::tint, -150, 150, 1, 0, QString());
addRow(QStringLiteral("Boost"), &RawDevelopSettings::boost, 0, 1, 1000, 2, QString());
for (const Row &row : m_rows) {
auto *line = new QHBoxLayout;
line->setSpacing(10);
auto *label = new QLabel(row.slider->accessibleName(), this);
label->setFixedWidth(90);
label->setBuddy(row.slider);
line->addWidget(label);
line->addWidget(row.slider);
line->addWidget(row.value);
column->addLayout(line);
}
auto *buttons = new QHBoxLayout;
m_reset->setObjectName(QStringLiteral("rawReset"));
m_reset->setAutoDefault(false);
connect(m_reset, &QPushButton::clicked, this, [this] {
m_settings.reset();
changed();
});
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("rawCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
auto *importButton = new QPushButton(QStringLiteral("Import"), this);
importButton->setObjectName(QStringLiteral("rawImport"));
importButton->setDefault(true);
connect(importButton, &QPushButton::clicked, this, [this] { m_finish(m_settings); });
buttons->addWidget(m_reset);
buttons->addStretch(1);
buttons->addWidget(cancel);
buttons->addWidget(importButton);
column->addLayout(buttons);
// The first preview starts at once; slider bursts coalesce.
m_wait->setObjectName(QStringLiteral("rawWait"));
m_wait->setSingleShot(true);
m_wait->setTimerType(Qt::PreciseTimer);
connect(m_wait, &QTimer::timeout, this, &RawDevelopSheet::develop);
m_wait->setInterval(60);
QTimer::singleShot(0, this, &RawDevelopSheet::develop);
synchronize();
}
// SwiftUI cancels the task when the sheet goes.
RawDevelopSheet::~RawDevelopSheet()
{
if (m_cancelled)
m_cancelled->store(true);
}
void RawDevelopSheet::addRow(const QString &title, float RawDevelopSettings::*field, double low, double high, double scale, int precision, const QString &unit)
{
auto *slider = new QSlider(Qt::Horizontal, this);
slider->setObjectName(QStringLiteral("raw") + title);
slider->setAccessibleName(title);
slider->setFixedWidth(300);
slider->setRange(int(std::lround(low * scale)), int(std::lround(high * scale)));
auto *value = new QLabel(this);
value->setObjectName(slider->objectName() + QStringLiteral("Value"));
value->setFixedWidth(80);
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
value->setForegroundRole(QPalette::PlaceholderText);
connect(slider, &QSlider::valueChanged, this, [this, field, scale](int step) {
m_settings.*field = float(step / scale);
changed();
});
m_rows.push_back({slider, value, field, scale, precision, unit});
}
// Swift's onChange: a new revision, developed after 60 ms.
void RawDevelopSheet::changed()
{
if (m_cancelled)
m_cancelled->store(true);
m_wait->start();
synchronize();
}
void RawDevelopSheet::develop()
{
m_cancelled = std::make_shared<std::atomic_bool>(false);
m_spinner->show();
auto *watcher = new QFutureWatcher<QImage>(this);
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, cancelled = m_cancelled] {
watcher->deleteLater();
if (cancelled->load())
return;
m_spinner->hide();
// A failed develop keeps the last preview, as Swift's.
if (const QImage image = watcher->future().takeResult(); !image.isNull()) {
m_preview->image = image;
m_preview->update();
}
});
watcher->setFuture(QtConcurrent::run([path = m_path, settings = m_settings] {
try {
return RawImporter::Queue::shared().develop(path, settings, 800);
} catch (const ImageImportError &error) {
qCWarning(lcIO) << "no RAW preview:" << error.what();
} catch (const ExportError &error) {
qCWarning(lcIO) << "no RAW preview:" << error.what();
}
return QImage();
}));
}
void RawDevelopSheet::synchronize()
{
for (const Row &row : m_rows) {
const QSignalBlocker blocker(row.slider);
const double value = m_settings.*row.field;
row.slider->setValue(int(std::lround(value * row.scale)));
row.value->setText(QString::asprintf("%.*f", row.precision, value) + row.unit);
}
m_reset->setEnabled(!m_settings.isAsShot());
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "IO/RawImporter.h"
#include <QWidget>
#include <atomic>
#include <functional>
#include <memory>
#include <optional>
#include <vector>
class QLabel;
class QProgressBar;
class QPushButton;
class QSlider;
class QTimer;
// Swift's RawDevelopSheet: a screen-sized preview under four sliders.
class RawDevelopSheet : public QWidget {
Q_OBJECT
public:
RawDevelopSheet(RawDevelopRequest request, std::function<void(std::optional<RawDevelopSettings>)> finish, QWidget *parent = nullptr);
~RawDevelopSheet() override;
private:
class Preview;
struct Row {
QSlider *slider;
QLabel *value;
float RawDevelopSettings::*field;
double scale;
int precision;
QString unit;
};
void addRow(const QString &title, float RawDevelopSettings::*field, double low, double high, double scale, int precision, const QString &unit);
void changed();
void develop();
void synchronize();
const QString m_path;
const std::function<void(std::optional<RawDevelopSettings>)> m_finish;
RawDevelopSettings m_settings;
// The newest develop's flag, Swift's task cancellation.
std::shared_ptr<std::atomic_bool> m_cancelled;
QTimer *const m_wait;
Preview *const m_preview;
QProgressBar *const m_spinner;
std::vector<Row> m_rows;
QPushButton *const m_reset;
};