Black & White, Color Balance and Invert adjustments (12.7b)

This commit is contained in:
雷电芽衣 committed 2026-09-27 16:00:06 -04:00
1 parent 7d47cc367a
commit 4291f4249a
33 files changed
+819 -52

No files matched your search

+10 -1
View File
@@ -75,15 +75,21 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
m_hueSaturation.emplace(layer.id, made.asset.value(), std::nullopt, layer.transform);
m_hueSaturation->settings = original.resolvedHSV();
break;
case AdjustmentKind::invert:
throw std::logic_error("Invert has no editor to open");
case AdjustmentKind::curves:
case AdjustmentKind::exposure:
case AdjustmentKind::gradientMap:
case AdjustmentKind::grain: {
case AdjustmentKind::grain:
case AdjustmentKind::blackWhite:
case AdjustmentKind::colorBalance: {
FilterSettings settings;
settings.curves = original.curves;
settings.exposure = original.exposure();
settings.gradientMap = original.gradientMap();
settings.grain = original.grain();
settings.blackWhite = original.blackWhite();
settings.colorBalance = original.colorBalance();
m_filterEdit.emplace(filterKind(original.kind).value(), layer, std::nullopt, settings, std::nullopt);
break;
}
@@ -113,6 +119,9 @@ LayerAdjustment EditorSession::editedAdjustment() const
case AdjustmentKind::exposure: value.setExposure(m_filterEdit.value().settings.exposure); break;
case AdjustmentKind::gradientMap: value.setGradientMap(m_filterEdit.value().settings.gradientMap); break;
case AdjustmentKind::grain: value.setGrain(m_filterEdit.value().settings.grain); break;
case AdjustmentKind::blackWhite: value.setBlackWhite(m_filterEdit.value().settings.blackWhite); break;
case AdjustmentKind::colorBalance: value.setColorBalance(m_filterEdit.value().settings.colorBalance); break;
case AdjustmentKind::invert: throw std::logic_error("Invert has no editor to read");
}
return value;
}
+5
View File
@@ -121,6 +121,9 @@ void EditorSession::renderFilterPreview()
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
} catch (const ExportError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
} catch (const ProjectError &error) {
// Settings past their bounds: Black & White, Color Balance.
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
}
}));
}
@@ -228,6 +231,8 @@ void EditorSession::commitFilter(std::function<void()> done)
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
} catch (const ExportError &error) {
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
} catch (const ProjectError &error) {
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
+6 -1
View File
@@ -30,6 +30,8 @@ QString rawValue(FilterKind kind)
case FilterKind::exposure: return QStringLiteral("Exposure");
case FilterKind::gradientMap: return QStringLiteral("Gradient Map");
case FilterKind::grain: return QStringLiteral("Grain");
case FilterKind::blackWhite: return QStringLiteral("Black & White");
case FilterKind::colorBalance: return QStringLiteral("Color Balance");
}
throw std::logic_error("unknown filter kind");
}
@@ -46,7 +48,8 @@ QString rawValue(BackgroundQuality quality)
bool isImageAdjustment(FilterKind kind)
{
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain;
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain
|| kind == FilterKind::blackWhite || kind == FilterKind::colorBalance;
}
FilterSettings FilterSettings::normalized() const
@@ -113,6 +116,8 @@ QImage PixelFilter::run(const FilterJob &job)
case FilterKind::curves: image = settings.curves.apply(job.image); break;
case FilterKind::exposure: image = settings.exposure.apply(job.image); break;
case FilterKind::gradientMap: image = settings.gradientMap.apply(job.image); break;
case FilterKind::blackWhite: image = settings.blackWhite.apply(job.image); break;
case FilterKind::colorBalance: image = settings.colorBalance.apply(job.image); break;
// Grain sits in layer pixels; the job's seed patterns it.
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
+4 -2
View File
@@ -14,7 +14,7 @@
struct ImageLayer;
// The Filter and Image menus' filters.
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain };
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite, colorBalance };
// The menu's title, Swift's rawValue.
QString rawValue(FilterKind kind);
// Made without settings: its preview is its result.
@@ -25,7 +25,7 @@ bool isImageAdjustment(FilterKind kind);
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
FilterKind::lensCorrection, FilterKind::removeBackground, FilterKind::contentAwareFill,
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
FilterKind::grain};
FilterKind::grain, FilterKind::blackWhite, FilterKind::colorBalance};
// Remove Background's mask: the model's alone, or refined.
enum class BackgroundQuality { basic, advanced };
@@ -50,6 +50,8 @@ struct FilterSettings {
ExposureSettings exposure{};
GradientMapSettings gradientMap{};
GrainSettings grain{};
BlackWhiteSettings blackWhite{};
ColorBalanceSettings colorBalance{};
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
// How far, in pixels, the mask is pulled onto edges.
double refineEdges = 12;
+55
View File
@@ -2,6 +2,7 @@
#include "Document/BrushStroke.h"
#include "IO/ProjectStore.h"
#include <QPainter>
#include <algorithm>
#include <array>
#include <cmath>
extern "C" {
@@ -131,3 +132,57 @@ QImage GrainSettings::apply(const QImage &image, QPointF origin, double unitsPer
adjust_grain(pixels, size_t(width), size_t(height), size_t(stride), amount, size, roughness, pattern, origin.x(), origin.y(), unitsPerPixel);
});
}
bool BlackWhiteSettings::isValid() const
{
const std::array weights{reds, yellows, greens, cyans, blues, magentas};
return std::all_of(weights.begin(), weights.end(), [](double weight) { return weight >= low && weight <= high; })
&& tintHue >= 0 && tintHue <= 360 && tintSaturation >= 0 && tintSaturation <= 100;
}
QImage BlackWhiteSettings::apply(const QImage &image) const
{
if (!isValid())
throw ProjectError(ProjectError::Kind::invalid);
// The kernel's order: red, yellow, green, cyan, blue, magenta.
const std::array<float, 6> weights{float(reds / 100), float(yellows / 100), float(greens / 100),
float(cyans / 100), float(blues / 100), float(magentas / 100)};
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
adjust_black_white(pixels, size_t(width), size_t(height), size_t(stride), weights.data(), tint ? 1 : 0, tintHue, tintSaturation / 100);
});
}
namespace {
std::array<double, 9> amounts(const ColorBalanceSettings &settings)
{
return {settings.shadowCyanRed, settings.shadowMagentaGreen, settings.shadowYellowBlue, settings.midCyanRed, settings.midMagentaGreen,
settings.midYellowBlue, settings.highlightCyanRed, settings.highlightMagentaGreen, settings.highlightYellowBlue};
}
}
bool ColorBalanceSettings::isValid() const
{
const std::array<double, 9> all = amounts(*this);
return std::all_of(all.begin(), all.end(), [](double amount) { return amount >= low && amount <= high; });
}
bool ColorBalanceSettings::isIdentity() const
{
const std::array<double, 9> all = amounts(*this);
return std::all_of(all.begin(), all.end(), [](double amount) { return amount == 0; });
}
QImage ColorBalanceSettings::apply(const QImage &image) const
{
if (!isValid())
throw ProjectError(ProjectError::Kind::invalid);
if (isIdentity())
return image;
std::array<float, 9> ranges;
const std::array<double, 9> all = amounts(*this);
std::transform(all.begin(), all.end(), ranges.begin(), [](double amount) { return float(amount / 100); });
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
adjust_color_balance(pixels, size_t(width), size_t(height), size_t(stride), ranges.data(), ranges.data() + 3, ranges.data() + 6,
preserveLuminosity ? 1 : 0);
});
}
+39
View File
@@ -74,3 +74,42 @@ struct GrainSettings {
QImage apply(const QImage &image, QPointF origin = QPointF(), double unitsPerPixel = 1, std::optional<quint32> seed = std::nullopt) const;
friend bool operator==(const GrainSettings &, const GrainSettings &) = default;
};
// Photoshop's Black & White: how bright each colour family turns.
struct BlackWhiteSettings {
static constexpr double low = -200, high = 300;
// Photoshop's defaults, a percent each.
double reds = 40;
double yellows = 60;
double greens = 40;
double cyans = 60;
double blues = 20;
double magentas = 80;
// Colours the result, keeping its tones: sepia, cyanotype.
bool tint = false;
double tintHue = 40;
double tintSaturation = 20;
bool isValid() const;
QImage apply(const QImage &image) const;
friend bool operator==(const BlackWhiteSettings &, const BlackWhiteSettings &) = default;
};
// Color Balance: each pair shifted in shadows, midtones and highlights.
struct ColorBalanceSettings {
static constexpr double low = -100, high = 100;
double shadowCyanRed = 0;
double shadowMagentaGreen = 0;
double shadowYellowBlue = 0;
double midCyanRed = 0;
double midMagentaGreen = 0;
double midYellowBlue = 0;
double highlightCyanRed = 0;
double highlightMagentaGreen = 0;
double highlightYellowBlue = 0;
// Puts each pixel's brightness back, so only colour moves.
bool preserveLuminosity = true;
bool isValid() const;
bool isIdentity() const;
QImage apply(const QImage &image) const;
friend bool operator==(const ColorBalanceSettings &, const ColorBalanceSettings &) = default;
};
+42 -3
View File
@@ -1,5 +1,6 @@
#include "Document/LayerAdjustment.h"
#include "Document/EditorSession.h"
#include "Document/PixelInvert.h"
#include <QRandomGenerator>
#include <cmath>
@@ -12,6 +13,9 @@ QString rawValue(AdjustmentKind kind)
case AdjustmentKind::exposure: return QStringLiteral("Exposure");
case AdjustmentKind::gradientMap: return QStringLiteral("Gradient Map");
case AdjustmentKind::grain: return QStringLiteral("Grain");
case AdjustmentKind::invert: return QStringLiteral("Invert");
case AdjustmentKind::blackWhite: return QStringLiteral("Black & White");
case AdjustmentKind::colorBalance: return QStringLiteral("Color Balance");
}
throw std::logic_error("unknown adjustment kind");
}
@@ -32,12 +36,21 @@ std::optional<FilterKind> filterKind(AdjustmentKind kind)
case AdjustmentKind::exposure: return FilterKind::exposure;
case AdjustmentKind::gradientMap: return FilterKind::gradientMap;
case AdjustmentKind::grain: return FilterKind::grain;
case AdjustmentKind::blackWhite: return FilterKind::blackWhite;
case AdjustmentKind::colorBalance: return FilterKind::colorBalance;
// Panels of their own; Invert has nothing to set.
case AdjustmentKind::hsv:
case AdjustmentKind::levels: return std::nullopt;
case AdjustmentKind::levels:
case AdjustmentKind::invert: return std::nullopt;
}
throw std::logic_error("unknown adjustment kind");
}
bool isEditable(AdjustmentKind kind)
{
return kind != AdjustmentKind::invert;
}
HueSaturationSettings LayerAdjustment::resolvedHSV() const
{
return hsvSettings.value_or(HueSaturationSettings(hue, saturation, lightness, colorize));
@@ -73,6 +86,26 @@ void LayerAdjustment::setGrain(const GrainSettings &value)
grainSettings = value;
}
BlackWhiteSettings LayerAdjustment::blackWhite() const
{
return blackWhiteSettings.value_or(BlackWhiteSettings());
}
void LayerAdjustment::setBlackWhite(const BlackWhiteSettings &value)
{
blackWhiteSettings = value;
}
ColorBalanceSettings LayerAdjustment::colorBalance() const
{
return colorBalanceSettings.value_or(ColorBalanceSettings());
}
void LayerAdjustment::setColorBalance(const ColorBalanceSettings &value)
{
colorBalanceSettings = value;
}
// Bounds refuse what is no number: no isFinite terms.
bool LayerAdjustment::isValid() const
{
@@ -96,7 +129,8 @@ bool LayerAdjustment::isValid() const
if (!(range == range.normalized()))
return false;
}
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid();
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid() && blackWhite().isValid()
&& colorBalance().isValid();
}
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region) const
@@ -108,6 +142,9 @@ QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region)
case AdjustmentKind::curves: return curves.apply(image);
case AdjustmentKind::exposure: return exposure().apply(image);
case AdjustmentKind::gradientMap: return gradientMap().apply(image);
case AdjustmentKind::blackWhite: return blackWhite().apply(image);
case AdjustmentKind::colorBalance: return colorBalance().apply(image);
case AdjustmentKind::invert: return PixelInvert::run(PixelInvert::Job{image, false, QTransform(), std::nullopt});
case AdjustmentKind::grain: {
// Grain sits in the document, however the image is cut.
const QRectF area = region.value_or(QRectF(0, 0, image.width(), image.height()));
@@ -143,7 +180,9 @@ void EditorSession::addAdjustment(AdjustmentKind kind)
m_collapsedGroupIDs.remove(*layer.parentID);
setActiveLayerID(layer.id);
endEdit();
setAdjustmentEditingID(layer.id);
// Invert has nothing to set: it applies, no editor opens.
if (isEditable(kind))
setAdjustmentEditingID(layer.id);
}
void EditorSession::updateAdjustment(QUuid id, const LayerAdjustment &value)
+12 -3
View File
@@ -10,15 +10,18 @@
#include <optional>
// An adjustment layer's kind, in the Layer menu's order.
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain };
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, invert, blackWhite, colorBalance };
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain};
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
AdjustmentKind::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
// Swift's rawValue: the new layer's name and the manifest's word.
QString rawValue(AdjustmentKind kind);
// The kind a manifest names, or none.
std::optional<AdjustmentKind> adjustmentKind(const QString &text);
// The filter panel that edits it; Levels and Hue/Saturation: none.
// The filter panel that edits it; Levels, Hue/Saturation, Invert: none.
std::optional<FilterKind> filterKind(AdjustmentKind kind);
// Every kind but Invert opens an editor.
bool isEditable(AdjustmentKind kind);
// An adjustment layer's settings; its kind reads only its own.
struct LayerAdjustment {
@@ -35,6 +38,8 @@ struct LayerAdjustment {
std::optional<ExposureSettings> exposureSettings = std::nullopt;
std::optional<GradientMapSettings> gradientMapSettings = std::nullopt;
std::optional<GrainSettings> grainSettings = std::nullopt;
std::optional<BlackWhiteSettings> blackWhiteSettings = std::nullopt;
std::optional<ColorBalanceSettings> colorBalanceSettings = std::nullopt;
HueSaturationSettings resolvedHSV() const;
// Swift's computed properties over the optional settings.
ExposureSettings exposure() const;
@@ -43,6 +48,10 @@ struct LayerAdjustment {
void setGradientMap(const GradientMapSettings &value);
GrainSettings grain() const;
void setGrain(const GrainSettings &value);
BlackWhiteSettings blackWhite() const;
void setBlackWhite(const BlackWhiteSettings &value);
ColorBalanceSettings colorBalance() const;
void setColorBalance(const ColorBalanceSettings &value);
bool isValid() const;
// `region`: the document `image` covers, whole pixels by default.
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt) const;