// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Bitmap tracing settings dialog - second implementation. */ /* Authors: * Marc Jeanmougin * PBS * * Copyright (C) 2019-2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "tracedialog.h" #include #include #include #include #include #include #include #include "desktop.h" #include "io/resource.h" #include "selection.h" #include "trace/autotrace/inkscape-autotrace.h" #include "trace/depixelize/inkscape-depixelize.h" #include "trace/potrace/inkscape-potrace.h" #include "ui/util.h" // This maps the column ids in the glade file to useful enums static const std::map trace_types = { {"SS_BC", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS}, {"SS_ED", Inkscape::Trace::Potrace::TraceType::CANNY}, {"SS_CQ", Inkscape::Trace::Potrace::TraceType::QUANT}, {"SS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE}, {"SS_CT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE}, {"MS_BS", Inkscape::Trace::Potrace::TraceType::BRIGHTNESS_MULTI}, {"MS_C", Inkscape::Trace::Potrace::TraceType::QUANT_COLOR}, {"MS_BW", Inkscape::Trace::Potrace::TraceType::QUANT_MONO}, {"MS_AT", Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI}, }; namespace Inkscape { namespace UI { namespace Dialog { enum class EngineType { Potrace, Autotrace, Depixelize }; struct TraceData { std::unique_ptr engine; bool sioxEnabled; }; class TraceDialogImpl : public TraceDialog { public: TraceDialogImpl(); ~TraceDialogImpl() override; protected: void selectionModified(Selection *selection, unsigned flags) override; void selectionChanged(Selection *selection) override; private: TraceData getTraceData() const; bool paintPreview(Cairo::RefPtr const &cr); void setDefaults(); void adjustParamsVisible(); void onTraceClicked(); void onAbortClicked(); bool previewsEnabled() const; void schedulePreviewUpdate(int msecs, bool force = false); void updatePreview(bool force = false); void launchPreviewGeneration(); // Handles to ongoing asynchronous computations. Trace::TraceFuture trace_future; Trace::TraceFuture preview_future; // Delayed preview generation. sigc::connection preview_timeout_conn; bool preview_pending_recompute = false; Glib::RefPtr preview_image; Glib::RefPtr builder; Glib::RefPtr MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2, SS_AT_ET_T, SS_AT_FI_T, SS_BC_T, SS_CQ_T, SS_ED_T, optimize, smooth, speckles; Gtk::ComboBoxText *CBT_SS, *CBT_MS; Gtk::CheckButton *CB_invert, *CB_MS_smooth, *CB_MS_stack, *CB_MS_rb, *CB_speckles, *CB_smooth, *CB_optimize, *CB_PA_optimize, /* *CB_live,*/ *CB_SIOX; Gtk::CheckButton* CB_SIOX1; Gtk::CheckButton* CB_speckles1; Gtk::CheckButton* CB_smooth1; Gtk::CheckButton* CB_optimize1; Gtk::RadioButton *RB_PA_voronoi; Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update; Gtk::Box *mainBox; Gtk::Notebook *choice_tab; Gtk::DrawingArea *previewArea; Gtk::Box* orient_box; Gtk::Frame* _preview_frame; Gtk::Grid* _param_grid; Gtk::CheckButton* _live_preview; Gtk::Stack *stack; Gtk::ProgressBar *progressbar; Gtk::Box *boxchild1, *boxchild2; }; enum class Page { SingleScan, MultiScan, PixelArt }; TraceData TraceDialogImpl::getTraceData() const { auto current_page = static_cast(choice_tab->get_current_page()); auto cb_siox = current_page == Page::SingleScan ? CB_SIOX : CB_SIOX1; bool enable_siox = cb_siox->get_active(); auto trace_type_str = current_page == Page::SingleScan ? CBT_SS->get_active_id() : CBT_MS->get_active_id(); auto it = trace_types.find(trace_type_str); assert(it != trace_types.end()); auto trace_type = it->second; EngineType engine_type; if (current_page == Page::PixelArt) { engine_type = EngineType::Depixelize; } else { switch (trace_type) { case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE: case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE: case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI: engine_type = EngineType::Autotrace; break; default: engine_type = EngineType::Potrace; break; } } auto setup_potrace = [&, this] { auto eng = std::make_unique( trace_type, CB_invert->get_active(), (int)SS_CQ_T->get_value(), SS_BC_T->get_value(), 0, // Brightness floor SS_ED_T->get_value(), (int)MS_scans->get_value(), CB_MS_stack->get_active(), CB_MS_smooth->get_active(), CB_MS_rb->get_active()); auto cb_optimize = current_page == Page::SingleScan ? CB_optimize : CB_optimize1; eng->setOptiCurve(cb_optimize->get_active()); eng->setOptTolerance(optimize->get_value()); auto cb_smooth = current_page == Page::SingleScan ? CB_smooth : CB_smooth1; eng->setAlphaMax(cb_smooth->get_active() ? smooth->get_value() : 0); auto cb_speckles = current_page == Page::SingleScan ? CB_speckles : CB_speckles1; eng->setTurdSize(cb_speckles->get_active() ? (int)speckles->get_value() : 0); return eng; }; auto setup_autotrace = [&, this] { auto eng = std::make_unique(); switch (trace_type) { case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_SINGLE: eng->setColorCount(2); break; case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_CENTERLINE: eng->setColorCount(2); eng->setCenterLine(true); eng->setPreserveWidth(true); break; case Inkscape::Trace::Potrace::TraceType::AUTOTRACE_MULTI: eng->setColorCount((int)MS_scans->get_value() + 1); break; default: assert(false); break; } eng->setFilterIterations((int)SS_AT_FI_T->get_value()); eng->setErrorThreshold(SS_AT_ET_T->get_value()); return eng; }; auto setup_depixelize = [this] { return std::make_unique( RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::VORONOI : Inkscape::Trace::Depixelize::TraceType::BSPLINES, PA_curves->get_value(), (int) PA_islands->get_value(), (int) PA_sparse1->get_value(), PA_sparse2->get_value(), CB_PA_optimize->get_active()); }; TraceData data; switch (engine_type) { case EngineType::Potrace: data.engine = setup_potrace(); break; case EngineType::Autotrace: data.engine = setup_autotrace(); break; case EngineType::Depixelize: data.engine = setup_depixelize(); break; default: assert(false); break; } data.sioxEnabled = enable_siox; return data; } bool TraceDialogImpl::paintPreview(Cairo::RefPtr const &cr) { if (preview_image) { int width = preview_image->get_width(); int height = preview_image->get_height(); Gtk::Allocation const &allocation = previewArea->get_allocation(); double scaleFX = (double)allocation.get_width() / width; double scaleFY = (double)allocation.get_height() / height; double scaleFactor = std::min(scaleFX, scaleFY); int newWidth = (double)width * scaleFactor; int newHeight = (double)height * scaleFactor; int offsetX = (allocation.get_width() - newWidth) / 2; int offsetY = (allocation.get_height() - newHeight) / 2; cr->scale(scaleFactor, scaleFactor); Gdk::Cairo::set_source_pixbuf(cr, preview_image, offsetX / scaleFactor, offsetY / scaleFactor); cr->paint(); } else { cr->set_source_rgba(0, 0, 0, 0); cr->paint(); } return false; } void TraceDialogImpl::selectionChanged(Inkscape::Selection *selection) { updatePreview(); } void TraceDialogImpl::selectionModified(Selection *selection, unsigned flags) { auto mask = SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG; if ((flags & mask) == mask) { // All flags set - preview instantly. updatePreview(); } else if (flags & mask) { // At least one flag set - preview after a long delay. schedulePreviewUpdate(1000); } } void TraceDialogImpl::setDefaults() { MS_scans->set_value(8); PA_curves->set_value(1); PA_islands->set_value(5); PA_sparse1->set_value(4); PA_sparse2->set_value(1); SS_AT_FI_T->set_value(4); SS_AT_ET_T->set_value(2); SS_BC_T->set_value(0.45); SS_CQ_T->set_value(64); SS_ED_T->set_value(.65); optimize->set_value(0.2); smooth->set_value(1); speckles->set_value(2); CB_invert->set_active(false); CB_MS_smooth->set_active(true); CB_MS_stack->set_active(true); CB_MS_rb->set_active(false); CB_speckles->set_active(true); CB_smooth->set_active(true); CB_optimize->set_active(true); CB_speckles1->set_active(true); CB_smooth1->set_active(true); CB_optimize1->set_active(true); CB_PA_optimize->set_active(false); CB_SIOX->set_active(false); CB_SIOX1->set_active(false); } void TraceDialogImpl::onAbortClicked() { if (!trace_future) { // Not tracing; nothing to cancel. return; } stack->set_visible_child(*boxchild1); if (auto desktop = getDesktop()) desktop->clearWaitingCursor(); trace_future.cancel(); } void TraceDialogImpl::onTraceClicked() { if (trace_future) { // Still tracing; wait for either finished or cancelled. return; } // Attempt to fire off the tracer. auto data = getTraceData(); trace_future = Trace::trace(std::move(data.engine), data.sioxEnabled, // On progress: [this] (double progress) { progressbar->set_fraction(progress); }, // On completion without cancelling: [this] { progressbar->set_fraction(1.0); stack->set_visible_child(*boxchild1); if (auto desktop = getDesktop()) desktop->clearWaitingCursor(); trace_future.cancel(); } ); if (trace_future) { // Put the UI into the tracing state. if (auto desktop = getDesktop()) desktop->setWaitingCursor(); stack->set_visible_child(*boxchild2); progressbar->set_fraction(0.0); } } TraceDialogImpl::TraceDialogImpl() { Glib::ustring const req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2", "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T", "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth", "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize", "CB_speckles1", "CB_smooth1", "CB_optimize1", "CB_SIOX1", "CB_PA_optimize", /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS", "B_RESET", "B_STOP", "B_OK", "mainBox", "choice_tab", /*"choice_scan",*/ "previewArea", "_live_preview", "stack", "progressbar", "boxchild1", "boxchild2" }; auto gladefile = get_filename_string(Inkscape::IO::Resource::UIS, "dialog-trace.glade"); try { builder = Gtk::Builder::create_from_file(gladefile); } catch (Glib::Error const &) { g_warning("Trace dialog: Glade file loading failed"); return; } for (auto &w : req_widgets) { auto test = builder->get_object(w); if (!test) { g_warning("Trace dialog: Required widget %s does not exist", w.c_str()); return; } } #define GET_O(name) name = Glib::RefPtr::cast_dynamic(builder->get_object(#name)); GET_O(MS_scans) GET_O(PA_curves) GET_O(PA_islands) GET_O(PA_sparse1) GET_O(PA_sparse2) GET_O(SS_AT_FI_T) GET_O(SS_AT_ET_T) GET_O(SS_BC_T) GET_O(SS_CQ_T) GET_O(SS_ED_T) GET_O(optimize) GET_O(smooth) GET_O(speckles) #undef GET_O #define GET_W(name) builder->get_widget(#name, name); GET_W(CB_invert) GET_W(CB_MS_smooth) GET_W(CB_MS_stack) GET_W(CB_MS_rb) GET_W(CB_speckles) GET_W(CB_smooth) GET_W(CB_optimize) GET_W(CB_speckles1) GET_W(CB_smooth1) GET_W(CB_optimize1) GET_W(CB_PA_optimize) GET_W(CB_SIOX) GET_W(CB_SIOX1) GET_W(RB_PA_voronoi) GET_W(CBT_SS) GET_W(CBT_MS) GET_W(B_RESET) GET_W(B_STOP) GET_W(B_OK) GET_W(B_Update) GET_W(mainBox) GET_W(choice_tab) GET_W(previewArea) GET_W(orient_box) GET_W(_preview_frame) GET_W(_param_grid) GET_W(_live_preview) GET_W(stack) GET_W(progressbar) GET_W(boxchild1) GET_W(boxchild2) #undef GET_W add(*mainBox); Inkscape::Preferences* prefs = Inkscape::Preferences::get(); _live_preview->set_active(prefs->getBool(getPrefsPath() + "liveUpdate", true)); B_Update->signal_clicked().connect([=] { updatePreview(true); }); B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onTraceClicked)); B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::onAbortClicked)); B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl::setDefaults)); previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl::paintPreview)); // attempt at making UI responsive: relocate preview to the right or bottom of dialog depending on dialog size signal_size_allocate().connect([=] (Gtk::Allocation const &alloc) { // skip bogus sizes if (alloc.get_width() < 10 || alloc.get_height() < 10) return; // ratio: is dialog wide or is it tall? double const ratio = alloc.get_width() / static_cast(alloc.get_height()); // g_warning("size alloc: %d x %d - %f", alloc.get_width(), alloc.get_height(), ratio); double constexpr hysteresis = 0.01; if (ratio < 1 - hysteresis) { // narrow/tall choice_tab->set_valign(Gtk::ALIGN_START); orient_box->set_orientation(Gtk::ORIENTATION_VERTICAL); } else if (ratio > 1 + hysteresis) { // wide/short orient_box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); choice_tab->set_valign(Gtk::ALIGN_FILL); } }); CBT_SS->signal_changed().connect([=] { adjustParamsVisible(); }); adjustParamsVisible(); // watch for changes, but only in params that can impact preview bitmap for (auto adj : {SS_BC_T, SS_ED_T, SS_CQ_T, SS_AT_FI_T, SS_AT_ET_T, /* optimize, smooth, speckles,*/ MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2 }) { adj->signal_value_changed().connect([=] { updatePreview(); }); } for (auto checkbtn : {CB_invert, CB_MS_rb, /* CB_MS_smooth, CB_MS_stack, CB_optimize1, CB_optimize, */ CB_PA_optimize, CB_SIOX1, CB_SIOX, /* CB_smooth1, CB_smooth, CB_speckles1, CB_speckles, */ _live_preview}) { checkbtn->signal_toggled().connect([=] { updatePreview(); }); } for (auto combo : {CBT_SS, CBT_MS}) { combo->signal_changed().connect([=] { updatePreview(); }); } choice_tab->signal_switch_page().connect([=] (Gtk::Widget*, unsigned) { updatePreview(); }); signal_set_focus_child().connect([=] (Gtk::Widget *w) { if (w) updatePreview(); }); } TraceDialogImpl::~TraceDialogImpl() { Inkscape::Preferences* prefs = Inkscape::Preferences::get(); prefs->setBool(getPrefsPath() + "liveUpdate", _live_preview->get_active()); preview_timeout_conn.disconnect(); } bool TraceDialogImpl::previewsEnabled() const { return _live_preview->get_active() && is_widget_effectively_visible(this); } void TraceDialogImpl::schedulePreviewUpdate(int msecs, bool force) { if (!previewsEnabled() && !force) { return; } // Restart timeout. preview_timeout_conn.disconnect(); preview_timeout_conn = Glib::signal_timeout().connect([this] { updatePreview(true); return false; }, msecs); } void TraceDialogImpl::updatePreview(bool force) { if (!previewsEnabled() && !force) { return; } preview_timeout_conn.disconnect(); if (preview_future) { // Preview generation already running - flag for recomputation when finished. preview_pending_recompute = true; return; } preview_pending_recompute = false; auto data = getTraceData(); preview_future = Trace::preview(std::move(data.engine), data.sioxEnabled, // On completion: [this] (Glib::RefPtr result) { preview_image = std::move(result); previewArea->queue_draw(); preview_future.cancel(); // Recompute if invalidated during computation. if (preview_pending_recompute) { updatePreview(); } } ); if (!preview_future) { // On instant failure: preview_image.reset(); previewArea->queue_draw(); } } void TraceDialogImpl::adjustParamsVisible() { int constexpr start_row = 2; int option = CBT_SS->get_active_row_number(); if (option >= 3) option = 3; int show1 = start_row + option; int show2 = show1; if (option == 3) ++show2; for (int row = start_row; row < start_row + 5; ++row) { for (int col = 0; col < 4; ++col) { if (auto widget = _param_grid->get_child_at(col, row)) { if (row == show1 || row == show2) { widget->show(); } else { widget->hide(); } } } } } std::unique_ptr TraceDialog::create() { return std::make_unique(); } } // namespace Dialog } // namespace UI } // namespace Inkscape