Files
OmaPhoto/tests/MaskTracingTests.cpp

194 lines
7.2 KiB
C++

#include "Document/BrushStroke.h"
#include "Document/MaskTracing.h"
#include "AddressSpaceLimit.h"
#include <QtTest>
namespace {
// '#' marks dark mask pixels and opaque layer pixels.
QImage mask(const QStringList &rows, int dark = 0, int light = 255)
{
QImage image = BrushRaster::context(rows[0].size(), rows.size(), true);
for (int y = 0; y < rows.size(); ++y) {
for (int x = 0; x < rows[y].size(); ++x)
image.scanLine(y)[x] = rows[y][x] == '#' ? dark : light;
}
return image;
}
QImage layer(const QStringList &rows, int opaque = 255, int clear = 0)
{
QImage image = BrushRaster::context(rows[0].size(), rows.size(), false);
for (int y = 0; y < rows.size(); ++y) {
for (int x = 0; x < rows[y].size(); ++x) {
const int alpha = rows[y][x] == '#' ? opaque : clear;
image.setPixel(x, y, qRgba(0, 0, 0, alpha));
}
}
return image;
}
QStringList filled(const QPainterPath &path, int width, int height)
{
QImage surface = BrushRaster::context(width, height, true);
QPainter painter(&surface);
painter.fillPath(path, Qt::white);
painter.end();
QStringList rows;
for (int y = 0; y < height; ++y) {
QString row;
for (int x = 0; x < width; ++x)
row += surface.constScanLine(y)[x] == 255 ? '#' : '.';
rows << row;
}
return rows;
}
// The corners of a path with one closed subpath.
QList<QPointF> corners(const QPainterPath &path)
{
QList<QPointF> result;
for (int index = 0; index + 1 < path.elementCount(); ++index)
result << QPointF(path.elementAt(index));
return result;
}
}
class MaskTracingTests : public QObject {
Q_OBJECT
private slots:
void aSinglePixelBecomesAClockwiseSquare();
void aBlockKeepsOnlyItsFourCorners();
void anEllShapeHasSixCorners();
void aRingTracesItsHoleTheOtherWay();
void everyPatternFillsBackToItself_data();
void everyPatternFillsBackToItself();
void darkMeansBelowHalfGray();
void whiteMeansAtLeastHalfGray();
void opaqueMeansAtLeastHalfAlpha();
void nothingToTraceIsNil();
void otherPixelFormatsAreConverted();
void transparentPixelsCountAsDarkOverTheClearedBitmap();
void allocationFailureIsNil();
void aDevicePixelRatioNeverShrinksTheTrace();
};
void MaskTracingTests::aSinglePixelBecomesAClockwiseSquare()
{
const QPainterPath path = MaskTracing::darkPixels(mask({"....", "..#.", "...."})).value();
QCOMPARE(path.fillRule(), Qt::WindingFill);
QCOMPARE(corners(path), QList<QPointF>({QPointF(2, 1), QPointF(3, 1), QPointF(3, 2), QPointF(2, 2)}));
}
void MaskTracingTests::aBlockKeepsOnlyItsFourCorners()
{
const QPainterPath path = MaskTracing::darkPixels(mask({".....", ".###.", ".###.", "....."})).value();
QCOMPARE(corners(path), QList<QPointF>({QPointF(1, 1), QPointF(4, 1), QPointF(4, 3), QPointF(1, 3)}));
}
void MaskTracingTests::anEllShapeHasSixCorners()
{
const QPainterPath path = MaskTracing::darkPixels(mask({"#..", "#..", "###"})).value();
QCOMPARE(corners(path), QList<QPointF>({QPointF(0, 0), QPointF(1, 0), QPointF(1, 2), QPointF(3, 2),
QPointF(3, 3), QPointF(0, 3)}));
}
void MaskTracingTests::aRingTracesItsHoleTheOtherWay()
{
const QStringList ring{"#####", "#...#", "#...#", "#####"};
const QPainterPath path = MaskTracing::darkPixels(mask(ring)).value();
QCOMPARE(filled(path, 5, 4), ring);
QVERIFY(path.contains(QPointF(0.5, 0.5)));
QVERIFY(!path.contains(QPointF(2.5, 1.5)));
int moves = 0;
for (int index = 0; index < path.elementCount(); ++index)
moves += path.elementAt(index).isMoveTo();
QCOMPARE(moves, 2);
}
void MaskTracingTests::everyPatternFillsBackToItself_data()
{
QTest::addColumn<QStringList>("rows");
QTest::newRow("diagonal touch") << QStringList{"#..", ".#.", "..#"};
QTest::newRow("checkerboard") << QStringList{"#.#.#", ".#.#.", "#.#.#"};
QTest::newRow("padded width") << QStringList{"##..#", ".###.", "#...#"};
QTest::newRow("nested") << QStringList{"#######", "#.....#", "#.###.#", "#.#.#.#", "#.###.#", "#.....#", "#######"};
QTest::newRow("full") << QStringList{"###", "###"};
QTest::newRow("comb") << QStringList{"#.#.#.#", "#.#.#.#", "#######"};
}
void MaskTracingTests::everyPatternFillsBackToItself()
{
QFETCH(QStringList, rows);
const int width = rows[0].size(), height = rows.size();
QCOMPARE(filled(MaskTracing::darkPixels(mask(rows)).value(), width, height), rows);
QCOMPARE(filled(MaskTracing::opaquePixels(layer(rows)).value(), width, height), rows);
}
void MaskTracingTests::darkMeansBelowHalfGray()
{
QCOMPARE(filled(MaskTracing::darkPixels(mask({"#.", ".."}, 127, 128)).value(), 2, 2), QStringList({"#.", ".."}));
QVERIFY(!MaskTracing::darkPixels(mask({"#.", ".."}, 128, 255)).has_value());
}
void MaskTracingTests::whiteMeansAtLeastHalfGray()
{
QCOMPARE(filled(MaskTracing::whitePixels(mask({"#.", ".."}, 128, 127)).value(), 2, 2), QStringList({"#.", ".."}));
QVERIFY(!MaskTracing::whitePixels(mask({"#.", ".."}, 127, 0)).has_value());
}
void MaskTracingTests::opaqueMeansAtLeastHalfAlpha()
{
QCOMPARE(filled(MaskTracing::opaquePixels(layer({"#.", ".."}, 128, 127)).value(), 2, 2), QStringList({"#.", ".."}));
QVERIFY(!MaskTracing::opaquePixels(layer({"#.", ".."}, 127, 0)).has_value());
}
void MaskTracingTests::nothingToTraceIsNil()
{
QVERIFY(!MaskTracing::darkPixels(mask({"..", ".."})).has_value());
QVERIFY(!MaskTracing::opaquePixels(layer({"..", ".."})).has_value());
QVERIFY(!MaskTracing::darkPixels(QImage()).has_value());
QVERIFY(!MaskTracing::opaquePixels(QImage()).has_value());
}
void MaskTracingTests::otherPixelFormatsAreConverted()
{
const QStringList rows{"#..", ".##"};
QCOMPARE(filled(MaskTracing::opaquePixels(layer(rows).convertToFormat(QImage::Format_ARGB32)).value(), 3, 2), rows);
QCOMPARE(filled(MaskTracing::darkPixels(mask(rows).convertToFormat(QImage::Format_RGB32)).value(), 3, 2), rows);
}
void MaskTracingTests::transparentPixelsCountAsDarkOverTheClearedBitmap()
{
QImage faint = BrushRaster::context(2, 1, false);
faint.setPixel(0, 0, qRgba(16, 16, 16, 16));
faint.setPixel(1, 0, qRgba(255, 255, 255, 255));
QCOMPARE(filled(MaskTracing::darkPixels(faint).value(), 2, 1), QStringList({"#."}));
QImage half = BrushRaster::context(2, 1, false);
half.setPixel(0, 0, qRgba(127, 127, 127, 127));
half.setPixel(1, 0, qRgba(128, 128, 128, 128));
QCOMPARE(filled(MaskTracing::darkPixels(half).value(), 2, 1), QStringList({"#."}));
}
void MaskTracingTests::allocationFailureIsNil()
{
const QImage large = BrushRaster::context(8192, 8192, true);
std::optional<AddressSpaceLimit> limit(std::in_place, 16 * 1024 * 1024);
const bool traced = MaskTracing::opaquePixels(large).has_value();
limit.reset();
QVERIFY(!traced);
}
void MaskTracingTests::aDevicePixelRatioNeverShrinksTheTrace()
{
const QStringList rows{"####", "#..#"};
QImage dense = layer(rows);
dense.setDevicePixelRatio(2);
QCOMPARE(filled(MaskTracing::opaquePixels(dense).value(), 4, 2), rows);
QImage denseMask = mask(rows);
denseMask.setDevicePixelRatio(2);
QCOMPARE(filled(MaskTracing::darkPixels(denseMask).value(), 4, 2), rows);
}
QTEST_APPLESS_MAIN(MaskTracingTests)
#include "MaskTracingTests.moc"