Files
OmaPhoto/src/Document/PixelAdjust.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

237 lines
9.8 KiB
C++

#include "Document/PixelAdjust.h"
#include "Rendering/PoolMap.h"
#include "Document/BrushStroke.h"
#include "IO/ImageExporter.h"
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
#include <memory>
#include <new>
#include <numeric>
#include <stdexcept>
namespace {
using Float4 = float __attribute__((vector_size(16)));
// A pixel in floats: gray has one lane, colour four.
template <typename Pixel> Pixel load(const uchar *bytes);
template <> float load<float>(const uchar *bytes)
{
return float(bytes[0]);
}
template <> Float4 load<Float4>(const uchar *bytes)
{
return Float4{float(bytes[0]), float(bytes[1]), float(bytes[2]), float(bytes[3])};
}
// Rounded once, as a render to eight bits does.
void store(float value, uchar *bytes)
{
bytes[0] = uchar(std::min(255.0f, std::max(0.0f, value) + 0.5f));
}
void store(Float4 value, uchar *bytes)
{
for (int c = 0; c < 4; ++c)
store(value[c], bytes + c);
}
// Normalised weights from the centre out, three sigmas long.
std::vector<float> gaussian(double sigma)
{
const int reach = int(std::ceil(3 * sigma));
std::vector<double> exact(size_t(reach) + 1);
double total = 0;
for (int i = 0; i <= reach; ++i) {
exact[size_t(i)] = std::exp(-double(i) * i / (2 * sigma * sigma));
total += i == 0 ? exact[0] : 2 * exact[size_t(i)];
}
std::vector<float> weights;
for (const double weight : exact)
weights.push_back(float(weight / total));
return weights;
}
// Rows for a parallel map; no memory: a render error.
std::vector<int> rowNumbers(int height)
{
try {
std::vector<int> rows(static_cast<size_t>(height));
std::iota(rows.begin(), rows.end(), 0);
return rows;
} catch (const std::bad_alloc &) {
throw ExportError(ExportError::Kind::render);
}
}
// Columns a block at a time keep the taps cached.
constexpr int block = 64;
template <typename Pixel> void blur(const QImage &image, const std::vector<float> &weights, bool clamped, Pixel *across, QImage &result)
{
const int width = image.width(), height = image.height(), reach = int(weights.size()) - 1, size = int(sizeof(Pixel) / sizeof(float));
const uchar *const in = image.constBits();
uchar *const out = result.bits();
const qsizetype inStride = image.bytesPerLine(), outStride = result.bytesPerLine();
// Past the edge: the edge itself, or nothing.
const auto source = [&](int at, int limit) { return clamped ? std::clamp(at, 0, limit - 1) : at; };
std::vector<int> rows = rowNumbers(height);
PoolMap::blocking(rows, [&](int y) {
const uchar *row = in + y * inStride;
Pixel *line = across + size_t(y) * size_t(width);
for (int x = 0; x < width; ++x) {
Pixel sum = weights[0] * load<Pixel>(row + x * size);
// Away from the edges every tap is inside: no checks.
if (x >= reach && x + reach < width) {
for (int i = 1; i <= reach; ++i)
sum += weights[size_t(i)] * (load<Pixel>(row + (x - i) * size) + load<Pixel>(row + (x + i) * size));
} else {
for (int i = 1; i <= reach; ++i) {
const int left = source(x - i, width), right = source(x + i, width);
if (left >= 0)
sum += weights[size_t(i)] * load<Pixel>(row + left * size);
if (right < width)
sum += weights[size_t(i)] * load<Pixel>(row + right * size);
}
}
line[x] = sum;
}
});
PoolMap::blocking(rows, [&](int y) {
uchar *line = out + y * outStride;
for (int start = 0; start < width; start += block) {
const int count = std::min(block, width - start);
Pixel sums[block];
for (int x = 0; x < count; ++x)
sums[x] = weights[0] * across[size_t(y) * size_t(width) + size_t(start + x)];
for (int i = -reach; i <= reach; ++i) {
const int from = source(y + i, height);
if (i == 0 || from < 0 || from >= height)
continue;
const Pixel *taps = across + size_t(from) * size_t(width) + size_t(start);
const float weight = weights[size_t(std::abs(i))];
for (int x = 0; x < count; ++x)
sums[x] += weight * taps[x];
}
for (int x = 0; x < count; ++x)
store(sums[x], line + (start + x) * size);
}
});
}
}
QImage PixelAdjust::thumbnail(const QImage &image)
{
const double factor = std::min(1.0, 96.0 / std::max(image.width(), image.height()));
const int width = std::max(1, int(image.width() * factor)), height = std::max(1, int(image.height() * factor));
QImage result = BrushRaster::context(width, height, false);
QPainter painter(&result);
BrushRaster::draw(image, QRectF(0, 0, width, height), painter);
return result;
}
QImage PixelAdjust::coverage(const SelectionClip &selection, int width, int height, const QTransform &pixelToDocument)
{
QImage mask = BrushRaster::context(width, height, true);
// An empty selection covers nothing.
if (!selection.coverage)
return mask;
QPainter painter(&mask);
painter.setTransform(pixelToDocument.inverted());
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.drawImage(selection.rect, *selection.coverage);
return mask;
}
QImage PixelAdjust::blend(const QImage &adjusted, const QImage &original, const SelectionClip &selection,
const QTransform &pixelToDocument, bool isMask)
{
const int width = adjusted.width(), height = adjusted.height();
if (original.size() != adjusted.size() || adjusted.format() != original.format())
throw std::logic_error("blend needs two images of one size and format");
const QImage mask = coverage(selection, width, height, pixelToDocument);
QImage result = BrushRaster::context(width, height, isMask);
const int channels = isMask ? 1 : 4;
for (int y = 0; y < height; ++y) {
const uchar *top = adjusted.constScanLine(y), *bottom = original.constScanLine(y), *weight = mask.constScanLine(y);
uchar *target = result.scanLine(y);
for (int x = 0; x < width; ++x) {
const int m = weight[x];
for (int channel = 0; channel < channels; ++channel) {
const int index = x * channels + channel;
target[index] = uchar((top[index] * m + bottom[index] * (255 - m) + 127) / 255);
}
}
}
return result;
}
QImage PixelAdjust::gaussianBlur(const QImage &image, double sigma, bool clamped)
{
const bool gray = image.format() == QImage::Format_Grayscale8;
if (!gray && image.format() != QImage::Format_RGBA8888_Premultiplied)
throw std::logic_error("a blur takes premultiplied RGBA or gray");
if (!(sigma > 0) || !std::isfinite(sigma))
throw std::logic_error("a blur needs a positive sigma");
const std::vector<float> weights = gaussian(sigma);
const size_t pixels = size_t(image.width()) * size_t(image.height());
QImage result = BrushRaster::context(image.width(), image.height(), gray);
// Allocated here, where a failure throws, before the workers run.
if (gray) {
const std::unique_ptr<float[]> across(new (std::nothrow) float[pixels]);
if (!across)
throw ExportError(ExportError::Kind::render);
blur<float>(image, weights, clamped, across.get(), result);
} else {
const std::unique_ptr<Float4[]> across(new (std::nothrow) Float4[pixels]);
if (!across)
throw ExportError(ExportError::Kind::render);
blur<Float4>(image, weights, clamped, across.get(), result);
}
return result;
}
QImage PixelAdjust::motionBlur(const QImage &image, double radius, double angle)
{
if (image.format() != QImage::Format_RGBA8888_Premultiplied)
throw std::logic_error("a motion blur takes premultiplied RGBA");
if (!(radius > 0) || !std::isfinite(radius) || !std::isfinite(angle))
throw std::logic_error("a motion blur needs a finite positive radius and angle");
// Measured on a dot: a Gaussian taper of its radius.
const std::vector<float> weights = gaussian(radius);
const int width = image.width(), height = image.height(), reach = int(weights.size()) - 1;
// Core Image's y points up: counterclockwise climbs the screen.
const double dx = std::cos(angle), dy = -std::sin(angle);
QImage result = BrushRaster::context(width, height, false);
const uchar *const in = image.constBits();
uchar *const out = result.bits();
const qsizetype inStride = image.bytesPerLine(), outStride = result.bytesPerLine();
// Bilinear, clear past the edges, as an unclamped image reads.
const auto sample = [&](double x, double y) {
const double left = std::floor(x), top = std::floor(y);
const float across = float(x - left), down = float(y - top);
Float4 sum{0, 0, 0, 0};
for (int j = 0; j < 2; ++j) {
const int row = int(top) + j;
if (row < 0 || row >= height)
continue;
for (int i = 0; i < 2; ++i) {
const int column = int(left) + i;
if (column >= 0 && column < width)
sum += (j ? down : 1 - down) * (i ? across : 1 - across) * load<Float4>(in + row * inStride + column * 4);
}
}
return sum;
};
std::vector<int> rows = rowNumbers(height);
PoolMap::blocking(rows, [&](int y) {
uchar *line = out + y * outStride;
for (int x = 0; x < width; ++x) {
Float4 sum = weights[0] * load<Float4>(in + y * inStride + x * 4);
for (int i = 1; i <= reach; ++i)
sum += weights[size_t(i)] * (sample(x + i * dx, y + i * dy) + sample(x - i * dx, y - i * dy));
store(sum, line + x * 4);
}
});
return result;
}