File indexing completed on 2025-02-02 04:11:26
0001 /* 0002 * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best> 0003 * 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 */ 0006 0007 #include "trace_dialog.hpp" 0008 #include "ui_trace_dialog.h" 0009 0010 #include <vector> 0011 #include <algorithm> 0012 #include <unordered_map> 0013 0014 #include <QEvent> 0015 #include <QGraphicsScene> 0016 #include <QGraphicsRectItem> 0017 #include <QGraphicsPathItem> 0018 #include <QGraphicsPixmapItem> 0019 #include <QStandardItemModel> 0020 #include <QDesktopServices> 0021 #include <QScreen> 0022 0023 #include <QtColorWidgets/ColorDelegate> 0024 0025 #include "app/application.hpp" 0026 #include "app/settings/widget.hpp" 0027 #include "app_info.hpp" 0028 0029 #include "model/assets/bitmap.hpp" 0030 #include "model/shapes/group.hpp" 0031 #include "model/shapes/layer.hpp" 0032 #include "model/shapes/path.hpp" 0033 #include "model/shapes/fill.hpp" 0034 #include "model/shapes/stroke.hpp" 0035 #include "model/shapes/image.hpp" 0036 #include "model/shapes/rect.hpp" 0037 #include "utils/trace.hpp" 0038 #include "utils/quantize.hpp" 0039 #include "utils/trace_wrapper.hpp" 0040 #include "command/undo_macro_guard.hpp" 0041 #include "command/object_list_commands.hpp" 0042 #include "app/widgets/no_close_on_enter.hpp" 0043 #include "glaxnimate_app.hpp" 0044 0045 #include "color_quantization_dialog.hpp" 0046 0047 0048 class glaxnimate::gui::TraceDialog::Private 0049 { 0050 public: 0051 enum Mode 0052 { 0053 Alpha, 0054 Closest, 0055 Exact, 0056 Pixel 0057 }; 0058 0059 using TraceResult = glaxnimate::utils::trace::TraceWrapper::TraceResult; 0060 using Preset = glaxnimate::utils::trace::TraceWrapper::Preset; 0061 0062 utils::trace::TraceWrapper trace_wrapper; 0063 model::Group* created = nullptr; 0064 Ui::TraceDialog ui; 0065 QGraphicsScene scene; 0066 color_widgets::ColorDelegate delegate; 0067 qreal zoom = 1; 0068 QGraphicsRectItem *item_parent_shape; 0069 QGraphicsRectItem *item_parent_image; 0070 QGraphicsPixmapItem *item_image; 0071 app::widgets::NoCloseOnEnter ncoe; 0072 ColorQuantizationDialog color_options; 0073 app::settings::WidgetSettingGroup settings; 0074 QSize image_size; 0075 bool initialized = false; 0076 0077 explicit Private(model::Image* image) 0078 : trace_wrapper(image), image_size(trace_wrapper.size()) 0079 {} 0080 0081 std::vector<QRgb> colors() 0082 { 0083 int count = ui.list_colors->model()->rowCount(); 0084 std::vector<QRgb> colors; 0085 colors.reserve(count); 0086 for ( int i = 0; i < count; i++ ) 0087 { 0088 colors.push_back(ui.list_colors->item(i)->data(Qt::DisplayRole).value<QColor>().rgb()); 0089 } 0090 0091 return colors; 0092 } 0093 0094 std::vector<TraceResult> trace() 0095 { 0096 std::vector<TraceResult> result; 0097 0098 ui.progress_bar->show(); 0099 ui.progress_bar->setValue(0); 0100 0101 if ( !ui.button_advanced->isChecked() ) 0102 { 0103 std::vector<QRgb> colors; 0104 trace_wrapper.trace_preset(Preset(ui.list_presets->currentRow()), ui.spin_posterize->value(), colors, result); 0105 if ( !colors.empty() ) 0106 set_colors(colors); 0107 } 0108 else 0109 { 0110 0111 trace_wrapper.options().set_min_area(ui.spin_min_area->value()); 0112 trace_wrapper.options().set_smoothness(ui.spin_smoothness->value() / 100.0); 0113 0114 switch ( ui.combo_mode->currentIndex() ) 0115 { 0116 case Mode::Alpha: 0117 trace_wrapper.trace_mono( 0118 ui.color_mono->color(), 0119 ui.check_inverted->isChecked(), 0120 ui.spin_alpha_threshold->value(), 0121 result 0122 ); 0123 break; 0124 case Mode::Closest: 0125 trace_wrapper.trace_closest(colors(), result); 0126 break; 0127 case Mode::Exact: 0128 trace_wrapper.trace_exact( 0129 colors(), 0130 ui.spin_tolerance->value(), 0131 result 0132 ); 0133 break; 0134 case Mode::Pixel: 0135 trace_wrapper.trace_pixel(result); 0136 break; 0137 } 0138 } 0139 0140 std::reverse(result.begin(), result.end()); 0141 ui.progress_bar->hide(); 0142 return result; 0143 } 0144 0145 qreal outline() 0146 { 0147 if ( !ui.button_advanced->isChecked() ) 0148 return ui.list_presets->currentRow() != utils::trace::TraceWrapper::PixelPreset ? 1 : 0; 0149 if ( ui.combo_mode->currentIndex() == Mode::Closest || ui.combo_mode->currentIndex() == Mode::Exact ) 0150 return ui.spin_outline->value(); 0151 return 0; 0152 } 0153 0154 void add_color(const QColor& c = Qt::black) 0155 { 0156 auto item = new QListWidgetItem(); 0157 item->setData(Qt::EditRole, c); 0158 item->setData(Qt::DisplayRole, c); 0159 item->setFlags(item->flags() | Qt::ItemIsEditable); 0160 ui.list_colors->addItem(item); 0161 } 0162 0163 void fit_view() 0164 { 0165 ui.preview->fitInView(scene.sceneRect(), Qt::KeepAspectRatio); 0166 ui.preview->scale(zoom, zoom); 0167 rescale_preview_background(); 0168 } 0169 0170 void rescale_preview_background() 0171 { 0172 QBrush b = ui.preview->backgroundBrush(); 0173 b.setTransform(ui.preview->transform().inverted()); 0174 ui.preview->setBackgroundBrush(b); 0175 } 0176 0177 void init_scene() 0178 { 0179 item_parent_shape = scene.addRect(QRectF(0, 0, image_size.width(), image_size.height())); 0180 item_parent_shape->setFlag(QGraphicsItem::ItemClipsChildrenToShape); 0181 item_parent_shape->setBrush(Qt::NoBrush); 0182 item_parent_shape->setPen(Qt::NoPen); 0183 0184 item_parent_image = scene.addRect(QRectF(image_size.width(), 0, 0, image_size.height())); 0185 item_parent_image->setFlag(QGraphicsItem::ItemClipsChildrenToShape); 0186 item_parent_image->setBrush(Qt::NoBrush); 0187 item_parent_image->setPen(Qt::NoPen); 0188 0189 item_image = new QGraphicsPixmapItem(QPixmap::fromImage(trace_wrapper.image()), item_parent_image); 0190 } 0191 0192 void init_settings() 0193 { 0194 settings.add(ui.spin_tolerance, "internal", "trace_dialog_"); 0195 settings.add(ui.spin_outline, "internal", "trace_dialog_"); 0196 settings.add(ui.spin_smoothness, "internal", "trace_dialog_"); 0197 settings.add(ui.spin_alpha_threshold, "internal", "trace_dialog_"); 0198 settings.add(ui.spin_min_area, "internal", "trace_dialog_"); 0199 settings.add(ui.spin_posterize, "internal", "trace_dialog_"); 0200 settings.add(ui.button_advanced, "internal", "trace_dialog_"); 0201 settings.define(); 0202 color_options.init_settings(); 0203 } 0204 0205 void save_settings() 0206 { 0207 settings.save(); 0208 color_options.save_settings(); 0209 } 0210 0211 void reset_settings() 0212 { 0213 settings.reset(); 0214 color_options.reset_settings(); 0215 } 0216 0217 void init_colors() 0218 { 0219 if ( image_size.width() < 1024 && image_size.height() < 1024 ) 0220 { 0221 auto colors = trace_wrapper.eem_colors(); 0222 for ( QRgb rgb : colors ) 0223 add_color(QColor(rgb)); 0224 ui.spin_color_count->setValue(colors.size()); 0225 } 0226 else 0227 { 0228 ui.spin_color_count->setValue(4); 0229 auto_colors(); 0230 } 0231 0232 } 0233 0234 void set_colors(const std::vector<QRgb>& colors) 0235 { 0236 while ( ui.list_colors->model()->rowCount() ) 0237 ui.list_colors->model()->removeRow(0); 0238 0239 for ( QRgb rgb : colors ) 0240 add_color(QColor(rgb)); 0241 } 0242 0243 void auto_colors() 0244 { 0245 int n_colors = ui.spin_color_count->value(); 0246 if ( n_colors ) 0247 set_colors(color_options.quantize(trace_wrapper.image(), n_colors)); 0248 else 0249 set_colors({}); 0250 } 0251 0252 void auto_preset() 0253 { 0254 ui.list_presets->setCurrentRow(trace_wrapper.preset_suggestion()); 0255 } 0256 0257 #ifdef Q_OS_ANDROID 0258 void android_ui(TraceDialog* parent) 0259 { 0260 ui.button_quantize_options->hide(); 0261 ui.mode_container->hide(); 0262 ui.button_help->hide(); 0263 ui.button_defaults->hide(); 0264 ui.preview_slider->hide(); 0265 ui.spin_preview_zoom->hide(); 0266 ui.label_preview_zoom->hide(); 0267 ui.group_preview_layout->setMargin(0); 0268 ui.button_detect_colors->setText("Auto colors"); 0269 ui.gridLayout->addWidget(ui.button_detect_colors, 3, 0, 1, 2); 0270 ui.layout_buttons->insertWidget(0, ui.button_update_preview); 0271 0272 for ( auto w : parent->findChildren<QSpinBox*>() ) 0273 w->setMaximumHeight(40); 0274 for ( auto w : parent->findChildren<QDoubleSpinBox*>() ) 0275 w->setMaximumHeight(40); 0276 0277 for ( auto w : parent->findChildren<QLayout*>() ) 0278 w->setMargin(0); 0279 0280 zoom = 1.05; 0281 0282 connect( 0283 QGuiApplication::primaryScreen(), 0284 &QScreen::primaryOrientationChanged, 0285 parent, 0286 [this]{screen_rotation();} 0287 ); 0288 screen_rotation(); 0289 } 0290 0291 void screen_rotation() 0292 { 0293 auto scr = QApplication::primaryScreen(); 0294 if ( scr->size().height() > scr->size().width() ) 0295 { 0296 ui.layout_main->setDirection(QBoxLayout::TopToBottom); 0297 ui.stacked_preset_advanced->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0298 //ui.widget_controls->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0299 } 0300 else 0301 { 0302 ui.layout_main->setDirection(QBoxLayout::LeftToRight); 0303 ui.stacked_preset_advanced->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); 0304 //ui.widget_controls->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); 0305 } 0306 } 0307 #endif 0308 }; 0309 0310 glaxnimate::gui::TraceDialog::TraceDialog(model::Image* image, QWidget* parent) 0311 : QDialog(parent), d(std::make_unique<Private>(image)) 0312 { 0313 d->ui.setupUi(this); 0314 d->init_scene(); 0315 connect(&d->trace_wrapper, &utils::trace::TraceWrapper::progress_max_changed, d->ui.progress_bar, &QProgressBar::setMaximum); 0316 connect(&d->trace_wrapper, &utils::trace::TraceWrapper::progress_changed, d->ui.progress_bar, &QProgressBar::setValue); 0317 0318 d->ui.preview->setScene(&d->scene); 0319 d->ui.spin_min_area->setValue(qMax(d->trace_wrapper.options().min_area(), d->image_size.width() / 32)); 0320 d->ui.spin_smoothness->setValue(d->trace_wrapper.options().smoothness() * 100); 0321 d->ui.progress_bar->hide(); 0322 0323 d->delegate.setSizeHintForColor({24, 24}); 0324 d->ui.list_colors->setItemDelegate(&d->delegate); 0325 0326 d->ui.combo_mode->setCurrentIndex(Private::Closest); 0327 d->ui.button_defaults->setVisible(false); 0328 d->init_settings(); 0329 0330 d->ui.list_presets->item(utils::trace::TraceWrapper::ComplexPreset)->setIcon(QPixmap(GlaxnimateApp::instance()->data_file("images/trace/complex.jpg"))); 0331 d->ui.list_presets->item(utils::trace::TraceWrapper::FlatPreset)->setIcon(QPixmap(GlaxnimateApp::instance()->data_file("images/trace/flat.png"))); 0332 d->ui.list_presets->item(utils::trace::TraceWrapper::PixelPreset)->setIcon(QPixmap(GlaxnimateApp::instance()->data_file("images/trace/pixel.png"))); 0333 0334 if ( d->image_size.width() > 128 || d->image_size.height() > 128 ) 0335 { 0336 auto item = static_cast<QStandardItemModel*>(d->ui.combo_mode->model())->item(Private::Pixel); 0337 item->setFlags(item->flags() & ~Qt::ItemIsEnabled); 0338 0339 auto preset_item = d->ui.list_presets->item(utils::trace::TraceWrapper::PixelPreset); 0340 preset_item->setFlags(preset_item->flags() & ~Qt::ItemIsEnabled); 0341 } 0342 0343 if ( d->ui.button_advanced->isChecked() ) 0344 d->init_colors(); 0345 else 0346 d->auto_preset(); 0347 0348 d->initialized = true; 0349 update_preview(); 0350 0351 d->ui.preview->setBackgroundBrush(QPixmap(app::Application::instance()->data_file("images/widgets/background.png"))); 0352 0353 installEventFilter(&d->ncoe); 0354 0355 connect(this, &QDialog::accepted, this, [this]{ d->save_settings(); }); 0356 0357 #ifdef Q_OS_ANDROID 0358 d->android_ui(this); 0359 #endif 0360 0361 } 0362 0363 glaxnimate::gui::TraceDialog::~TraceDialog() = default; 0364 0365 void glaxnimate::gui::TraceDialog::changeEvent ( QEvent* e ) 0366 { 0367 QDialog::changeEvent(e); 0368 0369 if ( e->type() == QEvent::LanguageChange) 0370 { 0371 d->ui.retranslateUi(this); 0372 } 0373 } 0374 0375 void glaxnimate::gui::TraceDialog::update_preview() 0376 { 0377 if ( !d->initialized ) 0378 return; 0379 0380 for ( auto ch : d->item_parent_shape->childItems() ) 0381 delete ch; 0382 0383 for ( const auto& result : d->trace() ) 0384 { 0385 if ( !result.bezier.beziers().empty() ) 0386 { 0387 QPen pen = Qt::NoPen; 0388 qreal pen_width = d->outline(); 0389 if ( pen_width > 0 ) 0390 pen = QPen(result.color, pen_width); 0391 0392 auto path = new QGraphicsPathItem(result.bezier.painter_path(), d->item_parent_shape); 0393 path->setPen(pen); 0394 path->setBrush(result.color); 0395 } 0396 0397 if ( !result.rects.empty() ) 0398 { 0399 for ( const auto& rect : result.rects ) 0400 { 0401 auto item = new QGraphicsRectItem(rect, d->item_parent_shape); 0402 item->setPen(Qt::NoPen); 0403 item->setBrush(result.color); 0404 } 0405 } 0406 } 0407 0408 d->fit_view(); 0409 } 0410 0411 void glaxnimate::gui::TraceDialog::apply() 0412 { 0413 auto trace = d->trace(); 0414 d->created = d->trace_wrapper.apply(trace, d->outline()); 0415 accept(); 0416 } 0417 0418 glaxnimate::model::DocumentNode * glaxnimate::gui::TraceDialog::created() const 0419 { 0420 return d->created; 0421 } 0422 0423 void glaxnimate::gui::TraceDialog::change_mode(int mode) 0424 { 0425 if ( mode == Private::Alpha ) 0426 d->ui.stacked_widget->setCurrentIndex(0); 0427 else if ( mode == Private::Pixel ) 0428 d->ui.stacked_widget->setCurrentIndex(2); 0429 else 0430 d->ui.stacked_widget->setCurrentIndex(1); 0431 0432 d->ui.group_potrace->setEnabled(mode != Private::Pixel); 0433 d->ui.label_tolerance->setEnabled(mode == Private::Exact); 0434 d->ui.spin_tolerance->setEnabled(mode == Private::Exact); 0435 } 0436 0437 0438 void glaxnimate::gui::TraceDialog::add_color() 0439 { 0440 d->add_color(); 0441 d->ui.spin_color_count->setValue(d->ui.list_colors->model()->rowCount()); 0442 } 0443 0444 void glaxnimate::gui::TraceDialog::remove_color() 0445 { 0446 int curr = d->ui.list_colors->currentRow(); 0447 if ( curr == -1 ) 0448 { 0449 curr = d->ui.list_colors->model()->rowCount() - 1; 0450 if ( curr == -1 ) 0451 return; 0452 } 0453 d->ui.list_colors->model()->removeRow(curr); 0454 d->ui.spin_color_count->setValue(d->ui.list_colors->model()->rowCount()); 0455 } 0456 0457 void glaxnimate::gui::TraceDialog::auto_colors() 0458 { 0459 d->auto_colors(); 0460 update_preview(); 0461 } 0462 0463 void glaxnimate::gui::TraceDialog::resizeEvent(QResizeEvent* event) 0464 { 0465 QDialog::resizeEvent(event); 0466 d->fit_view(); 0467 } 0468 0469 void glaxnimate::gui::TraceDialog::zoom_preview(qreal percent) 0470 { 0471 qreal scale = percent / 100 / d->zoom; 0472 d->ui.preview->scale(scale, scale); 0473 d->zoom = percent / 100; 0474 d->rescale_preview_background(); 0475 } 0476 0477 void glaxnimate::gui::TraceDialog::show_help() 0478 { 0479 QUrl docs = AppInfo::instance().url_docs(); 0480 docs.setPath("/manual/ui/dialogs/"); 0481 docs.setFragment("trace-bitmap"); 0482 QDesktopServices::openUrl(docs); 0483 } 0484 0485 void glaxnimate::gui::TraceDialog::preview_slide(int percent) 0486 { 0487 qreal splitpoint = d->image_size.width() * percent / 100.0; 0488 d->item_parent_shape->setRect(0, 0, splitpoint, d->image_size.height()); 0489 d->item_parent_image->setRect(splitpoint, 0, d->image_size.width() - splitpoint, d->image_size.height()); 0490 } 0491 0492 void glaxnimate::gui::TraceDialog::reset_settings() 0493 { 0494 d->reset_settings(); 0495 d->ui.check_inverted->setChecked(false); 0496 d->ui.combo_mode->setCurrentIndex(Private::Closest); 0497 } 0498 0499 void glaxnimate::gui::TraceDialog::color_options() 0500 { 0501 d->color_options.exec(); 0502 } 0503 0504 void glaxnimate::gui::TraceDialog::showEvent(QShowEvent* event) 0505 { 0506 QDialog::showEvent(event); 0507 update_preview(); 0508 } 0509 0510 void glaxnimate::gui::TraceDialog::toggle_advanced(bool advanced) 0511 { 0512 d->ui.stacked_preset_advanced->setCurrentIndex(int(advanced)); 0513 d->ui.button_defaults->setVisible(advanced); 0514 if ( !advanced ) 0515 { 0516 d->auto_preset(); 0517 update_preview(); 0518 } 0519 else 0520 { 0521 d->ui.spin_min_area->setValue(16); 0522 d->ui.spin_smoothness->setValue(75); 0523 d->ui.spin_color_count->setValue(d->ui.list_colors->count()); 0524 switch ( d->ui.list_presets->currentRow() ) 0525 { 0526 case Private::Preset::ComplexPreset: 0527 case Private::Preset::FlatPreset: 0528 d->ui.combo_mode->setCurrentIndex(Private::Closest); 0529 break; 0530 case Private::Preset::PixelPreset: 0531 d->ui.combo_mode->setCurrentIndex(Private::Pixel); 0532 break; 0533 } 0534 } 0535 }