diff --git a/README.md b/README.md index 971211110..24779ec27 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Go to [this repository](https://github.com/ScanTailor-Advanced/scantailor-libs-b **Windows – supported OS versions (issue #101):** Release builds follow upstream **Qt** and **toolchain** support policies; **legacy Windows** (for example Windows 7) is **not** exercised in this project’s CI and is **best-effort only**. You may need an **older tagged release** or a **self-built** binary against an older Qt/MSVC stack. Community reports (including compatibility tips) are welcome in the issue tracker. -**Linux – Wayland (issue #97):** If you see rendering issues (blank or corrupted windows) when running under Wayland, try starting the application with `QT_QPA_PLATFORM=xcb` to use the X11 compatibility layer. +**Linux – Wayland (issue #97):** On Qt5 builds, if `XDG_SESSION_TYPE` is `wayland` and `QT_QPA_PLATFORM` is not set, the application defaults to the X11 (`xcb`) platform plugin to avoid broken dialogs and painting. Set **`SCANTAILOR_NO_XCB_FALLBACK=1`** in the environment to keep native Wayland and, if needed, set `QT_QPA_PLATFORM=wayland` or `QT_QPA_PLATFORM=xcb` yourself. **Linux – Flatpak / Flathub ([issue #105](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/105)):** diff --git a/src/app/MainWindow.cpp b/src/app/MainWindow.cpp index 070095b31..c17218894 100644 --- a/src/app/MainWindow.cpp +++ b/src/app/MainWindow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -301,6 +302,7 @@ MainWindow::MainWindow() }); connect(actionFixDpi, SIGNAL(triggered(bool)), SLOT(fixDpiDialogRequested())); + connect(actionReverseTwoPageOrder, SIGNAL(triggered(bool)), SLOT(toggleTwoPageSpreadReadingOrder())); connect(actionRelinking, SIGNAL(triggered(bool)), SLOT(showRelinkingDialog())); #ifdef ENABLE_DEBUG_FEATURES connect(actionDebug, SIGNAL(toggled(bool)), SLOT(debugToggled(bool))); @@ -1016,7 +1018,8 @@ void MainWindow::pageContextMenuRequested(const PageInfo& pageInfo_, const QPoin goToPage(pageInfo.id()); } - QMenu menu; + // Parent widget helps correct multi-monitor placement (issue #75). + QMenu menu(thumbView); auto& iconProvider = IconProvider::getInstance(); QAction* insBefore = menu.addAction(iconProvider.getIcon("insert-before"), tr("Insert before ...")); @@ -1036,12 +1039,24 @@ void MainWindow::pageContextMenuRequested(const PageInfo& pageInfo_, const QPoin } } // MainWindow::pageContextMenuRequested +void MainWindow::toggleTwoPageSpreadReadingOrder() { + if (!isProjectLoaded() || !m_pages) { + return; + } + const Qt::LayoutDirection nextDir + = (m_pages->layoutDirection() == Qt::LeftToRight) ? Qt::RightToLeft : Qt::LeftToRight; + m_pages->setLayoutDirection(nextDir); + m_outFileNameGen.setLayoutDirection(nextDir); + resetThumbSequence(currentPageOrderProvider(), ThumbnailSequence::KEEP_SELECTION); + invalidateAllThumbnails(); +} + void MainWindow::pastLastPageContextMenuRequested(const QPoint& screenPos) { if (!isProjectLoaded()) { return; } - QMenu menu; + QMenu menu(thumbView); menu.addAction(IconProvider::getInstance().getIcon("insert-here"), tr("Insert here ...")); if (menu.exec(screenPos)) { @@ -1579,6 +1594,7 @@ void MainWindow::updateProjectActions() { actionSaveProjectAs->setEnabled(loaded); actionFixDpi->setEnabled(loaded); actionRelinking->setEnabled(loaded); + actionReverseTwoPageOrder->setEnabled(loaded); } bool MainWindow::isBatchProcessingInProgress() const { diff --git a/src/app/MainWindow.h b/src/app/MainWindow.h index 12a68eedd..0bcd4a1ff 100644 --- a/src/app/MainWindow.h +++ b/src/app/MainWindow.h @@ -118,6 +118,8 @@ class MainWindow : public QMainWindow, private FilterUiInterface, private Ui::Ma void pastLastPageContextMenuRequested(const QPoint& screenPos); + void toggleTwoPageSpreadReadingOrder(); + void thumbViewFocusToggled(bool checked); void thumbViewScrolled(); diff --git a/src/app/MainWindow.ui b/src/app/MainWindow.ui index 0f541f36e..3f0d8e0db 100644 --- a/src/app/MainWindow.ui +++ b/src/app/MainWindow.ui @@ -75,6 +75,7 @@ + @@ -1214,6 +1215,20 @@ QToolButton:pressed { false + + + Reverse two-page spread order + + + Swap left/right page order for two-page scans (e.g. Japanese book reading order). + + + + + + false + + Switch filter to orientation diff --git a/src/app/main.cpp b/src/app/main.cpp index fe1f42724..e9f150121 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -18,6 +18,12 @@ int main(int argc, char* argv[]) { #if QT_VERSION_MAJOR == 5 + // Issue #97: Qt5 on Wayland can corrupt dialogs; use X11 unless opted out. + if (!qEnvironmentVariableIsSet("SCANTAILOR_NO_XCB_FALLBACK")) { + if (qgetenv("XDG_SESSION_TYPE") == "wayland" && qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) { + qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("xcb")); + } + } QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) diff --git a/src/core/OutputFileNameGenerator.h b/src/core/OutputFileNameGenerator.h index ea9adedd5..98c107171 100644 --- a/src/core/OutputFileNameGenerator.h +++ b/src/core/OutputFileNameGenerator.h @@ -24,6 +24,8 @@ class OutputFileNameGenerator { void performRelinking(const AbstractRelinker& relinker); + void setLayoutDirection(Qt::LayoutDirection dir) { m_layoutDirection = dir; } + Qt::LayoutDirection layoutDirection() const { return m_layoutDirection; } const QString& outDir() const { return m_outDir; } diff --git a/src/core/ProjectPages.cpp b/src/core/ProjectPages.cpp index 1737f3d5a..fb009ff72 100644 --- a/src/core/ProjectPages.cpp +++ b/src/core/ProjectPages.cpp @@ -72,6 +72,14 @@ Qt::LayoutDirection ProjectPages::layoutDirection() const { } } +void ProjectPages::setLayoutDirection(const Qt::LayoutDirection dir) { + { + QMutexLocker locker(&m_mutex); + initSubPagesInOrder(dir); + } + emit modified(); +} + void ProjectPages::initSubPagesInOrder(const Qt::LayoutDirection layoutDirection) { if (layoutDirection == Qt::LeftToRight) { m_subPagesInOrder[0] = PageId::LEFT_PAGE; diff --git a/src/core/ProjectPages.h b/src/core/ProjectPages.h index 37853f37d..bee8fef8d 100644 --- a/src/core/ProjectPages.h +++ b/src/core/ProjectPages.h @@ -48,6 +48,11 @@ class ProjectPages : public QObject { Qt::LayoutDirection layoutDirection() const; + /** + * \brief Swap logical order of left/right sub-pages for two-page layouts (issue #62). + */ + void setLayoutDirection(Qt::LayoutDirection dir); + PageSequence toPageSequence(PageView view) const; void listRelinkablePaths(const VirtualFunction& sink) const; diff --git a/src/core/filters/deskew/ApplyDialog.cpp b/src/core/filters/deskew/ApplyDialog.cpp index f5ce1f1d2..10457be0c 100644 --- a/src/core/filters/deskew/ApplyDialog.cpp +++ b/src/core/filters/deskew/ApplyDialog.cpp @@ -33,25 +33,28 @@ ApplyDialog::ApplyDialog(QWidget* parent, const PageId& curPage, const PageSelec ApplyDialog::~ApplyDialog() = default; void ApplyDialog::onSubmit() { + const bool applyDeskew = applyDeskewCheckBox->isChecked(); + const bool applyOblique = applyObliqueCheckBox->isChecked(); + std::set pages; // thisPageRB is intentionally not handled. if (allPagesRB->isChecked()) { m_pages.selectAll().swap(pages); - emit appliedToAllPages(pages); + emit appliedToAllPages(pages, applyDeskew, applyOblique); } else if (thisPageAndFollowersRB->isChecked()) { m_pages.selectPagePlusFollowers(m_curPage).swap(pages); - emit appliedTo(pages); + emit appliedTo(pages, applyDeskew, applyOblique); } else if (selectedPagesRB->isChecked()) { - emit appliedTo(m_selectedPages); + emit appliedTo(m_selectedPages, applyDeskew, applyOblique); } else if (everyOtherRB->isChecked()) { m_pages.selectEveryOther(m_curPage).swap(pages); - emit appliedTo(pages); + emit appliedTo(pages, applyDeskew, applyOblique); } else if (thisEveryOtherRB->isChecked()) { m_pages.selectThisPageAndFollowingEveryOther(m_curPage).swap(pages); - emit appliedTo(pages); + emit appliedTo(pages, applyDeskew, applyOblique); } else if (everyOtherSelectedRB->isChecked()) { m_pages.selectEveryOtherInSubsetFromPage(m_curPage, m_selectedPages).swap(pages); - emit appliedTo(pages); + emit appliedTo(pages, applyDeskew, applyOblique); } accept(); } // ApplyDialog::onSubmit diff --git a/src/core/filters/deskew/ApplyDialog.h b/src/core/filters/deskew/ApplyDialog.h index 5ea35dbd7..0c417706d 100644 --- a/src/core/filters/deskew/ApplyDialog.h +++ b/src/core/filters/deskew/ApplyDialog.h @@ -25,9 +25,9 @@ class ApplyDialog : public QDialog, private Ui::ApplyDialog { signals: - void appliedTo(const std::set& pages); + void appliedTo(const std::set& pages, bool applyDeskew, bool applyOblique); - void appliedToAllPages(const std::set& pages); + void appliedToAllPages(const std::set& pages, bool applyDeskew, bool applyOblique); private slots: diff --git a/src/core/filters/deskew/ApplyDialog.ui b/src/core/filters/deskew/ApplyDialog.ui index 7f4a89c48..918cf9e0a 100644 --- a/src/core/filters/deskew/ApplyDialog.ui +++ b/src/core/filters/deskew/ApplyDialog.ui @@ -139,6 +139,35 @@ + + + + Apply parameters + + + + + + Deskew angle and mode + + + true + + + + + + + Oblique angle and mode + + + true + + + + + + diff --git a/src/core/filters/deskew/CacheDrivenTask.cpp b/src/core/filters/deskew/CacheDrivenTask.cpp index fdfacb08d..0b4a95871 100644 --- a/src/core/filters/deskew/CacheDrivenTask.cpp +++ b/src/core/filters/deskew/CacheDrivenTask.cpp @@ -27,7 +27,9 @@ void CacheDrivenTask::process(const PageInfo& pageInfo, const ImageTransformation& xform) { const Dependencies deps(xform.preCropArea(), xform.preRotation()); std::unique_ptr params(m_settings->getPageParams(pageInfo.id())); - if (!params || (!deps.matches(params->dependencies()) && (params->mode() == MODE_AUTO))) { + if (!params + || (!deps.matches(params->dependencies()) + && ((params->mode() == MODE_AUTO) || (params->obliqueMode() == MODE_AUTO)))) { if (auto* thumbCol = dynamic_cast(collector)) { thumbCol->processThumbnail(std::unique_ptr(new IncompleteThumbnail( thumbCol->thumbnailCache(), thumbCol->maxLogicalThumbSize(), pageInfo.imageId(), xform))); diff --git a/src/core/filters/deskew/OptionsWidget.cpp b/src/core/filters/deskew/OptionsWidget.cpp index 30db6ad6e..a8efec53d 100644 --- a/src/core/filters/deskew/OptionsWidget.cpp +++ b/src/core/filters/deskew/OptionsWidget.cpp @@ -6,9 +6,29 @@ #include #include "ApplyDialog.h" +#include "Params.h" #include "Settings.h" namespace deskew { +namespace { + +Params mergeParamsForApply(const std::unique_ptr& existing, + const OptionsWidget::UiData& cur, + const bool applyDeskew, + const bool applyOblique) { + const Dependencies deps(cur.dependencies()); + if (!existing) { + return Params(applyDeskew ? cur.effectiveDeskewAngle() : 0.0, applyOblique ? cur.effectiveObliqueAngle() : 0.0, + deps, applyDeskew ? cur.mode() : MODE_AUTO, applyOblique ? cur.obliqueMode() : MODE_AUTO); + } + return Params(applyDeskew ? cur.effectiveDeskewAngle() : existing->deskewAngle(), + applyOblique ? cur.effectiveObliqueAngle() : existing->obliqueAngle(), deps, + applyDeskew ? cur.mode() : existing->mode(), + applyOblique ? cur.obliqueMode() : existing->obliqueMode()); +} + +} // namespace + const double OptionsWidget::MAX_ANGLE = 45.0; OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelectionAccessor& pageSelectionAccessor) @@ -31,20 +51,21 @@ void OptionsWidget::showDeskewDialog() { auto* dialog = new ApplyDialog(this, m_pageId, m_pageSelectionAccessor); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(tr("Apply Deskew")); - connect(dialog, SIGNAL(appliedTo(const std::set&)), this, SLOT(appliedTo(const std::set&))); - connect(dialog, SIGNAL(appliedToAllPages(const std::set&)), this, - SLOT(appliedToAllPages(const std::set&))); + connect(dialog, &ApplyDialog::appliedTo, this, &OptionsWidget::appliedTo); + connect(dialog, &ApplyDialog::appliedToAllPages, this, &OptionsWidget::appliedToAllPages); dialog->show(); } -void OptionsWidget::appliedTo(const std::set& pages) { - if (pages.empty()) { +void OptionsWidget::appliedTo(const std::set& pages, const bool applyDeskew, const bool applyOblique) { + if (pages.empty() || (!applyDeskew && !applyOblique)) { return; } - const Params params(m_uiData.effectiveDeskewAngle(), m_uiData.effectiveObliqueAngle(), m_uiData.dependencies(), - m_uiData.mode(), m_uiData.autoOblique()); - m_settings->setDegrees(pages, params); + for (const PageId& pageId : pages) { + std::unique_ptr existing(m_settings->getPageParams(pageId)); + const Params merged(mergeParamsForApply(existing, m_uiData, applyDeskew, applyOblique)); + m_settings->setPageParams(pageId, merged); + } if (pages.size() > 1) { emit invalidateAllThumbnails(); @@ -55,14 +76,16 @@ void OptionsWidget::appliedTo(const std::set& pages) { } } -void OptionsWidget::appliedToAllPages(const std::set& pages) { - if (pages.empty()) { +void OptionsWidget::appliedToAllPages(const std::set& pages, const bool applyDeskew, const bool applyOblique) { + if (pages.empty() || (!applyDeskew && !applyOblique)) { return; } - const Params params(m_uiData.effectiveDeskewAngle(), m_uiData.effectiveObliqueAngle(), m_uiData.dependencies(), - m_uiData.mode(), m_uiData.autoOblique()); - m_settings->setDegrees(pages, params); + for (const PageId& pageId : pages) { + std::unique_ptr existing(m_settings->getPageParams(pageId)); + const Params merged(mergeParamsForApply(existing, m_uiData, applyDeskew, applyOblique)); + m_settings->setPageParams(pageId, merged); + } emit invalidateAllThumbnails(); } @@ -80,9 +103,9 @@ void OptionsWidget::manualObliqueAngleSetExternally(const double degrees) { auto block = m_connectionManager.getScopedBlock(); m_uiData.setEffectiveObliqueAngle(degrees); - m_uiData.setAutoOblique(false); + m_uiData.setObliqueMode(MODE_MANUAL); + updateObliqueModeIndication(MODE_MANUAL); obliqueSpinBox->setValue(degrees); - autoObliqueCheckBox->setChecked(false); commitCurrentParams(); emit invalidateThumbnail(m_pageId); @@ -96,6 +119,8 @@ void OptionsWidget::preUpdateUI(const PageId& pageId) { autoBtn->setChecked(true); autoBtn->setEnabled(false); manualBtn->setEnabled(false); + obliqueAutoBtn->setEnabled(false); + obliqueManualBtn->setEnabled(false); } void OptionsWidget::postUpdateUI(const UiData& uiData) { @@ -104,10 +129,12 @@ void OptionsWidget::postUpdateUI(const UiData& uiData) { m_uiData = uiData; autoBtn->setEnabled(true); manualBtn->setEnabled(true); + obliqueAutoBtn->setEnabled(true); + obliqueManualBtn->setEnabled(true); updateModeIndication(uiData.mode()); + updateObliqueModeIndication(uiData.obliqueMode()); setSpinBoxKnownState(degreesToSpinBox(uiData.effectiveDeskewAngle())); obliqueSpinBox->setValue(m_uiData.effectiveObliqueAngle()); - autoObliqueCheckBox->setChecked(m_uiData.autoOblique()); } void OptionsWidget::spinBoxValueChanged(const double value) { @@ -126,9 +153,10 @@ void OptionsWidget::spinBoxValueChanged(const double value) { void OptionsWidget::modeChanged(const bool autoMode) { if (autoMode) { m_uiData.setMode(MODE_AUTO); - m_uiData.setEffectiveObliqueAngle(0.0); // Auto ==> Oblique = 0 (PR #108 feedback) + if (m_uiData.obliqueMode() == MODE_AUTO) { + m_uiData.setEffectiveObliqueAngle(0.0); + } m_settings->clearPageParams(m_pageId); - m_settings->setPendingAutoOblique(m_pageId, autoObliqueCheckBox->isChecked()); emit reloadRequested(); } else { m_uiData.setMode(MODE_MANUAL); @@ -136,6 +164,19 @@ void OptionsWidget::modeChanged(const bool autoMode) { } } +void OptionsWidget::obliqueModeChanged(const bool autoMode) { + if (autoMode) { + m_uiData.setObliqueMode(MODE_AUTO); + m_uiData.setEffectiveObliqueAngle(0.0); + obliqueSpinBox->setValue(0.0); + commitCurrentParams(); + emit reloadRequested(); + } else { + m_uiData.setObliqueMode(MODE_MANUAL); + commitCurrentParams(); + } +} + void OptionsWidget::updateModeIndication(const AutoManualMode mode) { auto block = m_connectionManager.getScopedBlock(); @@ -146,6 +187,16 @@ void OptionsWidget::updateModeIndication(const AutoManualMode mode) { } } +void OptionsWidget::updateObliqueModeIndication(const AutoManualMode mode) { + auto block = m_connectionManager.getScopedBlock(); + + if (mode == MODE_AUTO) { + obliqueAutoBtn->setChecked(true); + } else { + obliqueManualBtn->setChecked(true); + } +} + void OptionsWidget::setSpinBoxUnknownState() { auto block = m_connectionManager.getScopedBlock(); @@ -155,7 +206,8 @@ void OptionsWidget::setSpinBoxUnknownState() { angleSpinBox->setEnabled(false); obliqueSpinBox->setValue(0.0); obliqueSpinBox->setEnabled(false); - autoObliqueCheckBox->setEnabled(false); + obliqueAutoBtn->setEnabled(false); + obliqueManualBtn->setEnabled(false); } void OptionsWidget::setSpinBoxKnownState(const double angle) { @@ -168,12 +220,13 @@ void OptionsWidget::setSpinBoxKnownState(const double angle) { angleSpinBox->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); angleSpinBox->setEnabled(true); obliqueSpinBox->setEnabled(true); - autoObliqueCheckBox->setEnabled(true); + obliqueAutoBtn->setEnabled(true); + obliqueManualBtn->setEnabled(true); } void OptionsWidget::commitCurrentParams() { Params params(m_uiData.effectiveDeskewAngle(), m_uiData.effectiveObliqueAngle(), m_uiData.dependencies(), - m_uiData.mode(), m_uiData.autoOblique()); + m_uiData.mode(), m_uiData.obliqueMode()); m_settings->setPageParams(m_pageId, params); } @@ -203,35 +256,21 @@ void OptionsWidget::obliqueSpinBoxValueChanged(double value) { auto block = m_connectionManager.getScopedBlock(); m_uiData.setEffectiveObliqueAngle(value); - m_uiData.setAutoOblique(false); - autoObliqueCheckBox->setChecked(false); + if (value != 0.0) { + m_uiData.setObliqueMode(MODE_MANUAL); + updateObliqueModeIndication(MODE_MANUAL); + } commitCurrentParams(); emit manualObliqueAngleSet(value); emit invalidateThumbnail(m_pageId); } -void OptionsWidget::autoObliqueToggled(const bool checked) { - auto block = m_connectionManager.getScopedBlock(); - - m_uiData.setAutoOblique(checked); - if (m_settings->isParamsNull(m_pageId)) { - m_settings->setPendingAutoOblique(m_pageId, checked); - } else { - commitCurrentParams(); - } - if (autoBtn->isChecked()) { - emit reloadRequested(); - } else { - emit invalidateThumbnail(m_pageId); - } -} - void OptionsWidget::setupUiConnections() { CONNECT(angleSpinBox, SIGNAL(valueChanged(double)), this, SLOT(spinBoxValueChanged(double))); CONNECT(obliqueSpinBox, SIGNAL(valueChanged(double)), this, SLOT(obliqueSpinBoxValueChanged(double))); CONNECT(autoBtn, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + CONNECT(obliqueAutoBtn, SIGNAL(toggled(bool)), this, SLOT(obliqueModeChanged(bool))); CONNECT(topEdgeCheckBox, SIGNAL(toggled(bool)), this, SLOT(topEdgeToggled(bool))); - CONNECT(autoObliqueCheckBox, SIGNAL(toggled(bool)), this, SLOT(autoObliqueToggled(bool))); CONNECT(applyDeskewBtn, SIGNAL(clicked()), this, SLOT(showDeskewDialog())); } @@ -240,7 +279,7 @@ void OptionsWidget::setupUiConnections() { /*========================== OptionsWidget::UiData =========================*/ OptionsWidget::UiData::UiData() - : m_effDeskewAngle(0.0), m_effObliqueAngle(0.0), m_mode(MODE_AUTO), m_autoOblique(false) {} + : m_effDeskewAngle(0.0), m_effObliqueAngle(0.0), m_mode(MODE_AUTO), m_obliqueMode(MODE_AUTO) {} OptionsWidget::UiData::~UiData() = default; } // namespace deskew \ No newline at end of file diff --git a/src/core/filters/deskew/OptionsWidget.h b/src/core/filters/deskew/OptionsWidget.h index aa62d9cec..d2fc613a4 100644 --- a/src/core/filters/deskew/OptionsWidget.h +++ b/src/core/filters/deskew/OptionsWidget.h @@ -46,16 +46,16 @@ class OptionsWidget : public FilterOptionsWidget, private Ui::OptionsWidget { AutoManualMode mode() const; - void setAutoOblique(bool enabled); + void setObliqueMode(AutoManualMode mode); - bool autoOblique() const; + AutoManualMode obliqueMode() const; private: double m_effDeskewAngle; double m_effObliqueAngle; Dependencies m_deps; AutoManualMode m_mode; - bool m_autoOblique; + AutoManualMode m_obliqueMode; }; @@ -88,19 +88,21 @@ class OptionsWidget : public FilterOptionsWidget, private Ui::OptionsWidget { void modeChanged(bool autoMode); - void topEdgeToggled(bool checked); + void obliqueModeChanged(bool autoMode); - void autoObliqueToggled(bool checked); + void topEdgeToggled(bool checked); void showDeskewDialog(); - void appliedTo(const std::set& pages); + void appliedTo(const std::set& pages, bool applyDeskew, bool applyOblique); - void appliedToAllPages(const std::set& pages); + void appliedToAllPages(const std::set& pages, bool applyDeskew, bool applyOblique); private: void updateModeIndication(AutoManualMode mode); + void updateObliqueModeIndication(AutoManualMode mode); + void setSpinBoxUnknownState(); void setSpinBoxKnownState(double angle); @@ -157,12 +159,12 @@ inline AutoManualMode OptionsWidget::UiData::mode() const { return m_mode; } -inline void OptionsWidget::UiData::setAutoOblique(const bool enabled) { - m_autoOblique = enabled; +inline void OptionsWidget::UiData::setObliqueMode(const AutoManualMode mode) { + m_obliqueMode = mode; } -inline bool OptionsWidget::UiData::autoOblique() const { - return m_autoOblique; +inline AutoManualMode OptionsWidget::UiData::obliqueMode() const { + return m_obliqueMode; } } // namespace deskew diff --git a/src/core/filters/deskew/OptionsWidget.ui b/src/core/filters/deskew/OptionsWidget.ui index a1daa68c3..3ee6b713a 100644 --- a/src/core/filters/deskew/OptionsWidget.ui +++ b/src/core/filters/deskew/OptionsWidget.ui @@ -146,6 +146,52 @@ + + + + + + Oblique mode + + + + + + + Auto + + + Automatically estimate oblique (shear) correction. + + + true + + + true + + + true + + + + + + + Manual + + + Keep the oblique angle set in the spin box. + + + true + + + true + + + + + diff --git a/src/core/filters/deskew/Params.cpp b/src/core/filters/deskew/Params.cpp index 8deae466b..1b8686de8 100644 --- a/src/core/filters/deskew/Params.cpp +++ b/src/core/filters/deskew/Params.cpp @@ -11,20 +11,40 @@ using namespace foundation; namespace deskew { -Params::Params(const double deskewAngleDeg, const Dependencies& deps, const AutoManualMode mode) - : m_rotation{deskewAngleDeg, mode}, m_oblique{0.0, true}, m_deps(deps) {} +namespace { + +AutoManualMode obliqueModeFromXml(const QDomElement& deskewEl) { + if (deskewEl.hasAttribute("oblique-mode")) { + return deskewEl.attribute("oblique-mode") == "manual" ? MODE_MANUAL : MODE_AUTO; + } + if (deskewEl.hasAttribute("autoOblique")) { + return deskewEl.attribute("autoOblique", "1") != "0" ? MODE_AUTO : MODE_MANUAL; + } + return deskewEl.attribute("mode") == "manual" ? MODE_MANUAL : MODE_AUTO; +} + +} // namespace + +Params::Params(const double deskewAngleDeg, const Dependencies& deps, const AutoManualMode deskewMode) + : m_rotation{deskewAngleDeg, deskewMode}, m_oblique{0.0, MODE_AUTO}, m_deps(deps) {} + +Params::Params(const double deskewAngleDeg, + const double obliqueDeg, + const Dependencies& deps, + const AutoManualMode deskewMode, + const AutoManualMode obliqueMode) + : m_rotation{deskewAngleDeg, deskewMode}, m_oblique{obliqueDeg, obliqueMode}, m_deps(deps) {} Params::Params(const double deskewAngleDeg, const double obliqueDeg, const Dependencies& deps, - const AutoManualMode mode, - const bool autoOblique) - : m_rotation{deskewAngleDeg, mode}, m_oblique{obliqueDeg, autoOblique}, m_deps(deps) {} + const AutoManualMode mode) + : m_rotation{deskewAngleDeg, mode}, m_oblique{obliqueDeg, mode}, m_deps(deps) {} Params::Params(const QDomElement& deskewEl) : m_rotation{deskewEl.attribute("angle").toDouble(), deskewEl.attribute("mode") == "manual" ? MODE_MANUAL : MODE_AUTO}, - m_oblique{deskewEl.attribute("oblique").toDouble(), deskewEl.attribute("autoOblique", "1") != "0"}, + m_oblique{deskewEl.attribute("oblique").toDouble(), obliqueModeFromXml(deskewEl)}, m_deps(deskewEl.namedItem("dependencies").toElement()) {} Params::~Params() = default; @@ -34,7 +54,7 @@ QDomElement Params::toXml(QDomDocument& doc, const QString& name) const { el.setAttribute("mode", m_rotation.mode == MODE_AUTO ? "auto" : "manual"); el.setAttribute("angle", Utils::doubleToString(m_rotation.angle)); el.setAttribute("oblique", Utils::doubleToString(m_oblique.obliqueAngle)); - el.setAttribute("autoOblique", m_oblique.autoOblique ? "1" : "0"); + el.setAttribute("oblique-mode", m_oblique.mode == MODE_AUTO ? "auto" : "manual"); el.appendChild(m_deps.toXml(doc, "dependencies")); return el; } diff --git a/src/core/filters/deskew/Params.h b/src/core/filters/deskew/Params.h index 6c8dcd9ca..31f8e249f 100644 --- a/src/core/filters/deskew/Params.h +++ b/src/core/filters/deskew/Params.h @@ -25,19 +25,26 @@ struct RotationParams { AutoManualMode mode = MODE_AUTO; }; -/** Oblique (shear) angle in degrees and whether to run automatic oblique detection. */ +/** Oblique (shear) angle in degrees and its own auto/manual mode (issue #117). */ struct ObliqueParams { double obliqueAngle = 0.0; - bool autoOblique = true; + AutoManualMode mode = MODE_AUTO; }; class Params { public: // Member-wise copying is OK. - Params(double deskewAngleDeg, const Dependencies& deps, AutoManualMode mode); + Params(double deskewAngleDeg, const Dependencies& deps, AutoManualMode deskewMode); + + Params(double deskewAngleDeg, + double obliqueDeg, + const Dependencies& deps, + AutoManualMode deskewMode, + AutoManualMode obliqueMode); - Params(double deskewAngleDeg, double obliqueDeg, const Dependencies& deps, AutoManualMode mode, bool autoOblique); + /** Legacy: oblique mode matches \p mode (coupled deskew/oblique). */ + Params(double deskewAngleDeg, double obliqueDeg, const Dependencies& deps, AutoManualMode mode); explicit Params(const QDomElement& deskewEl); @@ -47,12 +54,15 @@ class Params { double obliqueAngle() const; - bool autoOblique() const; - const Dependencies& dependencies() const; AutoManualMode mode() const; + AutoManualMode obliqueMode() const; + + /** Legacy (#114): true when oblique runs in automatic mode. */ + bool autoOblique() const; + QDomElement toXml(QDomDocument& doc, const QString& name) const; private: @@ -70,10 +80,6 @@ inline double Params::obliqueAngle() const { return m_oblique.obliqueAngle; } -inline bool Params::autoOblique() const { - return m_oblique.autoOblique; -} - inline const Dependencies& Params::dependencies() const { return m_deps; } @@ -81,6 +87,14 @@ inline const Dependencies& Params::dependencies() const { inline AutoManualMode Params::mode() const { return m_rotation.mode; } + +inline AutoManualMode Params::obliqueMode() const { + return m_oblique.mode; +} + +inline bool Params::autoOblique() const { + return m_oblique.mode == MODE_AUTO; +} } // namespace deskew #endif // ifndef SCANTAILOR_DESKEW_PARAMS_H_ diff --git a/src/core/filters/deskew/Task.cpp b/src/core/filters/deskew/Task.cpp index 65253954a..73c767881 100644 --- a/src/core/filters/deskew/Task.cpp +++ b/src/core/filters/deskew/Task.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,12 +23,8 @@ #include #include -#include #include -#include -#include - #include "DebugImagesImpl.h" #include "Dpm.h" #include "Filter.h" @@ -107,11 +105,11 @@ FilterResultPtr Task::process(const TaskStatus& status, FilterData data) { } else { uiData.setEffectiveDeskewAngle(params->deskewAngle()); uiData.setEffectiveObliqueAngle(params->obliqueAngle()); - uiData.setAutoOblique(params->autoOblique()); uiData.setMode(params->mode()); + uiData.setObliqueMode(params->obliqueMode()); Params newParams(uiData.effectiveDeskewAngle(), uiData.effectiveObliqueAngle(), deps, uiData.mode(), - uiData.autoOblique()); + uiData.obliqueMode()); m_settings->setPageParams(m_pageId, newParams); } } @@ -164,7 +162,7 @@ FilterResultPtr Task::process(const TaskStatus& status, FilterData data) { uiData.setEffectiveDeskewAngle(0); } uiData.setMode(MODE_AUTO); - uiData.setAutoOblique(autoObliqueEnabled); + uiData.setObliqueMode(autoObliqueEnabled ? MODE_AUTO : MODE_MANUAL); if (autoObliqueEnabled) { // Find oblique on the deskewed (horizontal) mask for better accuracy (PR #110 feedback). @@ -202,7 +200,7 @@ FilterResultPtr Task::process(const TaskStatus& status, FilterData data) { } Params newParams(uiData.effectiveDeskewAngle(), uiData.effectiveObliqueAngle(), deps, uiData.mode(), - uiData.autoOblique()); + uiData.obliqueMode()); m_settings->setPageParams(m_pageId, newParams); status.throwIfCancelled(); diff --git a/src/core/filters/deskew/Utils.cpp b/src/core/filters/deskew/Utils.cpp index e616ed469..30f092f91 100644 --- a/src/core/filters/deskew/Utils.cpp +++ b/src/core/filters/deskew/Utils.cpp @@ -14,6 +14,5 @@ Params Utils::buildDefaultParams() { const DefaultParams& defaultParams = DefaultParamsProvider::getInstance().getParams(); const DefaultParams::DeskewParams& deskewParams = defaultParams.getDeskewParams(); - return Params(deskewParams.getDeskewAngleDeg(), 0.0, Dependencies(), deskewParams.getMode(), - deskewParams.isAutoOblique()); + return Params(deskewParams.getDeskewAngleDeg(), 0.0, Dependencies(), deskewParams.getMode(), MODE_AUTO); } diff --git a/src/core/filters/select_content/Task.cpp b/src/core/filters/select_content/Task.cpp index 15fcf5e5a..4e4c451cd 100644 --- a/src/core/filters/select_content/Task.cpp +++ b/src/core/filters/select_content/Task.cpp @@ -93,6 +93,8 @@ FilterResultPtr Task::process(const TaskStatus& status, const FilterData& data) QRectF contentRect(newParams.contentRect()); if (needUpdatePageBox) { + const QRectF oldPageRect(pageRect); + if (newParams.pageDetectionMode() == MODE_AUTO) { pageRect = PageFinder::findPageBox(status, data, newParams.isFineTuningEnabled(), m_settings->pageDetectionBox(), @@ -105,8 +107,15 @@ FilterResultPtr Task::process(const TaskStatus& status, const FilterData& data) pageRect = data.xform().resultingRect(); } - // When offcut/page outline changes, preserve manual content box by clipping to new page rect (issue #90). - if (contentRect.isValid() && (contentRect.intersected(pageRect) != contentRect)) { + // When offcut/page outline changes, keep manual content aligned with page motion (issue #90). + if (newParams.contentDetectionMode() == MODE_MANUAL && contentRect.isValid() && oldPageRect.isValid() + && pageRect.isValid()) { + contentRect.translate(pageRect.center() - oldPageRect.center()); + contentRect &= pageRect; + if (!contentRect.isValid()) { + needUpdateContentBox = true; + } + } else if (contentRect.isValid() && (contentRect.intersected(pageRect) != contentRect)) { if (newParams.contentDetectionMode() == MODE_MANUAL) { contentRect = contentRect.intersected(pageRect); } else { @@ -122,6 +131,12 @@ FilterResultPtr Task::process(const TaskStatus& status, const FilterData& data) contentRect = ContentBoxFinder::findContentBox(status, data, pageRect, m_dbg.get()); } else if (newParams.contentDetectionMode() == MODE_DISABLED) { contentRect = pageRect; + } else if (newParams.contentDetectionMode() == MODE_MANUAL) { + if (contentRect.isValid() && pageRect.isValid()) { + contentRect &= pageRect; + } else { + contentRect = QRectF(); + } } if (contentRect.isValid()) { diff --git a/src/core/tests/TestDeskewParams.cpp b/src/core/tests/TestDeskewParams.cpp index 041481ed0..6d5432cea 100644 --- a/src/core/tests/TestDeskewParams.cpp +++ b/src/core/tests/TestDeskewParams.cpp @@ -21,7 +21,7 @@ BOOST_AUTO_TEST_CASE(params_oblique_roundtrip_xml) { const double deskewDeg = 1.5; const double obliqueDeg = 2.25; const Dependencies deps; - const Params original(deskewDeg, obliqueDeg, deps, MODE_MANUAL, true); + const Params original(deskewDeg, obliqueDeg, deps, MODE_MANUAL, MODE_MANUAL); QDomDocument doc; const QDomElement el = original.toXml(doc, "deskew-params"); @@ -32,12 +32,13 @@ BOOST_AUTO_TEST_CASE(params_oblique_roundtrip_xml) { BOOST_CHECK_CLOSE(restored.deskewAngle(), deskewDeg, 1e-6); BOOST_CHECK_CLOSE(restored.obliqueAngle(), obliqueDeg, 1e-6); BOOST_CHECK(restored.mode() == MODE_MANUAL); + BOOST_CHECK(restored.obliqueMode() == MODE_MANUAL); } BOOST_AUTO_TEST_CASE(params_zero_oblique_roundtrip_xml) { const double deskewDeg = -0.5; const Dependencies deps; - const Params original(deskewDeg, 0.0, deps, MODE_AUTO, true); + const Params original(deskewDeg, 0.0, deps, MODE_AUTO, MODE_AUTO); QDomDocument doc; const QDomElement el = original.toXml(doc, "deskew-params"); @@ -48,11 +49,26 @@ BOOST_AUTO_TEST_CASE(params_zero_oblique_roundtrip_xml) { BOOST_CHECK_CLOSE(restored.deskewAngle(), deskewDeg, 1e-6); BOOST_CHECK_CLOSE(restored.obliqueAngle(), 0.0, 1e-6); BOOST_CHECK(restored.mode() == MODE_AUTO); + BOOST_CHECK(restored.obliqueMode() == MODE_AUTO); +} + +BOOST_AUTO_TEST_CASE(params_independent_oblique_mode_xml) { + const Dependencies deps; + const Params original(1.0, 2.0, deps, MODE_MANUAL, MODE_AUTO); + + QDomDocument doc; + const QDomElement el = original.toXml(doc, "deskew-params"); + doc.appendChild(el); + + const Params restored(doc.documentElement()); + + BOOST_CHECK(restored.mode() == MODE_MANUAL); + BOOST_CHECK(restored.obliqueMode() == MODE_AUTO); } BOOST_AUTO_TEST_CASE(params_auto_oblique_false_roundtrip_xml) { const Dependencies deps; - const Params original(1.0, 0.25, deps, MODE_AUTO, false); + const Params original(1.0, 0.25, deps, MODE_AUTO, MODE_MANUAL); QDomDocument doc; const QDomElement el = original.toXml(doc, "deskew-params"); @@ -60,6 +76,7 @@ BOOST_AUTO_TEST_CASE(params_auto_oblique_false_roundtrip_xml) { const Params restored(doc.documentElement()); + BOOST_CHECK(restored.obliqueMode() == MODE_MANUAL); BOOST_CHECK(!restored.autoOblique()); BOOST_CHECK_CLOSE(restored.obliqueAngle(), 0.25, 1e-6); }