Files
OmaPhoto/src/Document/Levels.cpp
T

371 lines
13 KiB
C++

#include "Document/Levels.h"
#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include "Rendering/RasterSnapshot.h"
#include <QPainter>
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <vector>
extern "C" {
#include "LevelsPixels.h"
}
QString rawValue(LevelsChannel channel)
{
switch (channel) {
case LevelsChannel::rgb: return QStringLiteral("RGB");
case LevelsChannel::red: return QStringLiteral("Red");
case LevelsChannel::green: return QStringLiteral("Green");
case LevelsChannel::blue: return QStringLiteral("Blue");
}
throw std::logic_error("no such channel");
}
namespace {
double clamped(double value, double low, double high, double fallback)
{
return std::isfinite(value) ? std::min(high, std::max(low, value)) : fallback;
}
}
LevelRange LevelRange::normalized() const
{
LevelRange result = *this;
result.black = clamped(black, 0, 254, 0);
result.white = clamped(white, result.black + 1, 255, 255);
result.gamma = clamped(gamma, 0.1, 9.99, 1);
result.outputBlack = clamped(outputBlack, 0, 255, 0);
result.outputWhite = clamped(outputWhite, 0, 255, 255);
return result;
}
double LevelRange::apply(double value) const
{
const LevelRange s = normalized();
const double input = std::min(1.0, std::max(0.0, (value * 255 - s.black) / (s.white - s.black)));
return (s.outputBlack + std::pow(input, 1 / s.gamma) * (s.outputWhite - s.outputBlack)) / 255;
}
LevelRange LevelsSettings::current() const
{
return ranges[size_t(channel)];
}
void LevelsSettings::setCurrent(const LevelRange &range)
{
ranges[size_t(channel)] = range.normalized();
}
bool LevelsSettings::isIdentity() const
{
return std::all_of(ranges.begin(), ranges.end(), [](const LevelRange &range) { return range.normalized() == LevelRange(); });
}
double LevelsSettings::apply(double value, LevelsChannel channel) const
{
return ranges[0].apply(ranges[size_t(channel)].apply(value));
}
double LevelsHistogramDisplay::scale(const std::array<double, 256> &bins)
{
const auto counted = [](double bin) { return std::isfinite(bin) && bin > 0; };
double peak = 0;
for (const double bin : bins) {
if (counted(bin))
peak = std::max(peak, bin);
}
// The ends and lone spikes cap at four typical peaks.
std::vector<double> interior;
std::copy_if(bins.begin() + 1, bins.end() - 1, std::back_inserter(interior), counted);
if (interior.empty())
return peak;
std::sort(interior.begin(), interior.end());
const double typicalPeak = interior[size_t(double(interior.size() - 1) * 0.95)];
return std::min(peak, typicalPeak * 4);
}
namespace {
// The job's image in a premultiplied context, rows packed.
QImage drawn(const QImage &image)
{
QImage context = BrushRaster::context(image.width(), image.height(), false);
QPainter painter(&context);
BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter);
return context;
}
}
QImage LevelsFilter::run(const LevelsJob &job)
{
if (job.settings.isIdentity())
return job.image;
QImage context = BrushRaster::copy(job.image);
// On the stack: a heap failure would escape the catch.
std::array<float, 3 * 256> tables;
for (size_t channel = 0; channel < 3; ++channel) {
for (size_t value = 0; value <= 255; ++value)
tables[channel * 256 + value] = float(job.settings.apply(double(value) / 255, allLevelsChannels[channel + 1]));
}
// The kernel unpremultiplies edges itself; bands use every core.
uchar *const pixels = context.bits();
BrushRaster::inBands(qsizetype(context.width()) * context.height(),
[&](qsizetype start, qsizetype length) { levels_apply(pixels + start * 4, size_t(length), tables.data()); });
if (job.selection)
return PixelAdjust::blend(context, job.image, *job.selection, job.mapping, false);
return context;
}
LevelsHistogram LevelsFilter::histogram(const LevelsJob &job)
{
const int width = job.image.width(), height = job.image.height();
const QImage context = drawn(job.image);
const std::optional<QImage> mask = job.selection ? std::optional(PixelAdjust::coverage(*job.selection, width, height, job.mapping)) : std::nullopt;
// The kernel accumulates: row by row, since Qt pads masks.
std::array<double, 1024> bins{};
for (int y = 0; y < height; ++y)
levels_histogram(context.constScanLine(y), mask ? mask->constScanLine(y) : nullptr, size_t(width), bins.data());
LevelsHistogram result{};
for (size_t channel = 0; channel < 4; ++channel)
std::copy_n(bins.begin() + ptrdiff_t(channel * 256), 256, result[channel].begin());
return result;
}
LevelsEdit::LevelsEdit(const ImageLayer &layer, std::optional<SelectionClip> selection)
: layerID(layer.id), original(layer.asset.value()), transform(layer.transform), selection(std::move(selection)),
mapping(BrushRaster::pixelToDocument(transform, original.size().width(), original.size().height()))
{
const QSize size = original.size();
// A lookup a pixel is quick; smaller copies looked coarse.
const double factor = std::min(1.0, 8000.0 / std::max(size.width(), size.height()));
if (factor < 1) {
const int width = std::max(1, int(size.width() * factor)), height = std::max(1, int(size.height() * factor));
previewSource = BrushRaster::context(width, height, false);
QPainter painter(&previewSource);
const QRectF rect(0, 0, width, height);
if (original.raster)
original.raster->draw(rect, painter);
else
BrushRaster::draw(original.image(), rect, painter);
previewMapping = BrushRaster::pixelToDocument(transform, width, height);
} else {
previewSource = original.image();
previewMapping = mapping;
}
}
std::optional<QImage> LevelsEdit::previewImage(QUuid layer) const
{
return layer == layerID ? preparedPreview : std::nullopt;
}
LevelsJob LevelsEdit::previewJob() const
{
return LevelsJob{previewSource, settings, selection, previewMapping};
}
// Swift's session extension in Levels.swift.
void EditorSession::beginLevels()
{
// `canAdjustColors` reads `levels`, not `hueSaturation`.
if (m_hueSaturation || !canAdjustColors())
return;
// A pending gradient lands first, then Levels begins.
if (m_gradientEdit) {
commitGradient([this] { beginLevels(); });
return;
}
commitTransform();
cancelCrop();
cancelLasso();
const std::optional<DocumentSelection> current = selection();
try {
m_levels.emplace(activeLayer().value(), current ? std::optional(current->clip(m_document->size())) : std::nullopt);
} catch (const ExportError &error) {
setBrushError(QString::fromUtf8(error.what()));
return;
}
countLevelsHistogram();
notify();
}
void EditorSession::countLevelsHistogram()
{
const LevelsJob job = m_levels.value().previewJob();
m_levelsHistogram.setFuture(QtConcurrent::run([job]() -> std::optional<LevelsHistogram> {
try {
return LevelsFilter::histogram(job);
} catch (const ExportError &error) {
qCWarning(lcApp) << "the Levels histogram cannot be made:" << error.what();
return std::nullopt;
}
}));
}
void EditorSession::finishLevelsHistogram()
{
const std::optional<LevelsHistogram> result = m_levelsHistogram.result();
// A commit cancels it, as Swift's; new edits replace it.
if (!m_levels || m_levels->committing)
return;
// A failed count leaves the bins empty, as Swift's try?.
if (result)
m_levels->histogram = *result;
m_levels->histogramReady = true;
notify();
}
void EditorSession::updateLevels(const LevelsSettings &settings, bool preview)
{
if (!m_levels || m_levels->committing)
return;
LevelsEdit &edit = *m_levels;
edit.settings = settings;
edit.preview = preview;
// An adjustment layer takes the settings; its pixels stay.
if (previewAdjustmentEditing(preview)) {
notify();
return;
}
if (!preview || settings.isIdentity()) {
// Swift cancels the running preview: its result drops.
m_levelsPreviewFor = QUuid();
edit.pending = std::nullopt;
edit.preparedPreview = std::nullopt;
++m_brushRevision;
notify();
return;
}
edit.pending = edit.previewJob();
renderLevelsPreview();
notify();
}
// Renders the newest job; one that arrives mid-render waits.
void EditorSession::renderLevelsPreview()
{
if (!m_levels || !m_levels->pending || m_levelsRendering)
return;
const LevelsJob job = *std::exchange(m_levels->pending, std::nullopt);
m_levelsPreviewFor = m_levels->id;
m_levelsRendering = true;
m_levelsPreview.setFuture(QtConcurrent::run([job]() -> std::optional<QImage> {
try {
return LevelsFilter::run(job);
} catch (const ExportError &error) {
qCWarning(lcApp) << "the Levels preview cannot be made:" << error.what();
return std::nullopt;
}
}));
}
void EditorSession::finishLevelsPreview()
{
m_levelsRendering = false;
const std::optional<QImage> result = m_levelsPreview.result();
// A cancelled run, or another edit's, lands nowhere.
if (m_levels && m_levels->id == std::exchange(m_levelsPreviewFor, QUuid())) {
m_levels->preparedPreview = result;
++m_brushRevision;
notify();
}
renderLevelsPreview();
}
void EditorSession::setLevelsSampleMode(std::optional<LevelsSample> mode)
{
if (!m_levels)
return;
m_levels->sampleMode = mode;
++m_brushRevision;
notify();
}
void EditorSession::cancelLevels()
{
if (finishAdjustmentEditing(false) || !m_levels || m_levels->committing)
return;
m_levels.reset();
++m_brushRevision;
resumeFileRequests();
notify();
}
void EditorSession::commitLevels(std::function<void()> done)
{
// The caller resumes from the event loop, as after await.
const auto finish = [this, done] {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
};
if (finishAdjustmentEditing(true) || !m_levels || m_levels->committing) {
finish();
return;
}
if (m_levels->settings.isIdentity()) {
cancelLevels();
finish();
return;
}
LevelsEdit &edit = *m_levels;
edit.committing = true;
// Swift cancels the preview: its result and queue drop.
m_levelsPreviewFor = QUuid();
edit.pending = std::nullopt;
m_committingLevels = Committing{edit.id, std::move(done)};
setIsProjectBusy(true);
// A painted layer flattens there, where a failure is caught.
m_levelsCommit.setFuture(QtConcurrent::run([original = edit.original, settings = edit.settings, selection = edit.selection,
mapping = edit.mapping]() -> Leveled {
try {
const QImage image = LevelsFilter::run(LevelsJob{original.image(), settings, selection, mapping});
return Leveled{ImportedImage(image, PixelAdjust::thumbnail(image), QStringLiteral("Levels")), std::nullopt};
} catch (const ExportError &error) {
return Leveled{std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
void EditorSession::finishLevelsCommit()
{
const Leveled made = m_levelsCommit.result();
const LevelsEdit edit = std::exchange(m_levels, std::nullopt).value();
const Committing pending = std::exchange(m_committingLevels, std::nullopt).value();
// Swift's defer: the edit goes, the project frees, redraw.
const auto finish = [&] {
++m_brushRevision;
setIsProjectBusy(false);
if (pending.done)
QMetaObject::invokeMethod(this, pending.done, Qt::QueuedConnection);
};
if (made.failure) {
setBrushError(made.failure);
finish();
return;
}
const int index = m_document ? indexOf(m_document->layers, edit.layerID) : -1;
if (index < 0) {
finish();
return;
}
const ImageLayer current = m_document->layers[index];
if (!current.asset || current.asset->identity() != edit.original.identity() || current.transform != edit.transform) {
finish();
return;
}
beginEdit(QStringLiteral("Levels"));
ImageLayer layer = current;
layer.asset = made.asset;
// Swift rebuilds the layer without shape and text.
layer.shape = std::nullopt;
layer.text = std::nullopt;
m_document->layers[index] = layer;
endEdit();
finish();
}