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

+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;