Files
OmaPhoto/src/Document/CameraRaw+Session.cpp
T

307 lines
13 KiB
C++

#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include <QFutureWatcher>
#include <QPainter>
#include <QtConcurrent>
#include <cmath>
// Swift's CameraRaw.swift session extension: samples, drags, guides.
namespace {
// One pixel's premultiplied bytes, or none past the image.
std::optional<std::array<int, 4>> pixelAt(const QImage &image, QPointF pixel)
{
if (!(pixel.x() >= 0 && pixel.y() >= 0 && pixel.x() < image.width() && pixel.y() < image.height()))
return std::nullopt;
QImage one = BrushRaster::context(1, 1, false);
{
QPainter painter(&one);
BrushRaster::draw(image, QRectF(0, 0, 1, 1), painter, QRectF(std::floor(pixel.x()), std::floor(pixel.y()), 1, 1));
}
const uchar *bytes = one.constBits();
return std::array<int, 4>{bytes[0], bytes[1], bytes[2], bytes[3]};
}
std::array<double, 3> straight(const std::array<int, 4> &bytes)
{
return {std::min(1.0, bytes[0] / double(bytes[3])), std::min(1.0, bytes[1] / double(bytes[3])), std::min(1.0, bytes[2] / double(bytes[3]))};
}
double hueDegrees(double red, double green, double blue)
{
const double high = std::max({red, green, blue}), low = std::min({red, green, blue}), chroma = high - low;
if (!(chroma > 1e-6))
return 0;
const double hue = high == red ? (green - blue) / chroma : high == green ? 2 + (blue - red) / chroma : 4 + (red - green) / chroma;
const double degrees = hue * 60;
return degrees < 0 ? degrees + 360 : degrees;
}
}
void EditorSession::setCameraRawPanel(const CameraRawPanel &panel)
{
if (!m_filterEdit)
return;
m_filterEdit->rawPanel = panel;
notify();
}
void EditorSession::sampleCameraRawWhiteBalance(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.samplesWhiteBalance || m_filterEdit->committing
|| !m_document || !(point.x() >= 0 && point.y() >= 0 && point.x() < m_document->size().width() && point.y() < m_document->size().height()))
return;
const FilterEdit &edit = *m_filterEdit;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(edit.original.image(), edit.mapping.inverted().map(point));
if (!bytes || (*bytes)[3] == 0)
return;
const std::array<double, 3> rgb = straight(*bytes);
const std::optional<CameraRawSettings::Balance> solved = CameraRawSettings::neutralizeStraight(rgb[0], rgb[1], rgb[2]);
if (!solved)
return;
FilterSettings settings = edit.settings;
settings.cameraRaw.temperature = solved->temperature;
settings.cameraRaw.tint = solved->tint;
settings.cameraRaw.whiteBalance = CameraRawWhiteBalance::custom;
updateFilter(settings, edit.preview);
} catch (const ExportError &error) {
setBrushError(QString::fromUtf8(error.what()));
}
}
void EditorSession::applyCameraRawAutoWhiteBalance(std::function<void()> done)
{
const auto finish = [this, done] {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
};
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || m_filterEdit->committing) {
finish();
return;
}
FilterSettings settings = m_filterEdit->settings;
settings.cameraRaw.whiteBalance = CameraRawWhiteBalance::automatic;
updateFilter(settings, m_filterEdit->preview);
// A worker takes the average; it lands if Auto stands.
auto *watcher = new QFutureWatcher<std::optional<CameraRawSettings::Balance>>(this);
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, id = m_filterEdit->id, finish] {
watcher->deleteLater();
const std::optional<CameraRawSettings::Balance> solved = watcher->result();
if (solved && m_filterEdit && m_filterEdit->id == id && !m_filterEdit->committing
&& m_filterEdit->settings.cameraRaw.whiteBalance == CameraRawWhiteBalance::automatic) {
FilterSettings value = m_filterEdit->settings;
value.cameraRaw.temperature = solved->temperature;
value.cameraRaw.tint = solved->tint;
updateFilter(value, m_filterEdit->preview);
}
finish();
});
// A painted layer flattens here; a failure scans nothing.
watcher->setFuture(QtConcurrent::run([original = m_filterEdit->original]() -> std::optional<CameraRawSettings::Balance> {
try {
return CameraRawSettings::autoBalance(original.image());
} catch (const ExportError &error) {
qCWarning(lcApp) << "Camera Raw's Auto found no memory:" << error.what();
return std::nullopt;
}
}));
}
void EditorSession::sampleCameraRawDefringe(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.samplesDefringe || m_filterEdit->committing
|| !m_document || !(point.x() >= 0 && point.y() >= 0 && point.x() < m_document->size().width() && point.y() < m_document->size().height()))
return;
const FilterEdit &edit = *m_filterEdit;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(edit.original.image(), edit.mapping.inverted().map(point));
if (!bytes || (*bytes)[3] == 0)
return;
const std::array<double, 3> rgb = straight(*bytes);
const double hue = hueDegrees(rgb[0], rgb[1], rgb[2]);
FilterSettings settings = edit.settings;
CameraRawOpticsSettings &optics = settings.cameraRaw.optics;
const double purpleCenter = 290, greenCenter = 90, span = 25;
if (std::abs(hue - purpleCenter) < std::abs(hue - greenCenter)) {
optics.purpleHueLow = hue - span;
optics.purpleHueHigh = hue + span;
if (optics.purpleAmount == 0)
optics.purpleAmount = 50;
} else {
optics.greenHueLow = hue - span;
optics.greenHueHigh = hue + span;
if (optics.greenAmount == 0)
optics.greenAmount = 50;
}
updateFilter(settings, edit.preview);
} catch (const ExportError &error) {
setBrushError(QString::fromUtf8(error.what()));
}
}
void EditorSession::updateCameraRawReadout(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw)
return;
FilterEdit &edit = *m_filterEdit;
const QImage &image = edit.preparedPreview ? *edit.preparedPreview : edit.previewSource;
std::optional<std::array<int, 3>> readout;
try {
const std::optional<std::array<int, 4>> bytes = pixelAt(image, edit.previewMapping.inverted().map(point));
if (bytes && (*bytes)[3] > 0) {
const int alpha = (*bytes)[3];
readout = std::array<int, 3>{};
for (size_t channel = 0; channel < 3; ++channel)
(*readout)[channel] = std::min(255, ((*bytes)[channel] * 255 + alpha / 2) / alpha);
}
} catch (const ExportError &error) {
// Swift's `try?`: no memory keeps the last readout.
qCWarning(lcApp) << "Camera Raw's readout found no memory:" << error.what();
return;
}
if (edit.rawPanel.readout == readout)
return;
edit.rawPanel.readout = readout;
notify();
}
void EditorSession::beginCameraRawDrag(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw)
return;
const std::optional<CameraRawSample> sample = cameraRawSample(point);
if (!sample)
return;
m_filterEdit->rawPanel.drag = CameraRawDrag{point.y(), m_filterEdit->settings.cameraRaw, sample->tone, sample->hue};
notify();
}
void EditorSession::dragCameraRaw(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.drag)
return;
const FilterEdit &edit = *m_filterEdit;
const CameraRawDrag &drag = *edit.rawPanel.drag;
const double delta = (drag.startY - point.y()) * 0.35;
FilterSettings settings = edit.settings;
settings.cameraRaw = drag.settings;
const CameraRawPanel &panel = edit.rawPanel;
if (panel.targetsCurve) {
if (panel.curvePage == CameraRawCurvePage::parametric) {
CameraRawCurveSettings before = drag.settings.curve;
settings.cameraRaw.curve.region(drag.tone) = std::min(100.0, std::max(-100.0, before.region(drag.tone) + delta));
} else {
settings.cameraRaw.curve = drag.settings.curve.nudged(panel.pointChannel, drag.tone, delta / 100);
}
} else if (panel.targetsMixer) {
const std::array<double, 8> weights = CameraRawMixerSettings::weights(drag.hue);
std::array<double, 8> &target = panel.mixerTab == CameraRawMixerTab::hue ? settings.cameraRaw.mixer.hue
: panel.mixerTab == CameraRawMixerTab::saturation ? settings.cameraRaw.mixer.saturation
: settings.cameraRaw.mixer.luminance;
const std::array<double, 8> &from = panel.mixerTab == CameraRawMixerTab::hue ? drag.settings.mixer.hue
: panel.mixerTab == CameraRawMixerTab::saturation ? drag.settings.mixer.saturation
: drag.settings.mixer.luminance;
for (size_t index = 0; index < 8; ++index)
if (weights[index] > 0)
target[index] = std::min(100.0, std::max(-100.0, from[index] + delta * weights[index]));
}
updateFilter(settings, edit.preview);
}
void EditorSession::sampleCameraRawPointColor(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.samplesPointColor)
return;
const std::optional<CameraRawSample> sample = cameraRawSample(point);
if (!sample)
return;
FilterEdit &edit = *m_filterEdit;
FilterSettings settings = edit.settings;
std::vector<CameraRawPointColor> &points = settings.cameraRaw.mixer.points;
CameraRawPointColor color{.hue = sample->hue, .saturation = sample->saturation, .luminance = sample->luminance};
const int index = edit.rawPanel.pointIndex;
if (index >= 0 && size_t(index) < points.size()) {
color.hueShift = points[size_t(index)].hueShift;
color.saturationShift = points[size_t(index)].saturationShift;
color.luminanceShift = points[size_t(index)].luminanceShift;
points[size_t(index)] = color;
} else if (points.size() < 8) {
points.push_back(color);
edit.rawPanel.pointIndex = int(points.size()) - 1;
}
updateFilter(settings, edit.preview);
}
void EditorSession::beginCameraRawGeometryGuide(QPointF point)
{
if (!m_filterEdit || m_filterEdit->kind != FilterKind::cameraRaw || !m_filterEdit->rawPanel.drawingGeometryGuide)
return;
const std::optional<QPointF> normalized = cameraRawNormalizedPoint(point);
if (!normalized)
return;
m_filterEdit->rawPanel.guideDraft = std::pair(*normalized, *normalized);
notify();
}
void EditorSession::continueCameraRawGeometryGuide(QPointF point)
{
if (!m_filterEdit || !m_filterEdit->rawPanel.drawingGeometryGuide || !m_filterEdit->rawPanel.guideDraft)
return;
const std::optional<QPointF> normalized = cameraRawNormalizedPoint(point);
if (!normalized)
return;
m_filterEdit->rawPanel.guideDraft->second = *normalized;
notify();
}
void EditorSession::commitCameraRawGeometryGuide()
{
if (!m_filterEdit || !m_filterEdit->rawPanel.guideDraft)
return;
FilterEdit &edit = *m_filterEdit;
const auto [start, end] = *std::exchange(edit.rawPanel.guideDraft, std::nullopt);
FilterSettings settings = edit.settings;
settings.cameraRaw.geometry.guides.push_back({start.x(), start.y(), end.x(), end.y()});
settings.cameraRaw.geometry.upright = CameraRawUprightMode::guided;
updateFilter(settings, edit.preview);
}
std::optional<QPointF> EditorSession::cameraRawNormalizedPoint(QPointF point) const
{
if (!m_filterEdit)
return std::nullopt;
const QImage &image = m_filterEdit->previewSource;
const QPointF pixel = m_filterEdit->previewMapping.inverted().map(point);
if (!(pixel.x() >= 0 && pixel.y() >= 0 && pixel.x() < image.width() && pixel.y() < image.height()))
return std::nullopt;
return QPointF(pixel.x() / image.width(), pixel.y() / image.height());
}
std::optional<EditorSession::CameraRawSample> EditorSession::cameraRawSample(QPointF point) const
{
if (!m_filterEdit)
return std::nullopt;
const QImage &image = m_filterEdit->preparedPreview ? *m_filterEdit->preparedPreview : m_filterEdit->previewSource;
std::optional<std::array<int, 4>> bytes;
try {
bytes = pixelAt(image, m_filterEdit->previewMapping.inverted().map(point));
} catch (const ExportError &error) {
// Swift's `try?`: no memory, no sample.
qCWarning(lcApp) << "Camera Raw's sample found no memory:" << error.what();
return std::nullopt;
}
if (!bytes || (*bytes)[3] == 0)
return std::nullopt;
const auto [red, green, blue] = straight(*bytes);
const double high = std::max({red, green, blue}), low = std::min({red, green, blue}), chroma = high - low;
double hue = 0;
if (chroma > 1e-6) {
hue = (high == red ? (green - blue) / chroma : high == green ? 2 + (blue - red) / chroma : 4 + (red - green) / chroma) / 6;
if (hue < 0)
hue += 1;
}
return CameraRawSample{0.2126 * red + 0.7152 * green + 0.0722 * blue, hue * 360, high == 0 ? 0 : chroma / high, (high + low) / 2};
}