#include "Document/EditorSession.h" #include "Document/LayerEffects+Renderer.h" #include "IO/ImageExporter.h" #include "IO/ProjectStore+Json.h" #include #include // 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>{ [](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"