The document's own pixel budget, marching ants at screen resolution (12.11a)
This commit is contained in:
1 parent
ffba1753c6
commit
8a9c56ee1f
75 files changed
+759
-189
No files matched your search
@@ -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}) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user