Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
export-batch.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/* Authors:
3 * Lauris Kaplinski <lauris@kaplinski.com>
4 * bulia byak <buliabyak@users.sf.net>
5 * Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
6 * Anshudhar Kumar Singh <anshudhar2001@gmail.com>
7 *
8 * Copyright (C) 1999-2007, 2021 Authors
9 * Copyright (C) 2001-2002 Ximian, Inc.
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
15
16#include <glibmm/convert.h>
17#include <glibmm/fileutils.h>
18#include <glibmm/i18n.h>
19#include <glibmm/main.h>
20#include <glibmm/miscutils.h>
21#include <gtkmm/builder.h>
22#include <gtkmm/button.h>
23#include <gtkmm/error.h>
24#include <gtkmm/filedialog.h>
25#include <gtkmm/flowbox.h>
26#include <gtkmm/messagedialog.h>
27#include <gtkmm/progressbar.h>
28#include <gtkmm/widget.h>
29#include <png.h>
30#include <regex>
31#include <sigc++/scoped_connection.h>
32#include <utility>
33
34#include "desktop.h"
35#include "document-undo.h"
36#include "document.h"
37#include "export-batch.h"
38#include "extension/output.h"
39#include "inkscape-window.h"
40#include "io/fix-broken-links.h"
41#include "io/sandbox.h"
42#include "io/sys.h"
43#include "layer-manager.h"
44#include "message-stack.h"
45#include "object/sp-namedview.h"
46#include "object/sp-page.h"
47#include "object/sp-root.h"
48#include "page-manager.h"
49#include "preferences.h"
50#include "selection.h"
51#include "ui/builder-utils.h"
52#include "ui/dialog-run.h"
53#include "ui/dialog/export.h"
54#include "ui/icon-names.h"
58#include "util/units.h"
59
60namespace Inkscape::UI::Dialog {
61
62BatchItem::BatchItem(SPItem *item, bool isolate_item, std::shared_ptr<PreviewDrawing> drawing)
63 : _item{item}
64 , _isolate_item{isolate_item}
65{
66 init(std::move(drawing));
67 _object_modified_conn = _item->connectModified([=, this](SPObject *obj, unsigned int flags) {
69 });
71}
72
73BatchItem::BatchItem(SPPage *page, std::shared_ptr<PreviewDrawing> drawing)
74 : _page{page}
75{
76 init(std::move(drawing));
77 _object_modified_conn = _page->connectModified([=, this](SPObject *obj, unsigned int flags) {
79 });
81}
82
83BatchItem::~BatchItem() = default;
84
86{
87 Glib::ustring label = "no-name";
88 if (_page) {
90 if (auto id = _page->label()) {
91 label = id;
92 }
93 } else if (_item) {
95 if (label.empty()) {
96 if (auto _id = _item->getId()) {
97 label = _id;
98 } else {
99 label = "no-id";
100 }
101 }
102 }
104 _label.set_text(label);
105 set_tooltip_text(label);
106}
107
111{
112 if (_isolate_item != isolate) {
113 _isolate_item = isolate;
115 }
116}
117
118void BatchItem::init(std::shared_ptr<PreviewDrawing> drawing) {
119 _grid.set_row_spacing(5);
120 _grid.set_column_spacing(5);
121 _grid.set_valign(Gtk::Align::CENTER);
122
123 _selector.set_active(true);
124 _selector.set_focusable(false);
125 _selector.set_margin_start(2);
126 _selector.set_margin_bottom(2);
127 _selector.set_valign(Gtk::Align::END);
128
129 _option.set_active(false);
130 _option.set_focusable(false);
131 _option.set_margin_start(2);
132 _option.set_margin_bottom(2);
133 _option.set_valign(Gtk::Align::END);
134
135 _preview.set_name("export_preview_batch");
137 _preview.setDrawing(std::move(drawing));
138 _preview.setSize(64);
139 _preview.set_halign(Gtk::Align::CENTER);
140 _preview.set_valign(Gtk::Align::CENTER);
141
142 _label.set_width_chars(10);
143 _label.set_ellipsize(Pango::EllipsizeMode::END);
144 _label.set_halign(Gtk::Align::CENTER);
145
146 set_valign(Gtk::Align::START);
147 set_halign(Gtk::Align::START);
148 set_child(_grid);
149 set_focusable(false);
150
151 _selector.signal_toggled().connect([this]() {
152 set_selected(_selector.get_active());
153 });
154 _option.signal_toggled().connect([this]() {
155 set_selected(_option.get_active());
156 });
157
158 // This initially packs the widgets with a hidden preview.
159 refresh(!is_hide, 0);
160
161 property_parent().signal_changed().connect([this] { on_parent_changed(); });
162}
163
167void BatchItem::set_selected(bool selected)
168{
169 auto box = dynamic_cast<Gtk::FlowBox *>(get_parent());
170 if (box && selected != is_selected()) {
171 if (selected) {
172 box->select_child(*this);
173 } else {
174 box->unselect_child(*this);
175 }
176 }
177}
178
183{
184 if (auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent()))
185 on_mode_changed(parent->get_selection_mode());
186 if (_selector.get_visible()) {
187 set_selected(_selector.get_active());
188 } else if (_option.get_visible()) {
189 set_selected(_option.get_active());
190 }
191}
192
196void BatchItem::on_mode_changed(Gtk::SelectionMode mode)
197{
198 _selector.set_visible(mode == Gtk::SelectionMode::MULTIPLE);
199 _option.set_visible(mode == Gtk::SelectionMode::SINGLE);
200}
201
206{
207 auto parent = dynamic_cast<Gtk::FlowBox *>(get_parent());
208 if (!parent) {
209 return;
210 }
211
212 _selection_widget_changed_conn = parent->signal_selected_children_changed().connect([this]() {
213 // Synchronise the active widget state to the Flowbox selection.
214 if (_selector.get_visible()) {
215 _selector.set_active(is_selected());
216 } else if (_option.get_visible()) {
217 _option.set_active(is_selected());
218 }
219 });
221
222 for (auto child = parent->get_first_child(); child; child = child->get_next_sibling()) {
223 if (child != this) {
224 if (auto item = dynamic_cast<BatchItem *>(child)) {
225 auto group = item->get_radio_group();
226 _option.set_group(*group);
227 break;
228 }
229 }
230 }
231}
232
233void BatchItem::refresh(bool hide, uint32_t bg_color)
234{
235 if (_page) {
237 }
238
240
241 // When hiding the preview, we show the items as a checklist
242 // So all items must be packed differently on refresh.
243 if (hide != is_hide) {
244 is_hide = hide;
245
246 auto remove_grid_child = [&] (Gtk::Widget &widget) {
247 if (widget.get_parent() == &_grid) {
248 _grid.remove(widget);
249 }
250 };
251 remove_grid_child(_selector);
252 remove_grid_child(_option);
253 remove_grid_child(_label);
254 remove_grid_child(_preview);
255
256 if (hide) {
257 _selector.set_valign(Gtk::Align::BASELINE);
258 _label.set_xalign(0.0);
259 _label.set_max_width_chars(-1);
260 _grid.attach(_selector, 0, 1, 1, 1);
261 _grid.attach(_option, 0, 1, 1, 1);
262 _grid.attach(_label, 1, 1, 1, 1);
263 } else {
264 _selector.set_valign(Gtk::Align::END);
265 _label.set_xalign(0.5);
266 _label.set_max_width_chars(18);
267 _grid.attach(_preview, 0, 0, 2, 2);
268 _grid.attach(_selector, 0, 1, 1, 1);
269 _grid.attach(_option, 0, 1, 1, 1);
270 _grid.attach(_label, 0, 2, 2, 1);
271 }
273 }
274
275 if (!hide) {
277 }
278}
279
280void BatchItem::setDrawing(std::shared_ptr<PreviewDrawing> drawing)
281{
282 _preview.setDrawing(std::move(drawing));
283}
284
293void BatchItem::syncItems(BatchItems &items, std::map<std::string, SPObject*> const &objects, Gtk::FlowBox &container, std::shared_ptr<PreviewDrawing> preview, bool isolate_items)
294{
295 // Pre- and post-condition of this function is that
296 // `items` and `container.children` have the same content.
297 // (They have different types and contain slightly different information,
298 // but there is still a 1:1 correspondence.)
299 //
300 // We update `items` so that it matches `objects`.
301 // Any necessary change to `items` is also applied to `container.children`.
302
303 // a) Remove any items not in objects
304 for (auto it = items.begin(); it != items.end();) {
305 if (!objects.contains(it->first)) {
306 container.remove(*it->second);
307 it = items.erase(it);
308 } else {
309 it->second->setIsolateItem(isolate_items);
310 ++it;
311 }
312 }
313
314 // b) Add any objects not in items
315
316 // A special container for pages allows them to be sorted correctly
317 std::set<SPPage *, SPPage::PageIndexOrder> pages;
318
319 for (auto &[id, obj] : objects) {
320 if (auto page = cast<SPPage>(obj)) {
321 if (!items[id] || items[id]->getPage() != page)
322 pages.insert(page);
323 continue;
324 }
325
326 auto item = cast<SPItem>(obj);
327
328 // If an Item or Page with same Id is already present, Skip
329 if (items[id] && items[id]->getItem() == item)
330 continue;
331
332 if (items[id]) {
333 // Remove existing item with same id
334 // (can occur when switching between document tabs)
335 container.remove(*items[id]);
336 }
337 // Add new item to the end of list
338 items[id] = std::make_unique<BatchItem>(item, isolate_items, preview);
339 container.insert(*items[id], -1);
340 items[id]->set_selected(true);
341 }
342
343 for (auto &page : pages) {
344 if (auto id = page->getId()) {
345 if (items[id]) {
346 container.remove(*items[id]);
347 }
348 items[id] = std::make_unique<BatchItem>(page, preview);
349 container.insert(*items[id], -1);
350 items[id]->set_selected(true);
351 }
352 }
353
354 // Check postconditions
355 g_assert(items.size() == objects.size());
356 g_assert(container.get_children().size() == items.size());
357}
358
359BatchExport::BatchExport(BaseObjectType * const cobject, Glib::RefPtr<Gtk::Builder> const &builder)
360 : Gtk::Box{cobject}
361 , preview_container(get_widget<Gtk::FlowBox> (builder, "b_preview_box"))
362 , show_preview (get_widget<Gtk::CheckButton> (builder, "b_show_preview"))
363 , num_elements (get_widget<Gtk::Label> (builder, "b_num_elements"))
364 , hide_all (get_widget<Gtk::CheckButton> (builder, "b_hide_all"))
365 , overwrite (get_widget<Gtk::CheckButton> (builder, "b_overwrite"))
366 , name_text (get_widget<Gtk::Entry> (builder, "b_name"))
367 , path_chooser (get_widget<Gtk::Button> (builder, "b_path"))
368 , export_btn (get_widget<Gtk::Button> (builder, "b_export"))
369 , cancel_btn (get_widget<Gtk::Button> (builder, "b_cancel"))
370 , progress_box (get_widget<Gtk::Box> (builder, "b_inprogress"))
371
372 , _prog (get_widget<Gtk::ProgressBar> (builder, "b_progress"))
373 , _prog_batch (get_widget<Gtk::ProgressBar> (builder, "b_progress_batch"))
374 , export_list (get_derived_widget<ExportList>(builder, "b_export_list"))
375 , _background_color(get_derived_widget<UI::Widget::ColorPicker>(builder, "b_backgnd", _("Background color"), true))
376{
378
382
383 selection_buttons[SELECTION_SELECTION] = &get_widget<Gtk::ToggleButton>(builder, "b_s_selection");
384 selection_buttons[SELECTION_LAYER] = &get_widget<Gtk::ToggleButton>(builder, "b_s_layers");
385 selection_buttons[SELECTION_PAGE] = &get_widget<Gtk::ToggleButton>(builder, "b_s_pages");
386
387 path_chooser.signal_clicked().connect([this] { pickBatchPath(); });
388
389 setup();
390}
391
392BatchExport::~BatchExport() = default;
393
395{
396 if (!_desktop || _desktop->getSelection() != selection) {
397 return;
398 }
399 if (!(flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
400 return;
401 }
403}
404
406{
407 if (!_desktop || _desktop->getSelection() != selection) {
408 return;
409 }
410 selection_buttons[SELECTION_SELECTION]->set_sensitive(!selection->isEmpty());
411 if (selection->isEmpty()) {
413 selection_buttons[SELECTION_LAYER]->set_active(true); // This causes refresh area
414 // return otherwise refreshArea will be called again
415 // even though we are at default key, selection is the one which was original key.
416 prefs->setString("/dialogs/export/batchexportarea/value", selection_names[SELECTION_SELECTION]);
417 return;
418 }
419 } else {
420 Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
423 return;
424 }
425 }
426 queueRefresh();
427}
428
430{
431 if (!_desktop || !_document) return;
432
433 bool has_pages = _document->getPageManager().hasPages();
434 selection_buttons[SELECTION_PAGE]->set_sensitive(has_pages);
435
436 if (current_key == SELECTION_PAGE && !has_pages) {
438 selection_buttons[SELECTION_LAYER]->set_active();
439 }
440
441 queueRefresh();
442}
443
444// Setup Single Export.Called by export on realize
446{
447 if (setupDone) {
448 return;
449 }
450 setupDone = true;
451
453
454 // set them before connecting to signals
456 setExporting(false);
457 queueRefresh(true);
458
459 // Connect Signals
460 for (auto [key, button] : selection_buttons) {
461 button->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &BatchExport::onAreaTypeToggle), key));
462 }
463 show_preview.signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshPreview));
464 export_conn = export_btn.signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onExport));
465 cancel_conn = cancel_btn.signal_clicked().connect(sigc::mem_fun(*this, &BatchExport::onCancel));
466 hide_all.signal_toggled().connect(sigc::mem_fun(*this, &BatchExport::refreshItems));
467 _background_color.connectChanged([=, this](Colors::Color const &color){
468 if (_desktop) {
470 }
472 });
473}
474
476{
477 if (!_desktop || !_document) return;
478
479 // Create New List of Items
480 std::map<std::string, SPObject*> objects;
481
482 bool isolate = false;
483 char *num_str = nullptr;
484 switch (current_key) {
485 case SELECTION_SELECTION: {
486 isolate = hide_all.get_active();
487 for (auto item : _desktop->getSelection()->items()) {
488 // Ignore empty items (empty groups, other bad items)
489 if (item && item->visualBounds() && item->getId()) {
490 objects[item->getId()] = item;
491 }
492 }
493 num_str = g_strdup_printf(ngettext("%d Item", "%d Items", objects.size()), (int)objects.size());
494 break;
495 }
496 case SELECTION_LAYER: {
497 isolate = true;
498 for (auto layer : _desktop->layerManager().getAllLayers()) {
499 // Ignore empty layers, they have no size.
500 if (layer->geometricBounds() && layer->getId()) {
501 objects[layer->getId()] = layer;
502 }
503 }
504 num_str = g_strdup_printf(ngettext("%d Layer", "%d Layers", objects.size()), (int)objects.size());
505 break;
506 }
507 case SELECTION_PAGE: {
508 for (auto page : _desktop->getDocument()->getPageManager().getPages()) {
509 if (auto id = page->getId()) {
510 objects[id] = page;
511 }
512 }
513 num_str = g_strdup_printf(ngettext("%d Page", "%d Pages", objects.size()), (int)objects.size());
514 break;
515 }
516 default:
517 break;
518 }
519 if (num_str) {
520 num_elements.set_text(num_str);
521 g_free(num_str);
522 }
523
525
527}
528
529
531{
532 if (!_desktop) return;
533
534 // For Batch Export we are now hiding all object except current object
535 bool hide = hide_all.get_active();
536 bool preview = show_preview.get_active();
537
538 if (preview) {
539 std::vector<SPItem const *> selected;
540 if (hide) {
541 auto sels = _desktop->getSelection()->items();
542 selected = {sels.begin(), sels.end()};
543 }
544 _preview_drawing->set_shown_items(std::move(selected));
545 }
546 for (auto &[key, val] : current_items) {
548 }
549}
556std::optional<Glib::RefPtr<Gio::File const>> BatchExport::getBatchPath() const
557{
558 if (export_path.has_value()) {
559 return export_path;
560 }
561 return getPreviousBatchPath();
562}
563
575std::optional<Glib::RefPtr<Gio::File const>> BatchExport::getPreviousBatchPath() const
576{
577 auto path = prefs->getString("/dialogs/export/batch/path");
578 if (auto attr = _document->getRoot()->getAttribute("inkscape:export-batch-path")) {
579 path = attr;
580 }
581 if (!path.empty() && Glib::path_is_absolute(Glib::filename_from_utf8(path))) {
582 return Gio::File::create_for_parse_name(path);
583 }
584
586 // With a sandboxed filesystem, automatically determined paths typically won't work.
587 // We give up instead of guessing some relative paths.
588 return std::nullopt;
589 }
590
591 // Relative to the document's position
592 // TODO: it is unclear which encoding `getDocumentFilename()` has. We assume it is platform-native.
593 if (const char *doc_filename = _document->getDocumentFilename()) {
594 auto doc_path = Glib::path_get_dirname(doc_filename);
595
596 if (!path.empty()) {
597 return Gio::File::create_for_path(Glib::canonicalize_filename(path.raw(), doc_path));
598 }
599 return Gio::File::create_for_path(doc_path);
600 }
601 return std::nullopt;
602}
603
608void BatchExport::setBatchPath(std::optional<Glib::RefPtr<Gio::File const>> path)
609{
610 export_path = path;
611 Glib::ustring path_utf8 = "";
612 if (path.has_value()) {
613 path_utf8 = path.value()->get_parse_name();
614 }
615
616 Glib::ustring path_label = Inkscape::IO::Sandbox::filesystem_get_display_path(export_path, _("Choose folder..."));
617
619 // We have direct access to the filesystem.
620 // Clean up the path (convert to relative directory).
621 // Show this path to the user.
622 if (const char *doc_filename = _document->getDocumentFilename()) {
623 auto doc_path = Glib::path_get_dirname(doc_filename);
624 path_utf8 = Inkscape::optimizePath(path_utf8, doc_path, 2);
625 path_label = path_utf8;
626 }
627 }
628 prefs->setString("/dialogs/export/batch/path", path_utf8);
629 _document->getRoot()->setAttribute("inkscape:export-batch-path", path_utf8);
630
631 path_chooser.set_label(path_label);
632}
633
641Glib::ustring BatchExport::getBatchName(bool fallback) const
642{
643 if (auto attr = _document->getRoot()->getAttribute("inkscape:export-batch-name")) {
644 return attr;
645 } else if (!fallback)
646 return "";
647 if (const char *doc_filename = _document->getDocumentFilename()) {
648 std::string name = Glib::path_get_basename(doc_filename);
650 return name;
651 }
652 return "batch";
653}
654
655void BatchExport::setBatchName(Glib::ustring const &name)
656{
657 _document->getRoot()->setAttribute("inkscape:export-batch-name", name);
658}
659
660void BatchExport::loadExportHints(bool rename_file)
661{
662 if (!_desktop) return;
663
664 // update labels
666
667 if (name_text.get_text().empty()) {
668 auto const name = getBatchName(rename_file);
669 name_text.set_text(name);
670 name_text.set_position(name.length());
671 }
672}
673
675{
676 auto dialog = Gtk::FileDialog::create();
677 dialog->select_folder(dynamic_cast<Gtk::Window &>(*get_root()), sigc::track_object([&dialog = *dialog, this] (auto &result) {
678 try {
679 if (auto old_file = dialog.select_folder_finish(result)) {
680 setBatchPath(old_file);
681 return;
682 }
683 } catch (Gtk::DialogError const &) {
684 }
685 }, *this), {});
686}
687
688// Signals CallBack
689
691{
692 // Prevent executing function twice
693 if (!selection_buttons[key]->get_active()) {
694 return;
695 }
696 // If you have reached here means the current key is active one ( not sure if multiple transitions happen but
697 // last call will change values)
699 prefs->setString("/dialogs/export/batchexportarea/value", selection_names[current_key]);
700
701 queueRefresh();
702}
703
705{
706 interrupted = true;
707 setExporting(false);
708}
709
711{
712 interrupted = false;
713 if (!_desktop)
714 return;
715
716 // If there are no selected button, simply flash message in status bar
717 int num = current_items.size();
718 if (current_items.size() == 0) {
719 _desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected."));
720 return;
721 }
722
723 setExporting(true);
724
725 auto path = getBatchPath();
726 if (!path.has_value()) {
727 return;
728 }
729 std::string name = name_text.get_text();
730
731 if (path.value()->query_file_type() != Gio::FileType::DIRECTORY) {
732 auto const window = _desktop->getInkscapeWindow();
733 if (path.value()->query_exists()) {
734 auto dialog = Gtk::MessageDialog(*window, _("Can not save to a directory that is actually a file."), true, Gtk::MessageType::ERROR, Gtk::ButtonsType::OK);
735 UI::dialog_run(dialog);
736 return;
737 }
738 Glib::ustring message = g_markup_printf_escaped(
739 _("<span weight=\"bold\" size=\"larger\">Directory \"%s\" doesn't exist. Create it now?</span>"),
740 path.value()->get_parse_name().c_str());
741
742 auto dialog = Gtk::MessageDialog(*window, message, true, Gtk::MessageType::WARNING, Gtk::ButtonsType::YES_NO);
743 if (UI::dialog_run(dialog) != Gtk::ResponseType::YES) {
744 return;
745 }
746 path.value()->dup()->make_directory_with_parents();
747 }
748
749 setBatchPath(path);
751 DocumentUndo::done(_document, _("Set Batch Export Options"), INKSCAPE_ICON("export"));
752
753 // create vector of exports
754 int num_rows = export_list.get_rows();
755 std::vector<Glib::ustring> suffixs;
756 std::vector<Inkscape::Extension::Output *> extensions;
757 std::vector<double> dpis;
758 for (int i = 0; i < num_rows; i++) {
759 suffixs.emplace_back(export_list.get_suffix(i));
760 extensions.push_back(export_list.getExtension(i));
761 dpis.push_back(export_list.get_dpi(i));
762 }
763
764 bool ow = overwrite.get_active();
765 bool hide = hide_all.get_active();
766 auto sels = _desktop->getSelection()->items();
767 std::vector<SPItem const *> selected_items(sels.begin(), sels.end());
768
769 // Start Exporting Each Item
770 for (int j = 0; j < num_rows && !interrupted; j++) {
771
772 auto suffix = export_list.get_suffix(j);
773 auto ext = export_list.getExtension(j);
774 float dpi = export_list.get_dpi(j);
775
776 if (!ext || ext->deactivated()) {
777 continue;
778 }
779
780 int count = 0;
781 for (auto i = current_items.begin(); i != current_items.end() && !interrupted; ++i) {
782 count++;
783
784 auto &batchItem = i->second;
785 if (!batchItem->is_selected()) {
786 continue;
787 }
788
789 SPItem *item = batchItem->getItem();
790 SPPage *page = batchItem->getPage();
791 bool isolate_item = batchItem->isolateItem();
792
793 std::vector<SPItem const *> show_only;
794 Geom::Rect area;
795 if (item) {
796 if (auto bounds = item->documentVisualBounds()) {
797 area = *bounds;
798 } else {
799 continue;
800 }
801 if (hide) {
802 for (auto &sel_item : selected_items) {
803 // Layers want their descendants, selections want themselves
804 if (item->isAncestorOf(sel_item) || sel_item == item) {
805 show_only.emplace_back(sel_item);
806 }
807 }
808 if (show_only.empty())
809 continue; // Nothing to export
810 } else if (isolate_item) {
811 // Layers are isolated even when they aren't hiding other items
812 show_only.emplace_back(item);
813 }
814 } else if (page) {
815 area = page->getDocumentRect();
816 if (hide)
817 show_only = selected_items;
818 } else {
819 continue;
820 }
821
822 std::string id = Glib::filename_from_utf8(batchItem->getLabel());
823 if (id.empty()) {
824 continue;
825 }
826
827 std::string item_name = name;
828 if (!name.empty()) {
829 std::string::value_type last_char = name.at(name.length() - 1);
830 if (last_char != '/' && last_char != '\\') {
831 item_name += "_";
832 }
833 }
834 if (id.at(0) == '#' && batchItem->getItem() && !batchItem->getItem()->label()) {
835 item_name += id.substr(1);
836 } else {
837 item_name += id;
838 }
839
840 if (!suffix.empty()) {
841 if (ext->is_raster()) {
842 // Put the dpi in at the user's requested location.
843 suffix = std::regex_replace(suffix.c_str(), std::regex("\\{dpi\\}"), std::to_string((int)dpi));
844 }
845 item_name += "_" + suffix;
846 }
847
848 if (item_name.empty()) {
849 g_error("Empty item name in batch export, refusing to export.");
850 continue;
851 }
852
853 // Add the path last so item_name has a chance to be filled without path confusion
855 std::string item_filename = Glib::build_filename(path.value()->get_path(), item_name);
856 if (!ow) {
857 if (!Export::unConflictFilename(_document, item_filename, ext->get_extension())) {
858 continue;
859 }
860 } else {
861 item_filename += ext->get_extension();
862 }
863 auto item_file = Gio::File::create_for_path(item_filename);
864 auto item_filename_utf8 = Glib::filename_to_utf8(item_filename);
865 auto item_filename_label = Inkscape::IO::Sandbox::filesystem_get_display_path(item_file);
866
867 // Set the progress bar with our updated information
868 double progress = (((double)count / num) + j) / num_rows;
869 _prog_batch.set_fraction(progress);
870
871 setExporting(true, Glib::ustring::compose(_("Exporting %1"), item_filename_label),
872 Glib::ustring::compose(_("Format %1, Selection %2"), j + 1, count));
873
874 if (ext->is_raster()) {
875 unsigned long int width = (int)(area.width() * dpi / DPI_BASE + 0.5);
876 unsigned long int height = (int)(area.height() * dpi / DPI_BASE + 0.5);
877
879 item_filename_utf8, true, onProgressCallback, this, ext, &show_only);
880 } else if (page || !show_only.empty()) {
881 auto copy_doc = _document->copy();
882 Export::exportVector(ext, copy_doc.get(), item_filename_utf8, true, show_only, page);
883 } else {
884 auto copy_doc = _document->copy();
885 Export::exportVector(ext, copy_doc.get(), item_filename_utf8, true, area);
886 }
887 }
888 }
889 // Do this right at the end to finish up
890 setExporting(false);
891}
892
894{
895 current_key = (selection_mode)0; // default key
896 bool found = false;
897 Glib::ustring pref_key_name = prefs->getString("/dialogs/export/batchexportarea/value");
898 for (auto [key, name] : selection_names) {
899 if (pref_key_name == name) {
901 found = true;
902 break;
903 }
904 }
905 if (!found) {
906 pref_key_name = selection_names[current_key];
907 }
908 if (_desktop) {
909 if (auto _sel = _desktop->getSelection()) {
910 selection_buttons[SELECTION_SELECTION]->set_sensitive(!_sel->isEmpty());
911 }
912 bool has_pages = _document->getPageManager().hasPages();
913 selection_buttons[SELECTION_PAGE]->set_sensitive(has_pages);
914 }
915 if (!selection_buttons[current_key]->get_sensitive()) {
917 }
918 selection_buttons[current_key]->set_active(true);
919
920 // we need to set pref key because signals above will set set pref == current key but we sometimes change
921 // current key like selection key
922 prefs->setString("/dialogs/export/batchexportarea/value", pref_key_name);
923}
924
925void BatchExport::setExporting(bool exporting, Glib::ustring const &text, Glib::ustring const &text_batch)
926{
927 if (exporting) {
928 set_sensitive(false);
929 set_opacity(0.2);
930 progress_box.set_visible(true);
931 _prog.set_text(text);
932 _prog.set_fraction(0.0);
933 _prog_batch.set_text(text_batch);
934 } else {
935 set_sensitive(true);
936 set_opacity(1.0);
937 progress_box.set_visible(false);
938 _prog.set_text("");
939 _prog.set_fraction(0.0);
940 _prog_batch.set_text("");
941 }
942}
943
944unsigned int BatchExport::onProgressCallback(float value, void *data)
945{
946 if (auto bi = static_cast<BatchExport *>(data)) {
947 bi->_prog.set_fraction(value);
948 auto main_context = Glib::MainContext::get_default();
949 main_context->iteration(false);
950 return !bi->interrupted;
951 }
952 return false;
953}
954
956{
957 if (desktop != _desktop) {
958 _pages_changed_connection.disconnect();
960 }
961}
962
964{
965 if (!_desktop) {
966 document = nullptr;
967 }
968 if (_document == document)
969 return;
970
971 _document = document;
972 _pages_changed_connection.disconnect();
973 if (document) {
974 // when the page selected is changed, update the export area
977 pagesChanged();
978
979 _preview_drawing = std::make_shared<PreviewDrawing>(document);
980 } else {
981 _preview_drawing.reset();
982 }
983
984 name_text.set_text("");
985 path_chooser.set_label("");
986 refreshItems();
987}
988
990{
991 if (refresh_items_conn) {
992 return;
993 }
994 // Asynchronously refresh the preview
995 refresh_items_conn = Glib::signal_idle().connect([this] {
996 refreshItems();
997 return false;
998 }, Glib::PRIORITY_HIGH);
999}
1000
1001void BatchExport::queueRefresh(bool rename_file)
1002{
1003 if (refresh_conn) {
1004 return;
1005 }
1006 refresh_conn = Glib::signal_idle().connect([this, rename_file] {
1007 refreshItems();
1008 loadExportHints(rename_file);
1009 return false;
1010 }, Glib::PRIORITY_HIGH);
1011}
1012
1013} // namespace Inkscape::UI::Dialog
1014
1015/*
1016 Local Variables:
1017 mode:c++
1018 c-file-style:"stroustrup"
1019 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1020 indent-tabs-mode:nil
1021 fill-column:99
1022 End:
1023*/
1024// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Gtk builder utilities.
uint64_t page
Definition canvas.cpp:171
Geom::IntRect bounds
Definition canvas.cpp:182
C height() const
Get the vertical extent of the rectangle.
C width() const
Get the horizontal extent of the rectangle.
Axis aligned, non-empty rectangle.
Definition rect.h:92
uint32_t toRGBA(double opacity=1.0) const
Return an sRGB conversion of the color in RGBA int32 format.
Definition color.cpp:117
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
std::list< SPItem * > getAllLayers()
MessageId flash(MessageType type, char const *message)
Temporarily pushes a message onto the stack.
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
bool isEmpty()
Returns true if no items are selected.
sigc::connection connectPagesChanged(const sigc::slot< void()> &slot)
const std::vector< SPPage * > & getPages() const
bool hasPages() const
Glib::ustring getString(Glib::ustring const &pref_path, Glib::ustring const &def="")
Retrieve an UTF-8 string.
static Preferences * get()
Access the singleton Preferences object.
void setString(Glib::ustring const &pref_path, Glib::ustring const &value)
Set an UTF-8 string value.
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
std::map< std::string, std::unique_ptr< BatchItem > > current_items
sigc::scoped_connection export_conn
void setBatchName(Glib::ustring const &name)
std::optional< Glib::RefPtr< Gio::File const > > export_path
Filesystem path to export folder.
Inkscape::Preferences * prefs
void loadExportHints(bool rename_file)
Glib::ustring getBatchName(bool fallback) const
Get the last used batch base name for the document:
sigc::scoped_connection cancel_conn
void onAreaTypeToggle(selection_mode key)
sigc::scoped_connection refresh_conn
std::map< selection_mode, Glib::ustring > selection_names
void setDesktop(SPDesktop *desktop)
Inkscape::UI::Widget::ColorPicker & _background_color
void setExporting(bool exporting, Glib::ustring const &text="", Glib::ustring const &test_batch="")
std::optional< Glib::RefPtr< Gio::File const > > getBatchPath() const
Get the currently selected batch path.
void queueRefresh(bool rename_file=false)
sigc::scoped_connection _pages_changed_connection
std::shared_ptr< PreviewDrawing > _preview_drawing
void setBatchPath(std::optional< Glib::RefPtr< Gio::File const > > path)
Set batch export folder.
void selectionModified(Inkscape::Selection *selection, guint flags)
static unsigned int onProgressCallback(float value, void *)
void selectionChanged(Inkscape::Selection *selection)
std::map< selection_mode, Gtk::ToggleButton * > selection_buttons
void setDocument(SPDocument *document)
BatchExport(BaseObjectType *cobject, const Glib::RefPtr< Gtk::Builder > &builder)
sigc::scoped_connection refresh_items_conn
std::optional< Glib::RefPtr< Gio::File const > > getPreviousBatchPath() const
Get the last used batch path for the document:
sigc::scoped_connection _object_modified_conn
void setIsolateItem(bool isolate)
Set "Export selected only".
static void syncItems(BatchItems &items, std::map< std::string, SPObject * > const &objects, Gtk::FlowBox &container, std::shared_ptr< PreviewDrawing > preview, bool isolate_items)
Add and remove batch items and their previews carefully and insert new ones into the container FlowBo...
void on_mode_changed(Gtk::SelectionMode mode)
A change in the selection mode for the flow box.
void setDrawing(std::shared_ptr< PreviewDrawing > drawing)
void on_parent_changed()
Update the connection to the parent FlowBox.
void refresh(bool hide, guint32 bg_color)
BatchItem(SPItem *item, bool isolate_item, std::shared_ptr< PreviewDrawing > drawing)
void set_selected(bool selected)
Syncronise the FlowBox selection to the active widget activity.
void update_selected()
Syncronise the FlowBox selection to the existing active widget state.
void init(std::shared_ptr< PreviewDrawing > drawing)
sigc::scoped_connection _selection_widget_changed_conn
Inkscape::Extension::Output * getExtension(int row)
void setBox(Geom::Rect const &bbox)
void setDrawing(std::shared_ptr< PreviewDrawing > drawing)
void setBackgroundColor(std::uint32_t bg_color)
void setItem(SPItem const *item, bool is_layer=false)
static bool exportRaster(Geom::Rect const &area, unsigned long int const &width, unsigned long int const &height, float const &dpi, guint32 bg_color, Glib::ustring const &filename, bool overwrite, unsigned(*callback)(float, void *), void *data, Inkscape::Extension::Output *extension, std::vector< SPItem const * > *items=nullptr)
Export to raster graphics.
Definition export.cpp:221
static bool exportVector(Inkscape::Extension::Output *extension, SPDocument *doc, Glib::ustring const &filename, bool overwrite, Geom::Rect const &area)
Definition export.cpp:318
static bool unConflictFilename(SPDocument *doc, std::string &filename, std::string const extension)
Definition export.cpp:169
Colors::Color get_current_color() const
sigc::connection connectChanged(sigc::slot< void(Colors::Color const &)> slot)
void setColor(Colors::Color const &)
Helperclass for Gtk::Entry widgets.
Definition entry.h:24
To do: update description of desktop.
Definition desktop.h:149
SPDocument * getDocument() const
Definition desktop.h:189
InkscapeWindow const * getInkscapeWindow() const
Definition desktop.cpp:975
Inkscape::MessageStack * messageStack() const
Definition desktop.h:160
SPNamedView * getNamedView() const
Definition desktop.h:191
Inkscape::Selection * getSelection() const
Definition desktop.h:188
Inkscape::LayerManager & layerManager()
Definition desktop.h:287
Typed SVG document implementation.
Definition document.h:103
SPRoot * getRoot()
Returns our SPRoot.
Definition document.h:202
std::unique_ptr< SPDocument > copy() const
Create a copy of the document, useful for modifying during save & export.
Definition document.cpp:508
char const * getDocumentFilename() const
Definition document.h:231
Inkscape::PageManager & getPageManager()
Definition document.h:164
SPNamedView * getNamedView()
Get the namedview for this document, creates it if it's not found.
Definition document.cpp:233
Base class for visual SVG elements.
Definition sp-item.h:109
Geom::OptRect documentVisualBounds() const
Get item's visual bbox in document coordinate system.
Definition sp-item.cpp:1018
Geom::OptRect visualBounds(Geom::Affine const &transform=Geom::identity(), bool wfilter=true, bool wclip=true, bool wmask=true) const
Get item's visual bounding box in this item's coordinate system.
Definition sp-item.cpp:925
SPObject is an abstract base class of all of the document nodes at the SVG document level.
Definition sp-object.h:160
char const * label() const
Gets the author-visible label property for the object or a default if no label is defined.
void setAttribute(Inkscape::Util::const_char_ptr key, Inkscape::Util::const_char_ptr value)
char const * getId() const
Returns the objects current ID string.
char const * defaultLabel() const
Returns a default label property for this object.
char const * getAttribute(char const *name) const
bool isAncestorOf(SPObject const *object) const
True if object is non-NULL and this is some in/direct parent of object.
sigc::connection connectModified(sigc::slot< void(SPObject *, unsigned int)> slot)
Connects to the modification notification signal.
Definition sp-object.h:705
std::string getDefaultLabel() const
Definition sp-page.cpp:640
Geom::Rect getDocumentRect() const
Get the rectangle of the page, scaled to the document.
Definition sp-page.cpp:199
Color picker button and window.
Css & result
Editable view implementation.
static char const *const parent
Definition dir-util.cpp:70
TODO: insert short description here.
Macro for icon names used in Inkscape.
SPItem * item
Inkscape - An SVG editor.
Glib::ustring label
Raw stack of active status messages.
Definition desktop.h:50
bool filesystem_is_sandboxed()
Query if the filesystem is "sandboxed", e.g., by using xdg-portal in flatpak/snap.
Definition sandbox.cpp:17
Glib::ustring filesystem_get_display_path(std::optional< Glib::RefPtr< Gio::File const > > path, Glib::ustring placeholder_if_empty)
Translate raw filesystem path to a path suitable for display.
Definition sandbox.cpp:27
void remove_file_extension(std::string &path)
Removes a file extension, if found, from the given path.
Definition sys.cpp:233
Dialog code.
Definition desktop.h:117
Inkscape::Colors::Color get_export_bg_color(SPObject *object, Inkscape::Colors::Color const &default_color)
Definition export.cpp:546
static void set_sensitive(Gtk::SearchEntry2 &entry, bool const sensitive)
void set_export_bg_color(SPObject *object, Inkscape::Colors::Color const &color)
Definition export.cpp:540
std::map< std::string, std::unique_ptr< BatchItem > > BatchItems
static constexpr int height
W & get_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id)
W & get_derived_widget(const Glib::RefPtr< Gtk::Builder > &builder, const char *id, Args &&... args)
int dialog_run(Gtk::Dialog &dialog)
This is a GTK4 porting aid meant to replace the removal of the Gtk::Dialog synchronous API.
@ ERROR_MESSAGE
Definition message.h:29
std::string optimizePath(std::string const &path, std::string const &base, unsigned int parents)
Convert an absolute path into a relative one if possible to do in the given number of parent steps.
static cairo_user_data_key_t key
int mode
Singleton class to access the preferences file in a convenient way.
Ocnode * child[8]
Definition quantize.cpp:33
GList * items
Inkscape::IO::Sandbox.
int num
Definition scribble.cpp:47
SPPage – a page object.
SPRoot: SVG <svg> implementation.
static const Point data[]
Gtk::Picture & preview
SPDesktop * desktop
double width
Glib::ustring name
Definition toolbars.cpp:55
Glib::RefPtr< Gtk::Builder > builder