#include "IO/ImageResizer.h" #include "Document/EditorSession.h" #include "Document/LayerMask.h" #include "IO/ImageExporter.h" #include "Logging.h" #include "Rendering/LayerRenderer.h" #include #include #include #include namespace { // A surface over the scaled layer, in document coordinates. QImage drawn(QImage::Format format, qint64 width, qint64 height, QPointF topLeft, double sx, double sy, const std::function &body) { QImage image(int(width), int(height), format); if (image.isNull()) throw ExportError(ExportError::Kind::render); image.fill(0); QPainter context(&image); context.translate(-topLeft.x(), -topLeft.y()); context.scale(sx, sy); body(context); return image; } 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 || !(options.resolution >= 1 && options.resolution <= 9600)) throw ProjectError(ProjectError::Kind::tooLarge); const ProjectManifest &old = snapshot.manifest; ProjectManifest manifest{.resolution = options.resolution, .documentID = old.documentID, .width = options.width, .height = options.height, .activeLayerID = old.activeLayerID, .layers = {}, .guides = old.guides}; if (old.width == options.width && old.height == options.height) { manifest.layers = old.layers; return {.manifest = manifest, .images = snapshot.images, .masks = snapshot.masks}; } if (options.width * options.height > 100'000'000) 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. if (manifest.guides) { for (CanvasGuide &guide : *manifest.guides) guide = guide.scaled(sx, sy); } std::map images, masks; qint64 usedPixels = 0, usedMaskPixels = 0; for (const ProjectLayerRecord &layer : old.layers) { // Each layer is drawn again: unequal scaling shears turned ones. double left = INFINITY, top = INFINITY, right = -INFINITY, bottom = -INFINITY; for (const QPointF unit : {QPointF(0, 0), QPointF(1, 0), QPointF(1, 1), QPointF(0, 1)}) { const QPointF corner = layer.transform.point(unit); left = std::min(left, corner.x() * sx), right = std::max(right, corner.x() * sx); top = std::min(top, corner.y() * sy), bottom = std::max(bottom, corner.y() * sy); } left = std::floor(left), top = std::floor(top); const LayerTransform transform{.origin = {left, top}, .size = {std::ceil(right) - left, std::ceil(bottom) - top}, .sampling = options.sampling}; // Judged before any number becomes an integer. if (!transform.isValid()) throw ProjectError(ProjectError::Kind::tooLarge); const qint64 width = qint64(transform.size.width()), height = qint64(transform.size.height()); LayerTransform sourceTransform = layer.transform; sourceTransform.sampling = options.sampling; if (layer.imageFile) { if (width > 30'000 || height > 30'000 || width * height > 100'000'000 - usedPixels) throw ProjectError(ProjectError::Kind::tooLarge); usedPixels += width * height; const auto source = snapshot.images.find(layer.id); if (source == snapshot.images.end()) throw ProjectError(ProjectError::Kind::missingImage); const QImage image = drawn(QImage::Format_RGBA8888_Premultiplied, width, height, {left, top}, sx, sy, [&](QPainter &context) { LayerRenderer::draw(source->second.image(), sourceTransform, sourceTransform.center(), context); }); const double factor = std::min(1.0, 96 / double(std::max(width, height))); const QSize small(std::max(1, int(double(width) * factor)), std::max(1, int(double(height) * factor))); const QImage thumbnail = image.scaled(small, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); if (thumbnail.isNull()) throw ExportError(ExportError::Kind::render); images.insert({layer.id, ImportedImage(image, thumbnail, source->second.name)}); } if (layer.maskFile) { const auto source = snapshot.masks.find(layer.id); if (source == snapshot.masks.end()) throw ProjectError(ProjectError::Kind::missingImage); // Solid masks have no resolution; placed ones keep their pixels. 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) throw ProjectError(ProjectError::Kind::tooLarge); usedMaskPixels += width * height; const QImage image = drawn(QImage::Format_Grayscale8, width, height, {left, top}, sx, sy, [&](QPainter &context) { LayerRenderer::drawCoverage(source->second.image(), sourceTransform, context); }); masks.insert({layer.id, LayerMask::assetFrom(image)}); } } std::optional placement; if (layer.maskPlacement) placement = layer.maskPlacement->placing(layer.maskPlacement->unitToDocument() * QTransform::fromScale(sx, sy)); manifest.layers.push_back({.id = layer.id, .name = layer.name, .isVisible = layer.isVisible, .transform = transform, .imageFile = layer.imageFile, .parentID = layer.parentID, .isGroup = layer.isGroup, .opacity = layer.opacity, .blendMode = layer.blendMode, .maskFile = layer.maskFile, .maskEnabled = layer.maskEnabled, .maskSourceID = layer.maskSourceID, .adjustment = layer.adjustment, .maskPlacement = placement, .maskLinked = layer.maskLinked}); } qCInfo(lcIO) << "resized" << old.width << "x" << old.height << "to" << options.width << "x" << options.height; return {.manifest = manifest, .images = images, .masks = masks}; } } ProjectSnapshot ImageResizer::resize(const ProjectSnapshot &snapshot, const ImageSizeOptions &options) { // Records, maps and tiles without memory fail as a render. try { return resized(snapshot, options); } catch (const std::bad_alloc &) { throw ExportError(ExportError::Kind::render); } } void EditorSession::applyImageSize(const ProjectSnapshot &snapshot) { applyDocumentSize(snapshot, QStringLiteral("Image Size")); } // Staged aside, then published: running out of memory changes nothing. void EditorSession::applyDocumentSize(const ProjectSnapshot &snapshot, const QString &actionName) { if (!m_document || m_document->id != snapshot.manifest.documentID) return; std::optional next = CanvasDocument(snapshot); DocumentHistory recorded = history; recorded.begin(actionName, m_document, m_activeLayerID); recorded.end(next, m_activeLayerID); m_document = std::move(next); history.adopt(std::move(recorded)); viewport.fit(m_document->size()); notify(); }