OmaPhoto 1.0.0: the Linux port of Compositor
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s

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>
This commit is contained in:
雷电芽衣andClaude committed 2026-09-26 00:44:52 -04:00
commit c864304695
564 files changed
+100804

No files matched your search

+242
View File
@@ -0,0 +1,242 @@
#include "Document/Filters.h"
#include "Document/BrushStroke.h"
#include "Document/ContentFill.h"
#include "Document/EditorSession+Model.h"
#include "Document/PixelAdjust.h"
#include "Document/SubjectRemoval.h"
#include "IO/ImageExporter.h"
#include "IO/ProjectStore.h"
#include "Rendering/RasterSnapshot.h"
#include <algorithm>
#include <cmath>
#include <numbers>
extern "C" {
#include "BrushPixels.h"
#include "LensPixels.h"
#include "NoisePixels.h"
}
QString rawValue(FilterKind kind)
{
switch (kind) {
case FilterKind::gaussianBlur: return QStringLiteral("Gaussian Blur");
case FilterKind::motionBlur: return QStringLiteral("Motion Blur");
case FilterKind::addNoise: return QStringLiteral("Add Noise");
case FilterKind::lensCorrection: return QStringLiteral("Lens Correction");
case FilterKind::removeBackground: return QStringLiteral("Remove Background");
case FilterKind::contentAwareFill: return QStringLiteral("Content-Aware Fill");
case FilterKind::curves: return QStringLiteral("Curves");
case FilterKind::exposure: return QStringLiteral("Exposure");
case FilterKind::gradientMap: return QStringLiteral("Gradient Map");
case FilterKind::grain: return QStringLiteral("Grain");
}
throw std::logic_error("unknown filter kind");
}
bool isAutomatic(FilterKind kind)
{
return kind == FilterKind::contentAwareFill || kind == FilterKind::removeBackground;
}
QString rawValue(BackgroundQuality quality)
{
return quality == BackgroundQuality::basic ? QStringLiteral("Basic") : QStringLiteral("Advanced");
}
bool isImageAdjustment(FilterKind kind)
{
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain;
}
FilterSettings FilterSettings::normalized() const
{
using ImageAdjustmentPixels::clamp;
FilterSettings result = *this;
result.radius = clamp(radius, 0.1, 250, 1);
result.angle = clamp(angle, -90, 90, 0);
result.distance = clamp(distance, 1, 2000, 10);
result.amount = clamp(amount, 0.1, 400, 10);
result.distortion = clamp(distortion, -100, 100, 0);
result.refineEdges = clamp(refineEdges, 0, 40, 12);
result.matteContrast = clamp(matteContrast, 0, 100, 25);
result.shiftEdge = clamp(shiftEdge, -10, 10, 0);
result.exposure = exposure.normalized();
result.gradientMap = gradientMap.normalized();
result.grain = grain.normalized();
return result;
}
PixelFilter::Trimmed PixelFilter::trimmed(const QImage &image, const LayerTransform &placed)
{
const QRectF full(image.rect());
// Bounds read premultiplied bytes; other formats get drawn.
QImage pixels = image;
if (pixels.format() != QImage::Format_RGBA8888_Premultiplied) {
pixels = BrushRaster::context(image.width(), image.height(), false);
QPainter painter(&pixels);
BrushRaster::draw(image, full, painter);
}
size_t edges[4] = {0, 0, 0, 0};
brush_alpha_bounds(pixels.constBits(), size_t(pixels.width()), size_t(pixels.height()), size_t(pixels.bytesPerLine()), edges);
const QRect crop(int(edges[0]), int(edges[1]), int(edges[2] - edges[0]), int(edges[3] - edges[1]));
if (crop.width() < 1 || crop.height() < 1 || QRectF(crop) == full)
return {image, placed};
LayerTransform result = placed;
result.size = {crop.width() * placed.size.width() / full.width(), crop.height() * placed.size.height() / full.height()};
const QPointF middle = BrushRaster::pixelToDocument(placed, image.width(), image.height()).map(QRectF(crop).center());
result.origin = {middle.x() - result.size.width() / 2, middle.y() - result.size.height() / 2};
// Swift's crop shares pixels; a copy may find no memory.
const QImage cropped = image.copy(crop);
if (cropped.isNull())
throw ExportError(ExportError::Kind::render);
return {cropped, result};
}
namespace {
// The image drawn into a fresh premultiplied context.
QImage drawn(const QImage &image)
{
QImage context = BrushRaster::context(image.width(), image.height(), false);
QPainter painter(&context);
BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter);
return context;
}
}
QImage PixelFilter::run(const FilterJob &job)
{
const FilterSettings settings = job.settings.normalized();
const int width = job.image.width(), height = job.image.height();
QImage image;
switch (job.kind) {
case FilterKind::curves: image = settings.curves.apply(job.image); break;
case FilterKind::exposure: image = settings.exposure.apply(job.image); break;
case FilterKind::gradientMap: image = settings.gradientMap.apply(job.image); break;
// Grain sits in layer pixels; the job's seed patterns it.
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
case FilterKind::contentAwareFill: image = ContentFill::run(job); break;
// Unclamped: a blur spreads into the room made for it.
case FilterKind::gaussianBlur: image = PixelAdjust::gaussianBlur(drawn(job.image), settings.radius * job.scale, false); break;
case FilterKind::motionBlur:
image = PixelAdjust::motionBlur(drawn(job.image), settings.distance * job.scale * motionRadiusPerPixel, settings.angle * std::numbers::pi / 180);
break;
case FilterKind::addNoise:
image = drawn(job.image);
noise_add(image.bits(), size_t(width), size_t(height), size_t(image.bytesPerLine()), float(settings.amount), settings.gaussian ? 1 : 0,
settings.monochromatic ? 1 : 0, job.seed);
break;
case FilterKind::lensCorrection: {
// Relative to the image's size, so previews bend alike.
const QImage source = drawn(job.image);
image = BrushRaster::context(width, height, false);
lens_distort(source.constBits(), image.bits(), size_t(width), size_t(height), size_t(source.bytesPerLine()), settings.distortion / 100 * lensStrength);
break;
}
}
if (!job.selection)
return image;
return PixelAdjust::blend(image, job.image, *job.selection, job.mapping, false);
}
namespace {
QRectF integral(const QRectF &rect)
{
return QRectF(QPointF(std::floor(rect.left()), std::floor(rect.top())), QPointF(std::ceil(rect.right()), std::ceil(rect.bottom())));
}
}
FilterEdit::FilterEdit(FilterKind kind, const ImageLayer &layer, std::optional<SelectionClip> selection, const FilterSettings &settings,
std::optional<QRectF> area)
: kind(kind), layerID(layer.id), original(layer.asset ? *layer.asset : throw ProjectError(ProjectError::Kind::invalid)),
transform(layer.transform), selection(std::move(selection)), settings(settings.normalized())
{
prepare(original, transform);
if (area)
grow(integral(mapping.inverted().mapRect(*area)));
growForBlur();
}
double FilterEdit::blurMargin(FilterKind kind, const FilterSettings &settings)
{
switch (kind) {
case FilterKind::gaussianBlur: return settings.radius * 3 + 2;
case FilterKind::motionBlur: return settings.distance / 2 + 2;
default: return 0;
}
}
void FilterEdit::growForBlur()
{
const double margin = blurMargin(kind, settings);
if (!(margin > grownMargin))
return;
const QSize size = original.size();
grow(QRectF(-margin, -margin, size.width() + 2 * margin, size.height() + 2 * margin));
}
// The layer's pixels in a grid covering `extent` (layer pixels).
void FilterEdit::grow(const QRectF &extent)
{
const QSize size = original.size();
const QRectF bounds(0, 0, size.width(), size.height());
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)
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());
QPainter painter(&grown);
if (original.raster)
original.raster->draw(inside, painter);
else
BrushRaster::draw(original.image(), inside, painter);
painter.end();
const QTransform toDocument = BrushRaster::pixelToDocument(transform, size.width(), size.height());
LayerTransform expanded = transform;
expanded.size = {target.width() * transform.size.width() / bounds.width(), target.height() * transform.size.height() / bounds.height()};
const QPointF middle = toDocument.map(target.center());
expanded.origin = {middle.x() - expanded.size.width() / 2, middle.y() - expanded.size.height() / 2};
grownImage = grown;
grownTransform = expanded;
grownMargin = std::min({bounds.left() - target.left(), bounds.top() - target.top(), target.right() - bounds.right(), target.bottom() - bounds.bottom()});
prepare(ImportedImage(grown, QImage(), QString()), expanded);
}
void FilterEdit::prepare(const ImportedImage &source, const LayerTransform &placed)
{
const QSize size = source.size();
mapping = BrushRaster::pixelToDocument(placed, size.width(), size.height());
// Noise and grain preview whole: enlarged, grain looks coarse.
const bool whole = kind == FilterKind::addNoise || kind == FilterKind::grain || kind == FilterKind::contentAwareFill || kind == FilterKind::removeBackground;
const double factor = whole ? 1 : std::min(1.0, previewLimit / std::max(size.width(), size.height()));
if (!(factor < 1)) {
previewSource = source.image();
previewScale = 1;
previewMapping = mapping;
return;
}
const int width = std::max(1, int(size.width() * factor)), height = std::max(1, int(size.height() * factor));
previewSource = BrushRaster::context(width, height, false);
QPainter painter(&previewSource);
const QRectF rect(0, 0, width, height);
if (source.raster)
source.raster->draw(rect, painter);
else
BrushRaster::draw(source.image(), rect, painter);
painter.end();
previewScale = double(width) / size.width();
previewMapping = BrushRaster::pixelToDocument(placed, width, height);
}
std::optional<QImage> FilterEdit::previewImage(QUuid id) const
{
return preview && id == layerID ? preparedPreview : std::nullopt;
}
FilterJob FilterEdit::previewJob() const
{
return FilterJob{kind, previewSource, settings, previewScale, selection, previewMapping, seed};
}