Files
OmaPhoto/src/Document/ImageTrim.cpp
T

106 lines
4.2 KiB
C++

#include "Document/ImageTrim.h"
#include "Document/CanvasSize.h"
#include "IO/CanvasResizer.h"
#include "IO/ImageExporter.h"
#include <cstdlib>
extern "C" {
#include "BrushPixels.h"
}
QString rawValue(TrimBasedOn basedOn)
{
switch (basedOn) {
case TrimBasedOn::transparentPixels: return QStringLiteral("Transparent Pixels");
case TrimBasedOn::topLeftPixelColor: return QStringLiteral("Top Left Pixel Color");
case TrimBasedOn::bottomRightPixelColor: return QStringLiteral("Bottom Right Pixel Color");
}
throw std::logic_error("unknown trim basis");
}
bool TrimOptions::trimsAny() const
{
return top || bottom || left || right;
}
namespace {
// Unchecked edges stay at the image's; content is never empty.
QRect kept(int left, int top, int right, int bottom, int width, int height, const TrimOptions &options)
{
const int minX = options.left ? left : 0, minY = options.top ? top : 0;
const int maxX = options.right ? right : width, maxY = options.bottom ? bottom : height;
return QRect(minX, minY, maxX - minX, maxY - minY);
}
// Swift's calculateColorTrimRect: rows scanned in from both ends.
std::optional<QRect> colorTrimRect(const QImage &pixels, QPoint sample, const TrimOptions &options)
{
const int width = pixels.width(), height = pixels.height(), tolerance = options.tolerance;
const uchar *target = pixels.constScanLine(sample.y()) + sample.x() * 4;
const auto matches = [&](const uchar *row, int x) {
const uchar *pixel = row + x * 4;
for (int channel = 0; channel < 4; ++channel) {
if (std::abs(pixel[channel] - target[channel]) > tolerance)
return false;
}
return true;
};
int left = width, right = 0, top = height, bottom = 0;
for (int y = 0; y < height; ++y) {
const uchar *row = pixels.constScanLine(y);
int first = 0;
while (first < width && matches(row, first))
++first;
if (first == width)
continue;
// The unmatched pixel at `first` stops this scan.
int last = width;
while (matches(row, last - 1))
--last;
left = std::min(left, first);
right = std::max(right, last);
top = std::min(top, y);
bottom = y + 1;
}
// Every pixel matched the sample.
if (right == 0)
return std::nullopt;
return kept(left, top, right, bottom, width, height, options);
}
}
std::optional<QRect> ImageTrim::calculateTrimRect(const QImage &image, const TrimOptions &options)
{
if (!options.trimsAny() || image.isNull())
return std::nullopt;
// Swift's try? returns nil; running out here is explained instead.
const QImage pixels = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
if (pixels.isNull())
throw ExportError(ExportError::Kind::render);
const int width = pixels.width(), height = pixels.height();
switch (options.basedOn) {
case TrimBasedOn::transparentPixels: {
size_t edges[4] = {0, 0, 0, 0};
brush_alpha_bounds(pixels.constBits(), size_t(width), size_t(height), size_t(pixels.bytesPerLine()), edges);
// A right edge of zero: every pixel is clear.
if (edges[2] == 0)
return std::nullopt;
return kept(int(edges[0]), int(edges[1]), int(edges[2]), int(edges[3]), width, height, options);
}
case TrimBasedOn::topLeftPixelColor: return colorTrimRect(pixels, QPoint(0, 0), options);
case TrimBasedOn::bottomRightPixelColor: return colorTrimRect(pixels, QPoint(width - 1, height - 1), options);
}
throw std::logic_error("unknown trim basis");
}
std::optional<ProjectSnapshot> ImageTrim::trim(const ProjectSnapshot &snapshot, const TrimOptions &options)
{
const std::optional<QRect> rect = calculateTrimRect(ImageExporter::render(snapshot).image, options);
if (!rect)
return std::nullopt;
if (*rect == QRect(0, 0, int(snapshot.manifest.width), int(snapshot.manifest.height)))
return snapshot;
return CanvasResizer::resize(snapshot, CanvasSizeOptions{.width = rect->width(), .height = rect->height(),
.contentOffset = QPointF(-rect->left(), -rect->top())});
}