Blur and noise adjustment layers, Image Trim, Inner Glow (12.8c)

This commit is contained in:
雷电芽衣 committed 2026-09-28 08:35:59 -04:00
1 parent 7ae263de80
commit 31e2dd3864
60 files changed
+1673 -105

No files matched your search

+19
View File
@@ -81,6 +81,9 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
case AdjustmentKind::exposure:
case AdjustmentKind::gradientMap:
case AdjustmentKind::grain:
case AdjustmentKind::addNoise:
case AdjustmentKind::gaussianBlur:
case AdjustmentKind::motionBlur:
case AdjustmentKind::blackWhite:
case AdjustmentKind::colorBalance: {
FilterSettings settings;
@@ -90,6 +93,12 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
settings.grain = original.grain();
settings.blackWhite = original.blackWhite();
settings.colorBalance = original.colorBalance();
settings.radius = original.gaussianRadius();
settings.angle = original.resolvedMotionAngle();
settings.distance = original.resolvedMotionDistance();
settings.amount = original.resolvedNoiseAmount();
settings.gaussian = original.resolvedNoiseGaussian();
settings.monochromatic = original.resolvedNoiseMonochromatic();
m_filterEdit.emplace(filterKind(original.kind).value(), layer, std::nullopt, settings, std::nullopt);
break;
}
@@ -121,6 +130,16 @@ LayerAdjustment EditorSession::editedAdjustment() const
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::gaussianBlur: value.setGaussianRadius(m_filterEdit.value().settings.radius); break;
case AdjustmentKind::motionBlur:
value.setResolvedMotionAngle(m_filterEdit.value().settings.angle);
value.setResolvedMotionDistance(m_filterEdit.value().settings.distance);
break;
case AdjustmentKind::addNoise:
value.setResolvedNoiseAmount(m_filterEdit.value().settings.amount);
value.setResolvedNoiseGaussian(m_filterEdit.value().settings.gaussian);
value.setResolvedNoiseMonochromatic(m_filterEdit.value().settings.monochromatic);
break;
case AdjustmentKind::invert: throw std::logic_error("Invert has no editor to read");
}
return value;
+2 -2
View File
@@ -134,8 +134,8 @@ QImage PixelFilter::run(const FilterJob &job)
break;
case FilterKind::addNoise:
image = drawn(job.image);
noise_add(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);
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::lensCorrection: {
// Relative to the image's size, so previews bend alike.
+2
View File
@@ -87,6 +87,8 @@ struct FilterJob {
// The point colour shown alone, or −1 for the grade.
int visualizesPointColor = -1;
bool showsSharpenMask = false;
// A live adjustment's noise field, fixed in its region's pixels.
QPointF noiseOrigin{};
};
// Swift's PixelFilter.
+105
View File
@@ -0,0 +1,105 @@
#include "Document/ImageTrim.h"
#include "Document/CanvasSize.h"
#include "IO/CanvasResizer.h"
#include "IO/ImageExporter.h"
#include <cstdlib>
extern "C" {
#include "BrushPixels.h"
}
QString rawValue(TrimBasedOn basedOn)
{
switch (basedOn) {
case TrimBasedOn::transparentPixels: return QStringLiteral("Transparent Pixels");
case TrimBasedOn::topLeftPixelColor: return QStringLiteral("Top Left Pixel Color");
case TrimBasedOn::bottomRightPixelColor: return QStringLiteral("Bottom Right Pixel Color");
}
throw std::logic_error("unknown trim basis");
}
bool TrimOptions::trimsAny() const
{
return top || bottom || left || right;
}
namespace {
// Unchecked edges stay at the image's; content is never empty.
QRect kept(int left, int top, int right, int bottom, int width, int height, const TrimOptions &options)
{
const int minX = options.left ? left : 0, minY = options.top ? top : 0;
const int maxX = options.right ? right : width, maxY = options.bottom ? bottom : height;
return QRect(minX, minY, maxX - minX, maxY - minY);
}
// Swift's calculateColorTrimRect: rows scanned in from both ends.
std::optional<QRect> colorTrimRect(const QImage &pixels, QPoint sample, const TrimOptions &options)
{
const int width = pixels.width(), height = pixels.height(), tolerance = options.tolerance;
const uchar *target = pixels.constScanLine(sample.y()) + sample.x() * 4;
const auto matches = [&](const uchar *row, int x) {
const uchar *pixel = row + x * 4;
for (int channel = 0; channel < 4; ++channel) {
if (std::abs(pixel[channel] - target[channel]) > tolerance)
return false;
}
return true;
};
int left = width, right = 0, top = height, bottom = 0;
for (int y = 0; y < height; ++y) {
const uchar *row = pixels.constScanLine(y);
int first = 0;
while (first < width && matches(row, first))
++first;
if (first == width)
continue;
// The unmatched pixel at `first` stops this scan.
int last = width;
while (matches(row, last - 1))
--last;
left = std::min(left, first);
right = std::max(right, last);
top = std::min(top, y);
bottom = y + 1;
}
// Every pixel matched the sample.
if (right == 0)
return std::nullopt;
return kept(left, top, right, bottom, width, height, options);
}
}
std::optional<QRect> ImageTrim::calculateTrimRect(const QImage &image, const TrimOptions &options)
{
if (!options.trimsAny() || image.isNull())
return std::nullopt;
// Swift's try? returns nil; running out here is explained instead.
const QImage pixels = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
if (pixels.isNull())
throw ExportError(ExportError::Kind::render);
const int width = pixels.width(), height = pixels.height();
switch (options.basedOn) {
case TrimBasedOn::transparentPixels: {
size_t edges[4] = {0, 0, 0, 0};
brush_alpha_bounds(pixels.constBits(), size_t(width), size_t(height), size_t(pixels.bytesPerLine()), edges);
// A right edge of zero: every pixel is clear.
if (edges[2] == 0)
return std::nullopt;
return kept(int(edges[0]), int(edges[1]), int(edges[2]), int(edges[3]), width, height, options);
}
case TrimBasedOn::topLeftPixelColor: return colorTrimRect(pixels, QPoint(0, 0), options);
case TrimBasedOn::bottomRightPixelColor: return colorTrimRect(pixels, QPoint(width - 1, height - 1), options);
}
throw std::logic_error("unknown trim basis");
}
std::optional<ProjectSnapshot> ImageTrim::trim(const ProjectSnapshot &snapshot, const TrimOptions &options)
{
const std::optional<QRect> rect = calculateTrimRect(ImageExporter::render(snapshot).image, options);
if (!rect)
return std::nullopt;
if (*rect == QRect(0, 0, int(snapshot.manifest.width), int(snapshot.manifest.height)))
return snapshot;
return CanvasResizer::resize(snapshot, CanvasSizeOptions{.width = rect->width(), .height = rect->height(),
.contentOffset = QPointF(-rect->left(), -rect->top())});
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "IO/ProjectStore.h"
#include <QImage>
#include <QRect>
#include <array>
#include <optional>
// What Trim compares each edge against.
enum class TrimBasedOn { transparentPixels, topLeftPixelColor, bottomRightPixelColor };
inline constexpr std::array allTrimBasedOn{TrimBasedOn::transparentPixels, TrimBasedOn::topLeftPixelColor, TrimBasedOn::bottomRightPixelColor};
// Swift's rawValue: the sheet's words.
QString rawValue(TrimBasedOn basedOn);
struct TrimOptions {
TrimBasedOn basedOn = TrimBasedOn::transparentPixels;
bool top = true;
bool bottom = true;
bool left = true;
bool right = true;
// Per channel, premultiplied: how far a pixel may stray.
quint8 tolerance = 0;
bool trimsAny() const;
friend bool operator==(const TrimOptions &, const TrimOptions &) = default;
};
namespace ImageTrim {
// The rect to keep, or none when nothing would remain.
std::optional<QRect> calculateTrimRect(const QImage &image, const TrimOptions &options);
// The rendered canvas trimmed by the canvas resizer, or none.
std::optional<ProjectSnapshot> trim(const ProjectSnapshot &snapshot, const TrimOptions &options);
}
+105 -2
View File
@@ -13,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::addNoise: return QStringLiteral("Add Noise");
case AdjustmentKind::gaussianBlur: return QStringLiteral("Gaussian Blur");
case AdjustmentKind::motionBlur: return QStringLiteral("Motion Blur");
case AdjustmentKind::invert: return QStringLiteral("Invert");
case AdjustmentKind::blackWhite: return QStringLiteral("Black & White");
case AdjustmentKind::colorBalance: return QStringLiteral("Color Balance");
@@ -36,6 +39,9 @@ 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::addNoise: return FilterKind::addNoise;
case AdjustmentKind::gaussianBlur: return FilterKind::gaussianBlur;
case AdjustmentKind::motionBlur: return FilterKind::motionBlur;
case AdjustmentKind::blackWhite: return FilterKind::blackWhite;
case AdjustmentKind::colorBalance: return FilterKind::colorBalance;
// Panels of their own; Invert has nothing to set.
@@ -106,6 +112,85 @@ void LayerAdjustment::setColorBalance(const ColorBalanceSettings &value)
colorBalanceSettings = value;
}
double LayerAdjustment::gaussianRadius() const
{
return blurRadius.value_or(10);
}
void LayerAdjustment::setGaussianRadius(double value)
{
blurRadius = value;
}
double LayerAdjustment::resolvedMotionAngle() const
{
return motionAngle.value_or(0);
}
void LayerAdjustment::setResolvedMotionAngle(double value)
{
motionAngle = value;
}
double LayerAdjustment::resolvedMotionDistance() const
{
return motionDistance.value_or(10);
}
void LayerAdjustment::setResolvedMotionDistance(double value)
{
motionDistance = value;
}
double LayerAdjustment::resolvedNoiseAmount() const
{
return noiseAmount.value_or(10);
}
void LayerAdjustment::setResolvedNoiseAmount(double value)
{
noiseAmount = value;
}
bool LayerAdjustment::resolvedNoiseGaussian() const
{
return noiseGaussian.value_or(false);
}
void LayerAdjustment::setResolvedNoiseGaussian(bool value)
{
noiseGaussian = value;
}
bool LayerAdjustment::resolvedNoiseMonochromatic() const
{
return noiseMonochromatic.value_or(false);
}
void LayerAdjustment::setResolvedNoiseMonochromatic(bool value)
{
noiseMonochromatic = value;
}
quint32 LayerAdjustment::resolvedNoiseSeed() const
{
return noiseSeed.value_or(0);
}
void LayerAdjustment::setResolvedNoiseSeed(quint32 value)
{
noiseSeed = value;
}
double LayerAdjustment::samplingMargin() const
{
switch (kind) {
case AdjustmentKind::gaussianBlur: return gaussianRadius() * 3 + 2;
case AdjustmentKind::motionBlur: return resolvedMotionDistance() / 2 + 2;
default: return 0;
}
}
// Bounds refuse what is no number: no isFinite terms.
bool LayerAdjustment::isValid() const
{
@@ -130,10 +215,11 @@ bool LayerAdjustment::isValid() const
return false;
}
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid() && blackWhite().isValid()
&& colorBalance().isValid();
&& colorBalance().isValid() && gaussianRadius() >= 0.1 && gaussianRadius() <= 250 && std::abs(resolvedMotionAngle()) <= 90
&& resolvedMotionDistance() >= 1 && resolvedMotionDistance() <= 2000 && resolvedNoiseAmount() >= 0.1 && resolvedNoiseAmount() <= 400;
}
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region) const
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region, double scale) const
{
switch (kind) {
case AdjustmentKind::hsv:
@@ -144,6 +230,21 @@ QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region)
case AdjustmentKind::gradientMap: return gradientMap().apply(image);
case AdjustmentKind::blackWhite: return blackWhite().apply(image);
case AdjustmentKind::colorBalance: return colorBalance().apply(image);
case AdjustmentKind::addNoise:
case AdjustmentKind::gaussianBlur:
case AdjustmentKind::motionBlur: {
const FilterSettings settings{.radius = gaussianRadius(),
.angle = resolvedMotionAngle(),
.distance = resolvedMotionDistance(),
.amount = resolvedNoiseAmount(),
.gaussian = resolvedNoiseGaussian(),
.monochromatic = resolvedNoiseMonochromatic()};
FilterJob job{filterKind(kind).value(), image, settings, scale, std::nullopt, QTransform(), resolvedNoiseSeed()};
// Swift's region origin, counted in the image's own pixels.
if (region)
job.noiseOrigin = region->topLeft() * (image.width() / region->width());
return PixelFilter::run(job);
}
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.
@@ -169,6 +270,8 @@ void EditorSession::addAdjustment(AdjustmentKind kind)
grain.seed = QRandomGenerator::global()->generate();
adjustment.setGrain(grain);
}
if (kind == AdjustmentKind::addNoise)
adjustment.setResolvedNoiseSeed(QRandomGenerator::global()->generate());
layer.adjustment = adjustment;
const std::optional<ImageLayer> active = activeLayer();
layer.parentID = active && active->isGroup ? m_activeLayerID : active ? active->parentID : std::nullopt;
+31 -6
View File
@@ -10,10 +10,11 @@
#include <optional>
// An adjustment layer's kind, in the Layer menu's order.
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::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, addNoise, gaussianBlur, motionBlur, invert, blackWhite, colorBalance };
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
AdjustmentKind::addNoise, AdjustmentKind::gaussianBlur, AdjustmentKind::motionBlur,
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.
@@ -40,6 +41,14 @@ struct LayerAdjustment {
std::optional<GrainSettings> grainSettings = std::nullopt;
std::optional<BlackWhiteSettings> blackWhiteSettings = std::nullopt;
std::optional<ColorBalanceSettings> colorBalanceSettings = std::nullopt;
// Absent in projects from before blur and noise layers.
std::optional<double> blurRadius = std::nullopt;
std::optional<double> motionAngle = std::nullopt;
std::optional<double> motionDistance = std::nullopt;
std::optional<double> noiseAmount = std::nullopt;
std::optional<bool> noiseGaussian = std::nullopt;
std::optional<bool> noiseMonochromatic = std::nullopt;
std::optional<quint32> noiseSeed = std::nullopt;
HueSaturationSettings resolvedHSV() const;
// Swift's computed properties over the optional settings.
ExposureSettings exposure() const;
@@ -52,8 +61,24 @@ struct LayerAdjustment {
void setBlackWhite(const BlackWhiteSettings &value);
ColorBalanceSettings colorBalance() const;
void setColorBalance(const ColorBalanceSettings &value);
double gaussianRadius() const;
void setGaussianRadius(double value);
double resolvedMotionAngle() const;
void setResolvedMotionAngle(double value);
double resolvedMotionDistance() const;
void setResolvedMotionDistance(double value);
double resolvedNoiseAmount() const;
void setResolvedNoiseAmount(double value);
bool resolvedNoiseGaussian() const;
void setResolvedNoiseGaussian(bool value);
bool resolvedNoiseMonochromatic() const;
void setResolvedNoiseMonochromatic(bool value);
quint32 resolvedNoiseSeed() const;
void setResolvedNoiseSeed(quint32 value);
// Document pixels a partial redraw reads past its rect.
double samplingMargin() const;
bool isValid() const;
// `region`: the document `image` covers, whole pixels by default.
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt) const;
// Region: the document shown; scale: image pixels a document pixel.
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt, double scale = 1) const;
friend bool operator==(const LayerAdjustment &, const LayerAdjustment &) = default;
};
+5
View File
@@ -11,6 +11,7 @@ void take(LayerEffects &into, const LayerEffects &from, LayerEffectKind kind)
case LayerEffectKind::colorOverlay: into.colorOverlay = from.colorOverlay; return;
case LayerEffectKind::innerShadow: into.innerShadow = from.innerShadow; return;
case LayerEffectKind::outerGlow: into.outerGlow = from.outerGlow; return;
case LayerEffectKind::innerGlow: into.innerGlow = from.innerGlow; return;
}
throw std::logic_error("unknown effect kind");
}
@@ -75,6 +76,10 @@ void EditorSession::addEffect(LayerEffectKind kind)
if (!effects.outerGlow)
effects.outerGlow = OuterGlowEffect();
break;
case LayerEffectKind::innerGlow:
if (!effects.innerGlow)
effects.innerGlow = InnerGlowEffect();
break;
}
setEffects(effects, id, QStringLiteral("Add ") + rawValue(kind));
selectEffect(kind, id, true);
+14 -2
View File
@@ -61,6 +61,11 @@ bool OuterGlowEffect::isValid() const
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
}
bool InnerGlowEffect::isValid() const
{
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
}
QString rawValue(LayerEffectKind kind)
{
switch (kind) {
@@ -69,6 +74,7 @@ QString rawValue(LayerEffectKind kind)
case LayerEffectKind::colorOverlay: return QStringLiteral("Color Overlay");
case LayerEffectKind::innerShadow: return QStringLiteral("Inner Shadow");
case LayerEffectKind::outerGlow: return QStringLiteral("Outer Glow");
case LayerEffectKind::innerGlow: return QStringLiteral("Inner Glow");
}
throw std::logic_error("unknown effect kind");
}
@@ -76,7 +82,7 @@ QString rawValue(LayerEffectKind kind)
bool LayerEffects::isValid() const
{
return (!stroke || stroke->isValid()) && (!shadow || shadow->isValid()) && (!colorOverlay || colorOverlay->isValid())
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid());
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid()) && (!innerGlow || innerGlow->isValid());
}
std::vector<LayerEffectKind> LayerEffects::kinds() const
@@ -97,6 +103,7 @@ bool LayerEffects::contains(LayerEffectKind kind) const
case LayerEffectKind::colorOverlay: return colorOverlay.has_value();
case LayerEffectKind::innerShadow: return innerShadow.has_value();
case LayerEffectKind::outerGlow: return outerGlow.has_value();
case LayerEffectKind::innerGlow: return innerGlow.has_value();
}
throw std::logic_error("unknown effect kind");
}
@@ -109,6 +116,7 @@ bool LayerEffects::isEnabled(LayerEffectKind kind) const
case LayerEffectKind::colorOverlay: return colorOverlay && colorOverlay->isEnabled();
case LayerEffectKind::innerShadow: return innerShadow && innerShadow->isEnabled();
case LayerEffectKind::outerGlow: return outerGlow && outerGlow->isEnabled();
case LayerEffectKind::innerGlow: return innerGlow && innerGlow->isEnabled();
}
throw std::logic_error("unknown effect kind");
}
@@ -121,6 +129,7 @@ std::optional<PaletteColor> LayerEffects::color(LayerEffectKind kind) const
case LayerEffectKind::colorOverlay: return colorOverlay ? std::optional(colorOverlay->color()) : std::nullopt;
case LayerEffectKind::innerShadow: return innerShadow ? std::optional(innerShadow->color()) : std::nullopt;
case LayerEffectKind::outerGlow: return outerGlow ? std::optional(outerGlow->color()) : std::nullopt;
case LayerEffectKind::innerGlow: return innerGlow ? std::optional(innerGlow->color()) : std::nullopt;
}
throw std::logic_error("unknown effect kind");
}
@@ -156,6 +165,7 @@ void LayerEffects::setColor(const PaletteColor &color, LayerEffectKind kind)
case LayerEffectKind::colorOverlay: paint(colorOverlay, color); return;
case LayerEffectKind::innerShadow: paint(innerShadow, color); return;
case LayerEffectKind::outerGlow: paint(outerGlow, color); return;
case LayerEffectKind::innerGlow: paint(innerGlow, color); return;
}
throw std::logic_error("unknown effect kind");
}
@@ -168,6 +178,7 @@ void LayerEffects::remove(LayerEffectKind kind)
case LayerEffectKind::colorOverlay: colorOverlay.reset(); return;
case LayerEffectKind::innerShadow: innerShadow.reset(); return;
case LayerEffectKind::outerGlow: outerGlow.reset(); return;
case LayerEffectKind::innerGlow: innerGlow.reset(); return;
}
throw std::logic_error("unknown effect kind");
}
@@ -180,11 +191,12 @@ void LayerEffects::setEnabled(bool enabled, LayerEffectKind kind)
case LayerEffectKind::colorOverlay: enable(colorOverlay, enabled); return;
case LayerEffectKind::innerShadow: enable(innerShadow, enabled); return;
case LayerEffectKind::outerGlow: enable(outerGlow, enabled); return;
case LayerEffectKind::innerGlow: enable(innerGlow, enabled); return;
}
throw std::logic_error("unknown effect kind");
}
LayerEffects LayerEffects::visible() const
{
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow)};
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow), shown(innerGlow)};
}
+19 -4
View File
@@ -88,10 +88,24 @@ struct OuterGlowEffect {
friend bool operator==(const OuterGlowEffect &, const OuterGlowEffect &) = default;
};
// A glow inside the layer's edges, fading inward.
struct InnerGlowEffect {
std::optional<bool> enabled = std::nullopt;
double size = 10;
double red = 1;
double green = 1;
double blue = 1;
double opacity = 0.75;
bool isEnabled() const { return enabled.value_or(true); }
PaletteColor color() const { return {red, green, blue}; }
bool isValid() const;
friend bool operator==(const InnerGlowEffect &, const InnerGlowEffect &) = default;
};
// Swift's raw values are the panel's and the menu's words.
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow };
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow};
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow, innerGlow };
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow, LayerEffectKind::innerGlow};
QString rawValue(LayerEffectKind kind);
// What a layer draws round itself; its pixels stay untouched.
@@ -101,7 +115,8 @@ struct LayerEffects {
std::optional<ColorOverlayEffect> colorOverlay = std::nullopt;
std::optional<InnerShadowEffect> innerShadow = std::nullopt;
std::optional<OuterGlowEffect> outerGlow = std::nullopt;
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow; }
std::optional<InnerGlowEffect> innerGlow = std::nullopt;
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow && !innerGlow; }
bool isValid() const;
std::vector<LayerEffectKind> kinds() const;
bool contains(LayerEffectKind kind) const;
+32 -5
View File
@@ -1,9 +1,11 @@
#include "Document/ImageTrim.h"
#include "IO/CanvasResizer.h"
#include "IO/ImageExporter.h"
#include "IO/ImageResizer.h"
#include "IO/ProjectController.h"
#include "UI/CanvasSizeSheet.h"
#include "UI/ImageSizeSheet.h"
#include "UI/TrimSheet.h"
#include <QDialog>
#include <QFutureWatcher>
#include <QVBoxLayout>
@@ -37,7 +39,7 @@ void ProjectController::canvasSize(std::function<void()> done)
connect(dialog, &QDialog::finished, this, [this, chosen, done] {
const std::optional<CanvasSizeOptions> options = *chosen;
resizeProject(options ? [options = *options](const ProjectSnapshot &snapshot) { return CanvasResizer::resize(snapshot, options); }
: std::function<ProjectSnapshot(const ProjectSnapshot &)>(),
: std::function<std::optional<ProjectSnapshot>(const ProjectSnapshot &)>(),
[this](const ProjectSnapshot &resized) { session.applyDocumentSize(resized, QStringLiteral("Canvas Size")); },
QStringLiteral("Couldn’t change canvas size"), done);
});
@@ -60,14 +62,37 @@ void ProjectController::imageSize(std::function<void()> done)
connect(dialog, &QDialog::finished, this, [this, chosen, done] {
const std::optional<ImageSizeOptions> options = *chosen;
resizeProject(options ? [options = *options](const ProjectSnapshot &snapshot) { return ImageResizer::resize(snapshot, options); }
: std::function<ProjectSnapshot(const ProjectSnapshot &)>(),
: std::function<std::optional<ProjectSnapshot>(const ProjectSnapshot &)>(),
[this](const ProjectSnapshot &resized) { session.applyImageSize(resized); }, QStringLiteral("Couldn’t resize the image"), done);
});
dialog->open();
}
void ProjectController::trim(std::function<void()> done)
{
if (!window || !session.document() || !begin()) {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
return;
}
QDialog *dialog = sheet(QStringLiteral("Trim"));
auto chosen = std::make_shared<std::optional<TrimOptions>>();
dialog->layout()->addWidget(new TrimSheet([dialog, chosen](std::optional<TrimOptions> options) {
*chosen = options;
dialog->done(options ? QDialog::Accepted : QDialog::Rejected);
}, dialog));
connect(dialog, &QDialog::finished, this, [this, chosen, done] {
const std::optional<TrimOptions> options = *chosen;
resizeProject(options ? [options = *options](const ProjectSnapshot &snapshot) { return ImageTrim::trim(snapshot, options); }
: std::function<std::optional<ProjectSnapshot>(const ProjectSnapshot &)>(),
[this](const ProjectSnapshot &trimmed) { session.applyDocumentSize(trimmed, QStringLiteral("Trim")); },
QStringLiteral("Couldn’t trim image"), done);
});
dialog->open();
}
// Cancelled, nothing runs; running out of memory fails a render.
void ProjectController::resizeProject(std::function<ProjectSnapshot(const ProjectSnapshot &)> resize, std::function<void(const ProjectSnapshot &)> land,
void ProjectController::resizeProject(std::function<std::optional<ProjectSnapshot>(const ProjectSnapshot &)> resize, std::function<void(const ProjectSnapshot &)> land,
const QString &failure, std::function<void()> done)
{
const auto end = [this, done] {
@@ -89,11 +114,13 @@ void ProjectController::resizeProject(std::function<ProjectSnapshot(const Projec
watcher->deleteLater();
try {
const Resized result = watcher->future().takeResult();
if (!result.snapshot) {
if (!result.failure.isEmpty()) {
explain(result.failure);
return;
}
land(*result.snapshot);
// Swift's trim with nothing left changes nothing.
if (result.snapshot)
land(*result.snapshot);
} catch (const std::bad_alloc &) {
explain(render);
return;
+3 -2
View File
@@ -40,6 +40,7 @@ public:
// Swift's sheets over the window; resizes off the UI thread.
void canvasSize(std::function<void()> done = {});
void imageSize(std::function<void()> done = {});
void trim(std::function<void()> done = {});
void exportPNG(std::function<void()> done = {});
void exportJPEG(std::function<void()> done = {});
@@ -68,8 +69,8 @@ private:
void showError(const QString &title, const QString &message, std::function<void()> then);
// Swift's beginSheet: window-modal over the window; Escape cancels.
QDialog *sheet(const QString &title);
// Off the UI thread, then landed or explained; freed after.
void resizeProject(std::function<ProjectSnapshot(const ProjectSnapshot &)> resize, std::function<void(const ProjectSnapshot &)> land,
// Off the UI thread, then landed, skipped or explained.
void resizeProject(std::function<std::optional<ProjectSnapshot>(const ProjectSnapshot &)> resize, std::function<void(const ProjectSnapshot &)> land,
const QString &failure, std::function<void()> done);
void exportPanel(const QString &title, const QString &filter, const QStringList &suffixes, const QString &name, const QString &failure,
std::function<void(std::optional<QString>)> then);
+29 -6
View File
@@ -118,15 +118,20 @@ GradientMapSettings gradientMap(const QJsonValue &value)
return {colour(object.value("shadows")), colour(object.value("highlights")), boolean(object.value("reversed"))};
}
// Swift's UInt32 refuses what it cannot hold.
quint32 unsigned32(const QJsonValue &value)
{
const qint64 seed = integer(value);
if (seed < 0 || seed > std::numeric_limits<quint32>::max())
refuse();
return quint32(seed);
}
GrainSettings grain(const QJsonValue &value)
{
const QJsonObject object = value.toObject();
// Swift's UInt32 refuses what it cannot hold.
const qint64 seed = integer(object.value("seed"));
if (seed < 0 || seed > std::numeric_limits<quint32>::max())
refuse();
return {.amount = number(object.value("amount")), .size = number(object.value("size")), .roughness = number(object.value("roughness")),
.seed = quint32(seed)};
.seed = unsigned32(object.value("seed"))};
}
BlackWhiteSettings blackWhite(const QJsonValue &value)
@@ -205,7 +210,14 @@ LayerAdjustment ManifestJson::adjustment(const QJsonValue &value)
.gradientMapSettings = optional(object, "gradientMapSettings", gradientMap),
.grainSettings = optional(object, "grainSettings", grain),
.blackWhiteSettings = optional(object, "blackWhiteSettings", blackWhite),
.colorBalanceSettings = optional(object, "colorBalanceSettings", colorBalance)};
.colorBalanceSettings = optional(object, "colorBalanceSettings", colorBalance),
.blurRadius = optional(object, "blurRadius", number),
.motionAngle = optional(object, "motionAngle", number),
.motionDistance = optional(object, "motionDistance", number),
.noiseAmount = optional(object, "noiseAmount", number),
.noiseGaussian = optional(object, "noiseGaussian", boolean),
.noiseMonochromatic = optional(object, "noiseMonochromatic", boolean),
.noiseSeed = optional(object, "noiseSeed", unsigned32)};
}
QJsonObject ManifestJson::encoded(const LayerAdjustment &adjustment)
@@ -248,5 +260,16 @@ QJsonObject ManifestJson::encoded(const LayerAdjustment &adjustment)
{"highlightCyanRed", balance.highlightCyanRed}, {"highlightMagentaGreen", balance.highlightMagentaGreen},
{"highlightYellowBlue", balance.highlightYellowBlue}, {"preserveLuminosity", balance.preserveLuminosity}});
}
const auto put = [&object](const char *key, const auto &value) {
if (value)
object.insert(QLatin1String(key), *value);
};
put("blurRadius", adjustment.blurRadius);
put("motionAngle", adjustment.motionAngle);
put("motionDistance", adjustment.motionDistance);
put("noiseAmount", adjustment.noiseAmount);
put("noiseGaussian", adjustment.noiseGaussian);
put("noiseMonochromatic", adjustment.noiseMonochromatic);
put("noiseSeed", adjustment.noiseSeed ? std::optional(qint64(*adjustment.noiseSeed)) : std::nullopt);
return object;
}
+16 -1
View File
@@ -46,6 +46,13 @@ OuterGlowEffect outerGlow(const QJsonValue &value)
number(keys.value("blue")), number(keys.value("opacity"))};
}
InnerGlowEffect innerGlow(const QJsonValue &value)
{
const QJsonObject keys = object(value);
return {optional(keys, "enabled", boolean), number(keys.value("size")), number(keys.value("red")), number(keys.value("green")),
number(keys.value("blue")), number(keys.value("opacity"))};
}
void colour(QJsonObject &keys, double red, double green, double blue)
{
keys.insert("red", red);
@@ -58,7 +65,7 @@ LayerEffects ManifestJson::effects(const QJsonValue &value)
{
const QJsonObject keys = object(value);
return {optional(keys, "stroke", stroke), optional(keys, "shadow", shadow), optional(keys, "colorOverlay", colorOverlay),
optional(keys, "innerShadow", innerShadow), optional(keys, "outerGlow", outerGlow)};
optional(keys, "innerShadow", innerShadow), optional(keys, "outerGlow", outerGlow), optional(keys, "innerGlow", innerGlow)};
}
QJsonObject ManifestJson::encoded(const LayerEffects &effects)
@@ -104,5 +111,13 @@ QJsonObject ManifestJson::encoded(const LayerEffects &effects)
object.insert("enabled", *glow.enabled);
keys.insert("outerGlow", object);
}
if (effects.innerGlow) {
const InnerGlowEffect &glow = *effects.innerGlow;
QJsonObject object{{"size", glow.size}, {"opacity", glow.opacity}};
colour(object, glow.red, glow.green, glow.blue);
if (glow.enabled)
object.insert("enabled", *glow.enabled);
keys.insert("innerGlow", object);
}
return keys;
}
+8 -3
View File
@@ -62,7 +62,7 @@ void validate(const ProjectManifest &manifest)
{
if (manifest.format != QLatin1String("com.compositor.project"))
throw ProjectError(ProjectError::Kind::invalid);
if (manifest.version < 1 || manifest.version > 8)
if (manifest.version < 1 || manifest.version > 9)
throw ProjectError::unsupportedVersion(manifest.version);
if (manifest.colorSpace != QLatin1String("sRGB"))
throw ProjectError(ProjectError::Kind::invalid);
@@ -78,6 +78,11 @@ void validate(const ProjectManifest &manifest)
// Version 7's adjustments hold settings, never pixels or children.
if (layer.adjustment && (manifest.version < 7 || layer.isGroup == true || layer.imageFile || !layer.adjustment->isValid()))
throw ProjectError(ProjectError::Kind::invalid);
// Blur and noise layers came with version 9.
if (layer.adjustment && manifest.version < 9
&& (layer.adjustment->kind == AdjustmentKind::gaussianBlur || layer.adjustment->kind == AdjustmentKind::motionBlur
|| layer.adjustment->kind == AdjustmentKind::addNoise))
throw ProjectError(ProjectError::Kind::invalid);
const bool group = layer.isGroup == true;
// Layer masks came with version 4, folder masks with 6.
const bool maskAllowed = !layer.maskFile
@@ -251,7 +256,7 @@ ProjectError::ProjectError(Kind kind) : ProjectError(kind, std::nullopt, descrip
ProjectError ProjectError::unsupportedVersion(qint64 version)
{
return ProjectError(Kind::version, version,
QStringLiteral("This project uses format version %1. This app supports versions 1–8.").arg(version));
QStringLiteral("This project uses format version %1. This app supports versions 1–9.").arg(version));
}
ProjectError::ProjectError(Kind kind, std::optional<qint64> version, const QString &description)
@@ -327,7 +332,7 @@ ProjectSnapshot ProjectStore::load(const QString &path)
const auto [format, version] = ProjectManifest::header(metadata);
if (format != QLatin1String("com.compositor.project"))
throw ProjectError(ProjectError::Kind::invalid);
if (version < 1 || version > 8)
if (version < 1 || version > 9)
throw ProjectError::unsupportedVersion(version);
const ProjectManifest manifest = ProjectManifest::decoded(metadata);
validate(manifest);
+1 -1
View File
@@ -52,7 +52,7 @@ QString uuidString(const QUuid &id);
struct ProjectManifest {
QString format = QStringLiteral("com.compositor.project");
// Swift's Int: a wild number must reach validation whole.
qint64 version = 8;
qint64 version = 9;
QString colorSpace = QStringLiteral("sRGB");
// Version-1 projects carry none: 72 pixels per inch.
std::optional<double> resolution = std::nullopt;
+7 -3
View File
@@ -3,14 +3,18 @@
#include "Logging.h"
#include <QPaintEngine>
void AdjustmentSurface::draw(QPainter &context, const std::function<void(QPainter &)> &body, qint64 pixelBudget)
void AdjustmentSurface::draw(QPainter &context, const std::function<void(QPainter &)> &body, double padding, qint64 pixelBudget)
{
// The engine's device holds the pixels: a widget's window store.
const QPaintDevice &target = *context.paintEngine()->paintDevice();
const QRect device(0, 0, target.width(), target.height());
const QRect area = context.deviceTransform().mapRect(BrushRaster::visibleRect(context)).toAlignedRect().intersected(device);
if (area.isEmpty())
const QTransform toDevice = context.deviceTransform();
const QRect output = toDevice.mapRect(BrushRaster::visibleRect(context)).toAlignedRect().intersected(device);
if (output.isEmpty())
return;
// Spatial adjustments read a halo; the clip keeps what shows.
const QRectF shown = toDevice.inverted().mapRect(QRectF(output));
const QRect area = padding > 0 ? toDevice.mapRect(shown.adjusted(-padding, -padding, padding, padding)).toAlignedRect() : output;
if (qint64(area.width()) * area.height() > pixelBudget) {
qCWarning(lcRendering) << "an adjustment surface passes its pixel budget:" << area.size();
return;
+3 -1
View File
@@ -5,5 +5,7 @@
// Draws through an image surface over what the painter shows.
namespace AdjustmentSurface {
inline constexpr qint64 pixelBudget = 100'000'000;
void draw(QPainter &context, const std::function<void(QPainter &)> &body, qint64 pixelBudget = AdjustmentSurface::pixelBudget);
// Padding, in painter units, is drawn past what shows.
void draw(QPainter &context, const std::function<void(QPainter &)> &body, double padding = 0,
qint64 pixelBudget = AdjustmentSurface::pixelBudget);
}
+17 -2
View File
@@ -34,6 +34,18 @@ void drawShadow(const QRectF &rect, QPainter &context)
context.restore();
}
// Swift's halo: the widest reach of a visible adjustment.
double samplingMargin(const CanvasDocument &document)
{
const QSet<QUuid> visible = document.effectiveVisibleIDs();
double margin = 0;
for (const ImageLayer &layer : document.layers) {
if (layer.adjustment && visible.contains(layer.id))
margin = std::max(margin, layer.adjustment->samplingMargin());
}
return margin;
}
std::optional<ImageIdentity> enabledMaskID(const std::optional<LayerMask> &mask)
{
return mask && mask->isEnabled ? std::optional(mask->asset.identity()) : std::nullopt;
@@ -246,7 +258,7 @@ void CanvasView::draw(QPainter &context, const QRectF &dirty)
// Its own group: blend modes never meet the checkerboard.
AdjustmentSurface::draw(context, [&](QPainter &group) {
drawLayers(*document, points, [&](QPointF point) { return viewport.viewPoint(point, document->size()); }, group);
});
}, samplingMargin(*document) * points);
}
if (m_session.showsPixelGrid() && viewport.zoom() >= pixelGridZoom && !visible.isEmpty())
drawPixelGrid(visible, *document, context);
@@ -419,6 +431,7 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
LiveMaskRenderer live([&](QUuid id) { return byID.contains(id) ? byID.at(id).maskSourceID : std::nullopt; }, drawOwnWithDraft);
live.adjustment = [&](QUuid id) { return byID.contains(id) ? byID.at(id).adjustment : std::nullopt; };
live.adjustmentOpacity = [&](QUuid id) { return byID.at(id).effectiveOpacity(byID); };
live.adjustmentScale = scale;
live.adjustmentClip = [&](QUuid id, const QPainter &painter, QImage &coverage) {
const ImageLayer &layer = byID.at(id);
if (!layer.mask || !layer.mask->isEnabled)
@@ -468,7 +481,9 @@ void CanvasView::drawDocumentPixels(const QRectF &view, const QRectF &pixels, co
}
{
QPainter painter(&raster);
drawLayers(document, 1, [&](QPointF point) { return QPointF(point.x() - region.left(), point.y() - region.top()); }, painter);
AdjustmentSurface::draw(painter, [&](QPainter &group) {
drawLayers(document, 1, [&](QPointF point) { return QPointF(point.x() - region.left(), point.y() - region.top()); }, group);
}, samplingMargin(document));
}
const QPointF origin = viewport.viewPoint(region.topLeft(), document.size());
const QRectF target(origin, QSizeF(region.width() * viewport.pointsPerPixel(), region.height() * viewport.pointsPerPixel()));
+3 -1
View File
@@ -150,13 +150,15 @@ std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::render(const Req
effects.shadow->distance *= factor;
effects.shadow->blur *= factor;
}
// Swift forgets the inner shadow and glow: too wide.
// Swift forgets the inner shadow and glows: too wide.
if (effects.innerShadow) {
effects.innerShadow->distance *= factor;
effects.innerShadow->blur *= factor;
}
if (effects.outerGlow)
effects.outerGlow->size *= factor;
if (effects.innerGlow)
effects.innerGlow->size *= factor;
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(pixels, mask, effects);
return Result{rendered.image, rendered.inset, std::nullopt};
} catch (const ExportError &error) {
+14 -1
View File
@@ -206,13 +206,24 @@ QImage LayerEffectsKernel::render(const QImage &pixels, const LayerEffects &effe
std::copy(shape.get(), shape.get() + count, glowing.get());
blurred(glowing.get(), buffer(count).get(), width, height, float(glow->size / 2));
}
const std::optional<InnerGlowEffect> innerGlow =
effects.innerGlow && effects.innerGlow->isEnabled() && effects.innerGlow->size > 0 && effects.innerGlow->opacity > 0 ? effects.innerGlow : std::nullopt;
Buffer glowingInside;
if (innerGlow) {
// The shape softened; what it loses stays inside.
glowingInside = buffer(count);
std::copy(shape.get(), shape.get() + count, glowingInside.get());
blurred(glowingInside.get(), buffer(count).get(), width, height, float(innerGlow->size / 2));
for (qint64 index = 0; index < count; ++index)
glowingInside[index] = std::clamp(shape[index] * (1.f - glowingInside[index]), 0.f, 1.f);
}
const std::optional<ColorOverlayEffect> overlay = effects.colorOverlay && effects.colorOverlay->isEnabled() && effects.colorOverlay->opacity > 0
? effects.colorOverlay : std::nullopt;
QImage result = BrushRaster::context(width, height, false);
// Taken once: scanLine() detaches, which workers must not race.
uchar *const bits = result.bits();
const qsizetype stride = result.bytesPerLine();
// Shadow, glow, outside stroke, pixels, overlay, inner shadow, inside stroke.
// Shadow, glow, outside stroke, pixels, overlay, inner glow, shadow, stroke.
bands(height, [&](int, int y) {
const uchar *in = source.constScanLine(y);
uchar *out = bits + y * stride;
@@ -232,6 +243,8 @@ QImage LayerEffectsKernel::render(const QImage &pixels, const LayerEffects &effe
alpha = sourceAlpha + alpha * (1.f - sourceAlpha);
if (overlay)
over(colour, alpha, overlay->color(), std::clamp(shape[index] * float(overlay->opacity), 0.f, 1.f));
if (innerGlow)
over(colour, alpha, innerGlow->color(), std::clamp(glowingInside[index] * float(innerGlow->opacity), 0.f, 1.f));
if (inner)
over(colour, alpha, inner->color(), std::clamp(inside[index] * float(inner->opacity), 0.f, 1.f));
if (stroke && stroke->inside)
+2
View File
@@ -54,6 +54,8 @@ double LayerEffectsSurface::reach() const
reach = std::max(reach, m_effects.innerShadow->distance + m_effects.innerShadow->blur * 3 + 2);
if (m_effects.outerGlow)
reach = std::max(reach, m_effects.outerGlow->size * 3 + 2);
if (m_effects.innerGlow)
reach = std::max(reach, m_effects.innerGlow->size * 3 + 2);
return reach;
}
+2 -1
View File
@@ -216,7 +216,8 @@ void LiveMaskRenderer::adjust(QUuid id, QPainter &context, const QImage &clip)
if (original.isNull())
throw ExportError(ExportError::Kind::render);
original.setDevicePixelRatio(1);
QImage adjusted = adjustment(id).value().apply(original, region);
// Device pixels a document pixel, where Swift's surface counts points.
QImage adjusted = adjustment(id).value().apply(original, region, adjustmentScale * device->width() / region.width());
const auto found = m_modes.find(id);
const LayerBlendMode mode = found != m_modes.end() ? found->second : LayerBlendMode::normal;
if (mode != LayerBlendMode::normal)
+2
View File
@@ -28,6 +28,8 @@ public:
std::function<std::optional<LayerAdjustment>(QUuid)> adjustment = [](QUuid) { return std::optional<LayerAdjustment>(); };
std::function<double(QUuid)> adjustmentOpacity = [](QUuid) { return 1.0; };
std::function<void(QUuid, const QPainter &, QImage &coverage)> adjustmentClip = [](QUuid, const QPainter &, QImage &) {};
// Painter units a document pixel: the canvas's zoom.
double adjustmentScale = 1;
private:
// The pixels beneath, adjusted and mixed back through coverage.
+2
View File
@@ -209,6 +209,7 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
image->addSeparator();
add(image, QStringLiteral("canvasSize"), QStringLiteral("Canvas Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C), [this] { projects().canvasSize(); });
add(image, QStringLiteral("imageSize"), QStringLiteral("Image Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_I), [this] { projects().imageSize(); });
add(image, QStringLiteral("trim"), QStringLiteral("Trim…"), QKeySequence(), [this] { projects().trim(); });
image->addSeparator();
add(image, QStringLiteral("flipCanvasHorizontal"), QStringLiteral("Flip Canvas Horizontal"), QKeySequence(), [this] { session().flipCanvas(true); });
add(image, QStringLiteral("flipCanvasVertical"), QStringLiteral("Flip Canvas Vertical"), QKeySequence(), [this] { session().flipCanvas(false); });
@@ -365,6 +366,7 @@ void CompositorMenus::synchronize()
action(QStringLiteral("invert"))->setEnabled(s.canInvert());
action(QStringLiteral("canvasSize"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("imageSize"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("trim"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("flipCanvasHorizontal"))->setEnabled(s.canEditLayers());
action(QStringLiteral("flipCanvasVertical"))->setEnabled(s.canEditLayers());
action(QStringLiteral("newAdjustmentLayer"))->setEnabled(s.canEditLayers());
+7
View File
@@ -92,6 +92,13 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
slider(QStringLiteral("Opacity"), [](const LayerEffects &e) { return percent(e.outerGlow); },
[](LayerEffects &e, double value) { e.outerGlow->opacity = value / 100; }, 0, 100, QStringLiteral("%"));
break;
case LayerEffectKind::innerGlow:
header(QStringLiteral("Inner Glow"), false);
slider(QStringLiteral("Size"), [](const LayerEffects &e) { return e.innerGlow ? std::optional(e.innerGlow->size) : std::nullopt; },
[](LayerEffects &e, double value) { e.innerGlow->size = value; }, 0, 100, QStringLiteral("px"), 500);
slider(QStringLiteral("Opacity"), [](const LayerEffects &e) { return percent(e.innerGlow); },
[](LayerEffects &e, double value) { e.innerGlow->opacity = value / 100; }, 0, 100, QStringLiteral("%"));
break;
}
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("effectsCancel"));
+49
View File
@@ -1,6 +1,8 @@
#include "UI/LayerIcons.h"
#include "UI/ToolIcons.h"
#include <QPainterPath>
#include <cmath>
#include <numbers>
namespace {
void eye(QPainter &painter, bool slashed)
@@ -209,6 +211,47 @@ void sparkles(QPainter &painter)
star(QPointF(14.5, 14), 2);
}
// Swift's drop.fill: a filled teardrop, point up.
void drop(QPainter &painter)
{
QPainterPath path;
path.moveTo(9, 1.5);
path.cubicTo(11, 5, 14.5, 8.5, 14.5, 11.3);
path.cubicTo(14.5, 14.4, 12, 16.5, 9, 16.5);
path.cubicTo(6, 16.5, 3.5, 14.4, 3.5, 11.3);
path.cubicTo(3.5, 8.5, 7, 5, 9, 1.5);
painter.fillPath(path, painter.pen().color());
}
// Swift's wind: three strokes, the outer two curling back.
void wind(QPainter &painter)
{
QPainterPath top;
top.moveTo(1.5, 6);
top.lineTo(12, 6);
top.cubicTo(14.5, 6, 15, 2.5, 12.5, 2.5);
QPainterPath middle;
middle.moveTo(1.5, 9.5);
middle.lineTo(15, 9.5);
QPainterPath bottom;
bottom.moveTo(1.5, 13);
bottom.lineTo(10, 13);
bottom.cubicTo(12.5, 13, 13, 16.5, 10.5, 16.5);
for (const QPainterPath &path : {top, middle, bottom})
painter.drawPath(path);
}
// Swift's circle.dotted: twelve dots round a ring.
void dottedCircle(QPainter &painter)
{
painter.setBrush(painter.pen().color());
painter.setPen(Qt::NoPen);
for (int index = 0; index < 12; ++index) {
const double angle = index * std::numbers::pi / 6;
painter.drawEllipse(QPointF(9 + 6.8 * std::cos(angle), 9 + 6.8 * std::sin(angle)), 1.1, 1.1);
}
}
// Swift's scope: a ring with four ticks toward its middle.
void scope(QPainter &painter)
{
@@ -255,6 +298,9 @@ void LayerIcons::paint(QPainter &painter, LayerIcon icon, QPointF origin, double
case LayerIcon::axes: axes(painter); break;
case LayerIcon::scope: scope(painter); break;
case LayerIcon::lineDiagonal: painter.drawLine(QPointF(3, 15), QPointF(15, 3)); break;
case LayerIcon::drop: drop(painter); break;
case LayerIcon::wind: wind(painter); break;
case LayerIcon::dottedCircle: dottedCircle(painter); break;
}
painter.restore();
}
@@ -278,6 +324,9 @@ LayerIcon LayerIcons::symbol(AdjustmentKind kind)
case AdjustmentKind::exposure: return LayerIcon::plusMinusCircle;
case AdjustmentKind::gradientMap: return LayerIcon::paintPalette;
case AdjustmentKind::grain: return LayerIcon::circleGrid;
case AdjustmentKind::addNoise: return LayerIcon::dottedCircle;
case AdjustmentKind::gaussianBlur: return LayerIcon::drop;
case AdjustmentKind::motionBlur: return LayerIcon::wind;
case AdjustmentKind::invert: return LayerIcon::rightHalfCircle;
case AdjustmentKind::blackWhite: return LayerIcon::hatchedCircle;
case AdjustmentKind::colorBalance: return LayerIcon::axes;
+1 -1
View File
@@ -8,7 +8,7 @@
enum class LayerIcon {
eye, eyeSlash, chevronRight, chevronDown, folder, link, newLayer, newFolder, addMask, trash, layers, text,
halfFilledCircle, sliders, curvePath, plusMinusCircle, paintPalette, circleGrid, sparkles, rightHalfCircle, hatchedCircle, axes,
scope, lineDiagonal
scope, lineDiagonal, drop, wind, dottedCircle
};
namespace LayerIcons {
+96
View File
@@ -0,0 +1,96 @@
#include "UI/TrimSheet.h"
#include "UI/KeyboardShortcuts.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QFrame>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QRadioButton>
#include <QVBoxLayout>
#include <algorithm>
namespace {
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QWidget *parent)
{
auto *label = new QLabel(words, parent);
label->setTextFormat(Qt::PlainText);
QFont font = label->font();
font.setPixelSize(pixels);
font.setWeight(weight);
label->setFont(font);
return label;
}
QFrame *divider(QWidget *parent)
{
auto *line = new QFrame(parent);
line->setFrameShape(QFrame::HLine);
line->setForegroundRole(QPalette::Mid);
return line;
}
}
TrimSheet::TrimSheet(std::function<void(std::optional<TrimOptions>)> finish, QWidget *parent)
: QWidget(parent), m_finish(std::move(finish)), m_basedOn(new QButtonGroup(this)), m_ok(new QPushButton(QStringLiteral("OK"), this))
{
setFixedWidth(320);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(18);
column->addWidget(text(QStringLiteral("Trim"), 17, QFont::Bold, this));
// Swift's radio group, its label hidden behind the headline.
auto *basis = new QVBoxLayout;
basis->setSpacing(8);
basis->addWidget(text(QStringLiteral("Based On"), 13, QFont::DemiBold, this));
for (const TrimBasedOn option : allTrimBasedOn) {
auto *radio = new QRadioButton(rawValue(option), this);
radio->setObjectName(QStringLiteral("trimBasedOn%1").arg(int(option)));
radio->setChecked(option == TrimBasedOn::transparentPixels);
m_basedOn->addButton(radio, int(option));
basis->addWidget(radio);
}
column->addLayout(basis);
column->addWidget(divider(this));
auto *away = new QVBoxLayout;
away->setSpacing(8);
away->addWidget(text(QStringLiteral("Trim Away"), 13, QFont::DemiBold, this));
auto *grid = new QGridLayout;
grid->setHorizontalSpacing(24);
grid->setVerticalSpacing(8);
const std::array<QString, 4> names = {QStringLiteral("Top"), QStringLiteral("Bottom"), QStringLiteral("Left"), QStringLiteral("Right")};
for (size_t index = 0; index < names.size(); ++index) {
m_edges[index] = new QCheckBox(names[index], this);
m_edges[index]->setObjectName(QStringLiteral("trim") + names[index]);
m_edges[index]->setChecked(true);
grid->addWidget(m_edges[index], int(index / 2), int(index % 2));
// OK rests while no edge is checked.
connect(m_edges[index], &QCheckBox::toggled, this, [this] {
m_ok->setEnabled(std::any_of(m_edges.begin(), m_edges.end(), [](const QCheckBox *edge) { return edge->isChecked(); }));
});
}
grid->setColumnStretch(2, 1);
away->addLayout(grid);
column->addLayout(away);
column->addWidget(divider(this));
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("trimCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_ok->setObjectName(QStringLiteral("trimOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, cancel);
connect(m_ok, &QPushButton::clicked, this, [this] {
m_finish(TrimOptions{.basedOn = TrimBasedOn(m_basedOn->checkedId()), .top = m_edges[0]->isChecked(), .bottom = m_edges[1]->isChecked(),
.left = m_edges[2]->isChecked(), .right = m_edges[3]->isChecked()});
});
auto *buttons = new QHBoxLayout;
buttons->addWidget(cancel);
buttons->addStretch(1);
buttons->addWidget(m_ok);
column->addLayout(buttons);
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "Document/ImageTrim.h"
#include <QWidget>
#include <array>
#include <functional>
class QButtonGroup;
class QCheckBox;
class QPushButton;
// Swift's TrimSheet: the basis and the edges trimmed away.
class TrimSheet : public QWidget {
Q_OBJECT
public:
explicit TrimSheet(std::function<void(std::optional<TrimOptions>)> finish, QWidget *parent = nullptr);
private:
const std::function<void(std::optional<TrimOptions>)> m_finish;
QButtonGroup *const m_basedOn;
// Top, bottom, left, right.
std::array<QCheckBox *, 4> m_edges{};
QPushButton *const m_ok;
};