Finishing filters: Vignette, Bloom / Glow, Tonal Contrast (12.9a)

This commit is contained in:
雷电芽衣 committed 2026-09-28 10:50:06 -04:00
1 parent 96ce546508
commit fdb62978c5
20 files changed
+756 -35

No files matched your search

+4
View File
@@ -18,6 +18,9 @@ public:
void openGradientMapColorPicker(bool highlights);
// The Gradient Map follows the picker's working colour.
void previewGradientMapColor();
// The picker on the open Vignette's colour, which follows it.
void openVignetteColorPicker();
void previewVignetteColor();
// The picker on an effect of the open panel.
void openEffectColorPicker(LayerEffectKind kind);
// The effect follows the picker's working colour.
@@ -30,3 +33,4 @@ public:
private:
void setGradientMapColor(const PaletteColor &color, bool highlights);
void setVignetteColor(const PaletteColor &color);
+30
View File
@@ -105,6 +105,8 @@ QString ColorPickerTarget::title() const
return highlights ? QStringLiteral("Color Picker (Gradient Map Highlights)") : QStringLiteral("Color Picker (Gradient Map Shadows)");
if (kind == Kind::effect)
return QStringLiteral("Color Picker (%1 Color)").arg(rawValue(effect));
if (kind == Kind::vignette)
return QStringLiteral("Color Picker (Vignette Color)");
return background ? QStringLiteral("Color Picker (Background Color)") : QStringLiteral("Color Picker (Foreground Color)");
}
@@ -241,6 +243,8 @@ void EditorSession::closeColorPicker(bool commit)
} else if (picker.target.kind == ColorPickerTarget::Kind::gradientMap) {
// The end previewed the working colour; Cancel puts it back.
setGradientMapColor(commit ? color : picker.original, picker.target.highlights);
} else if (picker.target.kind == ColorPickerTarget::Kind::vignette) {
setVignetteColor(commit ? color : picker.original);
} else if (picker.target.kind == ColorPickerTarget::Kind::effect) {
const PaletteColor chosen = commit ? color : picker.original;
changeEffects([&](LayerEffects &effects) { effects.setColor(chosen, picker.target.effect); });
@@ -258,6 +262,16 @@ void EditorSession::openGradientMapColorPicker(bool highlights)
notify();
}
void EditorSession::openVignetteColorPicker()
{
// A commit keeps the project busy: no `committing` term.
if (!canEditPalette() || m_colorPicker || !m_filterEdit || m_filterEdit->kind != FilterKind::vignette)
return;
const AdjustmentColor value = m_filterEdit->settings.vignetteColor;
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::vignette}, PaletteColor{value.red, value.green, value.blue});
notify();
}
void EditorSession::openEffectColorPicker(LayerEffectKind kind)
{
if (!canEditPalette() || m_colorPicker || !m_effectsEditing)
@@ -281,6 +295,22 @@ void EditorSession::previewGradientMapColor()
setGradientMapColor(m_colorPicker->color(), m_colorPicker->target.highlights);
}
void EditorSession::previewVignetteColor()
{
if (m_colorPicker && m_colorPicker->target.kind == ColorPickerTarget::Kind::vignette)
setVignetteColor(m_colorPicker->color());
}
// Its picker lives only while its Vignette edit is open.
void EditorSession::setVignetteColor(const PaletteColor &color)
{
FilterSettings settings = m_filterEdit.value().settings;
settings.vignetteColor = AdjustmentColor(color);
if (settings == m_filterEdit->settings)
return;
updateFilter(settings, m_filterEdit->preview);
}
void EditorSession::setGradientMapColor(const PaletteColor &color, bool highlights)
{
// An adjustment's editor can close under its picker (9.5c).
+2 -2
View File
@@ -35,9 +35,9 @@ struct PickerHSB {
enum class LayerEffectKind;
// The picker's targets: palette, text, effect, Gradient Map end.
// The picker's targets: palette, text, effect, a filter's colour.
struct ColorPickerTarget {
enum class Kind { palette, text, gradientMap, effect };
enum class Kind { palette, text, gradientMap, effect, vignette };
Kind kind;
bool background = false;
std::optional<QUuid> draftID = std::nullopt;
+2
View File
@@ -102,6 +102,8 @@ struct SessionJobs {
FilterSettings settings;
// Camera Raw's scope of the grade itself.
std::optional<CameraRawScope> scope = std::nullopt;
// Its grid's version: a newer grid drops it.
quint64 sourceVersion = 0;
};
struct FilterMade {
std::optional<ImportedImage> asset;
+25 -15
View File
@@ -69,6 +69,9 @@ void EditorSession::updateFilter(const FilterSettings &settings, bool preview)
try {
edit.growForBlur();
edit.preparedPreview.reset();
// A waiting job moves to the grid its version names.
if (edit.pending)
edit.pending = edit.previewJob();
} catch (const ProjectError &error) {
setBrushError(QString::fromUtf8(error.what()));
} catch (const ExportError &error) {
@@ -112,22 +115,22 @@ void EditorSession::renderFilterPreview()
const FilterJob job = *std::exchange(edit.pending, std::nullopt);
m_filterPreviewFor = edit.id;
m_filterRendering = true;
m_filterPreview.setFuture(QtConcurrent::run([job]() -> Filtered {
m_filterPreview.setFuture(QtConcurrent::run([job, version = edit.previewSourceVersion]() -> Filtered {
try {
if (job.kind == FilterKind::cameraRaw) {
auto [image, scope] = CameraRawScope::preview(job);
return Filtered{std::move(image), std::nullopt, job.settings, std::move(scope)};
return Filtered{std::move(image), std::nullopt, job.settings, std::move(scope), version};
}
return Filtered{PixelFilter::run(job), std::nullopt, job.settings};
return Filtered{PixelFilter::run(job), std::nullopt, job.settings, std::nullopt, version};
} catch (const ContentFillError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings, std::nullopt, version};
} catch (const SubjectRemovalError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings, std::nullopt, version};
} catch (const ExportError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings, std::nullopt, version};
} catch (const ProjectError &error) {
// Settings past their bounds: Black & White, Color Balance.
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings, std::nullopt, version};
}
}));
}
@@ -138,8 +141,11 @@ void EditorSession::finishFilterPreview()
{
m_filterRendering = false;
const Filtered result = m_filterPreview.result();
// An edit cancelled meanwhile takes nothing; a new one waited.
if (m_filterEdit && m_filterEdit->id == m_filterPreviewFor) {
// A cancelled edit takes nothing; an older grid renders again.
if (m_filterEdit && m_filterEdit->id == m_filterPreviewFor && result.sourceVersion != m_filterEdit->previewSourceVersion) {
m_filterEdit->preparing = false;
notify();
} else if (m_filterEdit && m_filterEdit->id == m_filterPreviewFor) {
FilterEdit &edit = *m_filterEdit;
edit.preparing = false;
edit.previewError = result.failure;
@@ -164,8 +170,8 @@ void EditorSession::finishFilterPreview()
void EditorSession::cancelFilter()
{
// A Gradient Map colour being picked goes with the panel.
if (m_colorPicker && m_colorPicker->target.kind == ColorPickerTarget::Kind::gradientMap)
// A filter colour being picked goes with the panel.
if (m_colorPicker && (m_colorPicker->target.kind == ColorPickerTarget::Kind::gradientMap || m_colorPicker->target.kind == ColorPickerTarget::Kind::vignette))
closeColorPicker(false);
if (finishAdjustmentEditing(false) || !m_filterEdit || m_filterEdit->committing)
return;
@@ -181,7 +187,7 @@ void EditorSession::commitFilter(std::function<void()> done)
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
};
if (m_colorPicker && m_colorPicker->target.kind == ColorPickerTarget::Kind::gradientMap)
if (m_colorPicker && (m_colorPicker->target.kind == ColorPickerTarget::Kind::gradientMap || m_colorPicker->target.kind == ColorPickerTarget::Kind::vignette))
closeColorPicker(true);
if (finishAdjustmentEditing(true) || !m_filterEdit || m_filterEdit->committing) {
finish();
@@ -204,6 +210,11 @@ void EditorSession::commitFilter(std::function<void()> done)
// Nothing to change closes as Cancel does, with no step.
if ((edit.kind == FilterKind::lensCorrection && edit.settings.distortion == 0)
|| (edit.kind == FilterKind::exposure && edit.settings.exposure == ExposureSettings())
|| (edit.kind == FilterKind::vignette && edit.settings.vignetteAmount == 0)
|| (edit.kind == FilterKind::bloomGlow && edit.settings.bloomAmount == 0)
|| (edit.kind == FilterKind::tonalContrast
&& (edit.settings.tonalAmount == 0
|| (edit.settings.tonalShadows == 0 && edit.settings.tonalMidtones == 0 && edit.settings.tonalHighlights == 0)))
|| (edit.kind == FilterKind::grain && edit.settings.grain.amount == 0)
|| (edit.kind == FilterKind::cameraRaw && rendered.cameraRaw.isIdentity())) {
cancelFilter();
@@ -224,7 +235,7 @@ void EditorSession::commitFilter(std::function<void()> done)
return;
}
const std::optional<QImage> cached = isAutomatic(edit.kind) && edit.preparedSettings == edit.settings ? edit.preparedPreview : std::nullopt;
const bool spreads = edit.kind == FilterKind::gaussianBlur || edit.kind == FilterKind::motionBlur;
const bool spreads = edit.kind == FilterKind::gaussianBlur || edit.kind == FilterKind::motionBlur || edit.kind == FilterKind::bloomGlow;
// A painted layer flattens in the worker, which catches failures.
m_filterCommit.setFuture(QtConcurrent::run([kind = edit.kind, original = edit.original, grownImage = edit.grownImage, settings = rendered,
selection = edit.selection, mapping = edit.mapping, seed = edit.seed, cached,
@@ -339,9 +350,8 @@ void EditorSession::finishFilterCommit()
layer.asset = made.asset;
layer.transform = made.transform.value_or(current.transform);
layer.mask = mask;
// Swift rebuilds the layer without shape, effects and text.
// Swift rebuilds the layer without shape and text; effects stay.
layer.shape = std::nullopt;
layer.effects = std::nullopt;
layer.text = std::nullopt;
m_document->layers[index] = layer;
endEdit();
+47
View File
@@ -12,6 +12,7 @@
#include <numbers>
extern "C" {
#include "AdjustPixels.h"
#include "BrushPixels.h"
#include "LensPixels.h"
#include "NoisePixels.h"
@@ -23,6 +24,9 @@ QString rawValue(FilterKind kind)
case FilterKind::gaussianBlur: return QStringLiteral("Gaussian Blur");
case FilterKind::motionBlur: return QStringLiteral("Motion Blur");
case FilterKind::addNoise: return QStringLiteral("Add Noise");
case FilterKind::vignette: return QStringLiteral("Vignette");
case FilterKind::bloomGlow: return QStringLiteral("Bloom / Glow");
case FilterKind::tonalContrast: return QStringLiteral("Tonal Contrast");
case FilterKind::lensCorrection: return QStringLiteral("Lens Correction");
case FilterKind::cameraRaw: return QStringLiteral("Camera Raw Filter");
case FilterKind::removeBackground: return QStringLiteral("Remove Background");
@@ -61,6 +65,19 @@ FilterSettings FilterSettings::normalized() const
result.angle = clamp(angle, -90, 90, 0);
result.distance = clamp(distance, 1, 2000, 10);
result.amount = clamp(amount, 0.1, 400, 10);
result.vignetteAmount = clamp(vignetteAmount, 0, 100, 35);
result.vignetteColor = vignetteColor.clamped();
result.vignetteMidpoint = clamp(vignetteMidpoint, 0, 100, 50);
result.vignetteRoundness = clamp(vignetteRoundness, -100, 100, 0);
result.vignetteFeather = clamp(vignetteFeather, 0, 100, 60);
result.vignetteHighlights = clamp(vignetteHighlights, 0, 100, 25);
result.bloomAmount = clamp(bloomAmount, 0, 100, 40);
result.bloomRadius = clamp(bloomRadius, 1, 150, 24);
result.tonalAmount = clamp(tonalAmount, 0, 100, 50);
result.tonalRadius = clamp(tonalRadius, 1, 100, 16);
result.tonalShadows = clamp(tonalShadows, -100, 100, 40);
result.tonalMidtones = clamp(tonalMidtones, -100, 100, 60);
result.tonalHighlights = clamp(tonalHighlights, -100, 100, 30);
result.distortion = clamp(distortion, -100, 100, 0);
result.refineEdges = clamp(refineEdges, 0, 40, 12);
result.matteContrast = clamp(matteContrast, 0, 100, 25);
@@ -107,6 +124,19 @@ QImage drawn(const QImage &image)
BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter);
return context;
}
// CIBloom is private: the pixels plus their Gaussian glow, scaled.
QImage bloom(QImage pixels, double sigma, double intensity)
{
const QImage glow = PixelAdjust::gaussianBlur(pixels, sigma, false);
for (int y = 0; y < pixels.height(); ++y) {
uchar *row = pixels.scanLine(y);
const uchar *light = glow.constScanLine(y);
for (int index = 0; index < pixels.width() * 4; ++index)
row[index] = uchar(std::min(255.0, std::round(row[index] + intensity * light[index])));
}
return pixels;
}
}
QImage PixelFilter::run(const FilterJob &job)
@@ -137,6 +167,21 @@ QImage PixelFilter::run(const FilterJob &job)
noise_add_at(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), float(settings.amount), settings.gaussian ? 1 : 0,
settings.monochromatic ? 1 : 0, job.seed, int64_t(std::floor(job.noiseOrigin.x())), int64_t(std::floor(job.noiseOrigin.y())));
break;
case FilterKind::vignette:
image = drawn(job.image);
adjust_colored_vignette(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), settings.vignetteAmount,
settings.vignetteMidpoint, settings.vignetteRoundness, settings.vignetteFeather, settings.vignetteHighlights,
settings.vignetteColor.red, settings.vignetteColor.green, settings.vignetteColor.blue);
break;
case FilterKind::bloomGlow: image = bloom(drawn(job.image), settings.bloomRadius * job.scale, settings.bloomAmount / 50); break;
case FilterKind::tonalContrast: {
// The detail's base: the layer softened, clear past its edge.
const QImage base = PixelAdjust::gaussianBlur(drawn(job.image), settings.tonalRadius * job.scale, false);
image = drawn(job.image);
adjust_tonal_contrast(image.bits(), base.constBits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), size_t(base.bytesPerLine()),
settings.tonalAmount, settings.tonalShadows, settings.tonalMidtones, settings.tonalHighlights);
break;
}
case FilterKind::lensCorrection: {
// Relative to the image's size, so previews bend alike.
const QImage source = drawn(job.image);
@@ -173,6 +218,7 @@ double FilterEdit::blurMargin(FilterKind kind, const FilterSettings &settings)
switch (kind) {
case FilterKind::gaussianBlur: return settings.radius * 3 + 2;
case FilterKind::motionBlur: return settings.distance / 2 + 2;
case FilterKind::bloomGlow: return settings.bloomRadius * 3 + 2;
default: return 0;
}
}
@@ -218,6 +264,7 @@ void FilterEdit::grow(const QRectF &extent)
void FilterEdit::prepare(const ImportedImage &source, const LayerTransform &placed)
{
const QSize size = source.size();
++previewSourceVersion;
mapping = BrushRaster::pixelToDocument(placed, size.width(), size.height());
// Noise and grain preview whole: enlarged, grain looks coarse.
const bool whole = kind == FilterKind::addNoise || kind == FilterKind::grain || kind == FilterKind::contentAwareFill || kind == FilterKind::removeBackground;
+21 -2
View File
@@ -16,8 +16,8 @@ struct ImageLayer;
// The Filter and Image menus' filters.
enum class FilterKind {
gaussianBlur, motionBlur, addNoise, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite,
colorBalance
gaussianBlur, motionBlur, addNoise, vignette, bloomGlow, tonalContrast, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves,
exposure, gradientMap, grain, blackWhite, colorBalance
};
// The menu's title, Swift's rawValue.
QString rawValue(FilterKind kind);
@@ -27,6 +27,7 @@ bool isAutomatic(FilterKind kind);
bool isImageAdjustment(FilterKind kind);
// Swift's allCases, the menus' order.
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
FilterKind::vignette, FilterKind::bloomGlow, FilterKind::tonalContrast,
FilterKind::lensCorrection, FilterKind::cameraRaw, FilterKind::removeBackground,
FilterKind::contentAwareFill,
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
@@ -49,6 +50,22 @@ struct FilterSettings {
bool gaussian = false;
// The same noise on every channel: brightness alone.
bool monochromatic = false;
// Vignette: the edge colour, its strength and falloff.
double vignetteAmount = 35;
AdjustmentColor vignetteColor = AdjustmentColor(0, 0, 0);
double vignetteMidpoint = 50;
double vignetteRoundness = 0;
double vignetteFeather = 60;
double vignetteHighlights = 25;
// Bloom / Glow: strength, and blur radius in layer pixels.
double bloomAmount = 40;
double bloomRadius = 24;
// Tonal Contrast: one detail radius, a strength per tone.
double tonalAmount = 50;
double tonalRadius = 16;
double tonalShadows = 40;
double tonalMidtones = 60;
double tonalHighlights = 30;
// Positive straightens barrel distortion, negative pincushion.
double distortion = 0;
CurvesSettings curves{};
@@ -151,6 +168,8 @@ public:
std::optional<QImage> preparedPreview;
// The settings the prepared preview was made with.
std::optional<FilterSettings> preparedSettings;
// Moves with each new grid: an older render lands nothing.
quint64 previewSourceVersion = 0;
std::optional<FilterJob> pending;
CameraRawPanel rawPanel;
+50
View File
@@ -0,0 +1,50 @@
#include "UI/ColorPaletteControls.h"
#include "UI/FilterSheet.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QSlider>
#include <QVBoxLayout>
// Swift's finishing filters: Vignette, Bloom / Glow, Tonal Contrast.
void FilterSheet::finishing(FilterKind kind)
{
const auto key = [](double FilterSettings::*field) { return [field](FilterSettings &settings) -> double & { return settings.*field; }; };
const auto help = [this](const QString &words) { m_controls.back().slider->parentWidget()->setToolTip(words); };
if (kind == FilterKind::vignette) {
auto *box = new QWidget(this);
auto *row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(8);
auto *title = new QLabel(QStringLiteral("Color"), box);
title->setFixedWidth(95);
m_vignetteSwatch = new SwatchButton([this] {
const AdjustmentColor colour = settings().vignetteColor;
return PaletteColor{colour.red, colour.green, colour.blue};
}, 6, 1.5, 1, box);
m_vignetteSwatch->setObjectName(QStringLiteral("vignetteSwatch"));
m_vignetteSwatch->setFixedSize(24, 24);
m_vignetteSwatch->setToolTip(QStringLiteral("Choose the vignette color"));
m_vignetteSwatch->setAccessibleName(QStringLiteral("Vignette color"));
connect(m_vignetteSwatch, &QAbstractButton::clicked, this, [this] { m_session.openVignetteColorPicker(); });
row->addWidget(title);
row->addWidget(m_vignetteSwatch);
row->addStretch(1);
m_column->addWidget(box);
control(QStringLiteral("Amount"), key(&FilterSettings::vignetteAmount), 0, 100, QStringLiteral("%"), 0, false);
help(QStringLiteral("Blend the chosen color into the edges while keeping the center unchanged"));
control(QStringLiteral("Midpoint"), key(&FilterSettings::vignetteMidpoint), 0, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Roundness"), key(&FilterSettings::vignetteRoundness), -100, 100, QString(), 0, false);
control(QStringLiteral("Feather"), key(&FilterSettings::vignetteFeather), 0, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Highlights"), key(&FilterSettings::vignetteHighlights), 0, 100, QStringLiteral("%"), 0, false);
help(QStringLiteral("Protect bright areas near the edge"));
} else if (kind == FilterKind::bloomGlow) {
control(QStringLiteral("Amount"), key(&FilterSettings::bloomAmount), 0, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Radius"), key(&FilterSettings::bloomRadius), 1, 150, QStringLiteral("px"), 0, true);
} else {
control(QStringLiteral("Amount"), key(&FilterSettings::tonalAmount), 0, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Shadows"), key(&FilterSettings::tonalShadows), -100, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Midtones"), key(&FilterSettings::tonalMidtones), -100, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Highlights"), key(&FilterSettings::tonalHighlights), -100, 100, QStringLiteral("%"), 0, false);
control(QStringLiteral("Radius"), key(&FilterSettings::tonalRadius), 1, 100, QStringLiteral("px"), 0, true);
}
}
+6
View File
@@ -201,6 +201,9 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
control(QStringLiteral("Amount"), key(&FilterSettings::amount), 0.1, 400, QStringLiteral("%"), 1, true);
noise();
break;
case FilterKind::vignette:
case FilterKind::bloomGlow:
case FilterKind::tonalContrast: finishing(m_session.filterEdit()->kind); break;
case FilterKind::lensCorrection:
control(QStringLiteral("Remove Distortion"), key(&FilterSettings::distortion), -100, 100, QString(), 0, false);
m_column->addWidget(words(QStringLiteral("Positive straightens lines that bow outward (barrel); negative, lines that bow inward (pincushion)."),
@@ -388,7 +391,10 @@ void FilterSheet::synchronize()
if (picked != m_pickerColour) {
m_pickerColour = picked;
m_session.previewGradientMapColor();
m_session.previewVignetteColor();
}
if (m_vignetteSwatch)
m_vignetteSwatch->update();
const FilterEdit &edit = m_session.filterEdit().value();
for (Control &control : m_controls) {
FilterSettings copy = edit.settings;
+3
View File
@@ -53,6 +53,8 @@ private:
void control(const QString &title, std::function<double &(FilterSettings &)> key, double low, double high, const QString &unit,
int decimals, bool logarithmic);
void noise();
// Vignette's colour and rows, Bloom / Glow's, Tonal Contrast's.
void finishing(FilterKind kind);
// Remove Background's words, Quality, and Advanced's three rows.
void background();
// Black & White's six families and Tint; Color Balance's nine.
@@ -72,6 +74,7 @@ private:
std::vector<Control> m_controls;
CurvesControls *m_curves = nullptr;
GradientMapControls *m_gradientMap = nullptr;
SwatchButton *m_vignetteSwatch = nullptr;
QButtonGroup *m_distribution = nullptr;
QCheckBox *m_monochromatic = nullptr;
QButtonGroup *m_quality = nullptr;