The document's own pixel budget, marching ants at screen resolution (12.11a)

This commit is contained in:
雷电芽衣 committed 2026-09-29 00:19:45 -04:00
1 parent ffba1753c6
commit 8a9c56ee1f
75 files changed
+759 -189

No files matched your search

+3 -3
View File
@@ -269,9 +269,9 @@ void AdjustmentEditorTests::theEditorOpensOnlyForItsRequest()
void AdjustmentEditorTests::aFailedRenderEndsTheRequest()
{
// Past the export's hundred million pixels: the render refuses.
// Past the export's one surface: the render refuses.
EditorSession session;
session.createDocument(10'001, 10'000);
session.createDocument(20'001, 10'000);
session.addAdjustment(AdjustmentKind::curves);
const int steps = session.history.undoCount();
int ran = 0;
@@ -481,7 +481,7 @@ void AdjustmentEditorTests::everyStepIsAnnouncedAndItsLastSignalSeesIt()
// A failed render's last signal sees the request end.
EditorSession huge;
connect(&huge, &EditorSession::changed, this, [&] { seen = described(huge); });
huge.createDocument(10'001, 10'000);
huge.createDocument(20'001, 10'000);
huge.addAdjustment(AdjustmentKind::levels);
seen.clear();
QVERIFY(opened(huge, huge.adjustmentEditingID().value()));
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "Document/BrushStroke.h"
#include "Document/DocumentLimits.h"
#include "Rendering/RasterSnapshot.h"
#include <vector>
// A layer claiming its size in pixels, holding one.
inline ImportedImage claimed(int width, int height, const QString &name = QStringLiteral("Claimed"))
{
const auto raster = std::make_shared<const RasterSnapshot>(width, height, BrushRaster::context(1, 1, false), QRectF(0, 0, width, height),
std::vector<BrushPatch>{});
return ImportedImage(raster, QImage(), name);
}
// Layers claiming `pixels` in all, each within one surface.
inline std::vector<ImportedImage> claiming(qint64 pixels)
{
std::vector<ImportedImage> layers;
const int side = DocumentLimits::maxSide;
while (pixels > 0) {
const qint64 rows = std::min<qint64>(pixels / side, DocumentLimits::maxSurfacePixels / side);
layers.push_back(rows > 0 ? claimed(side, int(rows)) : claimed(int(pixels), 1));
pixels -= rows > 0 ? side * rows : pixels;
}
return layers;
}
+13 -7
View File
@@ -1,3 +1,4 @@
#include "BudgetFixtures.h"
#include "AddressSpaceLimit.h"
#include "Document/CanvasSize.h"
#include "Document/EditorSession.h"
@@ -264,13 +265,18 @@ void CanvasSizeTests::masksPlacedApartMoveWithTheirLayers()
void CanvasSizeTests::theExtensionCountsAgainstTheBudgets()
{
// A painted 9000 x 9000 layer leaves 19 megapixels.
const QUuid id = QUuid::createUuid();
const std::shared_ptr<const RasterSnapshot> raster = painted(solid(9000, 9000, qRgba(0, 0, 0, 0)), {});
const ProjectLayerRecord record{.id = id, .name = "Painted", .isVisible = true, .transform = {.origin = {0, 0}, .size = {9000, 9000}},
.imageFile = uuidString(id) + ".png"};
const ProjectSnapshot input{.manifest = {.documentID = QUuid::createUuid(), .width = 100, .height = 100, .activeLayerID = id, .layers = {record}},
.images = {{id, ImportedImage(raster, solid(1, 1, 0), "Painted")}}};
// Painted layers leave 19 megapixels of the document budget.
ProjectSnapshot input{.manifest = {.documentID = QUuid::createUuid(), .width = 100, .height = 100, .activeLayerID = std::nullopt, .layers = {}},
.images = {}};
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget() - 19'000'000)) {
const QUuid layerID = QUuid::createUuid();
input.manifest.layers.push_back({.id = layerID, .name = "Painted", .isVisible = true, .transform = {.origin = {0, 0}, .size = QSizeF(layer.size())},
.imageFile = uuidString(layerID) + ".png"});
input.images.insert({layerID, layer});
}
const QUuid id = input.manifest.layers.front().id;
input.manifest.activeLayerID = id;
const std::shared_ptr<const RasterSnapshot> raster = input.images.at(id).raster;
const CanvasExtensionColor white{1, 1, 1};
QVERIFY(!raster->hasMaterializedPixels());
QCOMPARE(resizeError(input, {.width = 4750, .height = 4001, .fill = white}), std::optional(ProjectError::Kind::tooLarge));
+32
View File
@@ -0,0 +1,32 @@
#include "Document/DocumentLimits.h"
#include <QtTest>
#include <unistd.h>
// Swift 1.2.8's DocumentLimits: one surface, and a document's budget.
class DocumentLimitsTests : public QObject {
Q_OBJECT
private slots:
void theCeilingsAreSwifts();
};
void DocumentLimitsTests::theCeilingsAreSwifts()
{
QCOMPARE(DocumentLimits::maxSide, 30'000);
QCOMPARE(DocumentLimits::maxSurfacePixels, qint64(200'000'000));
QCOMPARE(DocumentLimits::maxSurfaceMegapixels(), qint64(200));
QCOMPARE(DocumentLimits::maxSideText(), QString("30,000"));
// A quarter of memory, four bytes a pixel, clamped.
const qint64 memory = qint64(sysconf(_SC_PHYS_PAGES)) * qint64(sysconf(_SC_PAGE_SIZE));
const qint64 budget = std::clamp(memory / 16, qint64(200'000'000), qint64(800'000'000));
QCOMPARE(DocumentLimits::documentPixelBudget(), budget);
QCOMPARE(DocumentLimits::documentBudgetMegapixels(), budget / 1'000'000);
// Swift's 537 MP on 8 GB; the cap, the floor.
QCOMPARE(DocumentLimits::budgetFor(8ll << 30), qint64(536'870'912));
QCOMPARE(DocumentLimits::budgetFor(16ll << 30), qint64(800'000'000));
QCOMPARE(DocumentLimits::budgetFor(2ll << 30), qint64(200'000'000));
// Both stay under a square at the side limit.
QVERIFY(DocumentLimits::documentPixelBudget() < qint64(DocumentLimits::maxSide) * DocumentLimits::maxSide);
}
QTEST_GUILESS_MAIN(DocumentLimitsTests)
#include "DocumentLimitsTests.moc"
+1 -1
View File
@@ -104,7 +104,7 @@ void ExportTests::blankCanvasAndOversizedCanvas()
const auto sized = [](int width, int height) {
return ProjectSnapshot{.manifest = {.documentID = QUuid::createUuid(), .width = width, .height = height, .activeLayerID = std::nullopt, .layers = {}}, .images = {}};
};
for (const QSize &size : {QSize(30'000, 30'000), QSize(30'001, 1), QSize(1, 30'001), QSize(0, 5), QSize(5, 0), QSize(10'001, 10'000)}) {
for (const QSize &size : {QSize(30'000, 30'000), QSize(30'001, 1), QSize(1, 30'001), QSize(0, 5), QSize(5, 0), QSize(20'001, 10'000)}) {
try {
ImageExporter::render(sized(size.width(), size.height()));
QFAIL("an oversized or empty canvas must be refused");
+2 -2
View File
@@ -183,10 +183,10 @@ void FilterModelTests::aBlurGrowsTheGridAndNeverShrinksIt()
// A motion blur's reach is half its streak.
const FilterEdit motion(FilterKind::motionBlur, layer(4, 4), std::nullopt, FilterSettings{.distance = 9}, std::nullopt);
QCOMPARE(motion.grownImage.value().size(), QSize(18, 18));
// Past 30,000 a side or 100 million pixels: refused.
// Past 30,000 a side or one surface's pixels: refused.
QVERIFY_THROWS_EXCEPTION(ProjectError, FilterEdit(FilterKind::gaussianBlur, layer(29990, 1), std::nullopt, FilterSettings{.radius = 3}, std::nullopt));
QVERIFY_THROWS_EXCEPTION(ProjectError, FilterEdit(FilterKind::gaussianBlur, layer(1, 29990), std::nullopt, FilterSettings{.radius = 3}, std::nullopt));
QVERIFY_THROWS_EXCEPTION(ProjectError, FilterEdit(FilterKind::gaussianBlur, ImageLayer(painted(9990, 9990), QPointF()), std::nullopt,
QVERIFY_THROWS_EXCEPTION(ProjectError, FilterEdit(FilterKind::gaussianBlur, ImageLayer(painted(14'140, 14'140), QPointF()), std::nullopt,
FilterSettings{.radius = 3}, std::nullopt));
}
+2 -1
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "Document/EditorSession.h"
#include <QtTest>
@@ -272,7 +273,7 @@ void GroupTests::projectErrorsCarryTheirMessages()
const ProjectError tooLarge(ProjectError::Kind::tooLarge);
QCOMPARE(tooLarge.kind, ProjectError::Kind::tooLarge);
QCOMPARE(QString(tooLarge.what()),
QString("This project exceeds the supported canvas, layer, file-size, or 100-megapixel image limit."));
QStringLiteral("This project exceeds the supported canvas, layer, file-size, or %1-megapixel document limit.").arg(DocumentLimits::documentBudgetMegapixels()));
const ProjectError encode(ProjectError::Kind::encode);
QCOMPARE(encode.kind, ProjectError::Kind::encode);
+9 -12
View File
@@ -1,3 +1,4 @@
#include "BudgetFixtures.h"
#include "Document/EditorSession.h"
#include "Rendering/RasterSnapshot.h"
#include <QSignalSpy>
@@ -8,7 +9,7 @@
namespace {
const QString unreadable = QStringLiteral("The image could not be read. It may be damaged or unavailable.");
const QString unsupported = QStringLiteral("Choose a JPEG, PNG, HEIC, TIFF, or Photoshop (PSD) file.");
const QString tooLarge = QStringLiteral("This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit.");
const QString tooLarge = QStringLiteral("This import exceeds the current %1-megapixel document budget or 30,000-pixel side limit.").arg(DocumentLimits::documentBudgetMegapixels());
// A 64 by 32 PNG, as Swift's fixture is.
QUrl fixture(const QTemporaryDir &folder, const QString &name = QStringLiteral("fixture.png"))
@@ -52,12 +53,6 @@ public:
return true;
}
};
// Pixels the canvas holds without the memory for them.
ImportedImage hollow(int width, int height)
{
return ImportedImage(std::make_shared<const RasterSnapshot>(width, height, QImage(), QRectF(), std::vector<BrushPatch>()), QImage(), "Hollow");
}
}
class ImageImportSessionTests : public QObject {
@@ -314,17 +309,19 @@ void ImageImportSessionTests::thePixelBudgetCountsWhatTheCanvasHolds()
QTemporaryDir folder;
const QUrl url = fixture(folder);
EditorSession session;
// 100,000,000 less these leaves 2,048: the fixture exactly.
session.insert(hollow(10'000, 9'999));
session.insert(hollow(7'952, 1));
// The budget less these leaves 2,048: the fixture exactly.
const std::vector<ImportedImage> filling = claiming(DocumentLimits::documentPixelBudget() - 2'048);
for (const ImportedImage &layer : filling)
session.insert(layer);
session.addBlankLayer();
const int count = int(filling.size()) + 2;
QVERIFY(imported(session, {url}));
QCOMPARE(int(session.document().value().layers.size()), 4);
QCOMPARE(int(session.document().value().layers.size()), count);
QCOMPARE(session.importError(), std::nullopt);
// Full now: the next pixel is one too many.
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("^cannot import .*fixture\\.png: .*"));
QVERIFY(imported(session, {url}));
QCOMPARE(int(session.document().value().layers.size()), 4);
QCOMPARE(int(session.document().value().layers.size()), count);
QCOMPARE(session.importError(), std::optional("fixture.png: " + tooLarge));
QVERIFY(!session.document().value().layers[0].asset.value().raster->hasMaterializedPixels());
}
+2 -1
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "AddressSpaceLimit.h"
#include "IO/ImageImporter.h"
#include <QBuffer>
@@ -461,7 +462,7 @@ void ImageImportTests::errorsCarrySwiftsDescriptions()
QCOMPARE(QString(ImageImportError(ImageImportError::Kind::unreadable).what()), QString("The image could not be read. It may be damaged or unavailable."));
QCOMPARE(QString(ImageImportError(ImageImportError::Kind::unsupported).what()), QString("Choose a JPEG, PNG, HEIC, TIFF, or Photoshop (PSD) file."));
QCOMPARE(QString(ImageImportError(ImageImportError::Kind::tooLarge).what()),
QString("This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit."));
QStringLiteral("This import exceeds the current %1-megapixel document budget or 30,000-pixel side limit.").arg(DocumentLimits::documentBudgetMegapixels()));
QCOMPARE(ImageImportError(ImageImportError::Kind::tooLarge).kind, ImageImportError::Kind::tooLarge);
}
+6 -5
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "UI/ColorPickerSheet.h"
#include "UI/ImageSizeSheet.h"
#include <QCheckBox>
@@ -154,7 +155,7 @@ void ImageSizeSheetTests::aNewResolutionKeepsThePrintSize()
// Zero changes nothing but the resolution, which is refused.
shown.unit("Inches");
shown.type("imageResolution", "0");
QCOMPARE(shown.result(), QString("Use 1–30,000 pixels per side, up to 100 megapixels, and 1–9,600 pixels/inch."));
QCOMPARE(shown.result(), QStringLiteral("Use 1–30,000 pixels per side, up to %1 megapixels, and 1–9,600 pixels/inch.").arg(DocumentLimits::maxSurfaceMegapixels()));
shown.type("imageResolution", "150");
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
// Centimetres keep the print size alike.
@@ -203,14 +204,14 @@ void ImageSizeSheetTests::anInvalidSizeRestsResize()
QVERIFY(resize.isDefault() && resize.isEnabled());
shown.find<QCheckBox>("imageLocked").click();
shown.type("imageWidth", "30001");
QCOMPARE(shown.result(), QString("Use 1–30,000 pixels per side, up to 100 megapixels, and 1–9,600 pixels/inch."));
QCOMPARE(shown.result(), QStringLiteral("Use 1–30,000 pixels per side, up to %1 megapixels, and 1–9,600 pixels/inch.").arg(DocumentLimits::maxSurfaceMegapixels()));
QCOMPARE(shown.find<QLabel>("imageResult").foregroundRole(), QPalette::BrightText);
QVERIFY(!resize.isEnabled());
// Past 100 megapixels while resampling.
// Past one surface's 200 megapixels while resampling.
shown.type("imageWidth", "20000");
shown.type("imageHeight", "5001");
shown.type("imageHeight", "10001");
QVERIFY(!resize.isEnabled());
shown.type("imageHeight", "5000");
shown.type("imageHeight", "10000");
QVERIFY(resize.isEnabled());
QCOMPARE(shown.find<QLabel>("imageResult").foregroundRole(), QPalette::PlaceholderText);
shown.type("imageResolution", "9601");
+43 -25
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "AddressSpaceLimit.h"
#include "Document/EditorSession.h"
#include "Document/LayerMask.h"
@@ -170,8 +171,8 @@ void ImageSizeTests::limitsAreJudgedAtTheirEdges()
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 = 10'000, .height = 10'000, .resolution = 72}), std::nullopt);
QCOMPARE(resizeError(input, {.width = 10'000, .height = 10'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.
@@ -183,39 +184,56 @@ void ImageSizeTests::limitsAreJudgedAtTheirEdges()
void ImageSizeTests::budgetsCountEveryLayer()
{
// Scaled a hundredfold, the small layer takes a megapixel.
// 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 small = input.manifest.layers[0];
small.transform = {.origin = {0, 0}, .size = {10, 10}};
ProjectLayerRecord whole = small;
whole.id = QUuid::createUuid();
whole.imageFile = uuidString(whole.id) + ".png";
whole.transform = {.origin = {0, 0}, .size = {100, 100}};
input.images.insert({whole.id, asset(halfRed(), "whole")});
input.manifest.layers = {small, whole};
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);
QCOMPARE(resizeError(input, {.width = 10'000, .height = 10'000, .resolution = 72}), tooLarge);
// A megapixel and 99 more fill the budget exactly.
input.manifest.layers[1].transform.size = {99, 100};
const ProjectSnapshot full = ImageResizer::resize(input, {.width = 10'000, .height = 10'000, .resolution = 72, .sampling = LayerSampling::nearest});
QCOMPARE(full.images.at(whole.id).size(), QSize(9'900, 10'000));
input.manifest.layers[1].transform.size = {99.01, 100};
QCOMPARE(resizeError(input, {.width = 10'000, .height = 10'000, .resolution = 72}), 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 = {small, whole};
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, {.width = 10'000, .height = 10'000, .resolution = 72}), tooLarge);
input.manifest.layers[1].transform.size = {99, 100};
QCOMPARE(ImageResizer::resize(input, {.width = 10'000, .height = 10'000, .resolution = 72, .sampling = LayerSampling::nearest})
.masks.at(whole.id).size(), QSize(9'900, 10'000));
input.manifest.layers[1].transform.size = {99.01, 100};
QCOMPARE(resizeError(input, {.width = 10'000, .height = 10'000, .resolution = 72}), tooLarge);
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}};
+2 -2
View File
@@ -282,8 +282,8 @@ void ImageTrimTests::nothingToTrimOrACancelChangesNothing()
void ImageTrimTests::aTrimThatFailsIsExplained()
{
Desk desk;
// Past the render's hundred megapixels, the trim cannot look.
desk.session.createDocument(20'000, 10'000);
// Past the render's one surface, the trim cannot look.
desk.session.createDocument(20'001, 10'000);
const CanvasDocument before = desk.session.document().value();
DialogDesk alerts;
alerts.replies = {"OK"};
+2 -2
View File
@@ -280,8 +280,8 @@ void LayerEffectsKernelTests::switchedOffOrEmptyEffectsLeaveThePixels()
void LayerEffectsKernelTests::theKernelRefusesWhatItCannotHold()
{
// Past a hundred million pixels, before any pixel is read.
const QImage vast(10'001, 10'000, QImage::Format_Mono);
// Past one surface's pixels, before any pixel is read.
const QImage vast(20'001, 10'000, QImage::Format_Mono);
LayerEffects effects;
effects.stroke = StrokeEffect();
try {
+2 -2
View File
@@ -128,7 +128,7 @@ void LayerEffectsRendererTests::renderingPadsMasksThenRunsTheKernel()
LayerEffects shadow;
shadow.shadow = ShadowEffect{.distance = 10};
try {
LayerEffectsRenderer::render(QImage(9'990, 9'990, QImage::Format_Mono), std::nullopt, shadow);
LayerEffectsRenderer::render(QImage(14'140, 14'140, QImage::Format_Mono), std::nullopt, shadow);
QFAIL("a vast layer rendered");
} catch (const ProjectError &error) {
QCOMPARE(error.kind, ProjectError::Kind::tooLarge);
@@ -188,7 +188,7 @@ void LayerEffectsRendererTests::theCacheKeepsEightWithinItsBudget()
// Too large to make: the layer draws bare.
LayerEffects shadow;
shadow.shadow = ShadowEffect{.distance = 10};
QVERIFY(!LayerEffectsRenderer::cached(QImage(9'990, 9'990, QImage::Format_Mono), std::nullopt, shadow));
QVERIFY(!LayerEffectsRenderer::cached(QImage(14'140, 14'140, QImage::Format_Mono), std::nullopt, shadow));
}
void LayerEffectsRendererTests::theExportAndTheCompositeDrawTheEffects()
+201
View File
@@ -0,0 +1,201 @@
#include "SelectionCanvasFixtures.h"
// Swift 1.2.10 (8eba077): ants from a screen-resolution outline.
namespace {
// Alternate single pixels: far past the 20,000 elements drawn whole.
QPainterPath checkerboard(QRect region)
{
QPainterPath path;
path.setFillRule(Qt::WindingFill);
for (int y = region.top(); y <= region.bottom(); ++y) {
for (int x = region.left(); x <= region.right(); ++x) {
if ((x + y) % 2 == 0)
path.addRect(QRectF(x, y, 1, 1));
}
}
return path;
}
// Pixels the ants changed within `area`, against the bare canvas.
int marked(const QImage &shot, const QImage &bare, QRect area)
{
int count = 0;
for (int y = area.top(); y <= area.bottom(); ++y) {
for (int x = area.left(); x <= area.right(); ++x)
count += shot.pixel(x, y) != bare.pixel(x, y);
}
return count;
}
// A border band around a document rectangle, in view pixels.
QRect band(Canvas &shown, QRect region)
{
const QSizeF size = shown.documentSize();
return QRectF(shown.session.viewport.viewPoint(region.topLeft(), size), shown.session.viewport.viewPoint(region.bottomRight() + QPoint(1, 1), size))
.toAlignedRect()
.adjusted(-2, -2, 2, 2);
}
QTimer &ants(Canvas &shown)
{
return *shown.canvas->findChild<QTimer *>(QStringLiteral("antsTimer"));
}
QRect viewRect(Canvas &shown, QRect region)
{
const QSizeF size = shown.documentSize();
return QRectF(shown.session.viewport.viewPoint(region.topLeft(), size), shown.session.viewport.viewPoint(region.bottomRight() + QPoint(1, 1), size))
.toAlignedRect();
}
}
class MarchingAntsTests : public QObject {
Q_OBJECT
private slots:
void aComplexOutlineZoomedOutIsTracedAtScreenResolution();
void simpleOutlinesAndActualPixelsDrawInFull();
void aTickWaitsForTheRepaintBeforeIt();
void aNewSourceDropsTheOldOutline();
void aNewStepKeepsTheOldOutlineUntilItLands();
void theStepFollowsTheZoomAndTheFillRuleHolds();
};
void MarchingAntsTests::aComplexOutlineZoomedOutIsTracedAtScreenResolution()
{
Canvas shown;
shown.session.zoom(0.25);
shown.canvas->synchronizeDisplay();
const QImage bare = shown.canvas->grab().toImage();
const QRect region(100, 100, 150, 150);
const QPainterPath board = checkerboard(region);
QVERIFY(board.elementCount() > 20'000);
shown.session.setSelection(DocumentSelection{board}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
ants(shown).stop();
// While the first outline is traced, no ants show.
QCOMPARE(shown.canvas->grab().toImage(), bare);
QTRY_VERIFY(shown.canvas->grab().toImage() != bare);
// The trace covers the board: its edge alone shows.
const QImage shot = shown.canvas->grab().toImage();
const QRect view = viewRect(shown, region);
QCOMPARE(marked(shot, bare, view.adjusted(3, 3, -3, -3)), 0);
QVERIFY(marked(shot, bare, view.adjusted(-2, -2, 2, 2)) > view.width() * 2);
}
void MarchingAntsTests::simpleOutlinesAndActualPixelsDrawInFull()
{
// At actual pixels the complex outline draws whole, at once.
Canvas shown;
const QImage bare = shown.canvas->grab().toImage();
const QRect region(100, 100, 150, 150);
shown.session.setSelection(DocumentSelection{checkerboard(region)}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
ants(shown).stop();
const QImage full = shown.canvas->grab().toImage();
QVERIFY(marked(full, bare, viewRect(shown, region).adjusted(10, 10, -10, -10)) > 1'000);
// Zoomed out, a marquee's outline draws at once.
Canvas other;
other.session.zoom(0.25);
other.canvas->synchronizeDisplay();
const QImage plain = other.canvas->grab().toImage();
other.session.applySelection(rectPath(QRectF(region)), SelectionMode::replace, QStringLiteral("Select"));
other.canvas->synchronizeDisplay();
ants(other).stop();
QVERIFY(other.canvas->grab().toImage() != plain);
}
void MarchingAntsTests::aTickWaitsForTheRepaintBeforeIt()
{
Canvas first, second;
for (Canvas *shown : {&first, &second}) {
shown->session.applySelection(rectPath(QRectF(50, 50, 100, 100)), SelectionMode::replace, QStringLiteral("Select"));
shown->canvas->synchronizeDisplay();
ants(*shown).stop();
}
QCOMPARE(first.canvas->grab().toImage(), second.canvas->grab().toImage());
// Two ticks before a repaint step once, as one.
QVERIFY(QMetaObject::invokeMethod(&ants(first), "timeout"));
QVERIFY(QMetaObject::invokeMethod(&ants(first), "timeout"));
QVERIFY(QMetaObject::invokeMethod(&ants(second), "timeout"));
const QImage stepped = first.canvas->grab().toImage();
QCOMPARE(stepped, second.canvas->grab().toImage());
// Once painted, the next tick steps again.
QVERIFY(QMetaObject::invokeMethod(&ants(first), "timeout"));
QVERIFY(first.canvas->grab().toImage() != stepped);
}
void MarchingAntsTests::aNewSourceDropsTheOldOutline()
{
Canvas shown;
shown.session.zoom(0.25);
shown.canvas->synchronizeDisplay();
const QImage bare = shown.canvas->grab().toImage();
const QRect first(10, 10, 120, 120), second(200, 100, 120, 120);
shown.session.setSelection(DocumentSelection{checkerboard(first)}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
ants(shown).stop();
QTRY_VERIFY(shown.canvas->grab().toImage() != bare);
// Another outline: the first goes at once; the second shows.
shown.session.setSelection(DocumentSelection{checkerboard(second)}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
QCOMPARE(shown.canvas->grab().toImage(), bare);
QTRY_VERIFY(shown.canvas->grab().toImage() != bare);
const QImage shot = shown.canvas->grab().toImage();
QCOMPARE(marked(shot, bare, band(shown, first)), 0);
QVERIFY(marked(shot, bare, band(shown, second)) > 0);
}
void MarchingAntsTests::aNewStepKeepsTheOldOutlineUntilItLands()
{
Canvas shown;
shown.session.zoom(0.5);
shown.canvas->synchronizeDisplay();
const QImage half = shown.canvas->grab().toImage();
shown.session.zoom(0.25);
shown.canvas->synchronizeDisplay();
const QImage quarter = shown.canvas->grab().toImage();
const QRect region(10, 10, 120, 120);
shown.session.setSelection(DocumentSelection{checkerboard(region)}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
ants(shown).stop();
QTRY_VERIFY(shown.canvas->grab().toImage() != quarter);
// A step in, the quarter's outline stands in meanwhile.
shown.session.zoom(0.5);
shown.canvas->synchronizeDisplay();
QVERIFY(marked(shown.canvas->grab().toImage(), half, band(shown, region)) > 0);
}
void MarchingAntsTests::theStepFollowsTheZoomAndTheFillRuleHolds()
{
// Boards two pixels apart: a half step keeps the gap.
Canvas shown;
shown.session.zoom(0.5);
shown.canvas->synchronizeDisplay();
const QImage bare = shown.canvas->grab().toImage();
QPainterPath boards = checkerboard(QRect(10, 10, 60, 180));
boards.addPath(checkerboard(QRect(72, 10, 60, 180)));
shown.session.setSelection(DocumentSelection{boards}, QStringLiteral("Select"));
shown.canvas->synchronizeDisplay();
ants(shown).stop();
QTRY_VERIFY(shown.canvas->grab().toImage() != bare);
const QSizeF size = shown.documentSize();
const QPointF gap = shown.session.viewport.viewPoint(QPointF(71, 100), size);
QVERIFY(marked(shown.canvas->grab().toImage(), bare, QRect(gap.toPoint() - QPoint(1, 20), QSize(3, 40))) > 0);
// An odd-even ring keeps its hole through the trace.
Canvas ring;
ring.session.zoom(0.25);
ring.canvas->synchronizeDisplay();
const QImage plain = ring.canvas->grab().toImage();
QPainterPath holed = checkerboard(QRect(40, 40, 200, 50));
holed.addRect(QRectF(40, 40, 200, 200));
holed.addRect(QRectF(100, 100, 80, 80));
holed.setFillRule(Qt::OddEvenFill);
ring.session.setSelection(DocumentSelection{holed}, QStringLiteral("Select"));
ring.canvas->synchronizeDisplay();
ants(ring).stop();
QTRY_VERIFY(ring.canvas->grab().toImage() != plain);
QVERIFY(marked(ring.canvas->grab().toImage(), plain, band(ring, QRect(100, 100, 80, 80))) > 0);
}
QTEST_MAIN(MarchingAntsTests)
#include "MarchingAntsTests.moc"
+2 -1
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "ContentView.h"
#include "PSDFixture.h"
#include <QLabel>
@@ -300,7 +301,7 @@ void PSDImportTests::aFileTheSessionRefusesIsReported()
session.insertPhotoshop(blanks(9'999), "Many");
import(session, {url});
QCOMPARE(session.importError(),
std::optional<QString>("Sky.psd: This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit."));
std::optional(QStringLiteral("Sky.psd: This import exceeds the current %1-megapixel document budget or 30,000-pixel side limit.").arg(DocumentLimits::documentBudgetMegapixels())));
QCOMPARE(session.document().value().layers.size(), size_t(9'999));
}
+1 -1
View File
@@ -291,7 +291,7 @@ void PSDRoundTripTests::unsupportedHeadersAreRejected()
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(1, 8, 8, 8, 4)); }), std::optional(PSDError::Kind::unsupportedColorMode));
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header(1, 8, 8, 16)); }), std::optional(PSDError::Kind::unsupportedDepth));
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(header(1, 30'001, 10)); }), std::optional(ImageImportError::Kind::tooLarge));
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(header(1, 10'001, 10'000)); }), std::optional(ImageImportError::Kind::tooLarge));
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(header(1, 20'001, 10'000)); }), std::optional(ImageImportError::Kind::tooLarge));
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(header(1, 0, 10)); }), std::optional(ImageImportError::Kind::tooLarge));
QCOMPARE(refusal<ImageImportError>([] { PSDReader::read(QByteArray("8BPX")); }), std::optional(ImageImportError::Kind::unreadable));
QCOMPARE(refusal<PSDError>([] { PSDReader::read(header()); }), std::optional(PSDError::Kind::truncated));
+11 -7
View File
@@ -1,3 +1,4 @@
#include "BudgetFixtures.h"
#include "BrushFixtures.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore.h"
@@ -228,12 +229,14 @@ void PixelEditTests::aRasterEditLandsOnlyOnTheLayerItReadAndTheBudgetCountsEvery
QCOMPARE(session->history.undoCount(), 0);
QCOMPARE(int(layerWith(*session, id).mask.value().asset.image().constScanLine(5)[5]), 0);
QCOMPARE(pixel(layerWith(*session, id).asset.value().image(), 5, 5), (std::vector<int>{255, 0, 0, 255}));
// Six layers of 16 megapixels leave a stroke 4 million.
// The other layers leave a stroke 4 million pixels.
const auto big = std::make_unique<EditorSession>();
big->createDocument(4000, 4000, true);
for (int i = 0; i < 6; ++i) {
const auto raster = std::make_shared<const RasterSnapshot>(4000, 4000, BrushRaster::context(1, 1, false), QRectF(0, 0, 4000, 4000), std::vector<BrushPatch>{});
big->insert(ImportedImage(raster, QImage(), QStringLiteral("Big %1").arg(i)));
const QUuid first = big->activeLayerID().value();
QSet<QUuid> claims;
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget() - 4'000'000)) {
big->insert(layer);
claims.insert(big->activeLayerID().value());
}
big->addBlankLayer();
big->selectTool(NavigationTool::brush);
@@ -248,14 +251,15 @@ void PixelEditTests::aRasterEditLandsOnlyOnTheLayerItReadAndTheBudgetCountsEvery
QVERIFY(big->brushStroke());
big->cancelBrush();
// A masked layer's budget counts the other masks too.
QImage mask = BrushRaster::context(4000, 4000, true);
const QUuid last = big->activeLayerID().value();
rewrite(*big, [&](ProjectSnapshot &snapshot) {
for (ProjectLayerRecord &record : snapshot.manifest.layers)
setMask(snapshot, record.id, LayerMask::assetFrom(mask));
setMask(snapshot, record.id, claims.contains(record.id) ? claimed(snapshot.images.at(record.id).size().width(), snapshot.images.at(record.id).size().height())
: claimed(4000, 4000));
});
QVERIFY(layerWith(*big, first).mask);
big->selectLayerTarget(last, false);
// Seven other 16-million masks: the budget runs below zero.
// The claims' masks and a 16-million one: below zero.
QCOMPARE(big->makeRasterEdit(big->activeLayer().value())->pixelLimit, qint64(-12'000'000));
}
+15 -5
View File
@@ -1,3 +1,4 @@
#include "BudgetFixtures.h"
#include "Document/ProjectWorkspace.h"
#include "Rendering/RasterSnapshot.h"
#include "SessionFixtures.h"
@@ -45,7 +46,7 @@ class ProjectCopyTests : public QObject {
Q_OBJECT
private slots:
void whatMayNotBeCopiedChangesNothing();
void theHundredMegapixelsCountAcrossBothProjects();
void theBudgetCountsAcrossBothProjects();
void aFolderTravelsWithItsContentsAndItsLinks();
void aMaskFromOutsideTheCopyIsBakedIn();
void anAdjustmentLetsGoOfItsBaseUnbaked();
@@ -104,18 +105,25 @@ void ProjectCopyTests::whatMayNotBeCopiedChangesNothing()
QCOMPARE(edits.count(), before + 6);
}
void ProjectCopyTests::theHundredMegapixelsCountAcrossBothProjects()
void ProjectCopyTests::theBudgetCountsAcrossBothProjects()
{
ProjectWorkspace workspace;
ProjectTab &from = workspace.current();
from.session.createDocument(8, 8);
from.session.insert(vast(10'000, 10'000));
// A folder claiming the whole document budget.
QSet<QUuid> claims;
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget())) {
from.session.insert(layer);
claims.insert(from.session.activeLayerID().value());
}
from.session.selectLayers(claims, from.session.activeLayerID());
from.session.groupSelectedLayers();
const QUuid id = from.session.activeLayerID().value();
ProjectTab &target = workspace.addTab();
target.session.createDocument(8, 8);
// Exactly the limit fits.
QVERIFY(copied(workspace, id, target.id));
QCOMPARE(names(target.session), (QStringList{"Vast"}));
QCOMPARE(target.session.document().value().layers.size(), size_t(claims.size() + 1));
QCOMPARE(target.session.importError(), std::nullopt);
// One pixel more, counted across both, does not.
target.session.undo();
@@ -123,13 +131,15 @@ void ProjectCopyTests::theHundredMegapixelsCountAcrossBothProjects()
target.session.setRenamingLayerID(std::nullopt);
QVERIFY(copied(workspace, id, target.id));
QCOMPARE(names(target.session), (QStringList{"Dot"}));
QCOMPARE(target.session.importError(), std::optional(QString("The copied layers exceed this project’s 100-megapixel limit.")));
QCOMPARE(target.session.importError(),
std::optional(QStringLiteral("The copied layers exceed this project’s %1-megapixel limit.").arg(DocumentLimits::documentBudgetMegapixels())));
QVERIFY(!workspace.isManaging() && !from.session.isProjectBusy() && !target.session.isProjectBusy());
// While that error shows, the front tab lets nothing start.
QVERIFY(!workspace.canSwitch() && copied(workspace, id, std::nullopt));
QCOMPARE(int(workspace.tabs().size()), 2);
target.session.setImportError(std::nullopt);
// Refused for a new tab, that tab stays.
from.session.selectLayers({}, std::nullopt);
from.session.insert(vast(1, 1));
const QUuid extra = from.session.activeLayerID().value();
from.session.addGroup();
+44 -8
View File
@@ -1,10 +1,38 @@
#include "Document/DocumentLimits.h"
#include "Document/LayerMask.h"
#include "BudgetFixtures.h"
#include "ProjectFixtures.h"
#include <QBuffer>
#include <QImageWriter>
#include <QTemporaryDir>
#include <QtTest>
#include <limits>
namespace {
// A PNG whose header claims pixels it never holds.
QByteArray claimingPNG(int width, int height)
{
QByteArray data;
QBuffer buffer(&data);
QImageWriter(&buffer, "png").write(QImage(1, 1, QImage::Format_Grayscale8));
for (int index = 0; index < 4; ++index) {
data[16 + index] = char(quint32(width) >> (24 - 8 * index));
data[20 + index] = char(quint32(height) >> (24 - 8 * index));
}
// The header chunk's CRC-32 over its type and fields.
quint32 crc = 0xFFFFFFFF;
for (int index = 12; index < 29; ++index) {
crc ^= uchar(data[index]);
for (int bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ (0xEDB88320 & (0u - (crc & 1)));
}
crc = ~crc;
for (int index = 0; index < 4; ++index)
data[29 + index] = char(crc >> (24 - 8 * index));
return data;
}
}
class ProjectTests : public QObject {
Q_OBJECT
private slots:
@@ -381,22 +409,30 @@ void ProjectTests::sizeLimitsHoldOnSaveAndLoad()
ProjectSnapshot long1 = snapshot;
long1.images.insert_or_assign(id, asset(gray(30'001, 1, 9).convertToFormat(QImage::Format_RGBA8888_Premultiplied), "long"));
QCOMPARE(projectError([&] { ProjectStore::save(long1, root.filePath("Long.comp")); }), std::optional(ProjectError::Kind::tooLarge));
// Masks share 100 million pixels, apart from the images'.
// Masks share the document budget, apart from the images'.
ProjectSnapshot heavy = snapshot;
const QUuid second = heavy.manifest.layers[1].id;
heavy.manifest.layers[0].maskFile = uuidString(id) + ".mask.png";
heavy.manifest.layers[1].maskFile = uuidString(second) + ".mask.png";
heavy.masks.insert({id, asset(gray(10'000, 10'000, 255), "whole budget")});
heavy.masks.insert({second, asset(gray(1, 1, 255), "one too many")});
// Rows of 30,000 and a remainder fill the budget exactly.
const qint64 budget = DocumentLimits::documentPixelBudget();
const qint64 rows = (budget - 1) / 30'000, rest = budget - rows * 30'000;
// Saving judges sizes before pixels: one over, summed, refused.
heavy.masks.insert({id, asset(gray(int(rest) + 1, 1, 255), "one too many")});
heavy.masks.insert({second, claimed(30'000, int(rows))});
QCOMPARE(projectError([&] { ProjectStore::save(heavy, root.filePath("Heavy.comp")); }), std::optional(ProjectError::Kind::tooLarge));
// 99,990,000 and 10,000 pixels fill the budget exactly.
heavy.masks.insert_or_assign(id, asset(gray(10'000, 9'999, 255), "nearly all"));
heavy.masks.insert_or_assign(second, asset(gray(100, 100, 255), "the rest"));
QVERIFY(!heavy.masks.at(second).raster->hasMaterializedPixels());
heavy.masks.insert_or_assign(id, asset(gray(int(rest), 1, 255), "the rest"));
heavy.masks.insert_or_assign(second, asset(gray(1, 1, 255), "small"));
QCOMPARE(projectError([&] { ProjectStore::save(heavy, root.filePath("Heavy.comp")); }), std::nullopt);
QCOMPARE(projectError([&] { ProjectStore::load(root.filePath("Heavy.comp")); }), std::nullopt);
// On load the same sum comes from the files' headers.
// Loaded, headers sum exactly to it; images count apart.
const QString firstMask = root.filePath("Heavy.comp/images/") + uuidString(id) + ".mask.png";
overwrite(firstMask, png(gray(10'000, 10'000, 255)));
const QString secondMask = root.filePath("Heavy.comp/images/") + uuidString(second) + ".mask.png";
overwrite(secondMask, claimingPNG(30'000, int(rows)));
QCOMPARE(projectError([&] { ProjectStore::load(root.filePath("Heavy.comp")); }), std::optional(ProjectError::Kind::missingImage));
// The first mask's one more pixel tips the sum over.
overwrite(firstMask, png(gray(int(rest) + 1, 1, 255)));
QCOMPARE(projectError([&] { ProjectStore::load(root.filePath("Heavy.comp")); }), std::optional(ProjectError::Kind::tooLarge));
}
+2 -1
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "Document/DocumentHistory.h"
#include "IO/ImageExporter.h"
#include "Rendering/RasterSnapshot.h"
@@ -78,7 +79,7 @@ void RasterSnapshotTests::contextAllocationFailureThrowsARenderError()
QCOMPARE(QString(error.what()), QString("The canvas could not be rendered. Try a smaller canvas."));
}
QCOMPARE(QString(ExportError(ExportError::Kind::tooLarge).what()),
QString("Image export supports canvases up to 100 megapixels and 30,000 pixels per side."));
QStringLiteral("Image export supports canvases up to %1 megapixels and 30,000 pixels per side.").arg(DocumentLimits::maxSurfaceMegapixels()));
QCOMPARE(QString(ExportError(ExportError::Kind::encode).what()), QString("The image could not be encoded."));
}
+22 -9
View File
@@ -1,3 +1,4 @@
#include "BudgetFixtures.h"
#include "ContentView.h"
#include "DNGFixture.h"
#include "IO/RawImporter.h"
@@ -113,11 +114,13 @@ void RawDevelopTests::whatCannotBeDevelopedIsReported()
DNGFixture::Options wide, tall, big;
wide.claimed = QSize(30'001, 48);
tall.claimed = QSize(64, 30'001);
big.claimed = QSize(20'000, 5'001);
big.claimed = QSize(30'000, 30'000);
// Rows of 30,000 within the document budget.
const int rows = int(DocumentLimits::documentPixelBudget() / 30'000);
DNGFixture::Options fits;
fits.claimed = QSize(20'000, 5'000);
fits.claimed = QSize(30'000, rows);
const QUrl fitting = dng(folder, "Fits.dng", fits);
QCOMPARE(RawImporter::pixelSize(fitting.toLocalFile()), std::optional(QSize(20'000, 5'000)));
QCOMPARE(RawImporter::pixelSize(fitting.toLocalFile()), std::optional(QSize(30'000, rows)));
EditorSession session;
bool done = false;
const QList<QUrl> urls{QUrl::fromLocalFile(fake.fileName()), dng(folder, "Wide.dng", wide), dng(folder, "Tall.dng", tall), dng(folder, "Big.dng", big), png(folder)};
@@ -127,12 +130,6 @@ void RawDevelopTests::whatCannotBeDevelopedIsReported()
const QString unreadable = ImageImportError(ImageImportError::Kind::unreadable).what(), tooLarge = ImageImportError(ImageImportError::Kind::tooLarge).what();
QCOMPARE(session.importError().value(), QStringList({"Fake.dng: " + unreadable, "Wide.dng: " + tooLarge, "Tall.dng: " + tooLarge, "Big.dng: " + tooLarge}).join("\n\n"));
QCOMPARE(names(session), QStringList{"Plain"});
// The canvas's 4 by 2 pixels count against the budget.
session.setImportError(std::nullopt);
done = false;
session.importImages({fitting}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Fits.dng: " + tooLarge);
// Accepted, then unreadable when the full develop runs.
session.setImportError(std::nullopt);
done = false;
@@ -158,6 +155,22 @@ void RawDevelopTests::whatCannotBeDevelopedIsReported()
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Warm.dng: " + QString(ImageImportError(ImageImportError::Kind::whiteBalance).what()));
QCOMPARE(names(session), QStringList{"Plain"});
// Past one surface yet within the document budget, it opens.
session.setImportError(std::nullopt);
done = false;
session.importImages({fitting}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(session.rawDevelop());
session.finishRawDevelop(std::nullopt);
QTRY_VERIFY(done);
QCOMPARE(session.importError(), std::nullopt);
// The slack filled, the canvas's 8 pixels tip it.
for (const ImportedImage &layer : claiming(DocumentLimits::documentPixelBudget() - qint64(rows) * 30'000))
session.insert(layer);
session.setImportError(std::nullopt);
done = false;
session.importImages({fitting}, std::nullopt, [&done] { done = true; });
QTRY_VERIFY(done);
QCOMPARE(session.importError().value(), "Fits.dng: " + tooLarge);
}
void RawDevelopTests::theLimitsLetTheirEdgeThrough()
+2 -2
View File
@@ -175,8 +175,8 @@ void ShapeLayerTests::whatIsNotRedrawn()
QCOMPARE(session->activeLayer().value().transform.size, QSizeF(30.4, 19.6));
QCOMPARE(session->activeLayer().value().asset.value().identity(), asset.identity());
QVERIFY(session->activeLayer().value().liveShape());
// Past 100 megapixels it stretches as it is.
resize(*session, QSizeF(20'001, 5'000));
// Past one surface's 200 megapixels it stretches as it is.
resize(*session, QSizeF(20'001, 10'000));
session->commitTransform();
QCOMPARE(session->activeLayer().value().asset.value().identity(), asset.identity());
session->undo();
+5 -4
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "BrushFixtures.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
@@ -227,13 +228,13 @@ void ShapeToolTests::shiftSnapsALineToEighthsOfATurn()
void ShapeToolTests::aShapeTooLargeIsRefused()
{
EditorSession session;
session.createDocument(20'000, 12'000, true);
session.createDocument(30'000, 12'000, true);
session.selectTool(NavigationTool::shape);
// Each side truncated first: 10,001 × 10,000 is over.
// Each side truncated first: 20,001 × 10,000 is over.
session.beginShape(QPointF(0, 0));
session.dragShape(QPointF(10'001, 10'000), false, false);
session.dragShape(QPointF(20'001, 10'000), false, false);
session.finishShape();
QCOMPARE(session.brushError().value(), QString("That shape is too large. A shape can cover up to 100 megapixels."));
QCOMPARE(session.brushError().value(), QStringLiteral("That shape is too large. A shape can cover up to %1 megapixels.").arg(DocumentLimits::maxSurfaceMegapixels()));
QCOMPARE(session.document().value().layers.size(), size_t(1));
QVERIFY(!session.shapeDraft());
}
+2
View File
@@ -246,6 +246,8 @@ compositor_test(SelectionSignalTests)
compositor_test(SelectMenuTests)
compositor_test(SelectionCanvasTests)
compositor_test(SelectionOverlayTests)
compositor_test(MarchingAntsTests)
compositor_test(DocumentLimitsTests)
compositor_test(LassoControlsTests)
compositor_test(SelectionAmountSheetTests)
compositor_test(MagicWandTests)
+6 -5
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "BrushFixtures.h"
#include "Document/EditorSession.h"
#include "Rendering/TextLayout.h"
@@ -187,18 +188,18 @@ void TextLayoutTests::imagesKeepSwiftsLimits()
return std::nullopt;
};
QCOMPARE(refused(styled(QStringLiteral("Text"), 0)), std::optional(ProjectError::Kind::invalid));
// A side past 30,000, then the area past 100 megapixels.
// A side past 30,000, then the area past one surface.
const LayerTextStyle wide = styled(QStringLiteral("M").repeated(20), 2000);
const QSizeF wideSize = EditorSession::textBoxSize(wide);
QVERIFY(wideSize.width() > 30'000 && wideSize.width() < 40'000 && wideSize.width() * wideSize.height() < 100'000'000);
QVERIFY(wideSize.width() > 30'000 && wideSize.width() < 40'000 && wideSize.width() * wideSize.height() < DocumentLimits::maxSurfacePixels);
QCOMPARE(refused(wide), std::optional(ProjectError::Kind::tooLarge));
const LayerTextStyle tall = styled(QStringLiteral("a\n").repeated(13), 2000);
const QSizeF tallSize = EditorSession::textBoxSize(tall);
QVERIFY(tallSize.height() > 30'000 && tallSize.height() < 40'000 && tallSize.width() * tallSize.height() < 100'000'000);
QVERIFY(tallSize.height() > 30'000 && tallSize.height() < 40'000 && tallSize.width() * tallSize.height() < DocumentLimits::maxSurfacePixels);
QCOMPARE(refused(tall), std::optional(ProjectError::Kind::tooLarge));
const LayerTextStyle large = styled(QStringLiteral("M").repeated(14) + QStringLiteral("\nM"), 2000);
const LayerTextStyle large = styled(QStringLiteral("M").repeated(14) + QStringLiteral("\nM").repeated(3), 2000);
const QSizeF largeSize = EditorSession::textBoxSize(large);
QVERIFY(largeSize.width() < 30'000 && largeSize.width() * largeSize.height() > 100'000'000);
QVERIFY(largeSize.width() < 30'000 && largeSize.width() * largeSize.height() > DocumentLimits::maxSurfacePixels);
QCOMPARE(refused(large), std::optional(ProjectError::Kind::tooLarge));
QVERIFY(!refused(styled(QStringLiteral("M").repeated(10), 2000)));
// A fractional box rounds up to whole pixels.
+6 -5
View File
@@ -1,3 +1,4 @@
#include "Document/DocumentLimits.h"
#include "BrushFixtures.h"
#include "SelectionFixtures.h"
#include "SessionRecord.h"
@@ -299,7 +300,7 @@ void TypeSessionTests::aDraggedBoxIsRoundedAndBounded()
session->cancelText();
session->beginText(QRectF(0, 0, 30'001, 50));
QVERIFY(!session->textDraft());
QCOMPARE(session->brushError().value(), QString("That text box exceeds the 30,000-pixel or 100-megapixel limit."));
QCOMPARE(session->brushError().value(), QStringLiteral("That text box exceeds the 30,000-pixel or %1-megapixel limit.").arg(DocumentLimits::maxSurfaceMegapixels()));
// No number at all is refused without a word.
session->setBrushError(std::nullopt);
session->beginText(QRectF(0, 0, INFINITY, 50));
@@ -330,11 +331,11 @@ void TypeSessionTests::aStyleIsValidWithinSwiftsBounds()
for (const double value : {low - 0.01, high + 0.01, double(NAN), double(INFINITY), -double(INFINITY)})
QVERIFY(!valid([&](LayerTextStyle &style) { style.*member = value; }));
}
// A box: 16 to 30,000 a side, 100 megapixels.
// A box: 16 to 30,000 a side, one surface's pixels.
const auto box = [&](double width, double height) { return valid([&](LayerTextStyle &style) { style.boxSize = QSizeF(width, height); }); };
QVERIFY(box(16, 16) && box(30'000, 3'000) && box(10'000, 10'000) && box(3'000, 30'000));
QVERIFY(box(16, 16) && box(30'000, 3'000) && box(20'000, 10'000) && box(3'000, 30'000));
QVERIFY(!box(15.99, 100) && !box(100, 15.99) && !box(30'000.5, 100) && !box(100, 30'000.5));
QVERIFY(!box(10'000, 10'000.01) && !box(NAN, 100) && !box(100, INFINITY));
QVERIFY(!box(20'000, 10'000.01) && !box(NAN, 100) && !box(100, INFINITY));
}
void TypeSessionTests::everyChangeIsAnnouncedAndRefusalsAreSilent()
@@ -461,7 +462,7 @@ void TypeSessionTests::applyingTakesWhatSwiftTakes()
draft.style.boxSize = std::nullopt;
session->setTextDraft(draft);
QVERIFY(!session->finishText());
QCOMPARE(session->brushError().value(), QString("This project exceeds the supported canvas, layer, file-size, or 100-megapixel image limit."));
QCOMPARE(session->brushError().value(), QStringLiteral("This project exceeds the supported canvas, layer, file-size, or %1-megapixel document limit.").arg(DocumentLimits::documentBudgetMegapixels()));
QCOMPARE(session->textDraft().value().style.content, QString("Kept and grown further"));
QCOMPARE(layerWith(*session, id).liveText().value().style.content, QString("Kept and grown"));
}