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

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

No files matched your search

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