Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).

Expand Down
2 changes: 1 addition & 1 deletion src/core/DefaultParams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
69 changes: 36 additions & 33 deletions src/core/filters/deskew/ImageView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -110,26 +109,26 @@ void ImageView::onPaint(QPainter& painter, const InteractionState& interaction)
pen.setWidth(1);
painter.setPen(pen);
QVector<QLineF> 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);

// Draw the horizontal and vertical line crossing at the center.
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());
Expand Down Expand Up @@ -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);
Expand All @@ -291,15 +302,7 @@ std::pair<QPointF, QPointF> 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);
Expand Down
3 changes: 3 additions & 0 deletions src/core/filters/deskew/ImageView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QPointF, QPointF> getRotationHandles(const QRectF& arcSquare) const;
Expand Down
30 changes: 29 additions & 1 deletion src/core/filters/deskew/OptionsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

#include <utility>

#include <core/DefaultParams.h>
#include <core/DefaultParamsProvider.h>

#include "ApplyDialog.h"
#include "Params.h"
#include "Settings.h"
Expand All @@ -27,6 +30,16 @@ Params mergeParamsForApply(const std::unique_ptr<Params>& 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<DefaultParams>(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;
Expand All @@ -41,6 +54,9 @@ OptionsWidget::OptionsWidget(std::shared_ptr<Settings> 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();
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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();

Expand All @@ -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()));
}

Expand All @@ -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
2 changes: 2 additions & 0 deletions src/core/filters/deskew/OptionsWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<PageId>& pages, bool applyDeskew, bool applyOblique);
Expand Down
2 changes: 1 addition & 1 deletion src/core/filters/deskew/OptionsWidget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
<property name="autoExclusive">
<bool>true</bool>
Expand Down
2 changes: 1 addition & 1 deletion src/core/filters/deskew/Task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions src/core/tests/TestDeskewParams.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (C) 2019 Joseph Artsimovich <joseph.artsimovich@gmail.com>, 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 <DefaultParams.h>
#include <Dpi.h>
#include <ImageTransformation.h>
#include <filters/deskew/Dependencies.h>
Expand Down Expand Up @@ -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)
Expand Down