Files
OmaPhoto/src/Document/LayerMerge.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

141 lines
5.4 KiB
C++

#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "Document/Filters.h"
#include "Document/PixelAdjust.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
// What Ctrl+E merges, in stacking order, and its place.
std::optional<EditorSession::MergePlan> EditorSession::mergePlan() const
{
const std::optional<ImageLayer> active = activeLayer();
if (!canEditLayers() || !active)
return std::nullopt;
const std::vector<ImageLayer> &layers = m_document->layers;
if (m_selectedLayerIDs.size() > 1) {
// Several selected layers, with anything their folders hold.
QSet<QUuid> picked = m_selectedLayerIDs;
for (const QUuid &id : m_selectedLayerIDs)
picked.unite(descendantIDs(id));
std::vector<QUuid> ordered;
std::optional<ImageLayer> top;
bool pixels = false;
for (const ImageLayer &layer : layers) {
if (!picked.contains(layer.id))
continue;
ordered.push_back(layer.id);
pixels = pixels || !layer.isGroup;
if (m_selectedLayerIDs.contains(layer.id))
top = layer;
}
if (!pixels || !top)
return std::nullopt;
return MergePlan{ordered, picked, top->name, top->parentID, top->id, QStringLiteral("Merge Layers")};
}
if (active->isGroup) {
// A folder merges its contents, and the folder goes.
const QSet<QUuid> inside = descendantIDs(active->id);
std::vector<QUuid> ids;
bool pixels = false;
for (const ImageLayer &layer : layers) {
if (inside.contains(layer.id) || layer.id == active->id)
ids.push_back(layer.id);
pixels = pixels || (inside.contains(layer.id) && !layer.isGroup);
}
if (!pixels)
return std::nullopt;
return MergePlan{ids, QSet<QUuid>(ids.begin(), ids.end()), active->name, active->parentID, active->id, QStringLiteral("Merge Group")};
}
// One layer merges with the one beneath it.
for (int index = indexOf(layers, active->id) - 1; index >= 0; --index) {
const ImageLayer &below = layers[index];
if (below.parentID != active->parentID)
continue;
if (below.isGroup)
return std::nullopt;
return MergePlan{{below.id, active->id}, {below.id, active->id}, below.name, active->parentID, active->id, QStringLiteral("Merge Down")};
}
return std::nullopt;
}
bool EditorSession::canMergeLayers() const
{
return mergePlan().has_value();
}
QString EditorSession::mergeTitle() const
{
const std::optional<MergePlan> plan = mergePlan();
return plan ? plan->action : QStringLiteral("Merge Down");
}
// The layers as shown, baked into one pixel layer.
void EditorSession::mergeLayers()
{
commitTransform();
const std::optional<MergePlan> plan = mergePlan();
if (!plan)
return;
const std::vector<ImageLayer> &layers = m_document->layers;
const QSet<QUuid> kept(plan->ids.begin(), plan->ids.end());
// Only the merged layers, cut loose from the rest.
CanvasDocument flat(m_document->width, m_document->height);
flat.id = m_document->id;
flat.resolution = m_document->resolution;
for (const ImageLayer &layer : layers) {
if (!kept.contains(layer.id))
continue;
ImageLayer copy = layer;
if (copy.parentID && !kept.contains(*copy.parentID))
copy.parentID = std::nullopt;
if (copy.maskSourceID && !kept.contains(*copy.maskSourceID))
copy.maskSourceID = std::nullopt;
flat.layers.push_back(copy);
}
std::optional<ImageLayer> merged;
try {
QImage full = BrushRaster::context(m_document->width, m_document->height, false);
{
QPainter context(&full);
drawLiveComposite(flat, context);
}
const LayerTransform canvas{.origin = {0, 0}, .size = m_document->size()};
const PixelFilter::Trimmed trimmed = PixelFilter::trimmed(full, canvas);
// At 1:1 the trimmed size is the crop's: the constructor's.
merged = ImageLayer(ImportedImage(trimmed.image, PixelAdjust::thumbnail(trimmed.image), plan->name), trimmed.transform.origin);
} catch (const ExportError &error) {
qCWarning(lcRendering) << "merge: the layers could not be rendered:" << error.what();
return;
}
merged->parentID = plan->parent;
std::vector<ImageLayer> next;
int insertion = 0;
for (const ImageLayer &layer : layers) {
if (plan->removed.contains(layer.id))
continue;
next.push_back(layer);
// Layers clipped to anything merged now clip to the result.
if (next.back().maskSourceID && plan->removed.contains(*next.back().maskSourceID))
next.back().maskSourceID = merged->id;
}
for (const ImageLayer &layer : layers) {
if (layer.id == plan->anchor)
break;
insertion += plan->removed.contains(layer.id) ? 0 : 1;
}
next.insert(next.begin() + insertion, *merged);
std::vector<ProjectLayerRecord> records;
for (const ImageLayer &layer : next)
records.push_back(layer.hierarchyRecord());
try {
LayerHierarchy::validate(records);
} catch (const ProjectError &error) {
qCWarning(lcRendering) << "merge: the result is no valid hierarchy:" << error.what();
return;
}
beginEdit(plan->action);
m_document->layers = std::move(next);
setActiveLayerID(merged->id);
endEdit();
}