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
@@ -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();
|
||||
}
|
||||
@@ -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); });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 ¶ms = 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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user