Skip to content

Commit 4844f3a

Browse files
committed
feat(fix-orientation): manual per-image trim before page split
Add optional inward margins (source pixels) per image in Fix Orientation, persisted in project XML, applied as pre-crop after rotation. Page Split intersects its outline with any existing pre-crop. Panel: enable, margins, reset. Refs #129
1 parent a1b7bb8 commit 4844f3a

15 files changed

Lines changed: 409 additions & 18 deletions

src/core/ImageTransformation.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#include "ImageTransformation.h"
55

6+
#include <QPolygonF>
7+
68
#include <cmath>
79

810
#ifndef M_PI
@@ -192,6 +194,11 @@ void ImageTransformation::resetPostScale() {
192194
m_postScaleXform.reset();
193195
}
194196

197+
QPolygonF ImageTransformation::origRectToPreCropSpace(const QRectF& rect) const {
198+
const QTransform preScaleThenPreRotate(m_preScaleXform * m_preRotateXform);
199+
return preScaleThenPreRotate.map(QPolygonF(rect));
200+
}
201+
195202
void ImageTransformation::update() {
196203
const QTransform preScaleThenPreRotate(m_preScaleXform * m_preRotateXform); // 12
197204
const QTransform preCropThenPostRotate(m_preCropXform * m_postRotateXform); // 34

src/core/ImageTransformation.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ class ImageTransformation {
204204
*/
205205
const QRectF& resultingRect() const { return m_resultingRect; }
206206

207+
/**
208+
* \brief Map a rectangle from original image coordinates to the coordinate
209+
* system used by setPreCropArea() (after pre-scale and pre-rotate).
210+
*/
211+
QPolygonF origRectToPreCropSpace(const QRectF& rect) const;
212+
207213
private:
208214
QTransform calcCropXform(const QPolygonF& cropArea);
209215

src/core/filters/fix_orientation/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(ui_files
55
OptionsWidget.ui)
66

77
set(sources
8+
ImageTrim.cpp ImageTrim.h
89
ImageView.cpp ImageView.h
910
Filter.cpp Filter.h
1011
OptionsWidget.cpp OptionsWidget.h

src/core/filters/fix_orientation/CacheDrivenTask.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <utility>
77

88
#include "ImageTransformation.h"
9+
#include "ImageTrim.h"
910
#include "PageInfo.h"
1011
#include "Settings.h"
1112
#include "ThumbnailBase.h"
@@ -26,6 +27,12 @@ void CacheDrivenTask::process(const PageInfo& pageInfo, AbstractFilterDataCollec
2627
ImageTransformation xform(initialRect, pageInfo.metadata().dpi());
2728
xform.setPreRotation(m_settings->getRotationFor(pageInfo.imageId()));
2829

30+
const ImageTrim trim(m_settings->getTrim(pageInfo.imageId()));
31+
if (trim.enabled) {
32+
const QRect inner(trim.toInnerRect(pageInfo.metadata().size()));
33+
xform.setPreCropArea(xform.origRectToPreCropSpace(QRectF(inner)));
34+
}
35+
2936
if (auto* col = dynamic_cast<PageOrientationCollector*>(collector)) {
3037
col->process(xform.preRotation());
3138
}

src/core/filters/fix_orientation/Filter.cpp

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
#include "ProjectReader.h"
1717
#include "ProjectWriter.h"
1818
#include "Settings.h"
19+
#include "ImageTrim.h"
1920
#include "Task.h"
2021
#include "Utils.h"
21-
#include "XmlMarshaller.h"
2222

2323
namespace fix_orientation {
2424
Filter::Filter(const PageSelectionAccessor& pageSelectionAccessor)
@@ -44,7 +44,7 @@ void Filter::performRelinking(const AbstractRelinker& relinker) {
4444
void Filter::preUpdateUI(FilterUiInterface* ui, const PageInfo& pageInfo) {
4545
if (m_optionsWidget.get()) {
4646
const OrthogonalRotation rotation(m_settings->getRotationFor(pageInfo.id().imageId()));
47-
m_optionsWidget->preUpdateUI(pageInfo.id(), rotation);
47+
m_optionsWidget->preUpdateUI(pageInfo, rotation);
4848
ui->setOptionsWidget(m_optionsWidget.get(), ui->KEEP_OWNERSHIP);
4949
}
5050
}
@@ -85,9 +85,22 @@ void Filter::loadSettings(const ProjectReader& reader, const QDomElement& filter
8585
continue;
8686
}
8787

88-
const OrthogonalRotation rotation(el.namedItem("rotation").toElement());
88+
const QDomElement rotationEl(el.namedItem("rotation").toElement());
89+
if (!rotationEl.isNull()) {
90+
const OrthogonalRotation rotation(rotationEl);
91+
m_settings->applyRotation(imageId, rotation);
92+
}
8993

90-
m_settings->applyRotation(imageId, rotation);
94+
const QDomElement trimEl(el.namedItem("trim").toElement());
95+
if (!trimEl.isNull()) {
96+
ImageTrim trim;
97+
trim.enabled = (trimEl.attribute("enabled", "0") != "0");
98+
trim.left = trimEl.attribute("left", "0").toInt();
99+
trim.right = trimEl.attribute("right", "0").toInt();
100+
trim.top = trimEl.attribute("top", "0").toInt();
101+
trim.bottom = trimEl.attribute("bottom", "0").toInt();
102+
m_settings->setTrim(imageId, trim);
103+
}
91104
}
92105

93106
loadImageSettings(reader, filterEl.namedItem("image-settings").toElement());
@@ -106,15 +119,26 @@ std::shared_ptr<CacheDrivenTask> Filter::createCacheDrivenTask(std::shared_ptr<p
106119

107120
void Filter::writeParams(QDomDocument& doc, QDomElement& filterEl, const ImageId& imageId, int numericId) const {
108121
const OrthogonalRotation rotation(m_settings->getRotationFor(imageId));
109-
if (rotation.toDegrees() == 0) {
122+
const ImageTrim trim(m_settings->getTrim(imageId));
123+
124+
if ((rotation.toDegrees() == 0) && !trim.enabled) {
110125
return;
111126
}
112127

113-
XmlMarshaller marshaller(doc);
114-
115128
QDomElement imageEl(doc.createElement("image"));
116129
imageEl.setAttribute("id", numericId);
117-
imageEl.appendChild(rotation.toXml(doc, "rotation"));
130+
if (rotation.toDegrees() != 0) {
131+
imageEl.appendChild(rotation.toXml(doc, "rotation"));
132+
}
133+
if (trim.enabled) {
134+
QDomElement trimEl(doc.createElement("trim"));
135+
trimEl.setAttribute("enabled", "1");
136+
trimEl.setAttribute("left", QString::number(trim.left));
137+
trimEl.setAttribute("right", QString::number(trim.right));
138+
trimEl.setAttribute("top", QString::number(trim.top));
139+
trimEl.setAttribute("bottom", QString::number(trim.bottom));
140+
imageEl.appendChild(trimEl);
141+
}
118142
filterEl.appendChild(imageEl);
119143
}
120144

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (C) 2019 Joseph Artsimovich <joseph.artsimovich@gmail.com>, 4lex4 <4lex49@zoho.com>
2+
// Use of this source code is governed by the GNU GPLv3 license that can be found in the LICENSE file.
3+
4+
#include "ImageTrim.h"
5+
6+
#include <algorithm>
7+
8+
namespace fix_orientation {
9+
10+
namespace {
11+
12+
const int kMinInnerSide = 32;
13+
14+
} // namespace
15+
16+
QRect ImageTrim::toInnerRect(const QSize& imageSize) const {
17+
if (!enabled || imageSize.width() <= 0 || imageSize.height() <= 0) {
18+
return QRect(0, 0, imageSize.width(), imageSize.height());
19+
}
20+
21+
int l = std::max(0, left);
22+
int t = std::max(0, top);
23+
int r = std::max(0, right);
24+
int b = std::max(0, bottom);
25+
26+
int w = imageSize.width() - l - r;
27+
int h = imageSize.height() - t - b;
28+
w = std::max(kMinInnerSide, w);
29+
h = std::max(kMinInnerSide, h);
30+
31+
if (l + w > imageSize.width()) {
32+
l = std::max(0, imageSize.width() - w);
33+
}
34+
if (t + h > imageSize.height()) {
35+
t = std::max(0, imageSize.height() - h);
36+
}
37+
38+
return QRect(l, t, w, h);
39+
}
40+
41+
} // namespace fix_orientation
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (C) 2019 Joseph Artsimovich <joseph.artsimovich@gmail.com>, 4lex4 <4lex49@zoho.com>
2+
// Use of this source code is governed by the GNU GPLv3 license that can be found in the LICENSE file.
3+
4+
#ifndef SCANTAILOR_FIX_ORIENTATION_IMAGETRIM_H_
5+
#define SCANTAILOR_FIX_ORIENTATION_IMAGETRIM_H_
6+
7+
#include <QRect>
8+
9+
namespace fix_orientation {
10+
11+
/**
12+
* Inward crop from each edge of the source image, in integer source pixels.
13+
* When disabled, other fields are ignored.
14+
*/
15+
struct ImageTrim {
16+
bool enabled = false;
17+
int left = 0;
18+
int top = 0;
19+
int right = 0;
20+
int bottom = 0;
21+
22+
QRect toInnerRect(const QSize& imageSize) const;
23+
};
24+
25+
} // namespace fix_orientation
26+
27+
#endif

src/core/filters/fix_orientation/OptionsWidget.cpp

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
#include <core/IconProvider.h>
77

8+
#include <QSize>
9+
10+
#include <algorithm>
811
#include <cassert>
912
#include <utility>
1013

@@ -14,6 +17,12 @@
1417
#include "Settings.h"
1518

1619
namespace fix_orientation {
20+
namespace {
21+
22+
const int kMinInnerSide = 32;
23+
24+
} // namespace
25+
1726
OptionsWidget::OptionsWidget(std::shared_ptr<Settings> settings, const PageSelectionAccessor& pageSelectionAccessor)
1827
: m_settings(std::move(settings)),
1928
m_pageSelectionAccessor(pageSelectionAccessor),
@@ -26,12 +35,15 @@ OptionsWidget::OptionsWidget(std::shared_ptr<Settings> settings, const PageSelec
2635

2736
OptionsWidget::~OptionsWidget() = default;
2837

29-
void OptionsWidget::preUpdateUI(const PageId& pageId, const OrthogonalRotation rotation) {
38+
void OptionsWidget::preUpdateUI(const PageInfo& pageInfo, const OrthogonalRotation rotation) {
3039
auto block = m_connectionManager.getScopedBlock();
3140

32-
m_pageId = pageId;
41+
m_pageId = pageInfo.id();
42+
m_imagePixelSize = pageInfo.metadata().size();
3343
m_rotation = rotation;
3444
setRotationPixmap();
45+
updateTrimMaximums();
46+
pullTrimToControls();
3547
}
3648

3749
void OptionsWidget::postUpdateUI(const OrthogonalRotation rotation) {
@@ -100,6 +112,76 @@ void OptionsWidget::setRotation(const OrthogonalRotation& rotation) {
100112
emit invalidateThumbnail(m_pageId);
101113
}
102114

115+
void OptionsWidget::trimEnableToggled(const bool checked) {
116+
trimLeftSpin->setEnabled(checked);
117+
trimRightSpin->setEnabled(checked);
118+
trimTopSpin->setEnabled(checked);
119+
trimBottomSpin->setEnabled(checked);
120+
resetTrimBtn->setEnabled(checked);
121+
if (!checked) {
122+
m_settings->clearTrim(m_pageId.imageId());
123+
emit invalidateThumbnail(m_pageId);
124+
return;
125+
}
126+
pushTrimFromControls();
127+
}
128+
129+
void OptionsWidget::trimMarginsChanged(const int) {
130+
if (!trimEnabledCheck->isChecked()) {
131+
return;
132+
}
133+
pushTrimFromControls();
134+
}
135+
136+
void OptionsWidget::resetTrim() {
137+
auto block = m_connectionManager.getScopedBlock();
138+
trimLeftSpin->setValue(0);
139+
trimRightSpin->setValue(0);
140+
trimTopSpin->setValue(0);
141+
trimBottomSpin->setValue(0);
142+
if (trimEnabledCheck->isChecked()) {
143+
pushTrimFromControls();
144+
} else {
145+
m_settings->clearTrim(m_pageId.imageId());
146+
emit invalidateThumbnail(m_pageId);
147+
}
148+
}
149+
150+
void OptionsWidget::updateTrimMaximums() {
151+
const int w = m_imagePixelSize.width();
152+
const int h = m_imagePixelSize.height();
153+
const int maxSide = std::max(0, std::max(w, h) - kMinInnerSide);
154+
trimLeftSpin->setMaximum(maxSide);
155+
trimRightSpin->setMaximum(maxSide);
156+
trimTopSpin->setMaximum(maxSide);
157+
trimBottomSpin->setMaximum(maxSide);
158+
}
159+
160+
void OptionsWidget::pullTrimToControls() {
161+
const ImageTrim trim(m_settings->getTrim(m_pageId.imageId()));
162+
trimEnabledCheck->setChecked(trim.enabled);
163+
trimLeftSpin->setValue(trim.left);
164+
trimRightSpin->setValue(trim.right);
165+
trimTopSpin->setValue(trim.top);
166+
trimBottomSpin->setValue(trim.bottom);
167+
trimLeftSpin->setEnabled(trim.enabled);
168+
trimRightSpin->setEnabled(trim.enabled);
169+
trimTopSpin->setEnabled(trim.enabled);
170+
trimBottomSpin->setEnabled(trim.enabled);
171+
resetTrimBtn->setEnabled(trim.enabled);
172+
}
173+
174+
void OptionsWidget::pushTrimFromControls() {
175+
ImageTrim trim;
176+
trim.enabled = true;
177+
trim.left = trimLeftSpin->value();
178+
trim.right = trimRightSpin->value();
179+
trim.top = trimTopSpin->value();
180+
trim.bottom = trimBottomSpin->value();
181+
m_settings->setTrim(m_pageId.imageId(), trim);
182+
emit invalidateThumbnail(m_pageId);
183+
}
184+
103185
void OptionsWidget::setRotationPixmap() {
104186
QIcon icon;
105187
switch (m_rotation.toDegrees()) {
@@ -128,6 +210,12 @@ void OptionsWidget::setupUiConnections() {
128210
CONNECT(rotateRightBtn, SIGNAL(clicked()), this, SLOT(rotateRight()));
129211
CONNECT(resetBtn, SIGNAL(clicked()), this, SLOT(resetRotation()));
130212
CONNECT(applyToBtn, SIGNAL(clicked()), this, SLOT(showApplyToDialog()));
213+
CONNECT(trimEnabledCheck, SIGNAL(toggled(bool)), this, SLOT(trimEnableToggled(bool)));
214+
CONNECT(trimLeftSpin, SIGNAL(valueChanged(int)), this, SLOT(trimMarginsChanged(int)));
215+
CONNECT(trimRightSpin, SIGNAL(valueChanged(int)), this, SLOT(trimMarginsChanged(int)));
216+
CONNECT(trimTopSpin, SIGNAL(valueChanged(int)), this, SLOT(trimMarginsChanged(int)));
217+
CONNECT(trimBottomSpin, SIGNAL(valueChanged(int)), this, SLOT(trimMarginsChanged(int)));
218+
CONNECT(resetTrimBtn, SIGNAL(clicked()), this, SLOT(resetTrim()));
131219
}
132220

133221
#undef CONNECT
@@ -137,4 +225,4 @@ void OptionsWidget::setupIcons() {
137225
rotateLeftBtn->setIcon(iconProvider.getIcon("object-rotate-left"));
138226
rotateRightBtn->setIcon(iconProvider.getIcon("object-rotate-right"));
139227
}
140-
} // namespace fix_orientation
228+
} // namespace fix_orientation

0 commit comments

Comments
 (0)