summaryrefslogtreecommitdiffstats
path: root/src/trace/trace.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/trace/trace.cpp
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/trace/trace.cpp')
-rw-r--r--src/trace/trace.cpp538
1 files changed, 538 insertions, 0 deletions
diff --git a/src/trace/trace.cpp b/src/trace/trace.cpp
new file mode 100644
index 0000000..ed39825
--- /dev/null
+++ b/src/trace/trace.cpp
@@ -0,0 +1,538 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A generic interface for plugging different
+ * autotracers into Inkscape.
+ *
+ * Authors:
+ * Bob Jamison <rjamison@earthlink.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * PBS <pbs3141@gmail.com>
+ *
+ * Copyright (C) 2004-2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <limits>
+#include <mutex>
+#include <algorithm>
+#include <cassert>
+#include <2geom/transforms.h>
+#include <glibmm/i18n.h>
+
+#include "trace.h"
+#include "siox.h"
+
+#include "desktop.h"
+#include "document.h"
+#include "document-undo.h"
+#include "helper/geom.h"
+#include "inkscape.h"
+#include "message-stack.h"
+#include "selection.h"
+#include "svg/svg.h"
+
+#include "async/async.h"
+#include "async/progress-splitter.h"
+#include "async/background-progress.h"
+
+#include "display/cairo-utils.h"
+#include "display/drawing.h"
+#include "display/drawing-context.h"
+
+#include "object/sp-item.h"
+#include "object/sp-image.h"
+#include "object/weakptr.h"
+
+#include "ui/icon-names.h"
+
+#include "xml/repr.h"
+#include "xml/attribute-record.h"
+
+namespace Inkscape {
+namespace Trace {
+namespace {
+
+/**
+ * Grab the image and siox items from the current selection, performing some validation.
+ * \pre SP_ACTIVE_DESKTOP must not be null.
+ */
+std::optional<std::pair<SPImage*, std::vector<SPItem*>>> getImageAndItems(bool sioxEnabled, bool notifications = true)
+{
+ auto desktop = SP_ACTIVE_DESKTOP;
+ auto msgStack = desktop->getMessageStack();
+ auto sel = desktop->getSelection();
+
+ if (sioxEnabled) {
+ auto selection = std::vector<SPItem*>(sel->items().begin(), sel->items().end());
+ std::sort(selection.begin(), selection.end(), sp_item_repr_compare_position_bool);
+
+ SPImage *img = nullptr;
+ std::vector<SPItem*> items;
+
+ for (auto item : selection) {
+ if (auto itemimg = cast<SPImage>(item)) {
+ if (img) { // we want only one
+ if (notifications) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Select only one <b>image</b> to trace"));
+ return {};
+ }
+ img = itemimg;
+ } else if (img) { // Items are processed back-to-front, so this means "above the image".
+ items.emplace_back(item);
+ }
+ }
+
+ if (!img || items.empty()) {
+ if (notifications) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Select one image and one or more shapes above it"));
+ return {};
+ }
+
+ return std::make_pair(img, std::move(items));
+ } else {
+ // SIOX not enabled. We want exactly one image selected.
+ auto item = sel->singleItem();
+ if (!item) {
+ if (notifications) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Select an <b>image</b> to trace")); // same as above
+ return {};
+ }
+
+ auto img = cast<SPImage>(item);
+ if (!img) {
+ if (notifications) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Select an <b>image</b> to trace"));
+ return {};
+ }
+
+ return {{ img, {} }};
+ }
+}
+
+/**
+ * Given an SPImage, get the transform from pixbuf coordinates to the document.
+ */
+Geom::Affine getImageTransform(SPImage const *img)
+{
+ double x = img->x.computed;
+ double y = img->y.computed;
+ double w = img->width.computed;
+ double h = img->height.computed;
+
+ int iw = img->pixbuf->width();
+ int ih = img->pixbuf->height();
+
+ double wscale = w / iw;
+ double hscale = h / ih;
+
+ return Geom::Scale(wscale, hscale) * Geom::Translate(x, y) * img->transform;
+}
+
+Geom::IntPoint dimensions(Inkscape::Pixbuf const &pixbuf)
+{
+ return { pixbuf.width(), pixbuf.height() };
+}
+
+bool confirm_image_size(TracingEngine const *engine, Geom::IntPoint const &dimensions)
+{
+ if (engine->check_image_size(dimensions)) {
+ char const *msg = _("Image looks too big. Process may take a while and it is"
+ " wise to save your document before continuing."
+ "\n\nContinue the procedure (without saving)?");
+ Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true);
+
+ if (dialog.run() != Gtk::RESPONSE_OK) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Given a list of SPItems, apply a transform and rasterize them to a surface of the specified dimensions.
+ */
+Cairo::RefPtr<Cairo::ImageSurface> rasterizeItems(std::vector<SPItem*> &items, Geom::Affine const &affine, Geom::IntPoint dimensions)
+{
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, dimensions.x(), dimensions.y());
+ auto dc = Inkscape::DrawingContext(surface->cobj(), {});
+ auto const inv = affine.inverse();
+
+ auto dkey = SPItem::display_key_new(1);
+ Inkscape::Drawing drawing;
+
+ for (auto item : items) {
+ auto ai = item->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY);
+ drawing.setRoot(ai);
+ auto rect = Geom::IntRect({0, 0}, dimensions);
+ drawing.update(rect, inv);
+ drawing.render(dc, rect);
+ item->invoke_hide(dkey);
+ }
+
+ return surface;
+}
+
+class SioxImageCache
+{
+public:
+ static auto &get()
+ {
+ static SioxImageCache const instance;
+ return instance;
+ }
+
+ Glib::RefPtr<Gdk::Pixbuf> process(SioxImage const &sioximage, Async::Progress<double> &progress) const;
+
+private:
+ mutable std::mutex mutables;
+ mutable unsigned last_hash = 0;
+ mutable Glib::RefPtr<Gdk::Pixbuf> last_result;
+
+ SioxImageCache() = default;
+};
+
+Glib::RefPtr<Gdk::Pixbuf> SioxImageCache::process(SioxImage const &sioximage, Async::Progress<double> &progress) const
+{
+ auto hash = sioximage.hash();
+
+ auto g = std::lock_guard(mutables);
+
+ if (hash == last_hash) {
+ return last_result;
+ }
+
+ auto result = Siox(progress).extractForeground(sioximage, 0xffffff);
+
+ // result.writePPM("siox2.ppm");
+
+ last_hash = hash;
+ last_result = result.getGdkPixbuf();
+
+ return last_result;
+}
+
+Glib::RefPtr<Gdk::Pixbuf> sioxProcessImage(Glib::RefPtr<Gdk::Pixbuf> pixbuf, Cairo::RefPtr<Cairo::ImageSurface> siox_mask, Async::Progress<double> &progress)
+{
+ // Copy the pixbuf into the siox image.
+ auto sioximage = SioxImage(pixbuf);
+ int iwidth = sioximage.getWidth();
+ int iheight = sioximage.getHeight();
+
+ // Copy the mask into the siox image.
+ assert(iwidth == siox_mask->get_width());
+ assert(iheight == siox_mask->get_height());
+ for (int y = 0; y < iheight; y++) {
+ for (int x = 0; x < iwidth; x++) {
+ auto p = siox_mask->get_data() + y * siox_mask->get_stride() + 4 * x;
+ float a = p[3] / 255.0f;
+ float cm = Siox::CERTAIN_BACKGROUND_CONFIDENCE + (Siox::UNKNOWN_REGION_CONFIDENCE - Siox::CERTAIN_BACKGROUND_CONFIDENCE) * a;
+ sioximage.setConfidence(x, y, cm);
+ }
+ }
+
+ /*auto tmp = simage;
+ for (int i = 0; i < iwidth * iheight; i++) {
+ tmp.getImageData()[i] = 255 * tmp.getConfidenceData()[i];
+ }
+ tmp.writePPM("/tmp/x1.ppm");*/
+
+ // Process or retrieve from cache.
+ return SioxImageCache::get().process(sioximage, progress);
+}
+
+} // namespace
+
+namespace detail {
+
+struct TraceFutureCreate
+{
+ TraceFutureCreate() = delete;
+
+ static auto create(decltype(TraceFuture::channel) &&channel, decltype(TraceFuture::image_watcher) &&image_watcher)
+ {
+ TraceFuture result;
+ result.channel = std::move(channel);
+ result.image_watcher = std::move(image_watcher);
+ return result;
+ }
+};
+
+} // namespace detail
+
+// Todo: Consider rewriting using C++20 coroutines.
+class TraceTask
+{
+public:
+ TraceTask(std::unique_ptr<TracingEngine> engine, bool sioxEnabled, std::function<void(double)> onprogress, std::function<void()> onfinished)
+ : engine(std::move(engine))
+ , sioxEnabled(sioxEnabled)
+ , type(Type::Trace)
+ , onprogress(std::move(onprogress))
+ , onfinished_trace(std::move(onfinished)) {}
+
+ TraceTask(std::unique_ptr<TracingEngine> engine, bool sioxEnabled, std::function<void(Glib::RefPtr<Gdk::Pixbuf>)> onfinished)
+ : engine(std::move(engine))
+ , sioxEnabled(sioxEnabled)
+ , type(Type::Preview)
+ , onprogress([] (auto&&) {})
+ , onfinished_preview(std::move(onfinished)) {}
+
+ TraceTask(TraceTask const &) = delete;
+ TraceTask &operator=(TraceTask const &) = delete;
+
+ TraceFuture launch(std::unique_ptr<TraceTask> self);
+
+private:
+ std::unique_ptr<TracingEngine> engine;
+ bool sioxEnabled;
+
+ // Whether this is the full trace task, or just the preview task.
+ enum class Type
+ {
+ Trace,
+ Preview
+ };
+ Type type;
+
+ // Unsafe. Cannot call from worker thread since may perform actions in main thread. (This isn't Rust so we need a comment.)
+ std::function<void(double)> onprogress;
+ std::function<void()> onfinished_trace; // For trace task.
+ std::function<void(Glib::RefPtr<Gdk::Pixbuf>)> onfinished_preview; // For preview task.
+
+ // Unsafe. Cannot lock from worker thread since must be destroyed by main thread. (See above.)
+ std::weak_ptr<SPWeakPtr<SPImage>> image_watcher_weak;
+
+ std::shared_ptr<Inkscape::Pixbuf const> image_pixbuf;
+ Geom::Affine image_transform;
+ Cairo::RefPtr<Cairo::ImageSurface> siox_mask;
+ Async::Channel::Source channel;
+
+ TraceResult traceresult;
+
+ void do_async_work(std::unique_ptr<TraceTask> self);
+ void do_final_work(std::unique_ptr<TraceTask> self);
+};
+
+TraceFuture trace(std::unique_ptr<TracingEngine> engine, bool sioxEnabled, std::function<void(double)> onprogress, std::function<void()> onfinished)
+{
+ auto task = std::make_unique<TraceTask>(std::move(engine), sioxEnabled, std::move(onprogress), std::move(onfinished));
+ auto saved = task.get();
+ return saved->launch(std::move(task));
+}
+
+TraceFuture preview(std::unique_ptr<TracingEngine> engine, bool sioxEnabled, std::function<void(Glib::RefPtr<Gdk::Pixbuf>)> onfinished)
+{
+ auto task = std::make_unique<TraceTask>(std::move(engine), sioxEnabled, std::move(onfinished));
+ auto saved = task.get();
+ return saved->launch(std::move(task));
+}
+
+TraceFuture TraceTask::launch(std::unique_ptr<TraceTask> self)
+{
+ // Grab data and validate setup.
+
+ auto desktop = SP_ACTIVE_DESKTOP;
+ if (!desktop) {
+ g_warning("Trace: No active desktop\n");
+ return {};
+ }
+
+ auto msgStack = desktop->getMessageStack();
+
+ auto doc = SP_ACTIVE_DOCUMENT;
+ if (!doc) {
+ if (type == Type::Trace) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Trace: No active document"));
+ return {};
+ }
+ doc->ensureUpToDate();
+
+ auto imageanditems = getImageAndItems(sioxEnabled, type == Type::Trace);
+ if (!imageanditems) {
+ return {};
+ }
+
+ // Copy into coroutine frame.
+
+ auto image = imageanditems->first;
+
+ image_pixbuf = image->pixbuf; // Note: image->pixbuf is immutable, so can be shared thread-safely.
+ if (!image_pixbuf) {
+ if (type == Type::Trace) msgStack->flash(Inkscape::ERROR_MESSAGE, _("Trace: Image has no bitmap data"));
+ return {};
+ }
+
+ if (type == Type::Trace && !confirm_image_size(engine.get(), dimensions(*image_pixbuf))) {
+ // Image is too big and user decided to cancel.
+ return {};
+ }
+
+ image_transform = getImageTransform(image);
+
+ if (sioxEnabled) {
+ siox_mask = rasterizeItems(imageanditems->second, image_transform, dimensions(*image_pixbuf));
+ }
+
+ if (type == Type::Trace) msgStack->flash(Inkscape::NORMAL_MESSAGE, _("Trace: Starting trace..."));
+
+ // Open channel and launch background task.
+
+ auto [src, dst] = Async::Channel::create();
+ auto image_watcher = std::make_shared<SPWeakPtr<SPImage>>(image);
+
+ channel = std::move(src);
+ image_watcher_weak = image_watcher;
+
+ Async::fire_and_forget([this, self = std::move(self)] () mutable {
+ do_async_work(std::move(self));
+ });
+
+ return detail::TraceFutureCreate::create(std::move(dst), std::move(image_watcher));
+}
+
+void TraceTask::do_async_work(std::unique_ptr<TraceTask> self)
+{
+ if (!channel) {
+ // Cancelled while suspended.
+ return;
+ }
+
+ try {
+ auto progress = Async::BackgroundProgress(channel, onprogress);
+ auto throttled = Async::ProgressTimeThrottler(progress, std::chrono::milliseconds(10));
+
+ // Get progress subobjects for siox and trace sub-tasks.
+ std::optional<Async::SubProgress<double>> sub_siox, sub_trace;
+
+ Async::ProgressSplitter(throttled)
+ .add_if(sub_siox, 0.1, sioxEnabled)
+ .add_if(sub_trace, 0.9, type == Type::Trace);
+
+ // Convert the pixbuf to a GdkPixbuf, which due to immutability requires making a copy first.
+ auto copy = Pixbuf(*image_pixbuf);
+ auto gdkpixbuf = Glib::wrap(copy.getPixbufRaw(), true);
+
+ // If SIOX has been enabled, run SIOX processing.
+ if (sioxEnabled) {
+ gdkpixbuf = sioxProcessImage(gdkpixbuf, siox_mask, *sub_siox);
+ siox_mask.clear();
+ sub_siox->report_or_throw(1.0);
+ }
+
+ // If in preview mode, compute and return the preview and exit now.
+ if (type == Type::Preview) {
+ gdkpixbuf = engine->preview(gdkpixbuf);
+ channel.run(std::bind(onfinished_preview, std::move(gdkpixbuf)));
+ return;
+ }
+
+ // Actually perform the tracing.
+ traceresult = engine->trace(gdkpixbuf, *sub_trace);
+
+ gdkpixbuf.reset();
+
+ progress.report_or_throw(1.0);
+
+ // Return to the original thread for the remainder of the processing.
+ channel.run([this, self = std::move(self)] () mutable {
+ do_final_work(std::move(self));
+ });
+
+ } catch (Async::CancelledException const &) {
+
+ // no need to emit signals if manually aborted
+
+ } catch (...) {
+
+ g_warning("TraceTask::do_async_work: tracing aborted due to exception");
+ if (type == Type::Trace) {
+ channel.run(onfinished_trace);
+ } else {
+ channel.run(std::bind(onfinished_preview, Glib::RefPtr<Gdk::Pixbuf>()));
+ }
+
+ }
+}
+
+void TraceTask::do_final_work(std::unique_ptr<TraceTask> self)
+{
+ assert(type == Type::Trace);
+ assert(channel);
+
+ auto doc = SP_ACTIVE_DOCUMENT;
+ auto desktop = SP_ACTIVE_DESKTOP;
+ auto image_watcher = image_watcher_weak.lock();
+
+ if (!doc || !desktop || !image_watcher || traceresult.empty()) {
+ onfinished_trace();
+ return;
+ }
+
+ auto image = image_watcher->get();
+ if (!image) {
+ // Image was deleted.
+ onfinished_trace();
+ return;
+ }
+
+ auto msgStack = desktop->getMessageStack();
+ auto selection = desktop->getSelection();
+
+ // Get pointers to the <image> and its parent.
+ // XML Tree being used directly here while it shouldn't be
+ Inkscape::XML::Node *imgRepr = image->getRepr();
+ Inkscape::XML::Node *par = imgRepr->parent();
+
+ // Update the image transform - it may have changed from its initial value.
+ image_transform = getImageTransform(image);
+
+ // OK. Now let's start making new nodes.
+ Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *groupRepr = nullptr;
+
+ // If more than one path, make a <g>roup of <path>s.
+ int nrPaths = traceresult.size();
+ if (nrPaths > 1) {
+ groupRepr = xml_doc->createElement("svg:g");
+ par->addChild(groupRepr, imgRepr);
+ }
+
+ long totalNodeCount = 0;
+
+ for (auto const &result : traceresult) {
+ totalNodeCount += count_pathvector_nodes(result.path);
+
+ Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path");
+ pathRepr->setAttributeOrRemoveIfEmpty("style", result.style);
+ pathRepr->setAttributeOrRemoveIfEmpty("d", sp_svg_write_path(result.path * image_transform));
+
+ if (nrPaths > 1) {
+ groupRepr->addChild(pathRepr, nullptr);
+ } else {
+ par->addChild(pathRepr, imgRepr);
+ }
+
+ if (nrPaths == 1) {
+ selection->clear();
+ selection->add(pathRepr);
+ }
+
+ Inkscape::GC::release(pathRepr);
+ }
+
+ // If we have a group, then focus on it.
+ if (nrPaths > 1) {
+ selection->clear();
+ selection->add(groupRepr);
+ Inkscape::GC::release(groupRepr);
+ }
+
+ // Inform the document, so we can undo.
+ DocumentUndo::done(doc, _("Trace bitmap"), INKSCAPE_ICON("bitmap-trace"));
+
+ char *msg = g_strdup_printf(_("Trace: Done. %ld nodes created"), totalNodeCount);
+ msgStack->flash(Inkscape::NORMAL_MESSAGE, msg);
+ g_free(msg);
+
+ onfinished_trace();
+}
+
+} // namespace Trace
+} // namespace Inkscape