Files
OmaPhoto/src/Document/MagicWand.cpp
T

175 lines
7.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Document/MagicWand.h"
#include "Document/BrushStroke.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include "Logging.h"
#include "Rendering/LayerRenderer.h"
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
#include <cstdlib>
extern "C" {
#include "WandPixels.h"
}
QString title(WandSampleSize size)
{
switch (size) {
case WandSampleSize::point:
return QStringLiteral("Point Sample");
case WandSampleSize::threeByThree:
return QStringLiteral("3 by 3 Average");
case WandSampleSize::fiveByFive:
return QStringLiteral("5 by 5 Average");
}
throw std::logic_error("unknown sample size");
}
int radius(WandSampleSize size)
{
return int(size);
}
MagicWandError::MagicWandError(Kind kind)
: std::runtime_error(kind == Kind::tooDetailed
? "That selection is too detailed to outline. Try a different Tolerance, or turn on Contiguous."
: "There isn’t enough memory to make that selection."),
kind(kind)
{
}
std::optional<QPainterPath> MagicWand::select(const QImage &image, QPointF point, const WandSettings &settings)
{
const int width = image.width(), height = image.height();
if (!std::isfinite(point.x()) || !std::isfinite(point.y()))
return std::nullopt;
const double fx = std::floor(point.x()), fy = std::floor(point.y());
if (fx < 0 || fy < 0 || fx >= width || fy >= height)
return std::nullopt;
// Premultiplied RGBA, rows top down, as the kernel reads.
QImage rgba = BrushRaster::context(width, height, false);
QPainter painter(&rgba);
BrushRaster::draw(image, QRectF(0, 0, width, height), painter);
painter.end();
std::vector<uchar> selected(size_t(width) * size_t(height));
const long count = wand_mask(rgba.constBits(), size_t(width), size_t(height), size_t(rgba.bytesPerLine()), size_t(fx), size_t(fy),
size_t(radius(settings.sampleSize)), std::clamp(settings.tolerance, 0, 255), settings.contiguous ? 1 : 0, selected.data());
if (count < 0)
throw MagicWandError(MagicWandError::Kind::memory);
if (count == 0)
return std::nullopt;
return outline(selected, width, height);
}
std::optional<QPainterPath> MagicWand::outline(const std::vector<uchar> &mask, int width, int height)
{
if (width <= 0 || height <= 0 || mask.size() != size_t(width) * size_t(height))
return std::nullopt;
int32_t *points = nullptr, *loops = nullptr;
size_t pointCount = 0, loopCount = 0;
const int status = wand_trace(mask.data(), size_t(width), size_t(height), &points, &pointCount, &loops, &loopCount);
const std::unique_ptr<int32_t, decltype(&std::free)> ownedPoints(points, &std::free), ownedLoops(loops, &std::free);
if (status == -2)
throw MagicWandError(MagicWandError::Kind::tooDetailed);
if (status != 0)
throw MagicWandError(MagicWandError::Kind::memory);
if (loopCount == 0)
return std::nullopt;
// Outer loops clockwise, holes the other way: winding fills them.
QPainterPath path;
path.setFillRule(Qt::WindingFill);
size_t index = 0;
for (size_t loop = 0; loop < loopCount; ++loop) {
const size_t length = size_t(loops[loop]);
path.moveTo(points[index * 2], points[index * 2 + 1]);
for (size_t corner = index + 1; corner < index + length; ++corner)
path.lineTo(points[corner * 2], points[corner * 2 + 1]);
path.closeSubpath();
index += length;
}
return path;
}
void EditorSession::setWandSettings(const WandSettings &settings)
{
m_wandSettings = settings;
notify();
}
// What the wand reads: composite, or the active layer's pixels.
std::optional<QImage> EditorSession::selectionSample(const CanvasDocument &document, bool sampleAllLayers) const
{
try {
QImage context = BrushRaster::context(document.width, document.height, false);
QPainter painter(&context);
// A painted asset flattens here; the catch covers it too.
if (sampleAllLayers) {
drawLiveComposite(document, painter);
} else if (const std::optional<ImageLayer> layer = activeLayer(); layer && !layer->isGroup && layer->asset) {
const LayerTransform transform = displayedTransform(*layer);
LayerRenderer::draw(layer->asset->image(), transform, transform.center(), painter, {});
}
painter.end();
return context;
} catch (const ExportError &error) {
qCWarning(lcApp) << "a selection tool cannot sample the canvas:" << error.what();
return std::nullopt;
}
}
void EditorSession::magicWand(QPointF point, SelectionMode mode, std::function<void()> done)
{
// The caller resumes from the event loop, as after await.
const auto finish = [this, done] {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
};
const std::optional<CanvasDocument> &document = m_document;
// Swift's positive bounds refuse a point that is no number.
const bool inside = document && point.x() >= 0 && point.y() >= 0 && point.x() < document->width && point.y() < document->height;
if (!canEditSelection() || m_isProjectBusy || m_selectionMoveOrigin || !inside) {
finish();
return;
}
const std::optional<QImage> sample = selectionSample(*document, m_wandSettings.sampleAllLayers);
if (!sample) {
finish();
return;
}
setIsProjectBusy(true);
m_wanding = Wanding{mode, document->id, std::move(done), QStringLiteral("Magic Wand")};
m_wand.setFuture(QtConcurrent::run([sample = *sample, point, settings = m_wandSettings]() -> Wanded {
try {
return Wanded{MagicWand::select(sample, point, settings), std::nullopt};
} catch (const MagicWandError &error) {
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
} catch (const ExportError &error) {
return Wanded{std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
void EditorSession::finishWand()
{
const Wanded result = m_wand.result();
const Wanding pending = std::exchange(m_wanding, std::nullopt).value();
setIsProjectBusy(false);
if (result.failure) {
qCWarning(lcApp).noquote() << pending.name << "could not select:" << *result.failure;
setBrushError(result.failure);
} else if (m_document && m_document->id == pending.documentID) {
if (!result.path) {
// Nothing matched: New clears, as a lasso click enclosing nothing.
if (pending.mode == SelectionMode::replace)
deselect();
} else if (pending.mode == SelectionMode::replace) {
// A traced outline lies on the canvas: no costly clip.
setSelection(DocumentSelection{*result.path, m_selectionAntialiased}, pending.name);
} else {
applySelection(*result.path, pending.mode, pending.name);
}
}
if (pending.done)
QMetaObject::invokeMethod(this, pending.done, Qt::QueuedConnection);
}