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 }