File indexing completed on 2025-01-19 03:51:13

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2004-11-28
0007  * Description : a digiKam image editor tool to process image
0008  *               free rotation.
0009  *
0010  * SPDX-FileCopyrightText: 2004-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2009-2010 by Andi Clemens <andi dot clemens at gmail dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "freerotationtool.h"
0018 
0019 // C++ includes
0020 
0021 #include <cmath>
0022 
0023 // Qt includes
0024 
0025 #include <QGridLayout>
0026 #include <QImage>
0027 #include <QLabel>
0028 #include <QPainter>
0029 #include <QPushButton>
0030 #include <QIcon>
0031 #include <QStyle>
0032 #include <QApplication>
0033 
0034 // KDE includes
0035 
0036 #include <klocalizedstring.h>
0037 #include <ksharedconfig.h>
0038 #include <kconfiggroup.h>
0039 
0040 // Local includes
0041 
0042 #include "dimg.h"
0043 #include "dexpanderbox.h"
0044 #include "editortoolsettings.h"
0045 #include "freerotationfilter.h"
0046 #include "freerotationsettings.h"
0047 #include "imageiface.h"
0048 #include "imageguidewidget.h"
0049 
0050 namespace DigikamEditorFreeRotationToolPlugin
0051 {
0052 
0053 class Q_DECL_HIDDEN FreeRotationTool::Private
0054 {
0055 public:
0056 
0057     explicit Private()
0058       : configGroupName     (QLatin1String("freerotation Tool")),
0059         newHeightLabel      (nullptr),
0060         newWidthLabel       (nullptr),
0061         autoAdjustBtn       (nullptr),
0062         autoAdjustPoint1Btn (nullptr),
0063         autoAdjustPoint2Btn (nullptr),
0064         settingsView        (nullptr),
0065         expanderBox         (nullptr),
0066         gboxSettings        (nullptr),
0067         previewWidget       (nullptr)
0068     {
0069     }
0070 
0071     const QString         configGroupName;
0072 
0073     QLabel*               newHeightLabel;
0074     QLabel*               newWidthLabel;
0075 
0076     QPoint                autoAdjustPoint1;
0077     QPoint                autoAdjustPoint2;
0078 
0079     QPushButton*          autoAdjustBtn;
0080     QPushButton*          autoAdjustPoint1Btn;
0081     QPushButton*          autoAdjustPoint2Btn;
0082 
0083     FreeRotationSettings* settingsView;
0084 
0085     DExpanderBox*         expanderBox;
0086     EditorToolSettings*   gboxSettings;
0087     ImageGuideWidget*     previewWidget;
0088 };
0089 
0090 FreeRotationTool::FreeRotationTool(QObject* const parent)
0091     : EditorToolThreaded(parent),
0092       d                 (new Private)
0093 {
0094     setObjectName(QLatin1String("freerotation"));
0095 
0096     d->previewWidget = new ImageGuideWidget(nullptr, true, ImageGuideWidget::HVGuideMode);
0097     d->previewWidget->setWhatsThis(i18n("This is the free rotation operation preview. "
0098                                         "If you move the mouse cursor on this preview, "
0099                                         "a vertical and horizontal dashed line will be drawn "
0100                                         "to guide you in adjusting the free rotation correction. "
0101                                         "Release the left mouse button to freeze the dashed "
0102                                         "line's position."));
0103 
0104     setToolView(d->previewWidget);
0105     setPreviewModeMask(PreviewToolBar::UnSplitPreviewModes);
0106 
0107     // -------------------------------------------------------------
0108 
0109     QString temp;
0110     ImageIface iface;
0111 
0112     d->gboxSettings      = new EditorToolSettings(nullptr);
0113     d->gboxSettings->setTools(EditorToolSettings::ColorGuide);
0114 
0115     QLabel* const label1 = new QLabel(i18n("New width:"));
0116     d->newWidthLabel     = new QLabel(temp.setNum(iface.originalSize().width()) + i18n(" px"));
0117     d->newWidthLabel->setAlignment(Qt::AlignBottom | Qt::AlignRight);
0118 
0119     QLabel* const label2 = new QLabel(i18n("New height:"));
0120     d->newHeightLabel    = new QLabel(temp.setNum(iface.originalSize().height()) + i18n(" px"));
0121     d->newHeightLabel->setAlignment(Qt::AlignBottom | Qt::AlignRight);
0122 
0123     // -------------------------------------------------------------
0124 
0125     QString btnWhatsThis = i18n("Select a point in the preview widget, "
0126                                 "then click this button to assign the point for auto-correction.");
0127 
0128     QColor textColor     = qApp->palette().color(QPalette::Active, QPalette::Text);
0129 
0130     QPixmap pm1 = generateBtnPixmap(QLatin1String("1"), textColor);
0131     d->autoAdjustPoint1Btn = new QPushButton;
0132     d->autoAdjustPoint1Btn->setIcon(pm1);
0133     d->autoAdjustPoint1Btn->setSizePolicy(QSizePolicy::MinimumExpanding,
0134                                           QSizePolicy::MinimumExpanding);
0135 
0136     QPixmap pm2 = generateBtnPixmap(QLatin1String("2"), textColor);
0137     d->autoAdjustPoint2Btn = new QPushButton;
0138     d->autoAdjustPoint2Btn->setIcon(pm2);
0139     d->autoAdjustPoint2Btn->setSizePolicy(QSizePolicy::MinimumExpanding,
0140                                           QSizePolicy::MinimumExpanding);
0141 
0142     d->autoAdjustPoint1Btn->setToolTip(btnWhatsThis);
0143     d->autoAdjustPoint1Btn->setWhatsThis(btnWhatsThis);
0144     d->autoAdjustPoint2Btn->setToolTip(btnWhatsThis);
0145     d->autoAdjustPoint2Btn->setWhatsThis(btnWhatsThis);
0146 
0147     // --------------------------------------------------------
0148 
0149     // try to determine the maximum text width, to set the button minwidth
0150     QPoint p;
0151     setPointInvalid(p);
0152     QString invalidText = generateButtonLabel(p);
0153     p.setX(1);
0154     p.setY(2);
0155     QString validText   = generateButtonLabel(p);
0156 
0157     QFont fnt = d->autoAdjustPoint1Btn->font();
0158     QFontMetrics fm(fnt);
0159 
0160     const int offset = (pm1.width() * 2) + 10;
0161     int minWidth1    = fm.horizontalAdvance(invalidText) + offset;
0162     int minWidth2    = fm.horizontalAdvance(validText) + offset;
0163     int minWidth     = qMax<int>(minWidth1, minWidth2);
0164 
0165     // set new minwidth
0166     d->autoAdjustPoint1Btn->setMinimumWidth(minWidth);
0167     d->autoAdjustPoint2Btn->setMinimumWidth(minWidth);
0168 
0169     // --------------------------------------------------------
0170 
0171     d->autoAdjustPoint1Btn->setText(invalidText);
0172     d->autoAdjustPoint2Btn->setText(invalidText);
0173 
0174     d->autoAdjustBtn = new QPushButton(i18nc("Automatic Adjustment", "Adjust"));
0175     d->autoAdjustBtn->setSizePolicy(QSizePolicy::MinimumExpanding,
0176                                     QSizePolicy::Expanding);
0177 
0178     // --------------------------------------------------------
0179 
0180     QWidget* const autoAdjustContainer  = new QWidget;
0181     QGridLayout* const containerLayout2 = new QGridLayout;
0182     QLabel* const autoDescr             = new QLabel;
0183     autoDescr->setText(i18n("<p>Correct the rotation of your images automatically by assigning two points in the "
0184                             "preview widget and clicking <i>Adjust</i>.<br/>"
0185                             "You can either adjust horizontal or vertical lines.</p>"));
0186     autoDescr->setAlignment(Qt::AlignJustify);
0187     autoDescr->setWordWrap(true);
0188 
0189     const int cmargin = qMin(QApplication::style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0190                              qMin(QApplication::style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0191                                   qMin(QApplication::style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0192                                        QApplication::style()->pixelMetric(QStyle::PM_LayoutBottomMargin))));
0193 
0194     containerLayout2->addWidget(autoDescr,              0, 0, 1, -1);
0195     containerLayout2->addWidget(d->autoAdjustPoint1Btn, 1, 0, 1,  1);
0196     containerLayout2->addWidget(d->autoAdjustBtn,       1, 2, 2,  1);
0197     containerLayout2->addWidget(d->autoAdjustPoint2Btn, 2, 0, 1,  1);
0198     containerLayout2->setColumnStretch(1, 10);
0199     containerLayout2->setContentsMargins(cmargin, cmargin, cmargin, cmargin);
0200     autoAdjustContainer->setLayout(containerLayout2);
0201 
0202     // -------------------------------------------------------------
0203 
0204     DLineWidget* const line  = new DLineWidget(Qt::Horizontal);
0205     d->settingsView          = new FreeRotationSettings(d->gboxSettings->plainPage());
0206     d->expanderBox           = new DExpanderBox;
0207     d->expanderBox->setObjectName(QLatin1String("FreeRotationTool Expander"));
0208     d->expanderBox->addItem(autoAdjustContainer, QIcon::fromTheme(QLatin1String("transform-rotate")), i18n("Automatic Adjustment"),
0209                             QLatin1String("AutoAdjustContainer"), true);
0210     d->expanderBox->addItem(d->settingsView, QIcon::fromTheme(QLatin1String("transform-rotate")), i18n("Settings"),
0211                             QLatin1String("SettingsContainer"), true);
0212     d->expanderBox->addStretch();
0213 
0214     // -------------------------------------------------------------
0215 
0216     const int spacing = d->gboxSettings->spacingHint();
0217 
0218     QGridLayout* const grid2 = new QGridLayout;
0219     grid2->addWidget(label1,            0, 0, 1,  1);
0220     grid2->addWidget(d->newWidthLabel,  0, 1, 1,  1);
0221     grid2->addWidget(label2,            1, 0, 1,  1);
0222     grid2->addWidget(d->newHeightLabel, 1, 1, 1,  1);
0223     grid2->addWidget(line,              2, 0, 1, -1);
0224     grid2->addWidget(d->expanderBox,    3, 0, 1, -1);
0225     grid2->setRowStretch(3, 10);
0226     grid2->setContentsMargins(spacing, spacing, spacing, spacing);
0227     grid2->setSpacing(spacing);
0228     d->gboxSettings->plainPage()->setLayout(grid2);
0229 
0230     setToolSettings(d->gboxSettings);
0231 
0232     // -------------------------------------------------------------
0233 
0234     connect(d->settingsView, SIGNAL(signalSettingsChanged()),
0235             this, SLOT(slotTimer()));
0236 
0237     connect(d->gboxSettings, SIGNAL(signalColorGuideChanged()),
0238             this, SLOT(slotColorGuideChanged()));
0239 
0240     connect(d->autoAdjustPoint1Btn, SIGNAL(clicked()),
0241             this, SLOT(slotAutoAdjustP1Clicked()));
0242 
0243     connect(d->autoAdjustPoint2Btn, SIGNAL(clicked()),
0244             this, SLOT(slotAutoAdjustP2Clicked()));
0245 
0246     connect(d->autoAdjustBtn, SIGNAL(clicked()),
0247             this, SLOT(slotAutoAdjustClicked()));
0248 }
0249 
0250 FreeRotationTool::~FreeRotationTool()
0251 {
0252     delete d;
0253 }
0254 
0255 void FreeRotationTool::slotColorGuideChanged()
0256 {
0257     d->previewWidget->slotChangeGuideColor(d->gboxSettings->guideColor());
0258     d->previewWidget->slotChangeGuideSize(d->gboxSettings->guideSize());
0259 }
0260 
0261 void FreeRotationTool::readSettings()
0262 {
0263     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0264     KConfigGroup group        = config->group(d->configGroupName);
0265 
0266     d->settingsView->readSettings(group);
0267 
0268     d->expanderBox->readSettings(group);
0269 
0270     resetPoints();
0271     slotColorGuideChanged();
0272 }
0273 
0274 void FreeRotationTool::writeSettings()
0275 {
0276     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0277     KConfigGroup group        = config->group(d->configGroupName);
0278     d->settingsView->writeSettings(group);
0279 
0280     d->expanderBox->writeSettings(group);
0281 
0282     group.sync();
0283 }
0284 
0285 void FreeRotationTool::slotResetSettings()
0286 {
0287     d->settingsView->resetToDefault();
0288     resetPoints();
0289     slotPreview();
0290 }
0291 
0292 void FreeRotationTool::preparePreview()
0293 {
0294     FreeRotationContainer settings = d->settingsView->settings();
0295     ImageIface* const iface        = d->previewWidget->imageIface();
0296     DImg preview                   = iface->preview();
0297     settings.orgW                  = iface->originalSize().width();
0298     settings.orgH                  = iface->originalSize().height();
0299     setFilter(new FreeRotationFilter(&preview, this, settings));
0300 }
0301 
0302 void FreeRotationTool::prepareFinal()
0303 {
0304     ImageIface iface;
0305     FreeRotationContainer settings = d->settingsView->settings();
0306     DImg* const orgImage           = iface.original();
0307     settings.orgW                  = iface.originalSize().width();
0308     settings.orgH                  = iface.originalSize().height();
0309 
0310     setFilter(new FreeRotationFilter(orgImage, this, settings));
0311 }
0312 
0313 void FreeRotationTool::setPreviewImage()
0314 {
0315     ImageIface* const iface = d->previewWidget->imageIface();
0316     int w                   = iface->previewSize().width();
0317     int h                   = iface->previewSize().height();
0318     DImg imTemp             = filter()->getTargetImage().smoothScale(w, h, Qt::KeepAspectRatio);
0319     DImg imDest(w, h, filter()->getTargetImage().sixteenBit(), filter()->getTargetImage().hasAlpha());
0320 
0321     QColor background       = toolView()->backgroundRole();
0322     imDest.fill(DColor(background, filter()->getTargetImage().sixteenBit()));
0323     imDest.bitBltImage(&imTemp, (w-imTemp.width())/2, (h-imTemp.height())/2);
0324 
0325     iface->setPreview(imDest.smoothScale(iface->previewSize()));
0326     d->previewWidget->updatePreview();
0327 
0328     QString temp;
0329 
0330     FreeRotationFilter* const tool = dynamic_cast<FreeRotationFilter*>(filter());
0331 
0332     if (tool)
0333     {
0334         QSize newSize = tool->getNewSize();
0335         int new_w     = (newSize.width()  == -1) ? iface->originalSize().width()  : newSize.width();
0336         int new_h     = (newSize.height() == -1) ? iface->originalSize().height() : newSize.height();
0337         d->newWidthLabel->setText(temp.setNum(new_w)  + i18n(" px"));
0338         d->newHeightLabel->setText(temp.setNum(new_h) + i18n(" px"));
0339     }
0340 }
0341 
0342 void FreeRotationTool::setFinalImage()
0343 {
0344     ImageIface iface;
0345     DImg targetImage = filter()->getTargetImage();
0346     iface.setOriginal(i18n("Free Rotation"), filter()->filterAction(), targetImage);
0347 }
0348 
0349 QString FreeRotationTool::generateButtonLabel(const QPoint& p) const
0350 {
0351     QString clickToSet     = i18n("Click to set");
0352     QString isOk           = i18nc("point has been set and is valid", "Okay");
0353     bool clickToSetIsWider = clickToSet.count() >= isOk.count();
0354     QString widestString   = clickToSetIsWider ? clickToSet : isOk;
0355     int maxLength          = widestString.count();
0356     QString label          = clickToSetIsWider ? clickToSet : centerString(clickToSet, maxLength);
0357 
0358     if (pointIsValid(p))
0359     {
0360         label = clickToSetIsWider ? centerString(isOk, maxLength) : isOk;
0361     }
0362 
0363     return label;
0364 }
0365 
0366 QString FreeRotationTool::centerString(const QString& str, int maxLength) const
0367 {
0368     QString tmp = str;
0369     int max     = (maxLength == -1) ? tmp.count() : maxLength;
0370 
0371     // fill with additional whitespace, to match the original label length and center
0372     // the text, without moving the button icon
0373 
0374     int diff = qAbs<int>(max - str.count());
0375 
0376     if (diff > 0)
0377     {
0378         QString delimiter = QLatin1String(" ");
0379         int times = (diff / 2);
0380 
0381         tmp.prepend(delimiter.repeated(times));
0382         tmp.append(delimiter.repeated(times));
0383 
0384         diff = qAbs<int>(maxLength - tmp.count());
0385 
0386         if (diff != 0)
0387         {
0388             // too long?
0389             if      (tmp.count() > maxLength)
0390             {
0391                 tmp.chop(diff);
0392             }
0393             // too short?
0394             else if (tmp.count() < maxLength)
0395             {
0396                 tmp.append(delimiter.repeated(diff));
0397             }
0398         }
0399     }
0400 
0401     return tmp;
0402 }
0403 
0404 void FreeRotationTool::updatePoints()
0405 {
0406     // set labels
0407 
0408     QString tmp = generateButtonLabel(d->autoAdjustPoint1);
0409     d->autoAdjustPoint1Btn->setText(tmp);
0410 
0411     tmp = generateButtonLabel(d->autoAdjustPoint2);
0412     d->autoAdjustPoint2Btn->setText(tmp);
0413 
0414     // set points in preview widget, don't add invalid points
0415 
0416     QPolygon points;
0417 
0418     if (pointIsValid(d->autoAdjustPoint1))
0419     {
0420         points << d->autoAdjustPoint1;
0421         d->autoAdjustPoint2Btn->setEnabled(true);
0422     }
0423     else
0424     {
0425         d->autoAdjustPoint2Btn->setEnabled(false);
0426     }
0427 
0428     if (pointIsValid(d->autoAdjustPoint2))
0429     {
0430         points << d->autoAdjustPoint2;
0431     }
0432 
0433     d->previewWidget->setPoints(points, true);
0434 
0435     // enable / disable adjustment buttons
0436 
0437     bool valid  = (pointIsValid(d->autoAdjustPoint1)  &&
0438                    pointIsValid(d->autoAdjustPoint2)) &&
0439                   (d->autoAdjustPoint1 != d->autoAdjustPoint2);
0440     d->autoAdjustBtn->setEnabled(valid);
0441 }
0442 
0443 void FreeRotationTool::resetPoints()
0444 {
0445     setPointInvalid(d->autoAdjustPoint1);
0446     setPointInvalid(d->autoAdjustPoint2);
0447     d->previewWidget->resetPoints();
0448     updatePoints();
0449 }
0450 
0451 void FreeRotationTool::slotAutoAdjustP1Clicked()
0452 {
0453     d->autoAdjustPoint1 = d->previewWidget->getSpotPosition();
0454     updatePoints();
0455 }
0456 
0457 void FreeRotationTool::slotAutoAdjustP2Clicked()
0458 {
0459     d->autoAdjustPoint2 = d->previewWidget->getSpotPosition();
0460     updatePoints();
0461 }
0462 
0463 void FreeRotationTool::slotAutoAdjustClicked()
0464 {
0465     // we need to check manually here if the button is enabled,
0466     // because this slot can be called with an action now
0467 
0468     if (!d->autoAdjustBtn->isEnabled())
0469     {
0470         return;
0471     }
0472 
0473     double angle = calculateAutoAngle();
0474 
0475     if (fabs(angle) > 45.0)
0476     {
0477         if (angle < 0.0)
0478         {
0479             angle += 90.0;
0480         }
0481         else
0482         {
0483             angle -= 90.0;
0484         }
0485     }
0486 
0487     // we need to add the calculated angle to the currently set angle
0488 
0489     angle                  = d->settingsView->settings().angle + angle;
0490 
0491     // convert the angle to a string so we can easily split it up
0492 
0493     QString angleStr       = QString::number(angle, 'f', 2);
0494     QStringList anglesList = angleStr.split(QLatin1Char('.'));
0495 
0496     // try to set the angle widgets with the extracted values
0497 
0498     if (anglesList.count() == 2)
0499     {
0500         bool ok       = false;
0501         int mainAngle = anglesList.at(0).toInt(&ok);
0502 
0503         if (!ok)
0504         {
0505             mainAngle = 0;
0506         }
0507 
0508         double fineAngle = QString(QLatin1String("0.") + anglesList.at(1)).toDouble(&ok);
0509         fineAngle        = (angle < 0.0) ? -fineAngle : fineAngle;
0510 
0511         if (!ok)
0512         {
0513             fineAngle = 0.0;
0514         }
0515 
0516         FreeRotationContainer prm = d->settingsView->settings();
0517         prm.angle                 = mainAngle + fineAngle;
0518         d->settingsView->setSettings(prm);
0519         slotPreview();
0520     }
0521 
0522     resetPoints();
0523 }
0524 
0525 QPixmap FreeRotationTool::generateBtnPixmap(const QString& label, const QColor& color) const
0526 {
0527     QPixmap pm(22, 22);
0528     pm.fill(Qt::transparent);
0529 
0530     QPainter p(&pm);
0531     p.setRenderHint(QPainter::Antialiasing);
0532     p.setPen(color);
0533 
0534     p.drawEllipse(1, 1, 20, 20);
0535     p.drawText(pm.rect(), label, Qt::AlignHCenter | Qt::AlignVCenter);
0536 
0537     p.end();
0538 
0539     return pm;
0540 }
0541 
0542 double FreeRotationTool::calculateAutoAngle() const
0543 {
0544     // check if all points are valid
0545 
0546     if (!pointIsValid(d->autoAdjustPoint1) && !pointIsValid(d->autoAdjustPoint2))
0547     {
0548         return 0.0;
0549     }
0550 
0551     return FreeRotationFilter::calculateAngle(d->autoAdjustPoint1, d->autoAdjustPoint2);
0552 }
0553 
0554 void FreeRotationTool::setPointInvalid(QPoint& p)
0555 {
0556     p.setX(-1);
0557     p.setY(-1);
0558 }
0559 
0560 bool FreeRotationTool::pointIsValid(const QPoint& p) const
0561 {
0562     bool valid = true;
0563 
0564     if ((p.x() == -1) || (p.y() == -1))
0565     {
0566         valid = false;
0567     }
0568 
0569     return valid;
0570 }
0571 
0572 QString FreeRotationTool::repeatString(const QString& str, int times) const
0573 {
0574     QString tmp;
0575 
0576     for (int i = 0 ; i < times ; ++i)
0577     {
0578         tmp.append(str);
0579     }
0580 
0581     return tmp;
0582 }
0583 
0584 } // namespace DigikamEditorFreeRotationToolPlugin
0585 
0586 #include "moc_freerotationtool.cpp"