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

+253
View File
@@ -0,0 +1,253 @@
#include "UI/JPEGExportSheet.h"
#include "UI/ByteCounts.h"
#include "UI/ColorPaletteControls.h"
#include <QColorDialog>
#include <QFutureWatcher>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QProgressBar>
#include <QPushButton>
#include <QSettings>
#include <QSlider>
#include <QTimer>
#include <QVBoxLayout>
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
const QString JPEGExportSheet::qualityKey = QStringLiteral("jpegExportQuality");
namespace {
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QPalette::ColorRole role, QWidget *parent)
{
auto *label = new QLabel(words, parent);
label->setTextFormat(Qt::PlainText);
QFont font = label->font();
font.setPixelSize(pixels);
font.setWeight(weight);
label->setFont(font);
label->setForegroundRole(role);
return label;
}
QString grouped(qint64 value)
{
return QLocale(QLocale::English, QLocale::UnitedStates).toString(value);
}
// Swift reads a finite number alone, clamped to 0–1.
double startQuality()
{
bool number = false;
const double saved = QSettings().value(JPEGExportSheet::qualityKey).toDouble(&number);
return number && std::isfinite(saved) ? std::clamp(saved, 0.0, 1.0) : JPEGOptions().quality;
}
struct Encoded {
std::optional<JPEGResult> result;
std::optional<QString> failure;
};
}
// Swift's ZStack: dark gray, the fitted preview, a plate.
class JPEGExportSheet::Preview : public QWidget {
public:
using QWidget::QWidget;
QImage image;
bool updating = false;
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), QColor::fromRgbF(0.12f, 0.12f, 0.12f));
if (!image.isNull()) {
painter.setRenderHint(QPainter::SmoothPixmapTransform);
QRectF target(QPointF(), QSizeF(image.size()).scaled(QSizeF(size()), Qt::KeepAspectRatio));
target.moveCenter(QRectF(rect()).center());
painter.drawImage(target, image);
}
if (updating) {
// Swift's regular material behind the spinner.
QColor material = palette().color(QPalette::Window);
material.setAlphaF(0.85f);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(material);
QRectF plate(0, 0, 64, 64);
plate.moveCenter(QRectF(rect()).center());
painter.drawRoundedRect(plate, 8, 8);
}
}
};
JPEGExportSheet::JPEGExportSheet(ExportRaster raster, std::function<void(std::optional<QByteArray>)> finish, QWidget *parent)
: QWidget(parent), m_raster(std::move(raster)), m_finish(std::move(finish)), m_wait(new QTimer(this)), m_preview(new Preview(this)),
m_spinner(new QProgressBar(m_preview)), m_quality(new QSlider(Qt::Horizontal, this)),
m_percent(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)), m_matteSwatch(new SwatchButton([this] { return PaletteColor{m_options.red, m_options.green, m_options.blue}; }, 3, 0, 0.5, this)),
m_failure(text(QString(), 13, QFont::Normal, QPalette::BrightText, this)), m_bytes(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)),
m_note(text(QString(), 13, QFont::Normal, QPalette::PlaceholderText, this)), m_export(new QPushButton(QStringLiteral("Export…"), this))
{
m_options.quality = startQuality();
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(16);
column->addWidget(text(QStringLiteral("Export JPEG"), 17, QFont::Bold, QPalette::WindowText, this));
m_preview->setObjectName(QStringLiteral("jpegPreview"));
m_preview->setFixedSize(560, 330);
m_spinner->setObjectName(QStringLiteral("jpegSpinner"));
m_spinner->setRange(0, 0);
m_spinner->setTextVisible(false);
m_spinner->setGeometry(264, 149, 32, 32);
column->addWidget(m_preview);
auto *quality = new QHBoxLayout;
auto *qualityLabel = new QLabel(QStringLiteral("Quality"), this);
qualityLabel->setBuddy(m_quality);
// Swift's slider steps by hundredths.
m_quality->setObjectName(QStringLiteral("jpegQuality"));
m_quality->setRange(0, 100);
m_quality->setValue(int(std::lround(m_options.quality * 100)));
connect(m_quality, &QSlider::valueChanged, this, [this](int hundredths) {
m_options.quality = hundredths / 100.0;
request();
});
m_percent->setObjectName(QStringLiteral("jpegPercent"));
m_percent->setFixedWidth(45);
m_percent->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
quality->addWidget(qualityLabel);
quality->addWidget(m_quality, 1);
quality->addWidget(m_percent);
column->addLayout(quality);
// Swift's ColorPicker: its label and a well, no opacity.
auto *matte = new QHBoxLayout;
matte->addWidget(new QLabel(QStringLiteral("Background for transparency"), this));
m_matteSwatch->setObjectName(QStringLiteral("jpegMatte"));
m_matteSwatch->setFixedSize(36, 18);
m_matteSwatch->setAccessibleName(QStringLiteral("Background for transparency"));
connect(m_matteSwatch, &QAbstractButton::clicked, this, &JPEGExportSheet::pickMatte);
matte->addWidget(m_matteSwatch);
matte->addStretch(1);
column->addLayout(matte);
column->addWidget(text(QStringLiteral("%1 × %2 px · sRGB").arg(grouped(m_raster.image.width()), grouped(m_raster.image.height())), 13, QFont::Normal,
QPalette::PlaceholderText, this));
auto *buttons = new QHBoxLayout;
m_failure->setObjectName(QStringLiteral("jpegError"));
m_bytes->setObjectName(QStringLiteral("jpegBytes"));
m_note->setObjectName(QStringLiteral("jpegNote"));
buttons->addWidget(m_failure);
buttons->addWidget(m_bytes);
buttons->addWidget(m_note);
buttons->addStretch(1);
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("jpegCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_export->setObjectName(QStringLiteral("jpegExport"));
m_export->setDefault(true);
// Export rests until a result is ready.
connect(m_export, &QPushButton::clicked, this, [this] {
QSettings().setValue(qualityKey, m_options.quality);
m_finish(m_result.value().data);
});
buttons->addWidget(cancel);
buttons->addWidget(m_export);
column->addLayout(buttons);
m_wait->setObjectName(QStringLiteral("jpegWait"));
m_wait->setSingleShot(true);
m_wait->setInterval(200);
m_wait->setTimerType(Qt::PreciseTimer);
connect(m_wait, &QTimer::timeout, this, &JPEGExportSheet::encode);
request();
}
// SwiftUI cancels the task when the sheet goes.
JPEGExportSheet::~JPEGExportSheet()
{
m_cancelled->store(true);
}
// Swift's task(id:): the last request stops; this waits 200 ms.
void JPEGExportSheet::request()
{
if (m_cancelled)
m_cancelled->store(true);
m_cancelled = std::make_shared<std::atomic_bool>(false);
m_error.reset();
m_wait->start();
synchronize();
}
void JPEGExportSheet::encode()
{
const std::shared_ptr<std::atomic_bool> cancelled = m_cancelled;
const JPEGOptions requested = m_options;
auto *watcher = new QFutureWatcher<Encoded>(this);
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, cancelled, requested] {
watcher->deleteLater();
// Swift's check after the await: a newer request supersedes.
if (cancelled->load())
return;
Encoded encoded = watcher->future().takeResult();
if (encoded.result) {
m_result = std::move(encoded.result);
m_readyOptions = requested;
} else {
m_error = encoded.failure.value();
}
synchronize();
});
watcher->setFuture(QtConcurrent::run([raster = m_raster, requested, cancelled]() -> Encoded {
try {
return {ImageExporter::jpeg(raster, requested, [cancelled] { return cancelled->load(); }), std::nullopt};
} catch (const CancellationError &) {
return {};
} catch (const ExportError &error) {
return {std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
// The desktop's colour dialog, SwiftUI's colour panel, without alpha.
void JPEGExportSheet::pickMatte()
{
auto *dialog = new QColorDialog(QColor::fromRgbF(float(m_options.red), float(m_options.green), float(m_options.blue)), this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(QStringLiteral("Background for transparency"));
connect(dialog, &QColorDialog::colorSelected, this, [this](const QColor &picked) {
const QColor srgb = picked.toRgb();
JPEGOptions next = m_options;
next.red = srgb.redF();
next.green = srgb.greenF();
next.blue = srgb.blueF();
// Swift's onChange: the same colour asks nothing.
if (next == m_options)
return;
m_options = next;
m_matteSwatch->update();
request();
});
dialog->open();
}
void JPEGExportSheet::synchronize()
{
const bool ready = m_result && m_readyOptions == m_options;
const bool updating = m_readyOptions != m_options && !m_error;
m_preview->image = m_result ? m_result->preview : QImage();
m_preview->updating = updating;
m_preview->update();
m_spinner->setVisible(updating);
m_percent->setText(QStringLiteral("%1%").arg(std::lround(m_options.quality * 100)));
m_failure->setVisible(m_error.has_value());
m_failure->setText(m_error.value_or(QString()));
m_bytes->setVisible(ready && !m_error);
m_bytes->setText(m_result ? ByteCounts::file(m_result->data.size()) : QString());
m_note->setVisible(!m_error);
m_note->setText(ready ? QStringLiteral("· encoded preview, fitted to window") : QStringLiteral("Updating preview…"));
m_export->setEnabled(ready && !m_error);
}