Filter › Dither, as Compositor 1.3.3 (12.15)
This commit is contained in:
1 parent
aa5a44e517
commit
bc9d43f1e8
25 files changed
+1973
-17
No files matched your search
@@ -21,6 +21,9 @@ public:
|
||||
// The picker on the open Vignette's colour, which follows it.
|
||||
void openVignetteColorPicker();
|
||||
void previewVignetteColor();
|
||||
// The picker on Dither's dark or light colour.
|
||||
void openDitherColorPicker(bool light);
|
||||
void previewDitherColor();
|
||||
// The picker on an effect of the open panel.
|
||||
void openEffectColorPicker(LayerEffectKind kind);
|
||||
// The effect follows the picker's working colour.
|
||||
@@ -40,3 +43,4 @@ private:
|
||||
void restoreDraftTextColors(const LayerTextStyle &original);
|
||||
void setGradientMapColor(const PaletteColor &color, bool highlights);
|
||||
void setVignetteColor(const PaletteColor &color);
|
||||
void setDitherColor(const PaletteColor &color, bool light);
|
||||
@@ -107,6 +107,8 @@ QString ColorPickerTarget::title() const
|
||||
return QStringLiteral("Color Picker (%1 Color)").arg(rawValue(effect));
|
||||
if (kind == Kind::vignette)
|
||||
return QStringLiteral("Color Picker (Vignette Color)");
|
||||
if (kind == Kind::dither)
|
||||
return light ? QStringLiteral("Color Picker (Dither Light Color)") : QStringLiteral("Color Picker (Dither Dark Color)");
|
||||
return background ? QStringLiteral("Color Picker (Background Color)") : QStringLiteral("Color Picker (Foreground Color)");
|
||||
}
|
||||
|
||||
@@ -253,6 +255,8 @@ void EditorSession::closeColorPicker(bool commit)
|
||||
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::dither) {
|
||||
setDitherColor(commit ? color : picker.original, picker.target.light);
|
||||
} else if (picker.target.kind == ColorPickerTarget::Kind::effect) {
|
||||
const PaletteColor chosen = commit ? color : picker.original;
|
||||
changeEffects([&](LayerEffects &effects) { effects.setColor(chosen, picker.target.effect); });
|
||||
@@ -283,6 +287,33 @@ void EditorSession::openVignetteColorPicker()
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::openDitherColorPicker(bool light)
|
||||
{
|
||||
// A commit keeps the project busy: no `committing` term.
|
||||
if (!canEditPalette() || m_colorPicker || !m_filterEdit || m_filterEdit->kind != FilterKind::dither)
|
||||
return;
|
||||
const AdjustmentColor value = light ? m_filterEdit->settings.dither.light : m_filterEdit->settings.dither.dark;
|
||||
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::dither, false, std::nullopt, false, LayerEffectKind{}, light},
|
||||
PaletteColor{value.red, value.green, value.blue});
|
||||
notify();
|
||||
}
|
||||
|
||||
void EditorSession::previewDitherColor()
|
||||
{
|
||||
if (m_colorPicker && m_colorPicker->target.kind == ColorPickerTarget::Kind::dither)
|
||||
setDitherColor(m_colorPicker->color(), m_colorPicker->target.light);
|
||||
}
|
||||
|
||||
// Its picker lives only while its Dither edit is open.
|
||||
void EditorSession::setDitherColor(const PaletteColor &color, bool light)
|
||||
{
|
||||
FilterSettings settings = m_filterEdit.value().settings;
|
||||
(light ? settings.dither.light : settings.dither.dark) = AdjustmentColor(color);
|
||||
if (settings == m_filterEdit->settings)
|
||||
return;
|
||||
updateFilter(settings, m_filterEdit->preview);
|
||||
}
|
||||
|
||||
void EditorSession::openEffectColorPicker(LayerEffectKind kind)
|
||||
{
|
||||
if (!canEditPalette() || m_colorPicker || !m_effectsEditing)
|
||||
|
||||
@@ -38,7 +38,7 @@ enum class LayerEffectKind;
|
||||
|
||||
// The picker's targets: palette, text, effect, a filter's colour.
|
||||
struct ColorPickerTarget {
|
||||
enum class Kind { palette, text, gradientMap, effect, vignette };
|
||||
enum class Kind { palette, text, gradientMap, effect, vignette, dither };
|
||||
Kind kind;
|
||||
bool background = false;
|
||||
std::optional<QUuid> draftID = std::nullopt;
|
||||
@@ -46,6 +46,8 @@ struct ColorPickerTarget {
|
||||
bool highlights = false;
|
||||
// An effect's colour: which effect.
|
||||
LayerEffectKind effect{};
|
||||
// Dither's Two Colors: the light one, else the dark.
|
||||
bool light = false;
|
||||
QString title() const;
|
||||
friend bool operator==(const ColorPickerTarget &, const ColorPickerTarget &) = default;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "Document/Dither.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QTextBoundaryFinder>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
extern "C" {
|
||||
#include "DitherPixels.h"
|
||||
}
|
||||
|
||||
// Swift's Dither.swift: the settings, the chunky pixels, the glyphs.
|
||||
namespace {
|
||||
// Swift's Characters: the text's graphemes.
|
||||
QStringList graphemes(const QString &text)
|
||||
{
|
||||
QStringList found;
|
||||
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
|
||||
qsizetype start = 0;
|
||||
while (finder.toNextBoundary() >= 0) {
|
||||
found << text.mid(start, finder.position() - start);
|
||||
start = finder.position();
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// Swift's Character.isNewline.
|
||||
bool isNewline(const QString &grapheme)
|
||||
{
|
||||
static const QString breaks = QStringLiteral("\n\r\u000B\u000C\u0085
");
|
||||
return grapheme == QStringLiteral("\r\n") || (grapheme.size() == 1 && breaks.contains(grapheme.front()));
|
||||
}
|
||||
|
||||
std::array<uint8_t, 3> bytes(const AdjustmentColor &color)
|
||||
{
|
||||
return {uint8_t(std::lround(color.red * 255)), uint8_t(std::lround(color.green * 255)), uint8_t(std::lround(color.blue * 255))};
|
||||
}
|
||||
}
|
||||
|
||||
QString rawValue(DitherStyle style)
|
||||
{
|
||||
switch (style) {
|
||||
case DitherStyle::atkinson: return QStringLiteral("Atkinson (Classic Mac)");
|
||||
case DitherStyle::floydSteinberg: return QStringLiteral("Floyd–Steinberg");
|
||||
case DitherStyle::bayer2: return QStringLiteral("Bayer 2 × 2");
|
||||
case DitherStyle::bayer4: return QStringLiteral("Bayer 4 × 4");
|
||||
case DitherStyle::bayer8: return QStringLiteral("Bayer 8 × 8");
|
||||
case DitherStyle::dots: return QStringLiteral("Halftone Dots");
|
||||
case DitherStyle::lines: return QStringLiteral("Halftone Lines");
|
||||
case DitherStyle::diamonds: return QStringLiteral("Halftone Diamonds");
|
||||
case DitherStyle::patterns: return QStringLiteral("Mac Patterns");
|
||||
case DitherStyle::ascii: return QStringLiteral("ASCII");
|
||||
}
|
||||
throw std::logic_error("unknown dither style");
|
||||
}
|
||||
|
||||
int ditherGroup(DitherStyle style)
|
||||
{
|
||||
const int index = int(style);
|
||||
return index < 2 ? 0 : index < 5 ? 1 : index < 8 ? 2 : 3;
|
||||
}
|
||||
|
||||
bool diffuses(DitherStyle style)
|
||||
{
|
||||
return ditherGroup(style) == 0;
|
||||
}
|
||||
|
||||
bool hasTones(DitherStyle style)
|
||||
{
|
||||
return ditherGroup(style) < 2;
|
||||
}
|
||||
|
||||
bool isHalftone(DitherStyle style)
|
||||
{
|
||||
return ditherGroup(style) == 2;
|
||||
}
|
||||
|
||||
bool drawsMarks(DitherStyle style)
|
||||
{
|
||||
return !hasTones(style);
|
||||
}
|
||||
|
||||
QString rawValue(DitherPixelShape shape)
|
||||
{
|
||||
return shape == DitherPixelShape::square ? QStringLiteral("Square") : QStringLiteral("Dot");
|
||||
}
|
||||
|
||||
QString rawValue(DitherColors colors)
|
||||
{
|
||||
switch (colors) {
|
||||
case DitherColors::blackWhite: return QStringLiteral("Black & White");
|
||||
case DitherColors::twoColors: return QStringLiteral("Two Colors");
|
||||
case DitherColors::original: return QStringLiteral("Original");
|
||||
}
|
||||
throw std::logic_error("unknown dither colours");
|
||||
}
|
||||
|
||||
DitherSettings DitherSettings::normalized() const
|
||||
{
|
||||
using ImageAdjustmentPixels::clamp;
|
||||
DitherSettings result = *this;
|
||||
result.pixelSize = std::round(clamp(pixelSize, pixelSizeLow, pixelSizeHigh, 2));
|
||||
result.cellSize = std::round(clamp(cellSize, cellSizeLow, cellSizeHigh, 8));
|
||||
result.angle = clamp(angle, -90, 90, 45);
|
||||
result.levels = std::round(clamp(levels, levelsLow, levelsHigh, 2));
|
||||
result.diffusion = clamp(diffusion, 0, 100, 100);
|
||||
result.density = clamp(density, -100, 100, 0);
|
||||
result.contrast = clamp(contrast, -100, 100, 0);
|
||||
result.dark = dark.clamped();
|
||||
result.light = light.clamped();
|
||||
// Line breaks go; 64 characters at most.
|
||||
QStringList kept;
|
||||
for (const QString &grapheme : graphemes(characters)) {
|
||||
if (!isNewline(grapheme) && kept.size() < 64)
|
||||
kept << grapheme;
|
||||
}
|
||||
result.characters = kept.join(QString());
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage DitherSettings::apply(const QImage &image) const
|
||||
{
|
||||
const DitherSettings settings = normalized();
|
||||
const int block = int(settings.pixelSize);
|
||||
const QImage source = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
if (source.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
const int width = source.width(), height = source.height();
|
||||
QImage working = source;
|
||||
if (block > 1) {
|
||||
// Chunky pixels: each block's area averaged, then dithered small.
|
||||
working = BrushRaster::context((width + block - 1) / block, (height + block - 1) / block, false);
|
||||
const int area = block * block;
|
||||
for (int y = 0; y < working.height(); ++y) {
|
||||
uchar *out = working.scanLine(y);
|
||||
for (int x = 0; x < working.width(); ++x) {
|
||||
std::array<int, 4> sum{};
|
||||
for (int sy = y * block; sy < std::min(height, (y + 1) * block); ++sy) {
|
||||
const uchar *row = source.constScanLine(sy);
|
||||
for (int sx = x * block; sx < std::min(width, (x + 1) * block); ++sx) {
|
||||
for (int channel = 0; channel < 4; ++channel)
|
||||
sum[size_t(channel)] += row[sx * 4 + channel];
|
||||
}
|
||||
}
|
||||
for (int channel = 0; channel < 4; ++channel)
|
||||
out[x * 4 + channel] = uchar((sum[size_t(channel)] + area / 2) / area);
|
||||
}
|
||||
}
|
||||
}
|
||||
const QImage dithered = settings.dither(working);
|
||||
if (block <= 1)
|
||||
return dithered;
|
||||
// Blown back up without smoothing.
|
||||
QImage full = BrushRaster::context(width, height, false);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const uchar *row = dithered.constScanLine(y / block);
|
||||
uchar *out = full.scanLine(y);
|
||||
for (int x = 0; x < width; ++x)
|
||||
std::copy_n(row + (x / block) * 4, 4, out + x * 4);
|
||||
}
|
||||
if (settings.pixelShape == DitherPixelShape::dot) {
|
||||
// The gaps are the dark colour: black, or picked.
|
||||
const std::array<uint8_t, 3> gap = settings.colors == DitherColors::twoColors ? bytes(settings.dark) : std::array<uint8_t, 3>{0, 0, 0};
|
||||
dither_dots(full.bits(), size_t(width), size_t(height), size_t(full.bytesPerLine()), block, gap.data());
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
QImage DitherSettings::dither(const QImage &image) const
|
||||
{
|
||||
const int cell = int(cellSize);
|
||||
const Glyphs glyphs = style == DitherStyle::ascii ? DitherSettings::glyphs(characters.isEmpty() ? defaultCharacters() : characters, cell) : Glyphs();
|
||||
const bool two = colors == DitherColors::twoColors;
|
||||
const std::array<uint8_t, 3> darkColor = two ? bytes(dark) : std::array<uint8_t, 3>{0, 0, 0};
|
||||
const std::array<uint8_t, 3> lightColor = two ? bytes(light) : std::array<uint8_t, 3>{255, 255, 255};
|
||||
bool failed = false;
|
||||
const QImage result = ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
|
||||
DitherParams params{.style = int(style), .levels = int(levels), .diffusion = float(diffusion / 100), .density = float(density / 100),
|
||||
.contrast = float(contrast / 100), .cell = cell, .angle = float(angle * M_PI / 180), .lightOnDark = lightOnDark ? 1 : 0,
|
||||
.originalColors = colors == DitherColors::original ? 1 : 0, .dark = {darkColor[0], darkColor[1], darkColor[2]},
|
||||
.light = {lightColor[0], lightColor[1], lightColor[2]}, .glyphs = glyphs.maps.data(),
|
||||
.glyphCoverage = glyphs.coverage.data(), .glyphCount = int(glyphs.coverage.size())};
|
||||
failed = dither_apply(pixels, size_t(width), size_t(height), size_t(stride), ¶ms) == 0;
|
||||
});
|
||||
if (failed)
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
return result;
|
||||
}
|
||||
|
||||
DitherSettings::Glyphs DitherSettings::glyphs(const QString &characters, int cell)
|
||||
{
|
||||
// Swift's bold system monospace, at a whole pixel size.
|
||||
QFont font(QStringLiteral("monospace"));
|
||||
font.setWeight(QFont::Bold);
|
||||
font.setPixelSize(std::max(1, int(std::lround(cell * 1.15))));
|
||||
std::vector<std::pair<std::vector<uint8_t>, float>> drawn;
|
||||
// Swift's Set<Character>: canonically equivalent graphemes are one.
|
||||
std::set<QString> seen;
|
||||
for (const QString &character : graphemes(characters)) {
|
||||
if (!seen.insert(character.normalized(QString::NormalizationForm_C)).second)
|
||||
continue;
|
||||
QImage map(cell, cell, QImage::Format_Grayscale8);
|
||||
if (map.isNull())
|
||||
throw ExportError(ExportError::Kind::render);
|
||||
map.fill(0);
|
||||
QPainterPath path;
|
||||
path.addText(0, 0, font, character);
|
||||
// Overlapping outlines stay inked, as text rendering fills them.
|
||||
path.setFillRule(Qt::WindingFill);
|
||||
const QRectF bounds = path.boundingRect();
|
||||
{
|
||||
// Centred on its ink, as CTLineGetImageBounds centres it.
|
||||
QPainter painter(&map);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.translate((cell - bounds.width()) / 2 - bounds.left(), (cell - bounds.height()) / 2 - bounds.top());
|
||||
painter.fillPath(path, Qt::white);
|
||||
}
|
||||
std::vector<uint8_t> bytes(size_t(cell * cell));
|
||||
long sum = 0;
|
||||
for (int y = 0; y < cell; ++y) {
|
||||
std::copy_n(map.constScanLine(y), cell, bytes.begin() + y * cell);
|
||||
for (int x = 0; x < cell; ++x)
|
||||
sum += map.constScanLine(y)[x];
|
||||
}
|
||||
drawn.emplace_back(std::move(bytes), float(sum) / float(255 * cell * cell));
|
||||
}
|
||||
std::stable_sort(drawn.begin(), drawn.end(), [](const auto &a, const auto &b) { return a.second < b.second; });
|
||||
Glyphs result;
|
||||
for (const auto &[map, coverage] : drawn) {
|
||||
result.maps.insert(result.maps.end(), map.begin(), map.end());
|
||||
result.coverage.push_back(coverage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include "Document/ImageAdjustments.h"
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
// Filter › Dither's looks, in the panel's order and DitherPixels.h's.
|
||||
enum class DitherStyle { atkinson, floydSteinberg, bayer2, bayer4, bayer8, dots, lines, diamonds, patterns, ascii };
|
||||
inline constexpr std::array allDitherStyles{DitherStyle::atkinson, DitherStyle::floydSteinberg, DitherStyle::bayer2, DitherStyle::bayer4,
|
||||
DitherStyle::bayer8, DitherStyle::dots, DitherStyle::lines, DitherStyle::diamonds,
|
||||
DitherStyle::patterns, DitherStyle::ascii};
|
||||
QString rawValue(DitherStyle style);
|
||||
// Swift's groups: diffusion, ordered, halftone, marks.
|
||||
int ditherGroup(DitherStyle style);
|
||||
// Each pixel's rounding error passes to its neighbours.
|
||||
bool diffuses(DitherStyle style);
|
||||
// Diffusion and ordered styles have tones; the rest draw marks.
|
||||
bool hasTones(DitherStyle style);
|
||||
bool isHalftone(DitherStyle style);
|
||||
bool drawsMarks(DitherStyle style);
|
||||
|
||||
// A chunky pixel: a square, or a dot on dark.
|
||||
enum class DitherPixelShape { square, dot };
|
||||
inline constexpr std::array allDitherPixelShapes{DitherPixelShape::square, DitherPixelShape::dot};
|
||||
QString rawValue(DitherPixelShape shape);
|
||||
|
||||
enum class DitherColors { blackWhite, twoColors, original };
|
||||
inline constexpr std::array allDitherColors{DitherColors::blackWhite, DitherColors::twoColors, DitherColors::original};
|
||||
QString rawValue(DitherColors colors);
|
||||
|
||||
// Swift's DitherSettings: the filter's look, in layer pixels.
|
||||
struct DitherSettings {
|
||||
static constexpr double pixelSizeLow = 1, pixelSizeHigh = 32;
|
||||
static constexpr double cellSizeLow = 4, cellSizeHigh = 64;
|
||||
static constexpr double levelsLow = 2, levelsHigh = 8;
|
||||
static QString defaultCharacters() { return QStringLiteral(" .:-=+*#%@"); }
|
||||
DitherStyle style = DitherStyle::atkinson;
|
||||
// Each dithered pixel covers this many layer pixels a side.
|
||||
double pixelSize = 2;
|
||||
DitherPixelShape pixelShape = DitherPixelShape::square;
|
||||
// Halftone and character cells, in dithered pixels.
|
||||
double cellSize = 8;
|
||||
// The halftone screen's angle, in degrees.
|
||||
double angle = 45;
|
||||
// Tones per channel; 2 is 1-bit.
|
||||
double levels = 2;
|
||||
// How much error passes on, 0–100%.
|
||||
double diffusion = 100;
|
||||
// −100…100: more ink or less, flatter or punchier.
|
||||
double density = 0;
|
||||
double contrast = 0;
|
||||
DitherColors colors = DitherColors::blackWhite;
|
||||
AdjustmentColor dark = AdjustmentColor(0, 0, 0);
|
||||
AdjustmentColor light = AdjustmentColor(1, 1, 1);
|
||||
// Marks stand for the light tones, lit on the dark.
|
||||
bool lightOnDark = true;
|
||||
// ASCII's characters, sorted by their ink.
|
||||
QString characters = defaultCharacters();
|
||||
DitherSettings normalized() const;
|
||||
QImage apply(const QImage &image) const;
|
||||
friend bool operator==(const DitherSettings &, const DitherSettings &) = default;
|
||||
|
||||
// Each distinct character's coverage map, least ink first.
|
||||
struct Glyphs {
|
||||
std::vector<uint8_t> maps;
|
||||
std::vector<float> coverage;
|
||||
};
|
||||
static Glyphs glyphs(const QString &characters, int cell);
|
||||
|
||||
private:
|
||||
QImage dither(const QImage &image) const;
|
||||
};
|
||||
@@ -180,10 +180,18 @@ void EditorSession::finishFilterPreview()
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A filter's own colours: their pickers go with its panel.
|
||||
bool filterColour(ColorPickerTarget::Kind kind)
|
||||
{
|
||||
return kind == ColorPickerTarget::Kind::gradientMap || kind == ColorPickerTarget::Kind::vignette || kind == ColorPickerTarget::Kind::dither;
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSession::cancelFilter()
|
||||
{
|
||||
// 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))
|
||||
if (m_colorPicker && filterColour(m_colorPicker->target.kind))
|
||||
closeColorPicker(false);
|
||||
if (finishAdjustmentEditing(false) || !m_filterEdit || m_filterEdit->committing)
|
||||
return;
|
||||
@@ -199,7 +207,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 || m_colorPicker->target.kind == ColorPickerTarget::Kind::vignette))
|
||||
if (m_colorPicker && filterColour(m_colorPicker->target.kind))
|
||||
closeColorPicker(true);
|
||||
if (finishAdjustmentEditing(true) || !m_filterEdit || m_filterEdit->committing) {
|
||||
finish();
|
||||
|
||||
@@ -27,6 +27,7 @@ QString rawValue(FilterKind kind)
|
||||
case FilterKind::addNoise: return QStringLiteral("Add Noise");
|
||||
case FilterKind::vignette: return QStringLiteral("Vignette");
|
||||
case FilterKind::bloomGlow: return QStringLiteral("Bloom / Glow");
|
||||
case FilterKind::dither: return QStringLiteral("Dither");
|
||||
case FilterKind::tonalContrast: return QStringLiteral("Tonal Contrast");
|
||||
case FilterKind::lensCorrection: return QStringLiteral("Lens Correction");
|
||||
case FilterKind::cameraRaw: return QStringLiteral("Camera Raw Filter");
|
||||
@@ -86,6 +87,7 @@ FilterSettings FilterSettings::normalized() const
|
||||
result.exposure = exposure.normalized();
|
||||
result.gradientMap = gradientMap.normalized();
|
||||
result.grain = grain.normalized();
|
||||
result.dither = dither.normalized();
|
||||
result.cameraRaw = cameraRaw.normalized();
|
||||
return result;
|
||||
}
|
||||
@@ -156,6 +158,7 @@ QImage PixelFilter::run(const FilterJob &job)
|
||||
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::dither: image = settings.dither.apply(job.image); break;
|
||||
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
|
||||
case FilterKind::contentAwareFill: image = ContentFill::run(job); break;
|
||||
// Unclamped: a blur spreads into the room made for it.
|
||||
@@ -271,8 +274,9 @@ void FilterEdit::prepare(const ImportedImage &source, const LayerTransform &plac
|
||||
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;
|
||||
// Noise, grain and dither preview whole: enlarged, they look coarse.
|
||||
const bool whole = kind == FilterKind::addNoise || kind == FilterKind::grain || kind == FilterKind::dither || kind == FilterKind::contentAwareFill
|
||||
|| kind == FilterKind::removeBackground;
|
||||
const double factor = whole ? 1 : std::min(1.0, previewLimit / std::max(size.width(), size.height()));
|
||||
if (!(factor < 1)) {
|
||||
previewSource = source.image();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "Document/CameraRaw.h"
|
||||
#include "Document/Curves.h"
|
||||
#include "Document/Dither.h"
|
||||
#include "Document/ImageAdjustments.h"
|
||||
#include "Document/LayerTransform.h"
|
||||
#include "Document/Selection.h"
|
||||
@@ -16,7 +17,7 @@ struct ImageLayer;
|
||||
|
||||
// The Filter and Image menus' filters.
|
||||
enum class FilterKind {
|
||||
gaussianBlur, motionBlur, addNoise, vignette, bloomGlow, tonalContrast, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves,
|
||||
gaussianBlur, motionBlur, addNoise, vignette, bloomGlow, dither, tonalContrast, lensCorrection, cameraRaw, removeBackground, contentAwareFill, curves,
|
||||
exposure, gradientMap, grain, blackWhite, colorBalance
|
||||
};
|
||||
// The menu's title, Swift's rawValue.
|
||||
@@ -27,7 +28,8 @@ 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::vignette, FilterKind::bloomGlow, FilterKind::dither,
|
||||
FilterKind::tonalContrast,
|
||||
FilterKind::lensCorrection, FilterKind::cameraRaw, FilterKind::removeBackground,
|
||||
FilterKind::contentAwareFill,
|
||||
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
|
||||
@@ -74,6 +76,7 @@ struct FilterSettings {
|
||||
GrainSettings grain{};
|
||||
BlackWhiteSettings blackWhite{};
|
||||
ColorBalanceSettings colorBalance{};
|
||||
DitherSettings dither{};
|
||||
CameraRawSettings cameraRaw{};
|
||||
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
|
||||
// How far, in pixels, the mask is pulled onto edges.
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFontDatabase>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
// Swift's ditherControls, beside the filter sheet's other kinds.
|
||||
QComboBox *FilterSheet::menu(const QString &title, const QString &name, const QString &help)
|
||||
{
|
||||
auto *box = new QWidget(this);
|
||||
auto *row = new QHBoxLayout(box);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(10);
|
||||
auto *label = new QLabel(title, box);
|
||||
auto *choices = new QComboBox(box);
|
||||
choices->setObjectName(name);
|
||||
choices->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
label->setBuddy(choices);
|
||||
box->setToolTip(help);
|
||||
row->addWidget(label);
|
||||
row->addWidget(choices);
|
||||
row->addStretch(1);
|
||||
m_column->addWidget(box);
|
||||
return choices;
|
||||
}
|
||||
|
||||
void FilterSheet::dither()
|
||||
{
|
||||
const auto key = [](double DitherSettings::*field) { return [field](FilterSettings &settings) -> double & { return settings.dither.*field; }; };
|
||||
const auto box = [this] { return m_controls.back().slider->parentWidget(); };
|
||||
// Style: grouped as Swift's menu, a rule between groups.
|
||||
QComboBox *style = menu(QStringLiteral("Style"), QStringLiteral("ditherStyle"), QString());
|
||||
for (const DitherStyle each : allDitherStyles) {
|
||||
if (style->count() > 0 && ditherGroup(each) != ditherGroup(DitherStyle(style->itemData(style->count() - 1).toInt())))
|
||||
style->insertSeparator(style->count());
|
||||
style->addItem(rawValue(each), int(each));
|
||||
}
|
||||
connect(style, &QComboBox::activated, this,
|
||||
[this, style](int index) { update([&](FilterSettings &settings) { settings.dither.style = DitherStyle(style->itemData(index).toInt()); }); });
|
||||
m_follows.emplace_back([style](const FilterSettings &settings) {
|
||||
const QSignalBlocker quiet(style);
|
||||
style->setCurrentIndex(style->findData(int(settings.dither.style)));
|
||||
});
|
||||
control(QStringLiteral("Pixel Size"), key(&DitherSettings::pixelSize), DitherSettings::pixelSizeLow, DitherSettings::pixelSizeHigh, QStringLiteral("px"), 0, false);
|
||||
box()->setToolTip(QStringLiteral("Make each dithered pixel this many pixels across, for a chunky old-screen look"));
|
||||
control(QStringLiteral("Cell Size"), key(&DitherSettings::cellSize), DitherSettings::cellSizeLow, DitherSettings::cellSizeHigh, QStringLiteral("px"), 0, false);
|
||||
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return isHalftone(settings.dither.style) || settings.dither.style == DitherStyle::ascii; });
|
||||
control(QStringLiteral("Angle"), key(&DitherSettings::angle), -90, 90, QStringLiteral("°"), 0, false);
|
||||
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return isHalftone(settings.dither.style); });
|
||||
// ASCII's characters, typed as they will draw.
|
||||
auto *characters = new QWidget(this);
|
||||
auto *row = new QHBoxLayout(characters);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(10);
|
||||
auto *label = new QLabel(QStringLiteral("Characters"), characters);
|
||||
auto *field = new QLineEdit(characters);
|
||||
field->setObjectName(QStringLiteral("ditherCharacters"));
|
||||
field->setPlaceholderText(QStringLiteral("Characters"));
|
||||
field->setAccessibleName(QStringLiteral("Characters"));
|
||||
field->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
label->setBuddy(field);
|
||||
characters->setToolTip(QStringLiteral("The characters to draw with, in any order: each spot gets the one whose ink best matches its tone"));
|
||||
row->addWidget(label);
|
||||
row->addWidget(field, 1);
|
||||
m_column->addWidget(characters);
|
||||
connect(field, &QLineEdit::textEdited, this, [this](const QString &text) { update([&text](FilterSettings &settings) { settings.dither.characters = text; }); });
|
||||
// Left, it shows what the settings kept of the typing.
|
||||
connect(field, &QLineEdit::editingFinished, this, [this, field] { field->setText(settings().dither.characters); });
|
||||
m_follows.emplace_back([field](const FilterSettings &settings) {
|
||||
// Typing keeps its own text; the settings hold it normalized.
|
||||
if (!field->hasFocus() && field->text() != settings.dither.characters)
|
||||
field->setText(settings.dither.characters);
|
||||
});
|
||||
m_shownWhen.emplace_back(characters, [](const FilterSettings &settings) { return settings.dither.style == DitherStyle::ascii; });
|
||||
control(QStringLiteral("Tones"), key(&DitherSettings::levels), DitherSettings::levelsLow, DitherSettings::levelsHigh, QString(), 0, false);
|
||||
box()->setToolTip(QStringLiteral("Tones per channel: 2 is pure black and white"));
|
||||
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return hasTones(settings.dither.style); });
|
||||
control(QStringLiteral("Diffusion"), key(&DitherSettings::diffusion), 0, 100, QStringLiteral("%"), 0, false);
|
||||
box()->setToolTip(QStringLiteral("How much of each pixel's error spreads to its neighbors. Less gives flatter areas"));
|
||||
m_shownWhen.emplace_back(box(), [](const FilterSettings &settings) { return diffuses(settings.dither.style); });
|
||||
control(QStringLiteral("Density"), key(&DitherSettings::density), -100, 100, QString(), 0, false);
|
||||
box()->setToolTip(QStringLiteral("More ink (darker) or less before dithering"));
|
||||
control(QStringLiteral("Contrast"), key(&DitherSettings::contrast), -100, 100, QString(), 0, false);
|
||||
// Colors: a menu, as Swift's: the panel keeps its size.
|
||||
QComboBox *colors = menu(QStringLiteral("Colors"), QStringLiteral("ditherColors"), QString());
|
||||
for (const DitherColors each : allDitherColors)
|
||||
colors->addItem(rawValue(each), int(each));
|
||||
connect(colors, &QComboBox::activated, this, [this](int index) { update([index](FilterSettings &settings) { settings.dither.colors = DitherColors(index); }); });
|
||||
m_follows.emplace_back([colors](const FilterSettings &settings) {
|
||||
const QSignalBlocker quiet(colors);
|
||||
colors->setCurrentIndex(int(settings.dither.colors));
|
||||
});
|
||||
// Two Colors: the dark and the light swatch.
|
||||
auto *pair = new QWidget(this);
|
||||
auto *swatches = new QHBoxLayout(pair);
|
||||
swatches->setContentsMargins(0, 0, 0, 0);
|
||||
swatches->setSpacing(8);
|
||||
for (const bool light : {false, true}) {
|
||||
auto *title = new QLabel(light ? QStringLiteral("Light") : QStringLiteral("Dark"), pair);
|
||||
auto *swatch = new SwatchButton([this, light] {
|
||||
const AdjustmentColor colour = light ? settings().dither.light : settings().dither.dark;
|
||||
return PaletteColor{colour.red, colour.green, colour.blue};
|
||||
}, 6, 1.5, 1, pair);
|
||||
swatch->setObjectName(light ? QStringLiteral("ditherLight") : QStringLiteral("ditherDark"));
|
||||
swatch->setFixedSize(24, 24);
|
||||
swatch->setToolTip(light ? QStringLiteral("Choose the light color") : QStringLiteral("Choose the dark color"));
|
||||
swatch->setAccessibleName(light ? QStringLiteral("Light color") : QStringLiteral("Dark color"));
|
||||
connect(swatch, &QAbstractButton::clicked, this, [this, light] { m_session.openDitherColorPicker(light); });
|
||||
m_follows.emplace_back([swatch](const FilterSettings &) { swatch->update(); });
|
||||
if (light)
|
||||
swatches->addSpacing(10);
|
||||
swatches->addWidget(title);
|
||||
swatches->addWidget(swatch);
|
||||
}
|
||||
swatches->addStretch(1);
|
||||
m_column->addWidget(pair);
|
||||
m_shownWhen.emplace_back(pair, [](const FilterSettings &settings) { return settings.dither.colors == DitherColors::twoColors; });
|
||||
QComboBox *shape = menu(QStringLiteral("Pixel Shape"), QStringLiteral("ditherPixelShape"),
|
||||
QStringLiteral("Draw each chunky pixel as a solid square, or as a round dot like a dot-matrix screen"));
|
||||
for (const DitherPixelShape each : allDitherPixelShapes)
|
||||
shape->addItem(rawValue(each), int(each));
|
||||
connect(shape, &QComboBox::activated, this, [this](int index) { update([index](FilterSettings &settings) { settings.dither.pixelShape = DitherPixelShape(index); }); });
|
||||
m_follows.emplace_back([shape](const FilterSettings &settings) {
|
||||
const QSignalBlocker quiet(shape);
|
||||
shape->setCurrentIndex(int(settings.dither.pixelShape));
|
||||
});
|
||||
m_shownWhen.emplace_back(shape->parentWidget(), [](const FilterSettings &settings) { return settings.dither.pixelSize > 1; });
|
||||
flag(QStringLiteral("Light on Dark"), QStringLiteral("Draw the marks for the light tones on the dark color, like a glowing screen"),
|
||||
[](FilterSettings &settings) -> bool & { return settings.dither.lightOnDark; });
|
||||
m_shownWhen.emplace_back(m_flags.back().first, [](const FilterSettings &settings) { return drawsMarks(settings.dither.style); });
|
||||
}
|
||||
@@ -185,6 +185,7 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
|
||||
control(QStringLiteral("Amount"), key(&FilterSettings::amount), 0.1, 400, QStringLiteral("%"), 1, true);
|
||||
noise();
|
||||
break;
|
||||
case FilterKind::dither: dither(); break;
|
||||
case FilterKind::vignette:
|
||||
case FilterKind::bloomGlow:
|
||||
case FilterKind::tonalContrast: finishing(m_session.filterEdit()->kind); break;
|
||||
@@ -317,12 +318,15 @@ void FilterSheet::synchronize()
|
||||
m_pickerColour = picked;
|
||||
m_session.previewGradientMapColor();
|
||||
m_session.previewVignetteColor();
|
||||
m_session.previewDitherColor();
|
||||
}
|
||||
if (m_vignetteSwatch)
|
||||
m_vignetteSwatch->update();
|
||||
const FilterEdit &edit = m_session.filterEdit().value();
|
||||
for (Control &control : m_controls)
|
||||
showControl(control, edit.settings);
|
||||
for (const auto &follow : m_follows)
|
||||
follow(edit.settings);
|
||||
if (m_curves)
|
||||
m_curves->synchronize();
|
||||
if (m_gradientMap)
|
||||
@@ -345,8 +349,8 @@ void FilterSheet::synchronize()
|
||||
m_error->setVisible(edit.previewError.has_value());
|
||||
// An adjustment layer's editor ignores the selection.
|
||||
m_limited->setVisible(!m_session.adjustmentOriginal() && m_session.selection());
|
||||
// While the result is made, OK waits and says so.
|
||||
const bool working = edit.committing || edit.preparing;
|
||||
// Slow filters say they work; a quick preview would flicker.
|
||||
const bool working = edit.committing || (edit.preparing && isAutomatic(edit.kind));
|
||||
m_spinner->setVisible(working);
|
||||
m_activity->setVisible(working);
|
||||
m_activity->setText(edit.committing ? QStringLiteral("Applying…") : QStringLiteral("Working…"));
|
||||
|
||||
@@ -7,6 +7,7 @@ class CurvesControls;
|
||||
class PickerField;
|
||||
class QButtonGroup;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QProgressBar;
|
||||
class QPushButton;
|
||||
@@ -75,6 +76,10 @@ private:
|
||||
void noise();
|
||||
// Vignette's colour and rows, Bloom / Glow's, Tonal Contrast's.
|
||||
void finishing(FilterKind kind);
|
||||
// Swift's ditherControls: style, sizes, tones, colours, shape.
|
||||
void dither();
|
||||
// A titled menu of choices: the row and its box.
|
||||
QComboBox *menu(const QString &title, const QString &name, const QString &help);
|
||||
// Remove Background's words, Quality, and Advanced's three rows.
|
||||
void background();
|
||||
// Black & White's six families and Tint; Color Balance's nine.
|
||||
@@ -101,6 +106,8 @@ private:
|
||||
std::vector<std::pair<QCheckBox *, std::function<bool &(FilterSettings &)>>> m_flags;
|
||||
// Rows under Swift's `if`: Advanced's, Tint's.
|
||||
std::vector<std::pair<QWidget *, std::function<bool(const FilterSettings &)>>> m_shownWhen;
|
||||
// Controls that show the settings as they stand: Dither's menus.
|
||||
std::vector<std::function<void(const FilterSettings &)>> m_follows;
|
||||
QCheckBox *const m_preview;
|
||||
QLabel *const m_error;
|
||||
QLabel *const m_limited;
|
||||
|
||||
Reference in new issue
Block a user