Photoshop's remaining blend modes in its groups (12.7a); v1.2.2 kernels for 12.7b

This commit is contained in:
雷电芽衣 committed 2026-09-27 12:50:33 -04:00
1 parent 29eab10edc
commit 7d47cc367a
26 files changed
+348 -56

No files matched your search

+19 -6
View File
@@ -6,12 +6,15 @@
#include <utility>
namespace {
const std::array<std::pair<LayerBlendMode, const char *>, 14> names{{
{LayerBlendMode::normal, "Normal"}, {LayerBlendMode::multiply, "Multiply"}, {LayerBlendMode::screen, "Screen"},
{LayerBlendMode::overlay, "Overlay"}, {LayerBlendMode::softLight, "Soft Light"}, {LayerBlendMode::darken, "Darken"}, {LayerBlendMode::lighten, "Lighten"},
{LayerBlendMode::difference, "Difference"}, {LayerBlendMode::colorDodge, "Color Dodge"},
{LayerBlendMode::colorBurn, "Color Burn"}, {LayerBlendMode::hue, "Hue"}, {LayerBlendMode::saturation, "Saturation"},
{LayerBlendMode::color, "Color"}, {LayerBlendMode::luminosity, "Luminosity"},
const std::array<std::pair<LayerBlendMode, const char *>, 24> names{{
{LayerBlendMode::normal, "Normal"}, {LayerBlendMode::darken, "Darken"}, {LayerBlendMode::multiply, "Multiply"},
{LayerBlendMode::colorBurn, "Color Burn"}, {LayerBlendMode::linearBurn, "Linear Burn"}, {LayerBlendMode::lighten, "Lighten"},
{LayerBlendMode::screen, "Screen"}, {LayerBlendMode::colorDodge, "Color Dodge"}, {LayerBlendMode::linearDodge, "Linear Dodge (Add)"},
{LayerBlendMode::overlay, "Overlay"}, {LayerBlendMode::softLight, "Soft Light"}, {LayerBlendMode::hardLight, "Hard Light"},
{LayerBlendMode::vividLight, "Vivid Light"}, {LayerBlendMode::linearLight, "Linear Light"}, {LayerBlendMode::pinLight, "Pin Light"},
{LayerBlendMode::hardMix, "Hard Mix"}, {LayerBlendMode::difference, "Difference"}, {LayerBlendMode::exclusion, "Exclusion"},
{LayerBlendMode::subtract, "Subtract"}, {LayerBlendMode::divide, "Divide"}, {LayerBlendMode::hue, "Hue"},
{LayerBlendMode::saturation, "Saturation"}, {LayerBlendMode::color, "Color"}, {LayerBlendMode::luminosity, "Luminosity"},
}};
}
@@ -20,6 +23,16 @@ QString rawValue(LayerBlendMode mode)
return QString::fromLatin1(names.at(size_t(mode)).second);
}
int blendGroup(LayerBlendMode mode)
{
// Each group's modes sit together in the enum.
for (const auto &[first, group] : std::array<std::pair<LayerBlendMode, int>, 5>{{{LayerBlendMode::hue, 5},
{LayerBlendMode::difference, 4}, {LayerBlendMode::overlay, 3}, {LayerBlendMode::lighten, 2}, {LayerBlendMode::darken, 1}}})
if (mode >= first)
return group;
return 0;
}
std::optional<LayerBlendMode> layerBlendMode(const QString &rawValue)
{
for (const auto &[mode, name] : names) {
+14 -6
View File
@@ -4,16 +4,24 @@
#include <optional>
enum class LayerBlendMode {
normal, multiply, screen, overlay, softLight, darken, lighten, difference,
colorDodge, colorBurn, hue, saturation, color, luminosity
normal,
darken, multiply, colorBurn, linearBurn,
lighten, screen, colorDodge, linearDodge,
overlay, softLight, hardLight, vividLight, linearLight, pinLight, hardMix,
difference, exclusion, subtract, divide,
hue, saturation, color, luminosity
};
// Swift's `groups`, Photoshop's: the menu draws a line between.
int blendGroup(LayerBlendMode mode);
// Swift's `allCases`: the order of the blend menu.
inline constexpr std::array allLayerBlendModes{
LayerBlendMode::normal, LayerBlendMode::multiply, LayerBlendMode::screen, LayerBlendMode::overlay,
LayerBlendMode::softLight, LayerBlendMode::darken, LayerBlendMode::lighten, LayerBlendMode::difference, LayerBlendMode::colorDodge,
LayerBlendMode::colorBurn, LayerBlendMode::hue, LayerBlendMode::saturation, LayerBlendMode::color,
LayerBlendMode::luminosity};
LayerBlendMode::normal, LayerBlendMode::darken, LayerBlendMode::multiply, LayerBlendMode::colorBurn, LayerBlendMode::linearBurn,
LayerBlendMode::lighten, LayerBlendMode::screen, LayerBlendMode::colorDodge, LayerBlendMode::linearDodge,
LayerBlendMode::overlay, LayerBlendMode::softLight, LayerBlendMode::hardLight, LayerBlendMode::vividLight, LayerBlendMode::linearLight,
LayerBlendMode::pinLight, LayerBlendMode::hardMix, LayerBlendMode::difference, LayerBlendMode::exclusion, LayerBlendMode::subtract,
LayerBlendMode::divide, LayerBlendMode::hue, LayerBlendMode::saturation, LayerBlendMode::color, LayerBlendMode::luminosity};
// The name users see and the manifest stores.
QString rawValue(LayerBlendMode mode);
+6
View File
@@ -31,6 +31,12 @@ std::optional<LayerBlendMode> fromPSD(const QString &key)
{QStringLiteral("div "), LayerBlendMode::colorDodge}, {QStringLiteral("idiv"), LayerBlendMode::colorBurn},
{QStringLiteral("hue "), LayerBlendMode::hue}, {QStringLiteral("sat "), LayerBlendMode::saturation},
{QStringLiteral("colr"), LayerBlendMode::color}, {QStringLiteral("lum "), LayerBlendMode::luminosity},
{QStringLiteral("lbrn"), LayerBlendMode::linearBurn}, {QStringLiteral("lddg"), LayerBlendMode::linearDodge},
{QStringLiteral("hLit"), LayerBlendMode::hardLight}, {QStringLiteral("vLit"), LayerBlendMode::vividLight},
{QStringLiteral("lLit"), LayerBlendMode::linearLight}, {QStringLiteral("pLit"), LayerBlendMode::pinLight},
{QStringLiteral("hMix"), LayerBlendMode::hardMix}, {QStringLiteral("smud"), LayerBlendMode::exclusion},
{QStringLiteral("fsub"), LayerBlendMode::subtract}, {QStringLiteral("fdiv"), LayerBlendMode::divide},
// Dissolve, Darker Color and Lighter Color have no twin: Normal.
};
if (!modes.contains(key))
return std::nullopt;
+5 -3
View File
@@ -1,5 +1,6 @@
#include "Rendering/HslBlend.h"
#include "Rendering/PoolMap.h"
#include "Rendering/SeparableBlend.h"
#include "IO/ImageExporter.h"
#include <QtConcurrent>
#include <algorithm>
@@ -66,7 +67,8 @@ Color mixed(LayerBlendMode mode, const Color &backdrop, const Color &source)
case LayerBlendMode::luminosity:
return setLum(backdrop, lum(source));
default:
throw std::logic_error("HslBlend handles only the four non-separable modes");
return {SeparableBlend::channel(mode, backdrop[0], source[0]), SeparableBlend::channel(mode, backdrop[1], source[1]),
SeparableBlend::channel(mode, backdrop[2], source[2])};
}
}
}
@@ -74,7 +76,7 @@ Color mixed(LayerBlendMode mode, const Color &backdrop, const Color &source)
bool HslBlend::handles(LayerBlendMode mode)
{
return mode == LayerBlendMode::hue || mode == LayerBlendMode::saturation || mode == LayerBlendMode::color
|| mode == LayerBlendMode::luminosity;
|| mode == LayerBlendMode::luminosity || SeparableBlend::needsSurface(mode);
}
QImage HslBlend::blend(const QImage &backdrop, const QImage &source, LayerBlendMode mode, double opacity)
@@ -84,7 +86,7 @@ QImage HslBlend::blend(const QImage &backdrop, const QImage &source, LayerBlendM
throw std::logic_error("HslBlend needs two RGBA8888 premultiplied images of one size");
// Checked here: a worker's exception would come back wrapped.
if (!handles(mode))
throw std::logic_error("HslBlend blends hue, saturation, color and luminosity alone");
throw std::logic_error("HslBlend blends only the modes QPainter lacks");
QImage result(backdrop.size(), QImage::Format_RGBA8888_Premultiplied);
if (result.isNull())
throw ExportError(ExportError::Kind::render);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "Document/LayerAppearance.h"
#include <QImage>
// Hue, Saturation, Color, Luminosity: PDF blend modes QPainter lacks.
// Blend modes QPainter lacks: the four non-separable, SeparableBlend's eight.
namespace HslBlend {
bool handles(LayerBlendMode mode);
// Source over backdrop, same size, both RGBA8888 premultiplied.
+5 -1
View File
@@ -32,6 +32,10 @@ QPainter::CompositionMode compositionMode(LayerBlendMode mode)
return QPainter::CompositionMode_ColorDodge;
case LayerBlendMode::colorBurn:
return QPainter::CompositionMode_ColorBurn;
case LayerBlendMode::hardLight:
return QPainter::CompositionMode_HardLight;
case LayerBlendMode::exclusion:
return QPainter::CompositionMode_Exclusion;
default:
throw std::logic_error("QPainter has no composition mode for this blend mode");
}
@@ -214,7 +218,7 @@ void LayerRenderer::composite(QPainter &context, const QTransform &placement, co
if (byHand) {
const QImage *target = dynamic_cast<const QImage *>(context.device());
if (!target)
throw std::logic_error("hue, saturation, color and luminosity need an image-backed painter");
throw std::logic_error("modes blended by hand need an image-backed painter");
const QImage backdrop = target->copy(area).convertToFormat(QImage::Format_RGBA8888_Premultiplied);
if (backdrop.isNull())
throw ExportError(ExportError::Kind::render);
+61
View File
@@ -0,0 +1,61 @@
#include "Rendering/SeparableBlend.h"
#include <algorithm>
#include <stdexcept>
namespace {
// A zero divisor gives infinity; the clamp takes it home.
float burn(float backdrop, float source)
{
if (backdrop >= 1)
return 1;
return 1 - std::min(1.0f, (1 - backdrop) / source);
}
float dodge(float backdrop, float source)
{
if (backdrop <= 0)
return 0;
return std::min(1.0f, backdrop / (1 - source));
}
}
bool SeparableBlend::needsSurface(LayerBlendMode mode)
{
switch (mode) {
case LayerBlendMode::linearBurn:
case LayerBlendMode::linearDodge:
case LayerBlendMode::vividLight:
case LayerBlendMode::linearLight:
case LayerBlendMode::pinLight:
case LayerBlendMode::hardMix:
case LayerBlendMode::subtract:
case LayerBlendMode::divide:
return true;
default:
return false;
}
}
float SeparableBlend::channel(LayerBlendMode mode, float backdrop, float source)
{
switch (mode) {
case LayerBlendMode::linearBurn:
return std::max(0.0f, backdrop + source - 1);
case LayerBlendMode::linearDodge:
return std::min(1.0f, backdrop + source);
case LayerBlendMode::vividLight:
return source <= 0.5f ? burn(backdrop, 2 * source) : dodge(backdrop, 2 * source - 1);
case LayerBlendMode::linearLight:
return std::clamp(backdrop + 2 * source - 1, 0.0f, 1.0f);
case LayerBlendMode::pinLight:
return source <= 0.5f ? std::min(backdrop, 2 * source) : std::max(backdrop, 2 * source - 1);
case LayerBlendMode::hardMix:
return backdrop + source >= 1 ? 1 : 0;
case LayerBlendMode::subtract:
return std::max(0.0f, backdrop - source);
case LayerBlendMode::divide:
return source <= 0 ? 1 : std::min(1.0f, backdrop / source);
default:
throw std::logic_error("SeparableBlend blends only the modes QPainter lacks");
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "Document/LayerAppearance.h"
// Swift's SeparableBlend: channel-by-channel modes QPainter lacks.
namespace SeparableBlend {
// Blended by hand against a surface, Swift's `needsSurface`.
bool needsSurface(LayerBlendMode mode);
// Photoshop's formula on one channel: backdrop, source, 0 to 1.
float channel(LayerBlendMode mode, float backdrop, float source);
}
+5 -1
View File
@@ -4,8 +4,12 @@ BlendModePicker::BlendModePicker(EditorSession &session, QWidget *parent) : QCom
{
setAccessibleName(QStringLiteral("Blend mode"));
setObjectName(QStringLiteral("blendMode"));
for (const LayerBlendMode mode : allLayerBlendModes)
// Grouped as Photoshop groups them, a line between.
for (const LayerBlendMode mode : allLayerBlendModes) {
if (count() && blendGroup(mode) != blendGroup(*layerBlendMode(itemText(count() - 1))))
insertSeparator(count());
addItem(rawValue(mode));
}
connect(this, &QComboBox::highlighted, this, &BlendModePicker::highlight);
connect(this, &QComboBox::activated, this, &BlendModePicker::choose);
connect(&m_session, &EditorSession::changed, this, &BlendModePicker::synchronize);