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 "IO/CanvasResizer.h"
#include "Document/DocumentLimits.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include <QPainter>
@@ -9,7 +10,7 @@
namespace {
ProjectSnapshot resized(const ProjectSnapshot &snapshot, const CanvasSizeOptions &options)
{
if (options.width < 1 || options.width > 30'000 || options.height < 1 || options.height > 30'000 || options.anchor < 0
if (options.width < 1 || options.width > DocumentLimits::maxSide || options.height < 1 || options.height > DocumentLimits::maxSide || options.anchor < 0
|| options.anchor > 8)
throw ProjectError(ProjectError::Kind::tooLarge);
const ProjectManifest &old = snapshot.manifest;
@@ -49,7 +50,7 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const CanvasSizeOptions
qint64 used = 0;
for (const auto &[id, image] : images)
used += qint64(image.size().width()) * image.size().height();
if (options.width * options.height > 100'000'000 - used || manifest.layers.size() >= 10'000)
if (options.width * options.height > DocumentLimits::documentPixelBudget() - used || manifest.layers.size() >= 10'000)
throw ProjectError(ProjectError::Kind::tooLarge);
const CanvasExtensionColor &color = *options.fill;
for (const double part : {color.red, color.green, color.blue}) {
+4 -3
View File
@@ -1,4 +1,5 @@
#include "IO/ImageExporter.h"
#include "Document/DocumentLimits.h"
#include "Document/LayerEffects+Renderer.h"
#include "Document/LayerGroups.h"
#include "Document/LayerMask.h"
@@ -15,11 +16,11 @@
#include <cmath>
namespace {
const char *description(ExportError::Kind kind)
std::string description(ExportError::Kind kind)
{
switch (kind) {
case ExportError::Kind::tooLarge:
return "Image export supports canvases up to 100 megapixels and 30,000 pixels per side.";
return QStringLiteral("Image export supports canvases up to %1 megapixels and %2 pixels per side.").arg(DocumentLimits::maxSurfaceMegapixels()).arg(DocumentLimits::maxSideText()).toStdString();
case ExportError::Kind::render:
return "The canvas could not be rendered. Try a smaller canvas.";
case ExportError::Kind::encode:
@@ -50,7 +51,7 @@ ExportError::ExportError(Kind kind) : std::runtime_error(description(kind)), kin
ExportRaster ImageExporter::render(const ProjectSnapshot &snapshot)
try {
const qint64 width = snapshot.manifest.width, height = snapshot.manifest.height;
if (width < 1 || width > 30'000 || height < 1 || height > 30'000 || width * height > 100'000'000)
if (width < 1 || width > DocumentLimits::maxSide || height < 1 || height > DocumentLimits::maxSide || width * height > DocumentLimits::maxSurfacePixels)
throw ExportError(ExportError::Kind::tooLarge);
QImage image(int(width), int(height), QImage::Format_RGBA8888_Premultiplied);
if (image.isNull())
+3 -2
View File
@@ -1,4 +1,5 @@
#include "IO/ImageImporter.h"
#include "Document/DocumentLimits.h"
#include "IO/PSD/PSDDocumentBuilder.h"
#include "IO/PSD/PSDReader.h"
#include "Logging.h"
@@ -20,7 +21,7 @@ QString description(ImageImportError::Kind kind)
case ImageImportError::Kind::unsupported:
return QStringLiteral("Choose a JPEG, PNG, HEIC, TIFF, or Photoshop (PSD) file.");
case ImageImportError::Kind::tooLarge:
return QStringLiteral("This import exceeds the current 100-megapixel document budget or 30,000-pixel side limit.");
return QStringLiteral("This import exceeds the current %1-megapixel document budget or %2-pixel side limit.").arg(DocumentLimits::documentBudgetMegapixels()).arg(DocumentLimits::maxSideText());
case ImageImportError::Kind::whiteBalance:
return QStringLiteral("This camera can’t record that white balance. Move Temperature or Tint back toward the shot.");
}
@@ -37,7 +38,7 @@ void checkSize(const QSize &size, qint64 remainingPixels, const QString &path)
{
if (size.width() <= 0 || size.height() <= 0)
refuse(ImageImportError::Kind::unreadable, path, QStringLiteral("it states no size"));
if (size.width() > 30'000 || size.height() > 30'000 || qint64(size.width()) * size.height() > remainingPixels)
if (size.width() > DocumentLimits::maxSide || size.height() > DocumentLimits::maxSide || qint64(size.width()) * size.height() > remainingPixels)
refuse(ImageImportError::Kind::tooLarge, path, QStringLiteral("%1 x %2 pixels").arg(size.width()).arg(size.height()));
}
+3 -2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/DocumentLimits.h"
#include <QImage>
#include <QString>
#include <QUuid>
@@ -48,7 +49,7 @@ namespace ImageImporter {
// Qt refuses decodes over 128 MB; the app's budgets rule.
void liftAllocationLimit();
// JPEG, PNG, TIFF or HEIC: upright, sRGB, premultiplied.
ImportedImage decode(const QString &path, qint64 remainingPixels = 100'000'000);
PSDDocument loadPhotoshop(const QString &path, qint64 remainingPixels = 100'000'000);
ImportedImage decode(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
PSDDocument loadPhotoshop(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
std::map<QUuid, ImportedImage> photoshopAssets(const PSDDocument &document);
}
+5 -4
View File
@@ -1,4 +1,5 @@
#include "IO/ImageResizer.h"
#include "Document/DocumentLimits.h"
#include "Document/EditorSession.h"
#include "Document/LayerMask.h"
#include "IO/ImageExporter.h"
@@ -28,7 +29,7 @@ QImage drawn(QImage::Format format, qint64 width, qint64 height, QPointF topLeft
ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions &options)
{
// NaN fails both tests of a range.
if (options.width < 1 || options.width > 30'000 || options.height < 1 || options.height > 30'000
if (options.width < 1 || options.width > DocumentLimits::maxSide || options.height < 1 || options.height > DocumentLimits::maxSide
|| !(options.resolution >= 1 && options.resolution <= 9600))
throw ProjectError(ProjectError::Kind::tooLarge);
const ProjectManifest &old = snapshot.manifest;
@@ -38,7 +39,7 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions
manifest.layers = old.layers;
return {.manifest = manifest, .images = snapshot.images, .masks = snapshot.masks};
}
if (options.width * options.height > 100'000'000)
if (options.width * options.height > DocumentLimits::maxSurfacePixels)
throw ProjectError(ProjectError::Kind::tooLarge);
const double sx = double(options.width) / double(old.width), sy = double(options.height) / double(old.height);
// Guides scale with the pixels.
@@ -66,7 +67,7 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions
LayerTransform sourceTransform = layer.transform;
sourceTransform.sampling = options.sampling;
if (layer.imageFile) {
if (width > 30'000 || height > 30'000 || width * height > 100'000'000 - usedPixels)
if (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > DocumentLimits::documentPixelBudget() - usedPixels)
throw ProjectError(ProjectError::Kind::tooLarge);
usedPixels += width * height;
const auto source = snapshot.images.find(layer.id);
@@ -90,7 +91,7 @@ ProjectSnapshot resized(const ProjectSnapshot &snapshot, const ImageSizeOptions
if (source->second.size() == QSize(1, 1) || layer.maskPlacement) {
masks.insert({layer.id, source->second});
} else {
if (width > 30'000 || height > 30'000 || width * height > 100'000'000 - usedMaskPixels)
if (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > DocumentLimits::documentPixelBudget() - usedMaskPixels)
throw ProjectError(ProjectError::Kind::tooLarge);
usedMaskPixels += width * height;
const QImage image = drawn(QImage::Format_Grayscale8, width, height, {left, top}, sx, sy, [&](QPainter &context) {
+4 -3
View File
@@ -1,4 +1,5 @@
#include "IO/PSD/PSDReader.h"
#include "Document/DocumentLimits.h"
#include "IO/PSD/PSDChannelCoder.h"
#include "IO/PSD/PSDVector.h"
#include "Logging.h"
@@ -222,9 +223,9 @@ void decodeChannels(Cursor &cursor, RawLayer &layer, qint64 remainingPixels)
const qint64 width = std::max<qint64>(0, layer.right - layer.left), height = std::max<qint64>(0, layer.bottom - layer.top);
const qint64 maskWidth = std::max<qint64>(0, layer.maskRight - layer.maskLeft), maskHeight = std::max<qint64>(0, layer.maskBottom - layer.maskTop);
const qint64 budget = std::max<qint64>(0, remainingPixels);
if (width > 0 && height > 0 && (width > 30'000 || height > 30'000 || width * height > budget))
if (width > 0 && height > 0 && (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > budget))
throw ImageImportError(ImageImportError::Kind::tooLarge);
if (layer.hasMask && maskWidth > 0 && maskHeight > 0 && (maskWidth > 30'000 || maskHeight > 30'000 || maskWidth * maskHeight > budget))
if (layer.hasMask && maskWidth > 0 && maskHeight > 0 && (maskWidth > DocumentLimits::maxSide || maskHeight > DocumentLimits::maxSide || maskWidth * maskHeight > budget))
throw ImageImportError(ImageImportError::Kind::tooLarge);
for (const Channel &channel : layer.channels) {
const qint64 start = cursor.offset;
@@ -339,7 +340,7 @@ PSDDocument parse(const QByteArray &data, qint64 remainingPixels)
cursor.u16();
const qint64 canvasHeight = cursor.u32(), canvasWidth = cursor.u32();
const int depth = cursor.u16(), mode = cursor.u16();
if (canvasWidth < 1 || canvasWidth > 30'000 || canvasHeight < 1 || canvasHeight > 30'000 || canvasWidth * canvasHeight > 100'000'000)
if (canvasWidth < 1 || canvasWidth > DocumentLimits::maxSide || canvasHeight < 1 || canvasHeight > DocumentLimits::maxSide || canvasWidth * canvasHeight > DocumentLimits::maxSurfacePixels)
throw ImageImportError(ImageImportError::Kind::tooLarge);
if (depth != 8)
throw PSDError(PSDError::Kind::unsupportedDepth);
+3 -2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/DocumentLimits.h"
#include "IO/PSD/PSDTypes.h"
#include <QByteArray>
#include <QString>
@@ -9,8 +10,8 @@
namespace PSDReader {
bool matches(const QString &path);
bool matches(const QByteArray &data);
PSDDocument read(const QString &path, qint64 remainingPixels = 100'000'000);
PSDDocument read(const QByteArray &data, qint64 remainingPixels = 100'000'000);
PSDDocument read(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
PSDDocument read(const QByteArray &data, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
extern const std::set<QString> adjustmentKeys;
}
+4 -3
View File
@@ -1,4 +1,5 @@
#include "IO/PSD/PSDVector.h"
#include "Document/DocumentLimits.h"
#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include <QPainter>
@@ -76,10 +77,10 @@ QRectF integral(const QRectF &box)
return QRectF(left, top, std::ceil(box.right()) - left, std::ceil(box.bottom()) - top);
}
// Within 30,000 a side and the pixel budget, else tooLarge.
// Within the side limit and the pixel budget, else tooLarge.
QSize pixelSize(QSizeF size, qint64 remainingPixels)
{
if (std::abs(size.width()) > 30'000 || std::abs(size.height()) > 30'000)
if (std::abs(size.width()) > DocumentLimits::maxSide || std::abs(size.height()) > DocumentLimits::maxSide)
throw ImageImportError(ImageImportError::Kind::tooLarge);
const qint64 budget = std::min(EditorSession::maxShapePixels, std::max<qint64>(0, remainingPixels));
// Boxes are whole here, so the whole pixels decide alone.
@@ -214,7 +215,7 @@ std::optional<PSDVector::Raster> PSDVector::raster(const Extra &extra, QSizeF ca
return std::nullopt;
if (!std::isfinite(strokeWidth))
return std::nullopt;
if (strokeEnabled && !(strokeWidth >= 0 && strokeWidth <= 30'000))
if (strokeEnabled && !(strokeWidth >= 0 && strokeWidth <= DocumentLimits::maxSide))
throw ImageImportError(ImageImportError::Kind::tooLarge);
QRectF box = outline->boundingRect();
if (strokeEnabled) {
+3 -2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/DocumentLimits.h"
#include "Document/ShapeTool.h"
#include <QByteArray>
#include <QImage>
@@ -23,8 +24,8 @@ struct Live {
using Extra = std::map<QString, QByteArray>;
std::optional<Live> live(const Extra &extra, QSizeF canvas, qint64 remainingPixels = 100'000'000);
std::optional<Raster> raster(const Extra &extra, QSizeF canvas, qint64 remainingPixels = 100'000'000);
std::optional<Live> live(const Extra &extra, QSizeF canvas, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
std::optional<Raster> raster(const Extra &extra, QSizeF canvas, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
std::optional<QPainterPath> path(const QByteArray &data, QSizeF canvas);
struct RGB {
double r;
+20 -11
View File
@@ -1,4 +1,5 @@
#include "IO/ProjectStore.h"
#include "Document/DocumentLimits.h"
#include "Document/LayerGroups.h"
#include "Document/LayerMask.h"
#include "Document/LiveLayerMask.h"
@@ -29,7 +30,7 @@ QString description(ProjectError::Kind kind)
case ProjectError::Kind::missingImage:
return QStringLiteral("An image inside the project is missing or damaged. The current document has not been replaced.");
case ProjectError::Kind::tooLarge:
return QStringLiteral("This project exceeds the supported canvas, layer, file-size, or 100-megapixel image limit.");
return QStringLiteral("This project exceeds the supported canvas, layer, file-size, or %1-megapixel document limit.").arg(DocumentLimits::documentBudgetMegapixels());
case ProjectError::Kind::encode:
return QStringLiteral("An image could not be saved. The previous project has not been replaced.");
}
@@ -69,7 +70,7 @@ void validate(const ProjectManifest &manifest)
// The range refuses NaN and infinity as well.
if (manifest.resolution && !(*manifest.resolution >= 1 && *manifest.resolution <= 9600))
throw ProjectError(ProjectError::Kind::invalid);
if (manifest.width < 1 || manifest.width > 30'000 || manifest.height < 1 || manifest.height > 30'000 || manifest.layers.size() > 10'000)
if (manifest.width < 1 || manifest.width > DocumentLimits::maxSide || manifest.height < 1 || manifest.height > DocumentLimits::maxSide || manifest.layers.size() > 10'000)
throw ProjectError(ProjectError::Kind::tooLarge);
for (const ProjectLayerRecord &layer : manifest.layers) {
// Text needs a valid style and pixels to show it.
@@ -116,11 +117,18 @@ void validate(const ProjectManifest &manifest)
validateGuides(manifest);
}
// Images share 100 million pixels; masks share another.
void checkSize(QSize size, qint64 &used)
// Images share the document budget; masks share another.
struct Budgets {
qint64 images = 0;
qint64 masks = 0;
};
// Save and load count alike: one rule for both.
void checkSize(QSize size, bool isMask, Budgets &budgets)
{
qint64 &used = isMask ? budgets.masks : budgets.images;
const qint64 pixels = qint64(size.width()) * size.height();
if (size.width() < 1 || size.width() > 30'000 || size.height() < 1 || size.height() > 30'000 || pixels > 100'000'000 - used)
if (size.width() < 1 || size.width() > DocumentLimits::maxSide || size.height() < 1 || size.height() > DocumentLimits::maxSide || pixels > DocumentLimits::documentPixelBudget() - used)
throw ProjectError(ProjectError::Kind::tooLarge);
used += pixels;
}
@@ -225,7 +233,7 @@ void replace(const QString &staged, const QString &destination, const QString &p
}
// The pixels and a thumbnail of at most 96 pixels.
ImportedImage asset(const QString &file, bool isMask, const QString &name, qint64 &used)
ImportedImage asset(const QString &file, bool isMask, const QString &name, Budgets &used)
{
ImageImporter::liftAllocationLimit();
QImageReader reader(file);
@@ -234,7 +242,7 @@ ImportedImage asset(const QString &file, bool isMask, const QString &name, qint6
|| stored == QImage::Format_Grayscale16;
if (reader.format() != "png" || !reader.size().isValid() || deep)
throw ProjectError(ProjectError::Kind::missingImage);
checkSize(reader.size(), used);
checkSize(reader.size(), isMask, used);
QImage image = reader.read();
if (!isMask)
image = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
@@ -271,7 +279,7 @@ void ProjectStore::save(const ProjectSnapshot &snapshot, const QString &path)
{
validate(snapshot.manifest);
std::vector<std::pair<QString, QByteArray>> files;
qint64 pixels = 0, maskPixels = 0;
Budgets budgets;
for (const ProjectLayerRecord &layer : snapshot.manifest.layers) {
for (const bool isMask : {false, true}) {
const std::optional<QString> &filename = isMask ? layer.maskFile : layer.imageFile;
@@ -281,10 +289,11 @@ void ProjectStore::save(const ProjectSnapshot &snapshot, const QString &path)
const auto found = assets.find(layer.id);
if (found == assets.end())
throw ProjectError(ProjectError::Kind::missingImage);
// Swift's lazy image has its size unrendered: judged first.
checkSize(found->second.size(), isMask, budgets);
const QImage image = found->second.image();
if (isMask && !LayerMask::isValid(image))
throw ProjectError(ProjectError::Kind::invalid);
checkSize(image.size(), isMask ? maskPixels : pixels);
files.push_back({*filename, png(image)});
}
}
@@ -340,7 +349,7 @@ ProjectSnapshot ProjectStore::load(const QString &path)
const ProjectManifest manifest = ProjectManifest::decoded(metadata);
validate(manifest);
ProjectSnapshot snapshot{.manifest = manifest, .images = {}, .masks = {}};
qint64 pixels = 0, maskPixels = 0;
Budgets budgets;
for (const ProjectLayerRecord &layer : manifest.layers) {
for (const bool isMask : {false, true}) {
const std::optional<QString> &filename = isMask ? layer.maskFile : layer.imageFile;
@@ -348,7 +357,7 @@ ProjectSnapshot ProjectStore::load(const QString &path)
continue;
const QString file = path + QLatin1String("/images/") + *filename;
checkFile(file, path, assetBytes, ProjectError::Kind::missingImage);
(isMask ? snapshot.masks : snapshot.images).insert({layer.id, asset(file, isMask, layer.name, isMask ? maskPixels : pixels)});
(isMask ? snapshot.masks : snapshot.images).insert({layer.id, asset(file, isMask, layer.name, budgets)});
}
}
qCInfo(lcIO) << "loaded" << path << "with" << manifest.layers.size() << "layers and" << snapshot.images.size() + snapshot.masks.size() << "images";