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 -2
View File
@@ -1,4 +1,5 @@
#include "Document/BrushStroke.h"
#include "Document/DocumentLimits.h"
#include "Rendering/PoolMap.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
@@ -72,7 +73,7 @@ BrushStroke::Grid BrushStroke::grid(const ImageLayer &layer, bool mask, const Br
: layer.asset ? layer.asset->size().width() : int(std::round(layer.size().width()));
const int originalHeight = placedMask ? layer.mask->asset.size().height()
: layer.asset ? layer.asset->size().height() : int(std::round(layer.size().height()));
if (originalWidth < 1 || originalWidth > 30'000 || originalHeight < 1 || originalHeight > 30'000)
if (originalWidth < 1 || originalWidth > DocumentLimits::maxSide || originalHeight < 1 || originalHeight > DocumentLimits::maxSide)
throw ProjectError(ProjectError::Kind::tooLarge);
const QTransform originalMapping = BrushRaster::pixelToDocument(base, originalWidth, originalHeight);
const QRectF originalBounds(0, 0, originalWidth, originalHeight);
@@ -301,7 +302,7 @@ void BrushStroke::allocateTile(qint64 key, qint64 x, qint64 y)
return;
const QRectF rect(x * tileSize, y * tileSize, std::min<qint64>(tileSize, width - x * tileSize), std::min<qint64>(tileSize, height - y * tileSize));
const QRectF nextBounds = m_allocatedBounds ? m_allocatedBounds->united(rect) : m_source ? sourceRect.united(rect) : rect;
if (nextBounds.width() > 30'000 || nextBounds.height() > 30'000 || nextBounds.width() * nextBounds.height() > pixelLimit)
if (nextBounds.width() > DocumentLimits::maxSide || nextBounds.height() > DocumentLimits::maxSide || nextBounds.width() * nextBounds.height() > pixelLimit)
throw ProjectError(ProjectError::Kind::tooLarge);
m_allocatedBounds = nextBounds;
QImage context = BrushRaster::context(int(rect.width()), int(rect.height()), isMask);
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include "Document/EditorSession+Model.h"
#include "Document/DocumentLimits.h"
#include "Document/Gradient.h"
#include "Document/LayerTransform.h"
#include "Document/Selection.h"
@@ -98,7 +99,7 @@ public:
const QTransform pixelToDocument;
const LayerTransform paintTransform;
// Swift's Int: the budget left may run below zero.
qint64 pixelLimit = 100'000'000;
qint64 pixelLimit = DocumentLimits::documentPixelBudget();
// Limits every edit to the document selection; nil when none.
std::optional<SelectionClip> selectionClip;
// Clone Stamp: a document-sized image, and each point's offset.
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/CanvasSize.h"
#include "Document/DocumentLimits.h"
#include <cmath>
QString rawValue(CanvasUnit unit)
@@ -24,7 +25,7 @@ CanvasSizeDraft::CanvasSizeDraft(qint64 width, qint64 height, double resolution)
bool CanvasSizeDraft::valid() const
{
// NaN and infinity fail both range tests.
return std::round(width) >= 1 && std::round(width) <= 30'000 && std::round(height) >= 1 && std::round(height) <= 30'000;
return std::round(width) >= 1 && std::round(width) <= DocumentLimits::maxSide && std::round(height) >= 1 && std::round(height) <= DocumentLimits::maxSide;
}
double CanvasSizeDraft::displayed(bool widthAxis) const
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/Crop.h"
#include "Document/DocumentLimits.h"
#include "Document/Distort.h"
#include "Document/EditorSession.h"
#include "IO/CanvasResizer.h"
@@ -19,7 +20,7 @@ QRectF CropGeometry::snapped(const QRectF &rect)
bool CropGeometry::valid(const QRectF &rect)
{
const QRectF standard = rect.normalized();
return standard.width() >= 1 && standard.width() <= 30'000 && standard.height() >= 1 && standard.height() <= 30'000
return standard.width() >= 1 && standard.width() <= DocumentLimits::maxSide && standard.height() >= 1 && standard.height() <= DocumentLimits::maxSide
&& std::abs(standard.left()) <= 1'000'000 && std::abs(standard.top()) <= 1'000'000;
}
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/Distort.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "Document/LayerEffects+Renderer.h"
@@ -174,7 +175,7 @@ DistortWarp::Warped DistortWarp::warp(const QImage &image, const LayerTransform
if (!isUsable(corners))
throw ProjectError(ProjectError::Kind::invalid);
const QRectF bounds = shapeBounds(corners);
if (bounds.width() < 1 || bounds.height() < 1 || bounds.width() > 30'000 || bounds.height() > 30'000 || bounds.width() * bounds.height() > 100'000'000)
if (bounds.width() < 1 || bounds.height() < 1 || bounds.width() > DocumentLimits::maxSide || bounds.height() > DocumentLimits::maxSide || bounds.width() * bounds.height() > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
const LayerTransform placed{.origin = bounds.topLeft(), .size = bounds.size(), .sampling = transform.sampling};
// A uniform 1 × 1 mask already covers any shape.
+31
View File
@@ -0,0 +1,31 @@
#include "Document/DocumentLimits.h"
#include <QLocale>
#include <algorithm>
#include <unistd.h>
// Swift's physicalMemory / 16, read once as a static let.
qint64 DocumentLimits::documentPixelBudget()
{
static const qint64 budget = budgetFor(qint64(sysconf(_SC_PHYS_PAGES)) * qint64(sysconf(_SC_PAGE_SIZE)));
return budget;
}
qint64 DocumentLimits::budgetFor(qint64 memory)
{
return std::min<qint64>(800'000'000, std::max(maxSurfacePixels, memory / 16));
}
qint64 DocumentLimits::maxSurfaceMegapixels()
{
return maxSurfacePixels / 1'000'000;
}
qint64 DocumentLimits::documentBudgetMegapixels()
{
return documentPixelBudget() / 1'000'000;
}
QString DocumentLimits::maxSideText()
{
return QLocale(QLocale::English).toString(maxSide);
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QString>
#include <QtGlobal>
// Swift's DocumentLimits: one surface, and a whole document's raster.
namespace DocumentLimits {
// Longest side of any canvas, layer, mask or generated surface.
inline constexpr int maxSide = 30'000;
// Largest single surface; both ceilings stay under maxSide squared.
inline constexpr qint64 maxSurfacePixels = 200'000'000;
// All layers and masks: a quarter of memory, capped.
qint64 documentPixelBudget();
// The budget for this much memory, in bytes.
qint64 budgetFor(qint64 memory);
qint64 maxSurfaceMegapixels();
qint64 documentBudgetMegapixels();
// The side limit as Swift's `formatted()` prints it: 30,000.
QString maxSideText();
}
+3 -2
View File
@@ -1,4 +1,5 @@
#include "Document/BrushStroke.h"
#include "Document/DocumentLimits.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore.h"
@@ -50,7 +51,7 @@ std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &lay
if (image)
used += qint64(image->size().width()) * image->size().height();
}
stroke->pixelLimit = 100'000'000 - used;
stroke->pixelLimit = DocumentLimits::documentPixelBudget() - used;
const std::optional<DocumentSelection> current = selection();
if (current)
stroke->selectionClip = current->clip(m_document->size());
@@ -60,7 +61,7 @@ std::unique_ptr<BrushStroke> EditorSession::makeRasterEdit(const ImageLayer &lay
if (other.id != layer.id && other.mask)
maskPixels += qint64(other.mask->asset.size().width()) * other.mask->asset.size().height();
}
stroke->pixelLimit = std::min<qint64>(stroke->pixelLimit, 100'000'000 - maskPixels);
stroke->pixelLimit = std::min<qint64>(stroke->pixelLimit, DocumentLimits::documentPixelBudget() - maskPixels);
}
return stroke;
}
+5 -4
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Document/ProjectWorkspace.h"
#include "Document/PixelAdjust.h"
#include "IO/PSD/PSDReader.h"
@@ -108,12 +109,12 @@ void EditorSession::decodeNext()
}
const QUrl url = request.urls[m_importIndex];
if (url.isLocalFile() && RawImporter::matches(url.toLocalFile())) {
decodeRaw(url.toLocalFile(), 100'000'000 - usedPixels);
decodeRaw(url.toLocalFile(), DocumentLimits::documentPixelBudget() - usedPixels);
return;
}
if (url.isLocalFile() && PSDReader::matches(url.toLocalFile())) {
beginPSDReading(QStringLiteral("Open “%1”?").arg(url.fileName()), QStringLiteral("Import"));
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = 100'000'000 - usedPixels]() -> PhotoshopRead {
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> PhotoshopRead {
try {
PSDDocument document = ImageImporter::loadPhotoshop(path, remaining);
std::map<QUuid, ImportedImage> assets = ImageImporter::photoshopAssets(document);
@@ -128,7 +129,7 @@ void EditorSession::decodeNext()
}));
return;
}
m_decoder.setFuture(QtConcurrent::run([url, remaining = 100'000'000 - usedPixels]() -> Decoded {
m_decoder.setFuture(QtConcurrent::run([url, remaining = DocumentLimits::documentPixelBudget() - usedPixels]() -> Decoded {
try {
if (!url.isLocalFile())
throw ImageImportError(ImageImportError::Kind::unsupported);
@@ -150,7 +151,7 @@ void EditorSession::decodeRaw(const QString &path, qint64 remaining)
};
if (!size)
return refuse(ImageImportError::Kind::unreadable);
if (size->width() > 30'000 || size->height() > 30'000 || qint64(size->width()) * size->height() > remaining)
if (size->width() > DocumentLimits::maxSide || size->height() > DocumentLimits::maxSide || qint64(size->width()) * size->height() > remaining)
return refuse(ImageImportError::Kind::tooLarge);
developRaw(path, [this, path, name](std::optional<RawDevelopSettings> settings) {
if (!settings) {
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession+Model.h"
#include "Document/DocumentLimits.h"
#include "IO/ProjectStore.h"
#include <algorithm>
@@ -85,7 +86,7 @@ std::optional<int> CanvasDocument::validDimension(const QString &value)
while (last > first && isWhitespace(value[last - 1]))
--last;
const std::optional<int> number = wholeNumber(QStringView(value).mid(first, last - first));
if (!number || *number < 1 || *number > 30'000)
if (!number || *number < 1 || *number > DocumentLimits::maxSide)
return std::nullopt;
return number;
}
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Logging.h"
std::optional<ProjectSnapshot> EditorSession::projectSnapshot() const
@@ -69,7 +70,7 @@ void EditorSession::clearProject()
void EditorSession::createNewProject(int width, int height)
{
if (m_isProjectBusy || m_isImporting || width < 1 || width > 30'000 || height < 1 || height > 30'000)
if (m_isProjectBusy || m_isImporting || width < 1 || width > DocumentLimits::maxSide || height < 1 || height > DocumentLimits::maxSide)
return;
clearProject();
createDocument(width, height, true);
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Document/Crop.h"
#include "Logging.h"
#include <cmath>
@@ -282,7 +283,7 @@ void EditorSession::insert(const ImportedImage &asset, std::optional<QPointF> ce
void EditorSession::createDocument(int width, int height, bool emptyLayer)
{
if (m_isProjectBusy || m_isImporting || width < 1 || width > 30'000 || height < 1 || height > 30'000)
if (m_isProjectBusy || m_isImporting || width < 1 || width > DocumentLimits::maxSide || height < 1 || height > DocumentLimits::maxSide)
return;
commitTransform();
beginEdit(QStringLiteral("New Canvas"));
+1
View File
@@ -4,6 +4,7 @@
#include "Document/ColorPalette.h"
#include "Document/Gradient.h"
#include "Document/Distort.h"
#include "Document/DocumentLimits.h"
#include "Document/Filters.h"
#include "Document/LevelsAutomatic.h"
#include "Document/FloatingSelection.h"
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/Filters.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "Document/ContentFill.h"
#include "Document/EditorSession+Model.h"
@@ -244,7 +245,7 @@ void FilterEdit::grow(const QRectF &extent)
const QRectF target = integral(bounds.united(extent));
if (target == bounds)
return;
if (target.width() > 30'000 || target.height() > 30'000 || target.width() * target.height() > 100'000'000)
if (target.width() > DocumentLimits::maxSide || target.height() > DocumentLimits::maxSide || target.width() * target.height() > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
QImage grown = BrushRaster::context(int(target.width()), int(target.height()), false);
const QRectF inside = bounds.translated(-target.left(), -target.top());
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/FloatingSelection.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "Document/Distort.h"
#include "Document/EditorSession.h"
@@ -168,7 +169,7 @@ FloatingMerge::Merged FloatingMerge::merge(const QImage &pixels, const LayerTran
const QRectF floatingBounds = toPixels.mapRect(BrushRaster::pixelToDocument(transform, pixels.width(), pixels.height()).mapRect(QRectF(pixels.rect())));
const QRectF original(0, 0, width, height);
const QRectF extent = integral(original.united(floatingBounds));
if (extent.width() > 30'000 || extent.height() > 30'000 || extent.width() * extent.height() > 100'000'000)
if (extent.width() > DocumentLimits::maxSide || extent.height() > DocumentLimits::maxSide || extent.width() * extent.height() > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
QImage context = BrushRaster::context(int(extent.width()), int(extent.height()), false);
const QRectF placed = original.translated(-extent.topLeft());
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/LayerEffects+Renderer.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
@@ -116,7 +117,7 @@ LayerEffectsRenderer::Rendered LayerEffectsRenderer::render(const QImage &image,
throw ProjectError(ProjectError::Kind::invalid);
const double inset = margin(effects);
const qint64 width = image.width() + qint64(inset) * 2, height = image.height() + qint64(inset) * 2;
if (width <= 0 || height <= 0 || width * height > 100'000'000)
if (width <= 0 || height <= 0 || width * height > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
// The pixels with room round them, then the effects.
QImage padded = BrushRaster::context(int(width), int(height), false);
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/Distort.h"
#include "Document/DocumentLimits.h"
#include "Document/LayerMask.h"
#include "Document/PixelAdjust.h"
#include "Document/BrushStroke.h"
@@ -280,7 +281,7 @@ void EditorSession::addMask(bool revealing)
const int width = active->asset ? active->asset->size().width() : int(std::round(active->size().width()));
const int height = active->asset ? active->asset->size().height() : int(std::round(active->size().height()));
try {
if (width <= 0 || height <= 0 || qint64(width) * height > 100'000'000)
if (width <= 0 || height <= 0 || qint64(width) * height > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
// The selection's coverage, soft where it is feathered.
QImage image = PixelAdjust::coverage(current->clip(m_document->size()), width, height,
+3 -2
View File
@@ -1,4 +1,5 @@
#include "Document/ProjectWorkspace.h"
#include "Document/DocumentLimits.h"
#include "Document/LiveLayerMask.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
@@ -80,8 +81,8 @@ void ProjectWorkspace::copyLayers(const std::vector<QUuid> &ids, std::optional<Q
copy.layers.push_back(layer);
}
const qint64 used = copy.target->session.document() ? pixels(copy.target->session.document()->layers) : 0;
if (used + pixels(copy.layers) > 100'000'000) {
copy.target->session.setImportError(QStringLiteral("The copied layers exceed this project’s 100-megapixel limit."));
if (used + pixels(copy.layers) > DocumentLimits::documentPixelBudget()) {
copy.target->session.setImportError(QStringLiteral("The copied layers exceed this project’s %1-megapixel limit.").arg(DocumentLimits::documentBudgetMegapixels()));
finish(done);
return;
}
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/BrushStroke.h"
#include "Document/DocumentLimits.h"
#include "Document/EditorSession.h"
#include "Document/PixelAdjust.h"
#include "Document/PixelInvert.h"
@@ -11,7 +12,7 @@ namespace {
// A uniform 1×1 mask given the layer's pixel grid.
QImage expandedUniformMask(const QImage &image, int width, int height)
{
if (width <= 0 || height <= 0 || qint64(width) * height > 100'000'000)
if (width <= 0 || height <= 0 || qint64(width) * height > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
QImage expanded = BrushRaster::context(width, height, true);
QPainter painter(&expanded);
+1 -1
View File
@@ -1,7 +1,7 @@
// Swift's ShapeTool extension: shapes dragged out as new layers.
public:
// Pixels one shape layer may hold, an import's budget.
static constexpr qint64 maxShapePixels = 100'000'000;
static constexpr qint64 maxShapePixels = DocumentLimits::maxSurfacePixels;
ShapeKind shapeKind() const { return m_shapeKind; }
void setShapeKind(ShapeKind kind);
// A rectangle's corner radius, in pixels; 0 keeps corners square.
+2 -1
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
@@ -167,7 +168,7 @@ void EditorSession::finishShape()
return;
// Swift's Int(): each side truncated before multiplying.
if (std::trunc(rect.width()) * std::trunc(rect.height()) > double(maxShapePixels)) {
setBrushError(QStringLiteral("That shape is too large. A shape can cover up to 100 megapixels."));
setBrushError(QStringLiteral("That shape is too large. A shape can cover up to %1 megapixels.").arg(DocumentLimits::maxSurfaceMegapixels()));
return;
}
// The ends as fractions of the box, for resizing.
+4 -3
View File
@@ -1,4 +1,5 @@
#include "Document/EditorSession.h"
#include "Document/DocumentLimits.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
@@ -35,7 +36,7 @@ bool LayerTextStyle::boxIsValid() const
if (!boxSize)
return true;
const double width = boxSize->width(), height = boxSize->height();
return width >= 16 && width <= 30'000 && height >= 16 && height <= 30'000 && width * height <= 100'000'000;
return width >= 16 && width <= DocumentLimits::maxSide && height >= 16 && height <= DocumentLimits::maxSide && width * height <= DocumentLimits::maxSurfacePixels;
}
bool LayerTextStyle::isValid() const
@@ -195,7 +196,7 @@ void EditorSession::beginText(const QRectF &rect)
LayerTextStyle style = m_textDefaults;
style.boxSize = QSizeF(std::max(16.0, std::round(rect.width())), std::max(16.0, std::round(rect.height())));
if (!style.boxIsValid()) {
setBrushError(QStringLiteral("That text box exceeds the 30,000-pixel or 100-megapixel limit."));
setBrushError(QStringLiteral("That text box exceeds the %1-pixel or %2-megapixel limit.").arg(DocumentLimits::maxSideText()).arg(DocumentLimits::maxSurfaceMegapixels()));
return;
}
beginText(rect.topLeft(), true);
@@ -297,7 +298,7 @@ QImage EditorSession::textImage(const LayerTextStyle &style)
const QSizeF size = textBoxSize(style);
const double width = std::ceil(size.width()), height = std::ceil(size.height());
// A valid style measures sixteen pixels at least.
if (!(width <= 30'000 && height <= 30'000 && width * height <= 100'000'000))
if (!(width <= DocumentLimits::maxSide && height <= DocumentLimits::maxSide && width * height <= DocumentLimits::maxSurfacePixels))
throw ProjectError(ProjectError::Kind::tooLarge);
QImage context = BrushRaster::context(int(width), int(height), false);
const double padding = LayerTextStyle::padding;