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

+217
View File
@@ -0,0 +1,217 @@
#include "Document/EditorSession.h"
#include "Document/LayerEffects+Renderer.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore+Json.h"
#include <QPainter>
#include <QtTest>
// Swift's InnerGlowTests: the glow's model, drawing and export.
namespace {
// Swift's solidSquare: opaque, one colour.
QImage square(int size, const QColor &colour)
{
QImage image(size, size, QImage::Format_RGBA8888_Premultiplied);
image.fill(colour);
return image;
}
QColor at(const LayerEffectsRenderer::Rendered &rendered, int x, int y)
{
const int inset = int(rendered.inset);
return rendered.image.pixelColor(inset + x, inset + y);
}
LayerEffects glowing(double size, double red, double green, double blue, double opacity)
{
LayerEffects effects;
effects.innerGlow = InnerGlowEffect{.enabled = true, .size = size, .red = red, .green = green, .blue = blue, .opacity = opacity};
return effects;
}
}
class InnerGlowTests : public QObject {
Q_OBJECT
private slots:
void innerGlowDefaultsAndValidation();
void innerGlowCodableRoundTrip();
void innerGlowBackwardCompatibility();
void layerEffectsIntegration();
void innerGlowRendersInsideSourceWithoutBoundsExpansion();
void innerGlowRendersAroundTextGlyphs();
void itSitsOverTheOverlayAndUnderTheInnerShadow();
void innerGlowPreservedInExport();
void theSessionAddsCopiesAndCancelsIt();
};
void InnerGlowTests::innerGlowDefaultsAndValidation()
{
const InnerGlowEffect glow;
QVERIFY(glow.isEnabled() && glow.isValid());
QCOMPARE(glow.size, 10.0);
QCOMPARE(glow.opacity, 0.75);
QVERIFY(glow.color() == (PaletteColor{1, 1, 1}));
for (const auto &broken : std::vector<std::function<void(InnerGlowEffect &)>>{
[](InnerGlowEffect &e) { e.size = -1; }, [](InnerGlowEffect &e) { e.size = 500.5; }, [](InnerGlowEffect &e) { e.size = qQNaN(); },
[](InnerGlowEffect &e) { e.opacity = 1.5; }, [](InnerGlowEffect &e) { e.opacity = -0.1; }, [](InnerGlowEffect &e) { e.red = 2; },
[](InnerGlowEffect &e) { e.green = -0.1; }, [](InnerGlowEffect &e) { e.blue = qInf(); }}) {
InnerGlowEffect changed = glow;
broken(changed);
QVERIFY(!changed.isValid());
LayerEffects effects;
effects.innerGlow = changed;
QVERIFY(!effects.isValid());
}
InnerGlowEffect edge = glow;
edge.size = 500;
edge.opacity = 1;
QVERIFY(edge.isValid());
}
void InnerGlowTests::innerGlowCodableRoundTrip()
{
const LayerEffects original = glowing(25, 1, 0.5, 0.2, 0.85);
const QJsonObject encoded = ManifestJson::encoded(original);
const QJsonObject glow = encoded.value("innerGlow").toObject();
QCOMPARE(glow.keys(), (QStringList{"blue", "enabled", "green", "opacity", "red", "size"}));
QVERIFY(glow.value("size") == 25 && glow.value("opacity") == 0.85 && glow.value("green") == 0.5);
QCOMPARE(ManifestJson::effects(encoded), original);
// Absent `enabled` stays absent, read as shown.
LayerEffects shown = original;
shown.innerGlow->enabled = std::nullopt;
QVERIFY(!ManifestJson::encoded(shown).value("innerGlow").toObject().contains("enabled"));
QCOMPARE(ManifestJson::effects(ManifestJson::encoded(shown)), shown);
// A key of the wrong type, or missing, refuses it.
for (const char *key : {"size", "red", "green", "blue", "opacity"}) {
QJsonObject wrong = encoded, bent = glow;
bent.insert(QLatin1String(key), "big");
wrong.insert("innerGlow", bent);
QVERIFY_THROWS_EXCEPTION(ProjectError, ManifestJson::effects(wrong));
bent.remove(QLatin1String(key));
wrong.insert("innerGlow", bent);
QVERIFY_THROWS_EXCEPTION(ProjectError, ManifestJson::effects(wrong));
}
}
void InnerGlowTests::innerGlowBackwardCompatibility()
{
// Effects written before the glow read without it.
const QJsonObject older{{"shadow", QJsonObject{{"angle", 90}, {"distance", 10}, {"blur", 15}, {"red", 0}, {"green", 0}, {"blue", 0}, {"opacity", 0.5}}}};
const LayerEffects decoded = ManifestJson::effects(older);
QVERIFY(decoded.shadow && !decoded.innerGlow && decoded.isValid());
}
void InnerGlowTests::layerEffectsIntegration()
{
LayerEffects effects;
QVERIFY(effects.isEmpty() && !effects.contains(LayerEffectKind::innerGlow));
effects.innerGlow = InnerGlowEffect{.size = 15};
QVERIFY(!effects.isEmpty() && effects.contains(LayerEffectKind::innerGlow) && effects.isEnabled(LayerEffectKind::innerGlow));
QVERIFY((effects.kinds() == std::vector{LayerEffectKind::innerGlow}));
effects.setEnabled(false, LayerEffectKind::innerGlow);
QVERIFY(!effects.isEnabled(LayerEffectKind::innerGlow) && !effects.visible().innerGlow);
effects.setColor(PaletteColor{1, 0.8, 0}, LayerEffectKind::innerGlow);
QVERIFY(effects.color(LayerEffectKind::innerGlow) == (PaletteColor{1, 0.8, 0}));
effects.remove(LayerEffectKind::innerGlow);
QVERIFY(!effects.contains(LayerEffectKind::innerGlow) && effects.isEmpty());
}
void InnerGlowTests::innerGlowRendersInsideSourceWithoutBoundsExpansion()
{
const LayerEffects effects = glowing(12, 1, 1, 0, 1);
// The margin stays the baseline two: nothing spills out.
QCOMPARE(LayerEffectsRenderer::margin(effects), 2.0);
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects);
QCOMPARE(rendered.image.pixelColor(0, 0).alpha(), 0);
QCOMPARE(at(rendered, -1, 20).alpha(), 0);
// Yellow near the edge, black in the deep centre.
const QColor edge = at(rendered, 2, 20), centre = at(rendered, 20, 20);
QVERIFY(edge.alphaF() > 0.9 && edge.redF() > 0.3 && edge.greenF() > 0.3 && edge.blueF() < 0.05);
QVERIFY(centre.redF() < 0.2 && centre.greenF() < 0.2);
// It fades inward: the edge brightest.
QVERIFY(at(rendered, 0, 20).redF() > at(rendered, 4, 20).redF() && at(rendered, 4, 20).redF() > at(rendered, 10, 20).redF());
}
void InnerGlowTests::innerGlowRendersAroundTextGlyphs()
{
// Swift's black O: a ring on clear.
QImage glyph(72, 72, QImage::Format_RGBA8888_Premultiplied);
glyph.fill(Qt::transparent);
QPainter painter(&glyph);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(QPen(Qt::black, 12));
painter.drawEllipse(QRectF(12, 12, 48, 48));
painter.end();
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(glyph, std::nullopt, glowing(8, 1, 0, 0, 0.9));
QVERIFY(rendered.image.width() >= glyph.width() && rendered.image.height() >= glyph.height());
QCOMPARE(rendered.image.pixelColor(0, 0).alpha(), 0);
// Swift's search: some pixel glows red; the hole is clear.
bool found = false;
for (int y = 0; y < glyph.height() && !found; ++y) {
for (int x = 0; x < glyph.width() && !found; ++x)
found = at(rendered, x, y).alphaF() > 0.5 && at(rendered, x, y).redF() > 0.3;
}
QVERIFY(found);
QCOMPARE(at(rendered, 36, 36).alpha(), 0);
}
void InnerGlowTests::itSitsOverTheOverlayAndUnderTheInnerShadow()
{
// Metal's order: overlay, inner glow, inner shadow.
LayerEffects effects = glowing(12, 0, 1, 0, 1);
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 0, .blue = 1, .opacity = 1};
const LayerEffectsRenderer::Rendered covered = LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects);
QVERIFY(at(covered, 0, 20).greenF() > 0.4 && at(covered, 20, 20).blueF() > 0.9);
effects.innerShadow = InnerShadowEffect{.angle = 0, .distance = 0, .blur = 0.01, .red = 1, .green = 0, .blue = 0, .opacity = 1};
const LayerEffectsRenderer::Rendered shadowed = LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects);
QCOMPARE(at(shadowed, 0, 20), at(covered, 0, 20));
// A sideways shadow covers the glow on one edge.
effects.innerShadow->distance = 4;
const int x = effects.innerShadow->offset().width() > 0 ? 0 : 39;
const QColor edge = at(LayerEffectsRenderer::render(square(40, Qt::black), std::nullopt, effects), x, 20);
QVERIFY(std::abs(effects.innerShadow->offset().width()) > 3.9);
QVERIFY(edge.redF() > 0.9 && edge.greenF() < 0.1);
}
void InnerGlowTests::innerGlowPreservedInExport()
{
EditorSession session;
session.createDocument(100, 100);
session.insert(ImportedImage(square(30, Qt::black), QImage(), QStringLiteral("InnerGlowLayer")), QPointF(35, 35));
session.setEffects(glowing(8, 1, 0, 1, 0.9));
const QImage exported = ImageExporter::render(session.projectSnapshot().value()).image;
// The square spans 20 to 50: magenta at its edge.
const QColor edge = exported.pixelColor(21, 35), centre = exported.pixelColor(35, 35);
QVERIFY(edge.alphaF() > 0.8 && edge.redF() > 0.2 && edge.blueF() > 0.2 && edge.greenF() < 0.05);
QVERIFY(centre.redF() < 0.1 && centre.blueF() < 0.1);
QCOMPARE(exported.pixelColor(18, 35).alpha(), 0);
}
void InnerGlowTests::theSessionAddsCopiesAndCancelsIt()
{
EditorSession session;
session.createDocument(40, 40);
session.insert(ImportedImage(square(20, Qt::black), QImage(), QStringLiteral("One")));
const QUuid first = session.activeLayerID().value();
session.addEffect(LayerEffectKind::innerGlow);
QCOMPARE(session.history.undoName(), QString("Add Inner Glow"));
QVERIFY(session.activeEffects().innerGlow == InnerGlowEffect());
session.changeEffects([](LayerEffects &effects) { effects.innerGlow->size = 44; });
session.finishEffectsEditing(false);
QVERIFY(!session.activeEffects().innerGlow);
session.addEffect(LayerEffectKind::innerGlow);
session.changeEffects([](LayerEffects &effects) { effects.innerGlow->size = 44; });
session.finishEffectsEditing(true);
// Adding it again keeps the glow it has.
session.addEffect(LayerEffectKind::innerGlow);
QCOMPARE(session.activeEffects().innerGlow.value().size, 44.0);
session.finishEffectsEditing(true);
session.insert(ImportedImage(square(20, Qt::black), QImage(), QStringLiteral("Two")));
const QUuid second = session.activeLayerID().value();
QVERIFY(session.canCopyEffect(LayerEffectKind::innerGlow, first, second));
session.copyEffect(LayerEffectKind::innerGlow, first, second);
QCOMPARE(session.history.undoName(), QString("Copy Inner Glow"));
QCOMPARE(session.activeEffects().innerGlow.value().size, 44.0);
}
QTEST_MAIN(InnerGlowTests)
#include "InnerGlowTests.moc"