Blur and noise adjustment layers, Image Trim, Inner Glow (12.8c)
This commit is contained in:
1 parent
7ae263de80
commit
31e2dd3864
60 files changed
+1673
-105
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user