From d710384aa56d07dbe3c482c9a63ab6a301696139 Mon Sep 17 00:00:00 2001 From: Pablo Ventura Date: Sun, 24 May 2026 17:28:47 -0300 Subject: [PATCH 1/2] fix(deskew): wire auto-oblique default and align handle layout (#145) Connect Automatic oblique correction to DefaultParams and pending auto-oblique on deskew reload. Default oblique mode to Manual in UI and DefaultParams XML when the attribute is absent. Align deskew ImageView grid, arcs, and handles to the same content area rect (respecting visible scroll bars) so oblique levers stay positioned in windowed mode. Document AppImage glibc requirement (Ubuntu 24.04+) in README. --- README.md | 2 +- src/core/DefaultParams.cpp | 2 +- src/core/filters/deskew/ImageView.cpp | 69 ++++++++++++----------- src/core/filters/deskew/ImageView.h | 3 + src/core/filters/deskew/OptionsWidget.cpp | 32 ++++++++++- src/core/filters/deskew/OptionsWidget.h | 2 + src/core/filters/deskew/OptionsWidget.ui | 2 +- src/core/filters/deskew/Task.cpp | 2 +- src/core/tests/TestDeskewParams.cpp | 6 ++ 9 files changed, 82 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 24779ec27..bd0fd774e 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Go to [this repository](https://github.com/ScanTailor-Advanced/scantailor-libs-b **Application ID:** the manifest uses `org.scantailor.Advanced` so it does **not** replace the legacy `com.github._4lex4.*` Flatpak. Author docs: [for app authors](https://docs.flathub.org/docs/for-app-authors/). -**Linux – GitHub Releases (.deb / AppImage, [issue #64](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/64)):** Version tags matching `v*` run [`.github/workflows/release.yml`](.github/workflows/release.yml), which produces a `.deb` ([`build-deb.sh`](build-deb.sh)) and an AppImage attached to the GitHub Release when the workflow is enabled. Report problems with those binaries in [issue #64](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/64). +**Linux – GitHub Releases (.deb / AppImage, [issue #64](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/64)):** Version tags matching `v*` run [`.github/workflows/release.yml`](.github/workflows/release.yml), which produces a `.deb` ([`build-deb.sh`](build-deb.sh)) and an AppImage attached to the GitHub Release when the workflow is enabled. The AppImage is built on **Ubuntu 24.04** (`ubuntu-latest`) and requires a compatible **glibc** (typically **Ubuntu 24.04+** or equivalent). On **Ubuntu 22.04** and similar older bases, use the **`.deb`** package or build from source. Report problems with those binaries in [issue #64](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/64). **Community examples / test data:** See also [scantailor-testing](https://github.com/ImageProcessing-ElectronicPublications/scantailor-testing) (community repository; issue [#43](https://github.com/ScanTailor-Advanced/scantailor-advanced/issues/43)). diff --git a/src/core/DefaultParams.cpp b/src/core/DefaultParams.cpp index 02884bbce..c29b80482 100644 --- a/src/core/DefaultParams.cpp +++ b/src/core/DefaultParams.cpp @@ -71,7 +71,7 @@ DefaultParams::DeskewParams::DeskewParams() : m_deskewAngleDeg(0.0), m_mode(MODE DefaultParams::DeskewParams::DeskewParams(const QDomElement& el) : m_deskewAngleDeg(el.attribute("deskewAngleDeg").toDouble()), m_mode((el.attribute("mode") == "manual") ? MODE_MANUAL : MODE_AUTO), - m_autoOblique(el.attribute("autoOblique", "1") != "0") {} + m_autoOblique(el.attribute("autoOblique", "0") != "0") {} QDomElement DefaultParams::DeskewParams::toXml(QDomDocument& doc, const QString& name) const { QDomElement el(doc.createElement(name)); diff --git a/src/core/filters/deskew/ImageView.cpp b/src/core/filters/deskew/ImageView.cpp index 5664101ad..da73a7da2 100644 --- a/src/core/filters/deskew/ImageView.cpp +++ b/src/core/filters/deskew/ImageView.cpp @@ -100,8 +100,7 @@ void ImageView::onPaint(QPainter& painter, const InteractionState& interaction) painter.setWorldMatrixEnabled(false); painter.setRenderHints(QPainter::Antialiasing, false); - const double w = maxViewportRect().width(); - const double h = maxViewportRect().height(); + const QRectF contentArea(getContentAreaRect()); const QPointF center(getImageRotationOrigin()); // Draw the semi-transparent grid. @@ -110,17 +109,17 @@ void ImageView::onPaint(QPainter& painter, const InteractionState& interaction) pen.setWidth(1); painter.setPen(pen); QVector lines; - for (double y = center.y(); (y -= m_cellSize) > 0.0;) { - lines.push_back(QLineF(0.5, y, w - 0.5, y)); + for (double y = center.y(); (y -= m_cellSize) > contentArea.top();) { + lines.push_back(QLineF(contentArea.left() + 0.5, y, contentArea.right() - 0.5, y)); } - for (double y = center.y(); (y += m_cellSize) < h;) { - lines.push_back(QLineF(0.5, y, w - 0.5, y)); + for (double y = center.y(); (y += m_cellSize) < contentArea.bottom();) { + lines.push_back(QLineF(contentArea.left() + 0.5, y, contentArea.right() - 0.5, y)); } - for (double x = center.x(); (x -= m_cellSize) > 0.0;) { - lines.push_back(QLineF(x, 0.5, x, h - 0.5)); + for (double x = center.x(); (x -= m_cellSize) > contentArea.left();) { + lines.push_back(QLineF(x, contentArea.top() + 0.5, x, contentArea.bottom() - 0.5)); } - for (double x = center.x(); (x += m_cellSize) < w;) { - lines.push_back(QLineF(x, 0.5, x, h - 0.5)); + for (double x = center.x(); (x += m_cellSize) < contentArea.right();) { + lines.push_back(QLineF(x, contentArea.top() + 0.5, x, contentArea.bottom() - 0.5)); } painter.drawLines(lines); @@ -128,8 +127,8 @@ void ImageView::onPaint(QPainter& painter, const InteractionState& interaction) pen.setColor(QColor(0, 0, 0xd1)); painter.setPen(pen); painter.setBrush(Qt::NoBrush); - painter.drawLine(QPointF(0.5, center.y()), QPointF(w - 0.5, center.y())); - painter.drawLine(QPointF(center.x(), 0.5), QPointF(center.x(), h - 0.5)); + painter.drawLine(QPointF(contentArea.left() + 0.5, center.y()), QPointF(contentArea.right() - 0.5, center.y())); + painter.drawLine(QPointF(center.x(), contentArea.top() + 0.5), QPointF(center.x(), contentArea.bottom() - 0.5)); // Draw the rotation arcs. // Those will look like this ( ) const QRectF arcSquare(getRotationArcSquare()); @@ -247,27 +246,39 @@ void ImageView::dragFinished() { } /** - * Get the point at the center of the widget, in widget coordinates. - * The point may be adjusted to to ensure it's at the center of a pixel. + * Get the point at the center of the widget content area, in widget coordinates. */ QPointF ImageView::getImageRotationOrigin() const { - const QRectF viewportRect(maxViewportRect()); - return QPointF(std::floor(0.5 * viewportRect.width()) + 0.5, std::floor(0.5 * viewportRect.height()) + 0.5); + const QRectF contentArea(getContentAreaRect()); + return QPointF(std::floor(0.5 * contentArea.width()) + 0.5 + contentArea.x(), + std::floor(0.5 * contentArea.height()) + 0.5 + contentArea.y()); } -/** - * Get the square in widget coordinates where two rotation arcs will be drawn. - */ -QRectF ImageView::getRotationArcSquare() const { +QRectF ImageView::getContentAreaRect() const { const double hMargin = 0.5 * m_handlePixmap.width() - + verticalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, verticalScrollBar()); + + (verticalScrollBar()->isVisible() + ? verticalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, verticalScrollBar()) + : 0.0); const double vMargin = 0.5 * m_handlePixmap.height() - + horizontalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, horizontalScrollBar()); + + (horizontalScrollBar()->isVisible() + ? horizontalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, horizontalScrollBar()) + : 0.0); + + QRectF content(maxViewportRect()); + content.adjust(hMargin, vMargin, -hMargin, -vMargin); + if (content.isEmpty()) { + return maxViewportRect(); + } + return content; +} - QRectF reducedScreenRect(maxViewportRect()); - reducedScreenRect.adjust(hMargin, vMargin, -hMargin, -vMargin); +/** + * Get the square in widget coordinates where two rotation arcs will be drawn. + */ +QRectF ImageView::getRotationArcSquare() const { + const QRectF reducedScreenRect(getContentAreaRect()); QSizeF arcSize(1.0, m_maxRotationSin); arcSize.scale(reducedScreenRect.size(), Qt::KeepAspectRatio); @@ -291,15 +302,7 @@ std::pair ImageView::getRotationHandles(const QRectF& arcSquar } QRectF ImageView::getObliqueArcSquare() const { - const double hMargin - = 0.5 * m_handlePixmap.width() - + verticalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, verticalScrollBar()); - const double vMargin - = 0.5 * m_handlePixmap.height() - + horizontalScrollBar()->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, horizontalScrollBar()); - - QRectF reducedScreenRect(maxViewportRect()); - reducedScreenRect.adjust(hMargin, vMargin, -hMargin, -vMargin); + const QRectF reducedScreenRect(getContentAreaRect()); const double obliqueSin = std::sin(m_maxObliqueDeg * constants::DEG2RAD); QSizeF arcSize(obliqueSin, 1.0); diff --git a/src/core/filters/deskew/ImageView.h b/src/core/filters/deskew/ImageView.h index d32f12f30..b1c9761c9 100644 --- a/src/core/filters/deskew/ImageView.h +++ b/src/core/filters/deskew/ImageView.h @@ -61,6 +61,9 @@ class ImageView : public ImageViewBase, private InteractionHandler { QPointF getImageRotationOrigin() const; + /** Viewport area inset for scroll bars and handle pixmap margins. */ + QRectF getContentAreaRect() const; + QRectF getRotationArcSquare() const; std::pair getRotationHandles(const QRectF& arcSquare) const; diff --git a/src/core/filters/deskew/OptionsWidget.cpp b/src/core/filters/deskew/OptionsWidget.cpp index a8efec53d..07f8aa1be 100644 --- a/src/core/filters/deskew/OptionsWidget.cpp +++ b/src/core/filters/deskew/OptionsWidget.cpp @@ -5,6 +5,9 @@ #include +#include +#include + #include "ApplyDialog.h" #include "Params.h" #include "Settings.h" @@ -27,6 +30,16 @@ Params mergeParamsForApply(const std::unique_ptr& existing, applyOblique ? cur.obliqueMode() : existing->obliqueMode()); } +void setDefaultAutoOblique(const bool enabled) { + DefaultParamsProvider& provider = DefaultParamsProvider::getInstance(); + const DefaultParams& current = provider.getParams(); + auto updated = std::make_unique(current); + DefaultParams::DeskewParams deskewParams(current.getDeskewParams()); + deskewParams.setAutoOblique(enabled); + updated->setDeskewParams(deskewParams); + provider.setParams(std::move(updated), provider.getProfileName()); +} + } // namespace const double OptionsWidget::MAX_ANGLE = 45.0; @@ -41,6 +54,10 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec angleSpinBox->adjustSize(); setSpinBoxUnknownState(); topEdgeCheckBox->setChecked(!m_settings->algoContentBased()); + autoObliqueCheckBox->setChecked( + DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); + obliqueManualBtn->setChecked(true); + obliqueAutoBtn->setChecked(false); setupUiConnections(); } @@ -135,6 +152,8 @@ void OptionsWidget::postUpdateUI(const UiData& uiData) { updateObliqueModeIndication(uiData.obliqueMode()); setSpinBoxKnownState(degreesToSpinBox(uiData.effectiveDeskewAngle())); obliqueSpinBox->setValue(m_uiData.effectiveObliqueAngle()); + autoObliqueCheckBox->setChecked( + DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); } void OptionsWidget::spinBoxValueChanged(const double value) { @@ -156,6 +175,7 @@ void OptionsWidget::modeChanged(const bool autoMode) { if (m_uiData.obliqueMode() == MODE_AUTO) { m_uiData.setEffectiveObliqueAngle(0.0); } + m_settings->setPendingAutoOblique(m_pageId, autoObliqueCheckBox->isChecked()); m_settings->clearPageParams(m_pageId); emit reloadRequested(); } else { @@ -252,6 +272,15 @@ void OptionsWidget::topEdgeToggled(bool checked) { } } +void OptionsWidget::autoObliqueCheckBoxToggled(const bool checked) { + setDefaultAutoOblique(checked); + m_settings->setPendingAutoOblique(m_pageId, checked); + if (autoBtn->isChecked()) { + m_settings->clearPageParams(m_pageId); + emit reloadRequested(); + } +} + void OptionsWidget::obliqueSpinBoxValueChanged(double value) { auto block = m_connectionManager.getScopedBlock(); @@ -271,6 +300,7 @@ void OptionsWidget::setupUiConnections() { 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(autoObliqueCheckBoxToggled(bool))); CONNECT(applyDeskewBtn, SIGNAL(clicked()), this, SLOT(showDeskewDialog())); } @@ -279,7 +309,7 @@ void OptionsWidget::setupUiConnections() { /*========================== OptionsWidget::UiData =========================*/ OptionsWidget::UiData::UiData() - : m_effDeskewAngle(0.0), m_effObliqueAngle(0.0), m_mode(MODE_AUTO), m_obliqueMode(MODE_AUTO) {} + : m_effDeskewAngle(0.0), m_effObliqueAngle(0.0), m_mode(MODE_AUTO), m_obliqueMode(MODE_MANUAL) {} 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 d2fc613a4..405306abf 100644 --- a/src/core/filters/deskew/OptionsWidget.h +++ b/src/core/filters/deskew/OptionsWidget.h @@ -92,6 +92,8 @@ class OptionsWidget : public FilterOptionsWidget, private Ui::OptionsWidget { void topEdgeToggled(bool checked); + void autoObliqueCheckBoxToggled(bool checked); + void showDeskewDialog(); void appliedTo(const std::set& pages, bool applyDeskew, bool applyOblique); diff --git a/src/core/filters/deskew/OptionsWidget.ui b/src/core/filters/deskew/OptionsWidget.ui index 3ee6b713a..3bddccf4c 100644 --- a/src/core/filters/deskew/OptionsWidget.ui +++ b/src/core/filters/deskew/OptionsWidget.ui @@ -167,7 +167,7 @@ true - true + false true diff --git a/src/core/filters/deskew/Task.cpp b/src/core/filters/deskew/Task.cpp index 73c767881..1eea6cb91 100644 --- a/src/core/filters/deskew/Task.cpp +++ b/src/core/filters/deskew/Task.cpp @@ -120,7 +120,7 @@ FilterResultPtr Task::process(const TaskStatus& status, FilterData data) { status.throwIfCancelled(); - bool autoObliqueEnabled = true; + bool autoObliqueEnabled = false; if (priorParamsBeforeRecompute) { autoObliqueEnabled = priorParamsBeforeRecompute->autoOblique(); } else if (const auto pending = m_settings->takePendingAutoOblique(m_pageId)) { diff --git a/src/core/tests/TestDeskewParams.cpp b/src/core/tests/TestDeskewParams.cpp index 35dac92e5..99e1e4737 100644 --- a/src/core/tests/TestDeskewParams.cpp +++ b/src/core/tests/TestDeskewParams.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2019 Joseph Artsimovich , 4lex4 <4lex49@zoho.com> // Use of this source code is governed by the GNU GPLv3 license that can be found in the LICENSE file. +#include #include #include #include @@ -141,6 +142,11 @@ BOOST_AUTO_TEST_CASE(params_missing_autoOblique_attribute_defaults_true) { BOOST_CHECK(restored.autoOblique()); } +BOOST_AUTO_TEST_CASE(default_params_deskew_auto_oblique_off_by_default) { + const DefaultParams::DeskewParams deskew; + BOOST_CHECK(!deskew.isAutoOblique()); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(ImageTransformationObliqueTestSuite) From c3831d057f4bad243144ab0ea35f1f66d78761a7 Mon Sep 17 00:00:00 2001 From: Pablo Ventura Date: Sun, 24 May 2026 17:30:27 -0300 Subject: [PATCH 2/2] style(deskew): clang-format OptionsWidget --- src/core/filters/deskew/OptionsWidget.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/filters/deskew/OptionsWidget.cpp b/src/core/filters/deskew/OptionsWidget.cpp index 07f8aa1be..4f39ec9c0 100644 --- a/src/core/filters/deskew/OptionsWidget.cpp +++ b/src/core/filters/deskew/OptionsWidget.cpp @@ -54,8 +54,7 @@ OptionsWidget::OptionsWidget(std::shared_ptr settings, const PageSelec angleSpinBox->adjustSize(); setSpinBoxUnknownState(); topEdgeCheckBox->setChecked(!m_settings->algoContentBased()); - autoObliqueCheckBox->setChecked( - DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); + autoObliqueCheckBox->setChecked(DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); obliqueManualBtn->setChecked(true); obliqueAutoBtn->setChecked(false); @@ -152,8 +151,7 @@ void OptionsWidget::postUpdateUI(const UiData& uiData) { updateObliqueModeIndication(uiData.obliqueMode()); setSpinBoxKnownState(degreesToSpinBox(uiData.effectiveDeskewAngle())); obliqueSpinBox->setValue(m_uiData.effectiveObliqueAngle()); - autoObliqueCheckBox->setChecked( - DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); + autoObliqueCheckBox->setChecked(DefaultParamsProvider::getInstance().getParams().getDeskewParams().isAutoOblique()); } void OptionsWidget::spinBoxValueChanged(const double value) {