#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 #include #include #include #include #include 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 &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 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 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 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 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 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 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 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 { 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 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 { 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 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 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 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(); }