#include "Document/LayerEffects+Renderer.h" #include "Rendering/LayerEffectsSurface.h" #include "RenderFixtures.h" #include #include // Swift's LayerEffectsSurface: a painted layer's effects, kept up. namespace { LayerEffects everyKind() { LayerEffects effects; effects.stroke = StrokeEffect{.size = 5, .red = 1, .green = 1, .blue = 0, .opacity = 1}; effects.shadow = ShadowEffect{.angle = 45, .distance = 4, .blur = 16, .red = 1, .green = 0, .blue = 0, .opacity = 0.8}; effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 0.5}; effects.innerShadow = InnerShadowEffect{.angle = 120, .distance = 3, .blur = 60, .red = 0, .green = 0, .blue = 0, .opacity = 0.7}; return effects; } void addRows() { QTest::addColumn("effects"); const LayerEffects all = everyKind(); LayerEffects outside, inside, cast, overlay, inner; outside.stroke = all.stroke; inside.stroke = StrokeEffect{.size = 10, .red = 1, .green = 0, .blue = 1, .opacity = 0.9, .inside = true}; cast.shadow = all.shadow; overlay.colorOverlay = all.colorOverlay; inner.innerShadow = all.innerShadow; QTest::newRow("outside stroke") << outside; QTest::newRow("inside stroke") << inside; QTest::newRow("shadow") << cast; QTest::newRow("overlay") << overlay; QTest::newRow("inner shadow") << inner; QTest::newRow("every kind") << all; } // A soft disc well inside a clear layer. QImage disc() { QImage image = BrushRaster::context(80, 60, false); QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(Qt::NoPen); painter.setBrush(QColor(40, 90, 200)); painter.drawEllipse(QPointF(40, 30), 15.5, 13); return image; } // Opaque and translucent cells: a window cut short shows. QImage checker(int width, int height) { QImage image = BrushRaster::context(width, height, false); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { const int alpha = (x / 8 + y / 8) % 2 ? 255 : 96; image.setPixel(x, y, qRgba(40 * alpha / 255, 90 * alpha / 255, 200 * alpha / 255, alpha)); } } return image; } // A tile of `base` with a clear hole in it. BrushPatch holed(const QImage &base, const QRect &tile, const QRect &hole) { QImage image = base.copy(tile); QPainter painter(&image); painter.setCompositionMode(QPainter::CompositionMode_Clear); painter.fillRect(hole.translated(-tile.topLeft()), Qt::transparent); return {QRectF(tile), image}; } std::unique_ptr surfaceFor(const QImage &image, const LayerEffects &effects) { return LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(image.size()), QRectF(image.rect())); } ImportedImage asset(const QImage &image) { return ImportedImage(image, QImage(), QStringLiteral("Layer")); } } class LayerEffectsSurfaceTests : public QObject { Q_OBJECT private slots: void theFirstUpdateDrawsWhatTheRendererDraws_data() { addRows(); } void theFirstUpdateDrawsWhatTheRendererDraws(); void newPaintIsRedoneAsFarAsItReaches_data() { addRows(); } void newPaintIsRedoneAsFarAsItReaches(); void tilesAlreadyTakenRedoNothing(); void paintPastTheLayerShowsUnmasked(); void aSmallerMaskScalesSmoothlyAsTheRenderersDoes(); void aSurfaceHoldsEightyMillionPixelsAtMost(); void aSurfaceFitsTheStrokeItWasMadeFor(); }; void LayerEffectsSurfaceTests::theFirstUpdateDrawsWhatTheRendererDraws() { QFETCH(LayerEffects, effects); const QImage image = disc(); const std::unique_ptr surface = surfaceFor(image, effects); QVERIFY(surface && !surface->image()); surface->update(asset(image), {}, std::nullopt); const LayerEffectsRenderer::Rendered expected = LayerEffectsRenderer::render(image, std::nullopt, effects); QCOMPARE(surface->margin, expected.inset); QVERIFY(surface->image().value() == expected.image); // A painted base draws from its tiles, alike, never flattened. const std::unique_ptr tiled = surfaceFor(image, effects); const std::shared_ptr raster = painted(image, {}); tiled->update(ImportedImage(raster, QImage(), QStringLiteral("Painted")), {}, std::nullopt); QVERIFY(tiled->image().value() == expected.image && !raster->hasMaterializedPixels()); } void LayerEffectsSurfaceTests::newPaintIsRedoneAsFarAsItReaches() { QFETCH(LayerEffects, effects); // Two tiles painted at once, each with a hole. const QImage base = checker(900, 900); const std::vector patches{holed(base, QRect(0, 0, 256, 256), QRect(200, 180, 56, 76)), holed(base, QRect(256, 0, 256, 256), QRect(266, 40, 20, 30))}; const std::unique_ptr surface = surfaceFor(base, effects); surface->update(asset(base), {}, std::nullopt); surface->update(asset(base), patches, std::nullopt); // Everything the hole reaches, past its tile too, is redone. QVERIFY(surface->image().value() == LayerEffectsRenderer::render(composite(base, patches), std::nullopt, effects).image); } void LayerEffectsSurfaceTests::tilesAlreadyTakenRedoNothing() { const QImage base = solid(300, 300, qRgba(40, 90, 200, 255)); const std::unique_ptr surface = surfaceFor(base, everyKind()); const BrushPatch red{QRectF(0, 0, 256, 256), solid(256, 256, qRgba(255, 0, 0, 255))}; surface->update(asset(base), {red}, std::nullopt); const qint64 first = surface->image().value().cacheKey(); surface->update(asset(base), {red}, std::nullopt); QCOMPARE(surface->image().value().cacheKey(), first); // A tile published anew is taken in again. const BrushPatch green{red.rect, solid(256, 256, qRgba(0, 255, 0, 255))}; surface->update(asset(base), {green}, std::nullopt); QVERIFY(surface->image().value() == LayerEffectsRenderer::render(composite(base, {green}), std::nullopt, everyKind()).image); } void LayerEffectsSurfaceTests::paintPastTheLayerShowsUnmasked() { // The layer, wholly masked, sits inside a larger grid. LayerEffects effects; effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 1}; const std::unique_ptr surface = LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(40, 30), QRectF(10, 10, 20, 10)); const BrushPatch paint{QRectF(0, 0, 40, 30), solid(40, 30, qRgba(255, 0, 0, 255))}; surface->update(asset(solid(20, 10, qRgba(0, 0, 255, 255))), {paint}, gray(20, 10, 0)); const QImage image = surface->image().value(); QVERIFY(surface->margin == 2 && image.size() == QSize(44, 34)); QCOMPARE(image.pixel(4, 4), qRgba(0, 255, 0, 255)); QCOMPARE(image.pixel(29, 29), qRgba(0, 255, 0, 255)); QCOMPARE(qAlpha(image.pixel(12, 12)), 0); QCOMPARE(qAlpha(image.pixel(31, 21)), 0); } void LayerEffectsSurfaceTests::aSmallerMaskScalesSmoothlyAsTheRenderersDoes() { const QImage image = solid(64, 16, qRgba(40, 90, 200, 255)); QImage mask(2, 1, QImage::Format_Grayscale8); mask.scanLine(0)[0] = 0; mask.scanLine(0)[1] = 255; const std::unique_ptr surface = surfaceFor(image, everyKind()); surface->update(asset(image), {}, mask); QVERIFY(surface->image().value() == LayerEffectsRenderer::render(image, mask, everyKind()).image); } void LayerEffectsSurfaceTests::aSurfaceHoldsEightyMillionPixelsAtMost() { LayerEffects effects; effects.colorOverlay = ColorOverlayEffect(); // A margin of two: 8,000 by 10,000 fits exactly. QVERIFY(LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(7996, 9996), QRectF(0, 0, 7996, 9996))); QTest::ignoreMessage(QtWarningMsg, QRegularExpression("an effects surface passes its pixel budget")); QVERIFY(!LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(7997, 9996), QRectF(0, 0, 7997, 9996))); } void LayerEffectsSurfaceTests::aSurfaceFitsTheStrokeItWasMadeFor() { const QUuid id = QUuid::createUuid(); const std::unique_ptr surface = LayerEffectsSurface::make(id, everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10)); QVERIFY(surface->matches(id, everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10))); QVERIFY(!surface->matches(QUuid::createUuid(), everyKind(), QSizeF(40, 30), QRectF(5, 5, 20, 10))); QVERIFY(!surface->matches(id, LayerEffects(), QSizeF(40, 30), QRectF(5, 5, 20, 10))); QVERIFY(!surface->matches(id, everyKind(), QSizeF(41, 30), QRectF(5, 5, 20, 10))); QVERIFY(!surface->matches(id, everyKind(), QSizeF(40, 30), QRectF(6, 5, 20, 10))); } QTEST_GUILESS_MAIN(LayerEffectsSurfaceTests) #include "LayerEffectsSurfaceTests.moc"