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..4f39ec9c0 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,9 @@ 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 +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()); } void OptionsWidget::spinBoxValueChanged(const double value) { @@ -156,6 +173,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 +270,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 +298,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 +307,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)