419 lines
21 KiB
C++
419 lines
21 KiB
C++
#include "Document/DocumentLimits.h"
|
|
#include "AddressSpaceLimit.h"
|
|
#include "Document/EditorSession.h"
|
|
#include "Document/LayerMask.h"
|
|
#include "IO/ImageExporter.h"
|
|
#include "IO/ImageResizer.h"
|
|
#include "ProjectFixtures.h"
|
|
#include <QBuffer>
|
|
#include <QImageReader>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
namespace {
|
|
// A 64x32 layer, red left, clear right, filling its document.
|
|
ProjectSnapshot imported()
|
|
{
|
|
const QUuid id = QUuid::createUuid();
|
|
const ProjectLayerRecord record{.id = id, .name = "fixture", .isVisible = true, .transform = {.origin = {0, 0}, .size = {64, 32}},
|
|
.imageFile = uuidString(id) + ".png"};
|
|
return {.manifest = {.documentID = QUuid::createUuid(), .width = 64, .height = 32, .activeLayerID = id, .layers = {record}},
|
|
.images = {{id, asset(halfRed(), "fixture")}}};
|
|
}
|
|
|
|
std::optional<ProjectError::Kind> resizeError(const ProjectSnapshot &snapshot, const ImageSizeOptions &options)
|
|
{
|
|
return projectError([&] { ImageResizer::resize(snapshot, options); });
|
|
}
|
|
}
|
|
|
|
class ImageSizeTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void resizePreservesLayerIdentityAndUndoRestoresSource();
|
|
void resolutionOnlyRetainsPixelsAndSurvivesSaveAndExport();
|
|
void rotatedHiddenLayerScalesInDocumentAxesAndInvalidSizeIsRejected();
|
|
void limitsAreJudgedAtTheirEdges();
|
|
void budgetsCountEveryLayer();
|
|
void missingPixelsAreRefused();
|
|
void masksScaleWithTheirLayers();
|
|
void solidAndPlacedMasksKeepTheirPixels();
|
|
void recordsKeepEveryField_data();
|
|
void recordsKeepEveryField();
|
|
void thumbnailsTruncate_data();
|
|
void thumbnailsTruncate();
|
|
void aSurfaceThatCannotBeAllocatedThrows();
|
|
};
|
|
|
|
void ImageSizeTests::resizePreservesLayerIdentityAndUndoRestoresSource()
|
|
{
|
|
const ProjectSnapshot input = imported();
|
|
const QUuid id = input.manifest.layers[0].id;
|
|
QTest::ignoreMessage(QtInfoMsg, "resized 64 x 32 to 128 x 96");
|
|
const ProjectSnapshot result = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 300, .sampling = LayerSampling::nearest});
|
|
QCOMPARE(result.manifest.width, qint64(128));
|
|
QCOMPARE(result.manifest.height, qint64(96));
|
|
QCOMPARE(result.manifest.resolution, std::optional(300.0));
|
|
QCOMPARE(result.manifest.documentID, input.manifest.documentID);
|
|
QCOMPARE(result.manifest.activeLayerID, std::optional(id));
|
|
const ProjectLayerRecord &layer = result.manifest.layers[0];
|
|
QCOMPARE(layer.id, id);
|
|
QCOMPARE(layer.transform, (LayerTransform{.origin = {0, 0}, .size = {128, 96}, .sampling = LayerSampling::nearest}));
|
|
const ImportedImage &scaled = result.images.at(id);
|
|
QCOMPARE(scaled.name, QString("fixture"));
|
|
QCOMPARE(scaled.size(), QSize(128, 96));
|
|
QCOMPARE(scaled.image().format(), QImage::Format_RGBA8888_Premultiplied);
|
|
// Nearest keeps the edge hard: red to 63, clear after.
|
|
QCOMPARE(scaled.image().pixelColor(0, 0), QColor(255, 0, 0));
|
|
QCOMPARE(scaled.image().pixelColor(63, 95), QColor(255, 0, 0));
|
|
QCOMPARE(scaled.image().pixelColor(64, 0).alpha(), 0);
|
|
QCOMPARE(scaled.image().pixelColor(127, 0).alpha(), 0);
|
|
// The source is untouched.
|
|
QCOMPARE(input.images.at(id).size(), QSize(64, 32));
|
|
// In the session: one step; undo gives the source back.
|
|
EditorSession session;
|
|
session.installProject(input, "fixture.comp");
|
|
const std::optional<CanvasDocument> original = session.document();
|
|
session.applyImageSize(result);
|
|
QCOMPARE(session.document().value().size(), QSizeF(128, 96));
|
|
QCOMPARE(session.document().value().resolution, 300.0);
|
|
QCOMPARE(session.activeLayerID(), std::optional(id));
|
|
QCOMPARE(session.document().value().layers.front().asset.value().size(), QSize(128, 96));
|
|
QCOMPARE(session.history.undoName(), QString("Image Size"));
|
|
session.undo();
|
|
QCOMPARE(session.document(), original);
|
|
QVERIFY(session.document().value().layers.front().asset.value().identity() == input.images.at(id).identity());
|
|
session.redo();
|
|
QCOMPARE(session.document().value().resolution, 300.0);
|
|
// One side alone is a resize too.
|
|
QCOMPARE(ImageResizer::resize(input, {.width = 128, .height = 32, .resolution = 72}).images.at(id).size(), QSize(128, 32));
|
|
QCOMPARE(ImageResizer::resize(input, {.width = 64, .height = 48, .resolution = 72}).images.at(id).size(), QSize(64, 48));
|
|
}
|
|
|
|
void ImageSizeTests::resolutionOnlyRetainsPixelsAndSurvivesSaveAndExport()
|
|
{
|
|
ProjectSnapshot before = imported();
|
|
before.manifest.layers[0].transform = {.origin = {3.5, -2}, .size = {40, 20}, .rotation = 30, .flipX = true, .sampling = LayerSampling::smooth};
|
|
const QUuid id = before.manifest.layers[0].id;
|
|
before.manifest.layers[0].maskFile = uuidString(id) + ".mask.png";
|
|
before.masks.insert({id, LayerMask::assetFrom(gray(64, 32, 200))});
|
|
const ProjectSnapshot resized = ImageResizer::resize(before, {.width = 64, .height = 32, .resolution = 300});
|
|
QCOMPARE(resized.manifest.layers[0].transform, before.manifest.layers[0].transform);
|
|
QCOMPARE(resized.images.at(id).identity(), before.images.at(id).identity());
|
|
// The mask and its record come through untouched too.
|
|
QCOMPARE(resized.masks.at(id).identity(), before.masks.at(id).identity());
|
|
QCOMPARE(resized.manifest.layers[0].maskFile, before.manifest.layers[0].maskFile);
|
|
QCOMPARE(resized.manifest.resolution, std::optional(300.0));
|
|
// In the session a new resolution alone is one step.
|
|
EditorSession session;
|
|
session.installProject(before, "fixture.comp");
|
|
QCOMPARE(session.document().value().resolution, 72.0);
|
|
session.applyImageSize(resized);
|
|
QCOMPARE(session.document().value().resolution, 300.0);
|
|
QCOMPARE(session.document().value().size(), QSizeF(64, 32));
|
|
QCOMPARE(session.history.undoCount(), 1);
|
|
session.undo();
|
|
QCOMPARE(session.document().value().resolution, 72.0);
|
|
QTemporaryDir folder;
|
|
ProjectStore::save(resized, folder.filePath("Size.comp"));
|
|
const ProjectSnapshot loaded = ProjectStore::load(folder.filePath("Size.comp"));
|
|
QCOMPARE(loaded.manifest.resolution, std::optional(300.0));
|
|
QByteArray png = ImageExporter::pngData(loaded);
|
|
QBuffer buffer(&png);
|
|
QImageReader reader(&buffer);
|
|
// 300 pixels an inch is 11,811 dots a metre.
|
|
QCOMPARE(reader.read().dotsPerMeterX(), 11'811);
|
|
}
|
|
|
|
void ImageSizeTests::rotatedHiddenLayerScalesInDocumentAxesAndInvalidSizeIsRejected()
|
|
{
|
|
ProjectSnapshot input = imported();
|
|
ProjectLayerRecord &record = input.manifest.layers[0];
|
|
record.isVisible = false;
|
|
record.transform = {.origin = {-16, 4}, .size = {64, 32}, .rotation = 90};
|
|
const ProjectSnapshot result = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72, .sampling = LayerSampling::nearest});
|
|
const ProjectLayerRecord &output = result.manifest.layers[0];
|
|
QVERIFY(!output.isVisible);
|
|
// Turned: 32x64 at (0, -12); Swift allows a pixel.
|
|
QCOMPARE(output.transform.rotation, 0.0);
|
|
QVERIFY(qAbs(output.transform.size.width() - 64) <= 1 && qAbs(output.transform.size.height() - 192) <= 1);
|
|
QVERIFY(qAbs(output.transform.origin.x()) <= 1 && qAbs(output.transform.origin.y() + 36) <= 1);
|
|
const QImage image = result.images.at(record.id).image();
|
|
QCOMPARE(QSizeF(image.size()), output.transform.size);
|
|
// A quarter turn clockwise: the red half is on top.
|
|
QCOMPARE(image.pixelColor(32, 10), QColor(255, 0, 0));
|
|
QCOMPARE(image.pixelColor(32, 90), QColor(255, 0, 0));
|
|
QCOMPARE(image.pixelColor(32, 102).alpha(), 0);
|
|
// Off the right angles the box is exact: floor, ceil.
|
|
record.transform = {.origin = {10, 10}, .size = {40, 20}, .rotation = 30};
|
|
const ProjectSnapshot leaning = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72});
|
|
QCOMPARE(leaning.manifest.layers[0].transform, (LayerTransform{.origin = {15, 4}, .size = {90, 112}}));
|
|
QCOMPARE(resizeError(input, {.width = 30'000, .height = 30'000, .resolution = 72}), std::optional(ProjectError::Kind::tooLarge));
|
|
}
|
|
|
|
void ImageSizeTests::limitsAreJudgedAtTheirEdges()
|
|
{
|
|
ProjectSnapshot input = imported();
|
|
const auto tooLarge = std::optional(ProjectError::Kind::tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 0, .height = 10, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 0, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 30'001, .height = 10, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 30'001, .resolution = 72}), tooLarge);
|
|
for (const double wild : {0.99, 9600.01, std::nan(""), double(INFINITY)})
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 10, .resolution = wild}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 10, .resolution = 1}), std::nullopt);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 10, .resolution = 9600}), std::nullopt);
|
|
// An empty document scales to the limits for nothing.
|
|
input.manifest.layers.clear();
|
|
input.images.clear();
|
|
// No layer's own rule stands in for the document's here.
|
|
QCOMPARE(resizeError(input, {.width = 0, .height = 10, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 0, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 30'001, .height = 10, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 10, .height = 30'001, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 20'000, .height = 10'000, .resolution = 72}), std::nullopt);
|
|
QCOMPARE(resizeError(input, {.width = 20'000, .height = 10'001, .resolution = 72}), tooLarge);
|
|
QCOMPARE(resizeError(input, {.width = 30'000, .height = 1, .resolution = 72}), std::nullopt);
|
|
QCOMPARE(resizeError(input, {.width = 1, .height = 30'000, .resolution = 72}), std::nullopt);
|
|
// The same size skips every other rule: only resolution changes.
|
|
ProjectSnapshot wide = imported();
|
|
wide.manifest.width = 20'000;
|
|
wide.manifest.height = 20'000;
|
|
QCOMPARE(ImageResizer::resize(wide, {.width = 20'000, .height = 20'000, .resolution = 144}).manifest.resolution, std::optional(144.0));
|
|
}
|
|
|
|
void ImageSizeTests::budgetsCountEveryLayer()
|
|
{
|
|
// Widened twice: a narrow layer, the remainder; wide, rows.
|
|
const qint64 budget = DocumentLimits::documentPixelBudget();
|
|
const qint64 rows = (budget - 1) / 30'000, rest = budget - rows * 30'000;
|
|
ProjectSnapshot input = imported();
|
|
input.manifest.width = 100;
|
|
input.manifest.height = 100;
|
|
ProjectLayerRecord narrow = input.manifest.layers[0];
|
|
narrow.transform = {.origin = {0, 0}, .size = {double(rest) / 2, 1}};
|
|
ProjectLayerRecord wide = narrow;
|
|
wide.id = QUuid::createUuid();
|
|
wide.imageFile = uuidString(wide.id) + ".png";
|
|
wide.transform = {.origin = {0, 0}, .size = {15'000, double(rows)}};
|
|
const ImageSizeOptions widened{.width = 200, .height = 100, .resolution = 72};
|
|
const auto tooLarge = std::optional(ProjectError::Kind::tooLarge);
|
|
// Exactly the budget passes: the wide layer's missing image fails.
|
|
input.manifest.layers = {narrow, wide};
|
|
QCOMPARE(resizeError(input, widened), std::optional(ProjectError::Kind::missingImage));
|
|
// One pixel more, counted across both layers, does not.
|
|
input.manifest.layers[0].transform.size = {double(rest + 1) / 2, 1};
|
|
QCOMPARE(resizeError(input, widened), tooLarge);
|
|
input.manifest.layers = {wide};
|
|
QCOMPARE(resizeError(input, widened), std::optional(ProjectError::Kind::missingImage));
|
|
// Masks have their own budget, counted the same way.
|
|
input.manifest.layers = {narrow, wide};
|
|
input.manifest.layers[0].transform.size = {double(rest + 1) / 2, 1};
|
|
for (ProjectLayerRecord &layer : input.manifest.layers) {
|
|
layer.imageFile = std::nullopt;
|
|
layer.maskFile = uuidString(layer.id) + ".mask.png";
|
|
input.masks.insert({layer.id, LayerMask::assetFrom(gray(4, 4, 200))});
|
|
}
|
|
QCOMPARE(resizeError(input, widened), tooLarge);
|
|
// Exactly the budget passes: its surface then finds no memory.
|
|
input.manifest.layers[0].transform.size = {double(rest) / 2, 1};
|
|
std::optional<ExportError::Kind> kind;
|
|
{
|
|
const AddressSpaceLimit limit(16ll * 1024 * 1024);
|
|
try {
|
|
ImageResizer::resize(input, widened);
|
|
} catch (const ExportError &error) {
|
|
kind = error.kind;
|
|
}
|
|
}
|
|
QCOMPARE(kind, std::optional(ExportError::Kind::render));
|
|
// Past one surface, within the document's: a mask still fits.
|
|
if (budget > DocumentLimits::maxSurfacePixels + 30'000) {
|
|
const int past = int(DocumentLimits::maxSurfacePixels / 30'000) + 1;
|
|
input.manifest.layers = {input.manifest.layers[1]};
|
|
input.manifest.layers[0].transform.size = {15'000, double(past)};
|
|
QCOMPARE(ImageResizer::resize(input, widened).masks.at(wide.id).size(), QSize(30'000, past));
|
|
}
|
|
// A side past 30,000 is refused though the pixels fit.
|
|
ProjectSnapshot strip = imported();
|
|
strip.manifest.layers[0].transform = {.origin = {0, 0}, .size = {64, 1}};
|
|
strip.manifest.layers.push_back(strip.manifest.layers[0]);
|
|
QCOMPARE(resizeError(strip, {.width = 30'000, .height = 32, .resolution = 72}), std::nullopt);
|
|
strip.manifest.layers[0].transform.size = {64.01, 1};
|
|
QCOMPARE(resizeError(strip, {.width = 30'000, .height = 32, .resolution = 72}), tooLarge);
|
|
// 300 times 100.003 is 30,000.9: one pixel past, either way.
|
|
ProjectSnapshot edge = imported();
|
|
edge.manifest.width = 100;
|
|
edge.manifest.height = 100;
|
|
edge.manifest.layers[0].transform = {.origin = {0, 0}, .size = {100.003, 1}};
|
|
QCOMPARE(resizeError(edge, {.width = 30'000, .height = 100, .resolution = 72}), tooLarge);
|
|
edge.manifest.layers[0].transform = {.origin = {0, 0}, .size = {1, 100.003}};
|
|
QCOMPARE(resizeError(edge, {.width = 100, .height = 30'000, .resolution = 72}), tooLarge);
|
|
edge.manifest.layers[0].transform = {.origin = {0, 0}, .size = {1, 100}};
|
|
QCOMPARE(resizeError(edge, {.width = 100, .height = 30'000, .resolution = 72}), std::nullopt);
|
|
}
|
|
|
|
void ImageSizeTests::missingPixelsAreRefused()
|
|
{
|
|
ProjectSnapshot input = imported();
|
|
input.images.clear();
|
|
QCOMPARE(resizeError(input, {.width = 128, .height = 64, .resolution = 72}), std::optional(ProjectError::Kind::missingImage));
|
|
input = imported();
|
|
input.manifest.layers[0].maskFile = "mask.png";
|
|
QCOMPARE(resizeError(input, {.width = 128, .height = 64, .resolution = 72}), std::optional(ProjectError::Kind::missingImage));
|
|
// A layer scaled out of reach fails its own rule.
|
|
input = imported();
|
|
input.manifest.layers[0].transform.origin = {900'000, 0};
|
|
QCOMPARE(resizeError(input, {.width = 128, .height = 32, .resolution = 72}), std::optional(ProjectError::Kind::tooLarge));
|
|
}
|
|
|
|
void ImageSizeTests::masksScaleWithTheirLayers()
|
|
{
|
|
ProjectSnapshot input = imported();
|
|
const QUuid id = input.manifest.layers[0].id;
|
|
input.manifest.layers[0].maskFile = uuidString(id) + ".mask.png";
|
|
// White on the left half, 100 on the right.
|
|
QImage mask = gray(64, 32, 100);
|
|
for (int y = 0; y < 32; ++y)
|
|
std::fill_n(mask.scanLine(y), 32, uchar(255));
|
|
input.masks.insert({id, LayerMask::assetFrom(mask)});
|
|
const ProjectSnapshot result = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72, .sampling = LayerSampling::nearest});
|
|
const QImage scaled = result.masks.at(id).image();
|
|
QCOMPARE(scaled.format(), QImage::Format_Grayscale8);
|
|
QCOMPARE(scaled.size(), QSize(128, 96));
|
|
QCOMPARE(int(scaled.constScanLine(0)[0]), 255);
|
|
QCOMPARE(int(scaled.constScanLine(95)[63]), 255);
|
|
QCOMPARE(int(scaled.constScanLine(0)[64]), 100);
|
|
QCOMPARE(int(scaled.constScanLine(95)[127]), 100);
|
|
QCOMPARE(result.manifest.layers[0].maskFile, input.manifest.layers[0].maskFile);
|
|
// Turned, the box's corners hold no mask: coverage zero.
|
|
input.manifest.layers[0].transform.rotation = 45;
|
|
const QImage turned = ImageResizer::resize(input, {.width = 128, .height = 64, .resolution = 72}).masks.at(id).image();
|
|
QCOMPARE(int(turned.constScanLine(0)[0]), 0);
|
|
QCOMPARE(int(turned.constScanLine(turned.height() / 2)[turned.width() / 2 - 8]), 255);
|
|
}
|
|
|
|
void ImageSizeTests::solidAndPlacedMasksKeepTheirPixels()
|
|
{
|
|
ProjectSnapshot input = imported();
|
|
const QUuid id = input.manifest.layers[0].id;
|
|
input.manifest.layers[0].maskFile = uuidString(id) + ".mask.png";
|
|
input.masks.insert({id, LayerMask::solid(false).asset});
|
|
const ProjectSnapshot solid = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72});
|
|
QCOMPARE(solid.masks.at(id).identity(), input.masks.at(id).identity());
|
|
// A mask placed apart keeps its pixels; its placement scales.
|
|
input.masks.clear();
|
|
input.masks.insert({id, LayerMask::assetFrom(gray(30, 15, 200))});
|
|
input.manifest.layers[0].maskPlacement = LayerTransform{.origin = {10, 4}, .size = {30, 15}, .flipX = true};
|
|
const ProjectSnapshot placed = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72});
|
|
QCOMPARE(placed.masks.at(id).identity(), input.masks.at(id).identity());
|
|
const LayerTransform placement = placed.manifest.layers[0].maskPlacement.value();
|
|
QCOMPARE(placement.origin, QPointF(20, 12));
|
|
QCOMPARE(placement.size, QSizeF(60, 45));
|
|
QVERIFY(placement.flipX);
|
|
}
|
|
|
|
void ImageSizeTests::recordsKeepEveryField_data()
|
|
{
|
|
QTest::addColumn<bool>("enabled");
|
|
QTest::addColumn<bool>("linked");
|
|
QTest::newRow("a disabled, linked mask") << false << true;
|
|
QTest::newRow("an enabled, unlinked mask") << true << false;
|
|
}
|
|
|
|
void ImageSizeTests::recordsKeepEveryField()
|
|
{
|
|
QFETCH(bool, enabled);
|
|
QFETCH(bool, linked);
|
|
ProjectSnapshot input = imported();
|
|
const QUuid folder = QUuid::createUuid(), source = QUuid::createUuid();
|
|
ProjectLayerRecord &layer = input.manifest.layers[0];
|
|
layer.parentID = folder;
|
|
layer.opacity = 0.25;
|
|
layer.blendMode = LayerBlendMode::overlay;
|
|
layer.maskFile = "mask.png";
|
|
layer.maskEnabled = enabled;
|
|
layer.maskSourceID = source;
|
|
layer.maskLinked = linked;
|
|
input.masks.insert({layer.id, LayerMask::solid(true).asset});
|
|
const std::optional<QString> imageFile = layer.imageFile;
|
|
// A folder has no pixels; its box still scales.
|
|
input.manifest.layers.push_back({.id = folder, .name = "Folder", .isVisible = true, .transform = {.origin = {8, 4}, .size = {16, 8}},
|
|
.imageFile = std::nullopt, .isGroup = true});
|
|
const ProjectSnapshot output = ImageResizer::resize(input, {.width = 128, .height = 96, .resolution = 72});
|
|
const ProjectLayerRecord &kept = output.manifest.layers[0];
|
|
QCOMPARE(kept.name, QString("fixture"));
|
|
QCOMPARE(kept.imageFile, imageFile);
|
|
QCOMPARE(kept.parentID, std::optional(folder));
|
|
QCOMPARE(kept.isGroup, std::nullopt);
|
|
QCOMPARE(kept.opacity, std::optional(0.25));
|
|
QCOMPARE(kept.blendMode, std::optional(LayerBlendMode::overlay));
|
|
QCOMPARE(kept.maskFile, std::optional(QString("mask.png")));
|
|
QCOMPARE(kept.maskEnabled, std::optional(enabled));
|
|
QCOMPARE(kept.maskSourceID, std::optional(source));
|
|
QCOMPARE(kept.maskLinked, std::optional(linked));
|
|
QCOMPARE(kept.maskPlacement, std::nullopt);
|
|
const ProjectLayerRecord &group = output.manifest.layers[1];
|
|
QCOMPARE(group.isGroup, std::optional(true));
|
|
QCOMPARE(group.transform, (LayerTransform{.origin = {16, 12}, .size = {32, 24}}));
|
|
QCOMPARE(int(output.images.size()), 1);
|
|
}
|
|
|
|
void ImageSizeTests::thumbnailsTruncate_data()
|
|
{
|
|
QTest::addColumn<QSize>("size");
|
|
QTest::addColumn<QSize>("thumbnail");
|
|
QTest::newRow("small layers keep their size") << QSize(96, 40) << QSize(96, 40);
|
|
QTest::newRow("smaller still, never enlarged") << QSize(48, 20) << QSize(48, 20);
|
|
QTest::newRow("28.8 loses its fraction") << QSize(100, 30) << QSize(96, 28);
|
|
QTest::newRow("tall") << QSize(30, 100) << QSize(28, 96);
|
|
QTest::newRow("a strip stays one pixel high") << QSize(2400, 1) << QSize(96, 1);
|
|
}
|
|
|
|
void ImageSizeTests::thumbnailsTruncate()
|
|
{
|
|
QFETCH(QSize, size);
|
|
QFETCH(QSize, thumbnail);
|
|
const ProjectSnapshot input = imported();
|
|
const ProjectSnapshot output = ImageResizer::resize(input, {.width = size.width(), .height = size.height(), .resolution = 72});
|
|
QCOMPARE(output.images.begin()->second.thumbnail.size(), thumbnail);
|
|
// Inside the red half; halved edges fade, as on macOS.
|
|
QCOMPARE(output.images.begin()->second.thumbnail.pixelColor(thumbnail.width() / 4, thumbnail.height() / 2), QColor(255, 0, 0));
|
|
}
|
|
|
|
void ImageSizeTests::aSurfaceThatCannotBeAllocatedThrows()
|
|
{
|
|
const ProjectSnapshot input = imported();
|
|
std::optional<ExportError::Kind> kind;
|
|
{
|
|
// 9000 x 9000 asks for 324 MB.
|
|
const AddressSpaceLimit limit(200ll * 1024 * 1024);
|
|
try {
|
|
ImageResizer::resize(input, {.width = 9000, .height = 9000, .resolution = 72});
|
|
} catch (const ExportError &error) {
|
|
kind = error.kind;
|
|
}
|
|
}
|
|
QCOMPARE(kind, std::optional(ExportError::Kind::render));
|
|
// A mask's surface fails the same way.
|
|
ProjectSnapshot masked = imported();
|
|
masked.manifest.layers[0].imageFile = std::nullopt;
|
|
masked.manifest.layers[0].maskFile = "mask.png";
|
|
masked.masks.insert({masked.manifest.layers[0].id, LayerMask::assetFrom(gray(64, 32, 200))});
|
|
masked.images.clear();
|
|
kind.reset();
|
|
{
|
|
// 9000 x 9000 gray asks for 81 MB.
|
|
const AddressSpaceLimit limit(40ll * 1024 * 1024);
|
|
try {
|
|
ImageResizer::resize(masked, {.width = 9000, .height = 9000, .resolution = 72});
|
|
} catch (const ExportError &error) {
|
|
kind = error.kind;
|
|
}
|
|
}
|
|
QCOMPARE(kind, std::optional(ExportError::Kind::render));
|
|
}
|
|
|
|
QTEST_MAIN(ImageSizeTests)
|
|
#include "ImageSizeTests.moc"
|