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 "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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include <QPainter>
|
||||
#include <functional>
|
||||
|
||||
// Draws through an image surface over what the painter shows.
|
||||
namespace AdjustmentSurface {
|
||||
inline constexpr qint64 pixelBudget = 100'000'000;
|
||||
inline constexpr qint64 pixelBudget = DocumentLimits::maxSurfacePixels;
|
||||
// Padding, in painter units, is drawn past what shows.
|
||||
void draw(QPainter &context, const std::function<void(QPainter &)> &body, double padding = 0,
|
||||
qint64 pixelBudget = AdjustmentSurface::pixelBudget);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include <QImage>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -9,7 +10,7 @@
|
||||
class DownsampleCache {
|
||||
public:
|
||||
// Pixels of halved copies kept at once.
|
||||
static constexpr qint64 pixelBudget = 100'000'000;
|
||||
static constexpr qint64 pixelBudget = DocumentLimits::maxSurfacePixels;
|
||||
// Most halvings ever used; QPainter does the rest.
|
||||
static constexpr int maxLevel = 6;
|
||||
|
||||
|
||||
@@ -288,6 +288,15 @@ void CanvasView::stopMarqueeAutoscroll()
|
||||
m_marqueeAutoscrollPoint = std::nullopt;
|
||||
}
|
||||
|
||||
void CanvasView::stepAnts()
|
||||
{
|
||||
if (m_antsRepaintPending)
|
||||
return;
|
||||
m_overlay.antsPhase = std::fmod(m_overlay.antsPhase + 1, 8.0);
|
||||
m_antsRepaintPending = true;
|
||||
update(m_overlay.selectionRect(rect()));
|
||||
}
|
||||
|
||||
// Marching ants animate only while a visible selection exists.
|
||||
void CanvasView::updateAntsTimer()
|
||||
{
|
||||
|
||||
@@ -67,10 +67,8 @@ CanvasView::CanvasView(EditorSession &session, QWidget *parent)
|
||||
connect(&m_marqueeAutoscroll, &QTimer::timeout, this, &CanvasView::stepMarqueeAutoscroll);
|
||||
m_antsTimer.setObjectName(QStringLiteral("antsTimer"));
|
||||
m_antsTimer.setInterval(120);
|
||||
connect(&m_antsTimer, &QTimer::timeout, this, [this] {
|
||||
m_overlay.antsPhase = std::fmod(m_overlay.antsPhase + 1, 8.0);
|
||||
update(m_overlay.selectionRect(rect()));
|
||||
});
|
||||
connect(&m_antsTimer, &QTimer::timeout, this, &CanvasView::stepAnts);
|
||||
m_overlay.repaintAnts = [this] { update(m_overlay.selectionRect(rect())); };
|
||||
}
|
||||
|
||||
CanvasView::DisplayState CanvasView::displayState() const
|
||||
@@ -205,6 +203,7 @@ std::optional<QRectF> CanvasView::renderBounds() const
|
||||
|
||||
void CanvasView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
m_antsRepaintPending = false;
|
||||
// The dirty rect, image-backed: hand blends and coverage read pixels.
|
||||
QPainter painter(this);
|
||||
painter.setClipRect(event->rect());
|
||||
|
||||
@@ -202,6 +202,7 @@ private:
|
||||
void stepMarqueeAutoscroll();
|
||||
void stopMarqueeAutoscroll();
|
||||
void updateAntsTimer();
|
||||
void stepAnts();
|
||||
// Swift's brush gestures (EditorCanvas+Brush.cpp).
|
||||
void brushMouseDown(QPointF point, Qt::KeyboardModifiers modifiers);
|
||||
bool brushMouseMove(QPointF point, Qt::KeyboardModifiers modifiers, bool dragging);
|
||||
@@ -291,6 +292,8 @@ private:
|
||||
QTimer m_marqueeAutoscroll;
|
||||
std::optional<QPointF> m_marqueeAutoscrollPoint;
|
||||
QTimer m_antsTimer;
|
||||
// A repaint still pending skips a tick: slow outlines stutter.
|
||||
bool m_antsRepaintPending = false;
|
||||
std::optional<DocumentSelection> m_displayedOutline;
|
||||
std::optional<LassoDraft> m_displayedDraft;
|
||||
std::optional<ShapeShown> m_displayedShape;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Rendering/LayerEffectsKernel.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "Rendering/PoolMap.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
@@ -156,7 +157,7 @@ QImage LayerEffectsKernel::render(const QImage &pixels, const LayerEffects &effe
|
||||
const int width = pixels.width(), height = pixels.height();
|
||||
const qint64 count = qint64(width) * height;
|
||||
// Past Metal's 80 million Swift draws on its CPU.
|
||||
if (count <= 0 || count > 100'000'000)
|
||||
if (count <= 0 || count > DocumentLimits::maxSurfacePixels)
|
||||
throw ExportError(ExportError::Kind::tooLarge);
|
||||
const QImage source = pixels.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
if (source.isNull())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "Document/LayerAdjustment.h"
|
||||
#include "Document/LayerAppearance.h"
|
||||
#include <QImage>
|
||||
@@ -13,7 +14,7 @@
|
||||
// Per-render dependency cache: a layer shows through its source's alpha.
|
||||
class LiveMaskRenderer {
|
||||
public:
|
||||
static constexpr qint64 pixelBudget = 100'000'000;
|
||||
static constexpr qint64 pixelBudget = DocumentLimits::maxSurfacePixels;
|
||||
using Source = std::function<std::optional<QUuid>(QUuid)>;
|
||||
// Draws a layer through `clip`: device-sized coverage, null for none.
|
||||
using DrawOwn = std::function<void(QUuid, QPainter &, const QImage &clip)>;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "Document/MagicWand.h"
|
||||
#include "Rendering/TransformOverlay.h"
|
||||
#include "Logging.h"
|
||||
#include <QtConcurrent>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's level of detail for marching ants, zoomed out.
|
||||
namespace {
|
||||
// Marquees and lassos stay exact: this many elements draw whole.
|
||||
constexpr int fullDetailLimit = 20'000;
|
||||
}
|
||||
|
||||
std::optional<QPainterPath> TransformOverlay::antsOutline(const QPainterPath &path, double deviceScale) const
|
||||
{
|
||||
if (m_antsSource != path) {
|
||||
m_antsSource = path;
|
||||
m_antsTask.reset();
|
||||
m_antsLevel.reset();
|
||||
m_antsPendingStep.reset();
|
||||
m_antsSourceIsComplex = path.elementCount() > fullDetailLimit;
|
||||
}
|
||||
const double scale = m_session.viewport.pointsPerPixel() * deviceScale;
|
||||
if (!m_antsSourceIsComplex || scale >= 1)
|
||||
return path;
|
||||
// Screen pixels a document pixel, a power of two above.
|
||||
const double step = std::min(1.0, std::pow(2, std::ceil(std::log2(std::max(scale, 1.0 / 4096)))));
|
||||
if ((!m_antsLevel || m_antsLevel->step != step) && m_antsPendingStep != step) {
|
||||
m_antsPendingStep = step;
|
||||
m_antsTask = std::make_unique<QFutureWatcher<std::optional<QPainterPath>>>();
|
||||
QFutureWatcher<std::optional<QPainterPath>> *watcher = m_antsTask.get();
|
||||
QObject::connect(watcher, &QFutureWatcherBase::finished, [this, watcher, step] {
|
||||
m_antsPendingStep.reset();
|
||||
if (const std::optional<QPainterPath> traced = watcher->result())
|
||||
m_antsLevel = AntsLevel{*traced, step};
|
||||
if (repaintAnts)
|
||||
repaintAnts();
|
||||
});
|
||||
const QRectF canvas(QPointF(0, 0), m_session.document().value().size());
|
||||
watcher->setFuture(QtConcurrent::run([path, canvas, step] { return traceOutline(path, canvas, step); }));
|
||||
}
|
||||
// The last step stands in until the new one lands.
|
||||
return m_antsLevel ? std::optional(m_antsLevel->path) : std::nullopt;
|
||||
}
|
||||
|
||||
// Filled, averaged down to `step`, traced along the pixels' edges.
|
||||
std::optional<QPainterPath> TransformOverlay::traceOutline(const QPainterPath &path, QRectF canvas, double step)
|
||||
{
|
||||
const QRectF box = path.boundingRect().intersected(canvas);
|
||||
const QRectF region(QPointF(std::floor(box.left()), std::floor(box.top())), QPointF(std::ceil(box.right()), std::ceil(box.bottom())));
|
||||
if (box.isEmpty() || region.width() < 1 || region.height() < 1)
|
||||
return std::nullopt;
|
||||
// Half resolution at least, about 40 megapixels at most.
|
||||
const double fill = std::min(1.0, std::max(step, std::sqrt(40'000'000 / (region.width() * region.height()))));
|
||||
QImage filled(std::max(1, int(std::ceil(region.width() * fill))), std::max(1, int(std::ceil(region.height() * fill))), QImage::Format_Grayscale8);
|
||||
if (filled.isNull())
|
||||
return std::nullopt;
|
||||
filled.fill(0);
|
||||
{
|
||||
QPainter painter(&filled);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.scale(fill, fill);
|
||||
painter.translate(-region.topLeft());
|
||||
painter.fillPath(path, Qt::white);
|
||||
}
|
||||
const int width = std::max(1, int(std::ceil(region.width() * step))), height = std::max(1, int(std::ceil(region.height() * step)));
|
||||
const QImage small = filled.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
if (small.isNull())
|
||||
return std::nullopt;
|
||||
// Any coverage counts, so thin parts keep their outline.
|
||||
std::vector<uchar> pixels(size_t(width) * size_t(height), 0);
|
||||
for (int row = 0; row < height; ++row) {
|
||||
const uchar *line = small.constScanLine(row);
|
||||
for (int column = 0; column < width; ++column)
|
||||
pixels[size_t(row) * size_t(width) + size_t(column)] = line[column] > 0 ? 255 : 0;
|
||||
}
|
||||
std::optional<QPainterPath> traced;
|
||||
try {
|
||||
traced = MagicWand::outline(pixels, width, height);
|
||||
} catch (const MagicWandError &error) {
|
||||
// Swift's `try?` is silent; the ants wait a step.
|
||||
qCWarning(lcRendering) << "the ants' outline could not be traced:" << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!traced)
|
||||
return std::nullopt;
|
||||
return QTransform::fromTranslate(region.left(), region.top()).scale(1 / step, 1 / step).map(*traced);
|
||||
}
|
||||
@@ -273,7 +273,10 @@ void TransformOverlay::drawSelection(QPainter &context) const
|
||||
const std::optional<DocumentSelection> selection = m_session.displayedSelection();
|
||||
if (!selection || selection->isEmpty() || !m_session.document())
|
||||
return;
|
||||
const QPainterPath path = documentToView().map(selection->path);
|
||||
const std::optional<QPainterPath> outline = antsOutline(selection->path, context.device()->devicePixelRatioF());
|
||||
if (!outline)
|
||||
return;
|
||||
const QPainterPath path = documentToView().map(*outline);
|
||||
context.save();
|
||||
context.setRenderHint(QPainter::Antialiasing, true);
|
||||
context.setBrush(Qt::NoBrush);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <array>
|
||||
@@ -31,6 +32,8 @@ public:
|
||||
|
||||
// The marching ants' dash offset, stepped by the canvas.
|
||||
double antsPhase = 0;
|
||||
// Asked for when a traced outline lands: repaint the ants.
|
||||
std::function<void()> repaintAnts;
|
||||
|
||||
std::optional<TransformOverlayGeometry> geometry() const;
|
||||
// Swift's gradientLine: the pending gradient's ends in view points.
|
||||
@@ -57,8 +60,22 @@ private:
|
||||
void drawGradientLine(QPainter &context, QPointF start, QPointF end) const;
|
||||
void drawCrop(QPainter &context) const;
|
||||
void drawSelection(QPainter &context) const;
|
||||
// Swift's antsOutline: the path, or zoomed out, one traced.
|
||||
std::optional<QPainterPath> antsOutline(const QPainterPath &path, double deviceScale) const;
|
||||
static std::optional<QPainterPath> traceOutline(const QPainterPath &path, QRectF canvas, double step);
|
||||
void drawLassoDraft(QPainter &context) const;
|
||||
void drawSnapGuides(QPainter &context, const QPalette &palette) const;
|
||||
|
||||
const EditorSession &m_session;
|
||||
// Swift's level of detail, cached per power-of-two zoom step.
|
||||
struct AntsLevel {
|
||||
QPainterPath path;
|
||||
double step;
|
||||
};
|
||||
mutable std::optional<QPainterPath> m_antsSource;
|
||||
mutable bool m_antsSourceIsComplex = false;
|
||||
mutable std::optional<AntsLevel> m_antsLevel;
|
||||
mutable std::optional<double> m_antsPendingStep;
|
||||
// A new trace replaces the watcher: the old result drops.
|
||||
mutable std::unique_ptr<QFutureWatcher<std::optional<QPainterPath>>> m_antsTask;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/CanvasSizeSheet.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
@@ -280,7 +281,7 @@ void CanvasSizeSheet::synchronize()
|
||||
const qint64 width = qint64(std::round(m_draft.width)), height = qint64(std::round(m_draft.height));
|
||||
m_note->setText(QStringLiteral("New: %1 × %2 pixels · %3 uncompressed").arg(grouped(width), grouped(height), ByteCounts::memory(width * height * 4)));
|
||||
} else {
|
||||
m_note->setText(QStringLiteral("Final dimensions must be 1–30,000 pixels per side."));
|
||||
m_note->setText(QStringLiteral("Final dimensions must be 1–%1 pixels per side.").arg(DocumentLimits::maxSideText()));
|
||||
}
|
||||
m_note->setForegroundRole(valid ? QPalette::PlaceholderText : QPalette::BrightText);
|
||||
for (QAbstractButton *each : m_anchors->buttons()) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/ImageSizeSheet.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include <QCheckBox>
|
||||
@@ -163,8 +164,8 @@ ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<voi
|
||||
bool ImageSizeSheet::valid() const
|
||||
{
|
||||
const double width = std::round(m_width), height = std::round(m_height);
|
||||
return m_resolution >= 1 && m_resolution <= 9600 && width >= 1 && width <= 30'000 && height >= 1 && height <= 30'000
|
||||
&& (!m_resample || width * height <= 100'000'000);
|
||||
return m_resolution >= 1 && m_resolution <= 9600 && width >= 1 && width <= DocumentLimits::maxSide && height >= 1 && height <= DocumentLimits::maxSide
|
||||
&& (!m_resample || width * height <= DocumentLimits::maxSurfacePixels);
|
||||
}
|
||||
|
||||
double ImageSizeSheet::display(double pixels, qint64 original) const
|
||||
@@ -267,7 +268,7 @@ void ImageSizeSheet::synchronize()
|
||||
: QStringLiteral("Only print dimensions and resolution change. Pixels stay unchanged."));
|
||||
const bool isValid = valid();
|
||||
m_result->setText(isValid ? QStringLiteral("Result: %1 × %2 pixels").arg(grouped(qint64(std::round(m_width))), grouped(qint64(std::round(m_height))))
|
||||
: QStringLiteral("Use 1–30,000 pixels per side, up to 100 megapixels, and 1–9,600 pixels/inch."));
|
||||
: QStringLiteral("Use 1–%1 pixels per side, up to %2 megapixels, and 1–9,600 pixels/inch.").arg(DocumentLimits::maxSideText()).arg(DocumentLimits::maxSurfaceMegapixels()));
|
||||
m_result->setForegroundRole(isValid ? QPalette::PlaceholderText : QPalette::BrightText);
|
||||
m_resize->setEnabled(isValid);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/NewCanvasSheet.h"
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
@@ -99,7 +100,7 @@ void NewCanvasSheet::synchronize()
|
||||
void NewCanvasSheet::validate()
|
||||
{
|
||||
const bool valid = CanvasDocument::validDimension(m_width->text()) && CanvasDocument::validDimension(m_height->text());
|
||||
m_note->setText(valid ? QStringLiteral("Transparent canvas · sRGB") : QStringLiteral("Enter whole numbers from 1 to 30,000 pixels."));
|
||||
m_note->setText(valid ? QStringLiteral("Transparent canvas · sRGB") : QStringLiteral("Enter whole numbers from 1 to %1 pixels.").arg(DocumentLimits::maxSideText()));
|
||||
// The palette's bright text is the theme's warning colour.
|
||||
m_note->setForegroundRole(valid ? QPalette::PlaceholderText : QPalette::BrightText);
|
||||
m_create->setEnabled(valid);
|
||||
|
||||
Reference in new issue
Block a user