Files
OmaPhoto/src/IO/ImageResizer.cpp
T

146 lines
7.5 KiB
C++

#include "IO/ImageResizer.h"
#include "Document/EditorSession.h"
#include "Document/LayerMask.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include "Rendering/LayerRenderer.h"
#include <QPainter>
#include <algorithm>
#include <cmath>
#include <new>
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<void(QPainter &)> &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<QUuid, ImportedImage> 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<LayerTransform> 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<CanvasDocument> 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();
}