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

+131
View File
@@ -0,0 +1,131 @@
#include "Document/BrushStroke.h"
#include "Rendering/PoolMap.h"
#include "IO/ImageExporter.h"
#include <QtConcurrent>
#include <cmath>
#include <numeric>
#include <stdexcept>
QString rawValue(SpotHealingMode mode)
{
switch (mode) {
case SpotHealingMode::contentAware:
return QStringLiteral("Content-Aware");
case SpotHealingMode::createTexture:
return QStringLiteral("Create Texture");
case SpotHealingMode::proximityMatch:
return QStringLiteral("Proximity Match");
}
throw std::logic_error("unknown spot healing mode");
}
QImage BrushRaster::context(int width, int height, bool mask)
{
QImage surface(width, height, mask ? QImage::Format_Grayscale8 : QImage::Format_RGBA8888_Premultiplied);
if (surface.isNull())
throw ExportError(ExportError::Kind::render);
surface.fill(0);
return surface;
}
void BrushRaster::draw(const QImage &image, const QRectF &rect, QPainter &context, const QRectF &source)
{
context.save();
context.setRenderHint(QPainter::SmoothPixmapTransform, false);
context.setCompositionMode(QPainter::CompositionMode_Source);
context.drawImage(rect, image, source.isNull() ? QRectF(image.rect()) : source);
context.restore();
}
QImage BrushRaster::alphaView(const QImage &mask)
{
if (mask.format() != QImage::Format_Grayscale8)
throw std::logic_error("a layer mask must be 8-bit gray");
return QImage(mask.constBits(), mask.width(), mask.height(), mask.bytesPerLine(), QImage::Format_Alpha8);
}
QRectF BrushRaster::visibleRect(const QPainter &context)
{
const QPaintDevice &device = *context.device();
// A widget's size is logical; an image's is device pixels.
QTransform toDevice = context.combinedTransform();
if (device.devType() == QInternal::Widget)
toDevice *= QTransform::fromScale(1 / device.devicePixelRatio(), 1 / device.devicePixelRatio());
const QRectF bounds = toDevice.inverted().mapRect(QRectF(0, 0, device.width(), device.height()));
return context.hasClipping() ? bounds.intersected(context.clipBoundingRect()) : bounds;
}
QTransform BrushRaster::pixelToDocument(const LayerTransform &transform, int width, int height)
{
const QPointF center = transform.center();
QTransform map;
map.translate(center.x(), center.y());
map.rotateRadians(transform.radians());
map.scale(transform.size.width() / width * (transform.flipX ? -1 : 1),
transform.size.height() / height * (transform.flipY ? -1 : 1));
map.translate(-width / 2.0, -height / 2.0);
return map;
}
// Source-over of the colour at coverage × alpha, bytewise.
void BrushRaster::fill(const QColor &color, const QImage &coverage, const QRectF &rect, double alpha, QImage &context,
QPainter::CompositionMode mode, const QImage &clip)
{
if (coverage.format() != QImage::Format_Grayscale8 || coverage.size() != rect.size().toSize() || rect.topLeft() != rect.topLeft().toPoint())
throw std::logic_error("coverage must be gray and 1:1 with its rect");
if (!clip.isNull() && (clip.format() != QImage::Format_Grayscale8 || clip.size() != coverage.size()))
throw std::logic_error("a clip must be gray and sized as the coverage");
// Erasing takes coverage out; a mask never erases.
const bool erasing = mode == QPainter::CompositionMode_DestinationOut;
if ((!erasing && mode != QPainter::CompositionMode_SourceOver) || (erasing && context.format() == QImage::Format_Grayscale8))
throw std::logic_error("a brush fills source-over, or erases pixels");
const int left = int(rect.left()), top = int(rect.top());
const bool gray = context.format() == QImage::Format_Grayscale8;
if (!gray && context.format() != QImage::Format_RGBA8888_Premultiplied)
throw std::logic_error("a brush context is premultiplied RGBA or gray");
// What each channel moves towards; erasing moves towards nothing.
const double target[4] = {erasing ? 0 : color.redF() * 255, erasing ? 0 : color.greenF() * 255, erasing ? 0 : color.blueF() * 255,
erasing ? 0 : 255.0};
const int channels = gray ? 1 : 4;
for (int y = 0; y < coverage.height(); ++y) {
const uchar *mask = coverage.constScanLine(y);
// A clip scales the coverage, rounded once, as CoreGraphics composites.
const uchar *within = clip.isNull() ? nullptr : clip.constScanLine(y);
uchar *row = context.scanLine(top + y) + left * channels;
for (int x = 0; x < coverage.width(); ++x) {
const double k = mask[x] / 255.0 * alpha * (within ? within[x] / 255.0 : 1.0);
if (k <= 0)
continue;
// Values are never negative: adding a half rounds them.
uchar *pixel = row + x * channels;
for (int c = 0; c < channels; ++c)
pixel[c] = uchar(pixel[c] * (1 - k) + target[c] * k + 0.5);
}
}
}
QImage BrushRaster::shiftedByFraction(const QImage &image, double fx, double fy)
{
QImage shifted = BrushRaster::context(image.width() + 1, image.height() + 1, false);
// Weights in 1/65536: the pixel, left, above, above left.
const qint64 here = std::lround((1 - fx) * (1 - fy) * 65536), left = std::lround(fx * (1 - fy) * 65536);
const qint64 above = std::lround((1 - fx) * fy * 65536), corner = 65536 - here - left - above;
uchar *const out = shifted.bits();
const uchar *const in = image.constBits();
const qsizetype outStride = shifted.bytesPerLine(), inStride = image.bytesPerLine();
const int width = image.width(), height = image.height();
std::vector<int> rows(size_t(shifted.height()));
std::iota(rows.begin(), rows.end(), 0);
PoolMap::blocking(rows, [&](int y) {
const uchar *row = y < height ? in + y * inStride : nullptr, *up = y > 0 ? in + (y - 1) * inStride : nullptr;
uchar *line = out + y * outStride;
for (int x = 0; x <= width; ++x) {
for (int c = 0; c < 4; ++c) {
const qint64 sum = (row && x < width ? here * row[x * 4 + c] : 0) + (row && x > 0 ? left * row[(x - 1) * 4 + c] : 0)
+ (up && x < width ? above * up[x * 4 + c] : 0) + (up && x > 0 ? corner * up[(x - 1) * 4 + c] : 0);
line[x * 4 + c] = uchar((sum + 32768) >> 16);
}
}
});
return shifted;
}