1 /*
2 Scan Tailor - Interactive post-processing tool for scanned pages.
3 Copyright (C) Joseph Artsimovich <joseph.artsimovich@gmail.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "PageLayout.h"
20 #include <QTransform>
21 #include <cassert>
22 #include "NumericTraits.h"
23 #include "ToLineProjector.h"
24 #include "XmlMarshaller.h"
25 #include "XmlUnmarshaller.h"
26 #include "imageproc/PolygonUtils.h"
27
28 using namespace imageproc;
29
30 namespace page_split {
PageLayout()31 PageLayout::PageLayout() : m_type(SINGLE_PAGE_UNCUT) {}
32
PageLayout(const QRectF & full_rect)33 PageLayout::PageLayout(const QRectF& full_rect)
34 : m_uncutOutline(full_rect),
35 m_cutter1(full_rect.topLeft(), full_rect.bottomLeft()),
36 m_cutter2(full_rect.topRight(), full_rect.bottomRight()),
37 m_type(SINGLE_PAGE_UNCUT) {}
38
PageLayout(const QRectF & full_rect,const QLineF & cutter1,const QLineF & cutter2)39 PageLayout::PageLayout(const QRectF& full_rect, const QLineF& cutter1, const QLineF& cutter2)
40 : m_uncutOutline(full_rect), m_cutter1(cutter1), m_cutter2(cutter2), m_type(SINGLE_PAGE_CUT) {}
41
PageLayout(const QRectF full_rect,const QLineF & split_line)42 PageLayout::PageLayout(const QRectF full_rect, const QLineF& split_line)
43 : m_uncutOutline(full_rect), m_cutter1(split_line), m_type(TWO_PAGES) {}
44
PageLayout(const QPolygonF & outline,const QLineF & cutter1,const QLineF & cutter2,Type type)45 PageLayout::PageLayout(const QPolygonF& outline, const QLineF& cutter1, const QLineF& cutter2, Type type)
46 : m_uncutOutline(outline), m_cutter1(cutter1), m_cutter2(cutter2), m_type(type) {}
47
PageLayout(const QDomElement & layout_el)48 PageLayout::PageLayout(const QDomElement& layout_el)
49 : m_uncutOutline(XmlUnmarshaller::polygonF(layout_el.namedItem("outline").toElement())),
50 m_type(typeFromString(layout_el.attribute("type"))),
51 m_cutter1(XmlUnmarshaller::lineF(layout_el.namedItem("cutter1").toElement())),
52 m_cutter2(XmlUnmarshaller::lineF(layout_el.namedItem("cutter2").toElement())) {}
53
setType(Type type)54 void PageLayout::setType(Type type) {
55 m_type = type;
56 if (type == TWO_PAGES) {
57 m_cutter2 = m_cutter1;
58 }
59 }
60
setUncutOutline(const QRectF & outline)61 void PageLayout::setUncutOutline(const QRectF& outline) {
62 m_uncutOutline = outline;
63 if (m_uncutOutline.size() < 4) {
64 // Empty rect?
65 return;
66 }
67 }
68
cutterLine(int idx) const69 const QLineF& PageLayout::cutterLine(int idx) const {
70 assert(idx >= 0 && idx < numCutters());
71
72 return idx == 0 ? m_cutter1 : m_cutter2;
73 }
74
setCutterLine(int idx,const QLineF & cutter)75 void PageLayout::setCutterLine(int idx, const QLineF& cutter) {
76 assert(idx >= 0 && idx < numCutters());
77 (idx == 0 ? m_cutter1 : m_cutter2) = cutter;
78 }
79
toLayoutType() const80 LayoutType PageLayout::toLayoutType() const {
81 switch (m_type) {
82 case SINGLE_PAGE_UNCUT:
83 return page_split::SINGLE_PAGE_UNCUT;
84 case SINGLE_PAGE_CUT:
85 return page_split::PAGE_PLUS_OFFCUT;
86 case TWO_PAGES:
87 return page_split::TWO_PAGES;
88 }
89
90 assert(!"Unreachable");
91
92 return page_split::SINGLE_PAGE_UNCUT;
93 }
94
numCutters() const95 int PageLayout::numCutters() const {
96 switch (m_type) {
97 case SINGLE_PAGE_UNCUT:
98 return 0;
99 case SINGLE_PAGE_CUT:
100 return 2;
101 case TWO_PAGES:
102 return 1;
103 }
104
105 assert(!"Unreachable");
106
107 return 0;
108 }
109
numSubPages() const110 int PageLayout::numSubPages() const {
111 return m_type == TWO_PAGES ? 2 : 1;
112 }
113
inscribedCutterLine(int idx) const114 QLineF PageLayout::inscribedCutterLine(int idx) const {
115 assert(idx >= 0 && idx < numCutters());
116
117 if (m_uncutOutline.size() < 4) {
118 return QLineF();
119 }
120
121 const QLineF raw_line(cutterLine(idx));
122 const QPointF origin(raw_line.p1());
123 const QPointF line_vec(raw_line.p2() - origin);
124
125 double min_proj = NumericTraits<double>::max();
126 double max_proj = NumericTraits<double>::min();
127 QPointF min_pt;
128 QPointF max_pt;
129
130 QPointF p1, p2;
131 double projection;
132
133 for (int i = 0; i < 4; ++i) {
134 const QLineF poly_segment(m_uncutOutline[i], m_uncutOutline[(i + 1) & 3]);
135 QPointF intersection;
136 if (poly_segment.intersect(raw_line, &intersection) == QLineF::NoIntersection) {
137 continue;
138 }
139
140 // Project the intersection point on poly_segment to check
141 // if it's between its endpoints.
142 p1 = poly_segment.p2() - poly_segment.p1();
143 p2 = intersection - poly_segment.p1();
144 projection = p1.x() * p2.x() + p1.y() * p2.y();
145 if ((projection < 0) || (projection > p1.x() * p1.x() + p1.y() * p1.y())) {
146 // Intersection point not on the polygon segment.
147 continue;
148 }
149 // Now project it on raw_line and update min/max projections.
150 p1 = intersection - origin;
151 p2 = line_vec;
152 projection = p1.x() * p2.x() + p1.y() * p2.y();
153 if (projection <= min_proj) {
154 min_proj = projection;
155 min_pt = intersection;
156 }
157 if (projection >= max_proj) {
158 max_proj = projection;
159 max_pt = intersection;
160 }
161 }
162
163 QLineF res(min_pt, max_pt);
164 ensureSameDirection(raw_line, res);
165
166 return res;
167 } // PageLayout::inscribedCutterLine
168
singlePageOutline() const169 QPolygonF PageLayout::singlePageOutline() const {
170 if (m_uncutOutline.size() < 4) {
171 return QPolygonF();
172 }
173
174 switch (m_type) {
175 case SINGLE_PAGE_UNCUT:
176 return m_uncutOutline;
177 case SINGLE_PAGE_CUT:
178 break;
179 case TWO_PAGES:
180 return QPolygonF();
181 }
182
183 const QLineF line1(extendToCover(m_cutter1, m_uncutOutline));
184 QLineF line2(extendToCover(m_cutter2, m_uncutOutline));
185 ensureSameDirection(line1, line2);
186 const QLineF reverse_line1(line1.p2(), line1.p1());
187 const QLineF reverse_line2(line2.p2(), line2.p1());
188
189 QPolygonF poly;
190 poly << line1.p1();
191 maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
192 poly << line2.p1() << line2.p2();
193 maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
194 poly << line1.p2();
195
196 return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
197 }
198
leftPageOutline() const199 QPolygonF PageLayout::leftPageOutline() const {
200 if (m_uncutOutline.size() < 4) {
201 return QPolygonF();
202 }
203
204 switch (m_type) {
205 case SINGLE_PAGE_UNCUT:
206 case SINGLE_PAGE_CUT:
207 return QPolygonF();
208 case TWO_PAGES:
209 break;
210 }
211
212 const QLineF line1(m_uncutOutline[0], m_uncutOutline[3]);
213 QLineF line2(extendToCover(m_cutter1, m_uncutOutline));
214 ensureSameDirection(line1, line2);
215 const QLineF reverse_line1(line1.p2(), line1.p1());
216 const QLineF reverse_line2(line2.p2(), line2.p1());
217
218 QPolygonF poly;
219 poly << line1.p1();
220 maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
221 poly << line2.p1() << line2.p2();
222 maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
223 poly << line1.p2();
224
225 return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
226 }
227
rightPageOutline() const228 QPolygonF PageLayout::rightPageOutline() const {
229 if (m_uncutOutline.size() < 4) {
230 return QPolygonF();
231 }
232
233 switch (m_type) {
234 case SINGLE_PAGE_UNCUT:
235 case SINGLE_PAGE_CUT:
236 return QPolygonF();
237 case TWO_PAGES:
238 break;
239 }
240
241 const QLineF line1(m_uncutOutline[1], m_uncutOutline[2]);
242 QLineF line2(extendToCover(m_cutter1, m_uncutOutline));
243 ensureSameDirection(line1, line2);
244 const QLineF reverse_line1(line1.p2(), line1.p1());
245 const QLineF reverse_line2(line2.p2(), line2.p1());
246
247 QPolygonF poly;
248 poly << line1.p1();
249 maybeAddIntersectionPoint(poly, line1.normalVector(), line2.normalVector());
250 poly << line2.p1() << line2.p2();
251 maybeAddIntersectionPoint(poly, reverse_line1.normalVector(), reverse_line2.normalVector());
252 poly << line1.p2();
253
254 return PolygonUtils::round(m_uncutOutline).intersected(PolygonUtils::round(poly));
255 }
256
pageOutline(const PageId::SubPage page) const257 QPolygonF PageLayout::pageOutline(const PageId::SubPage page) const {
258 switch (page) {
259 case PageId::SINGLE_PAGE:
260 return singlePageOutline();
261 case PageId::LEFT_PAGE:
262 return leftPageOutline();
263 case PageId::RIGHT_PAGE:
264 return rightPageOutline();
265 }
266
267 assert(!"Unreachable");
268
269 return QPolygonF();
270 }
271
transformed(const QTransform & xform) const272 PageLayout PageLayout::transformed(const QTransform& xform) const {
273 return PageLayout(xform.map(m_uncutOutline), xform.map(m_cutter1), xform.map(m_cutter2), m_type);
274 }
275
toXml(QDomDocument & doc,const QString & name) const276 QDomElement PageLayout::toXml(QDomDocument& doc, const QString& name) const {
277 XmlMarshaller marshaller(doc);
278
279 QDomElement el(doc.createElement(name));
280 el.setAttribute("type", typeToString(m_type));
281 el.appendChild(marshaller.polygonF(m_uncutOutline, "outline"));
282
283 const int num_cutters = numCutters();
284 if (num_cutters > 0) {
285 el.appendChild(marshaller.lineF(m_cutter1, "cutter1"));
286 if (num_cutters > 1) {
287 el.appendChild(marshaller.lineF(m_cutter2, "cutter2"));
288 }
289 }
290
291 return el;
292 }
293
typeFromString(const QString & str)294 PageLayout::Type PageLayout::typeFromString(const QString& str) {
295 if (str == "two-pages") {
296 return TWO_PAGES;
297 } else if (str == "single-cut") {
298 return SINGLE_PAGE_CUT;
299 } else { // "single-uncut"
300 return SINGLE_PAGE_UNCUT;
301 }
302 }
303
typeToString(const Type type)304 QString PageLayout::typeToString(const Type type) {
305 const char* str = nullptr;
306 switch (type) {
307 case SINGLE_PAGE_UNCUT:
308 str = "single-uncut";
309 break;
310 case SINGLE_PAGE_CUT:
311 str = "single-cut";
312 break;
313 case TWO_PAGES:
314 str = "two-pages";
315 break;
316 }
317
318 return QString::fromLatin1(str);
319 }
320
321 /**
322 * Extends or shrinks a line segment in such a way that if you draw perpendicular
323 * lines through its endpoints, the given polygon would be squeezed between these
324 * two perpendiculars. This ensures that the resulting line segment intersects
325 * all the polygon edges it can possibly intersect.
326 */
extendToCover(const QLineF & line,const QPolygonF & poly)327 QLineF PageLayout::extendToCover(const QLineF& line, const QPolygonF& poly) {
328 if (poly.isEmpty()) {
329 return line;
330 }
331
332 // Project every vertex of the polygon onto the line and take extremas.
333
334 double min = NumericTraits<double>::max();
335 double max = NumericTraits<double>::min();
336 const ToLineProjector projector(line);
337
338 for (const QPointF& pt : poly) {
339 const double scalar = projector.projectionScalar(pt);
340 if (scalar < min) {
341 min = scalar;
342 }
343 if (scalar > max) {
344 max = scalar;
345 }
346 }
347
348 return QLineF(line.pointAt(min), line.pointAt(max));
349 }
350
351 /**
352 * Flips \p line2 if that would make the angle between the two lines more acute.
353 * The angle between lines is interpreted as an angle between vectors
354 * (line1.p2() - line1.p1()) and (line2.p2() - line2.p1()).
355 */
ensureSameDirection(const QLineF & line1,QLineF & line2)356 void PageLayout::ensureSameDirection(const QLineF& line1, QLineF& line2) {
357 const QPointF v1(line1.p2() - line1.p1());
358 const QPointF v2(line2.p2() - line2.p1());
359 const double dot = v1.x() * v2.x() + v1.y() * v2.y();
360 if (dot < 0.0) {
361 line2 = QLineF(line2.p2(), line2.p1());
362 }
363 }
364
365 /**
366 * Add the intersection point between \p line1 and \p line2
367 * to \p poly, provided they intersect at all and the intersection
368 * point is "between" line1.p1() and line2.p1(). We consider a point
369 * to be between two other points by projecting it to the line between
370 * those two points and checking if the projected point is between them.
371 * When finding the intersection point, we treat \p line1 and \p line2
372 * as lines, not line segments.
373 */
maybeAddIntersectionPoint(QPolygonF & poly,const QLineF & line1,const QLineF & line2)374 void PageLayout::maybeAddIntersectionPoint(QPolygonF& poly, const QLineF& line1, const QLineF& line2) {
375 QPointF intersection;
376 if (line1.intersect(line2, &intersection) == QLineF::NoIntersection) {
377 return;
378 }
379
380 const ToLineProjector projector(QLineF(line1.p1(), line2.p1()));
381 const double p = projector.projectionScalar(intersection);
382 if ((p > 0.0) && (p < 1.0)) {
383 poly << intersection;
384 }
385 }
386
isNull() const387 bool PageLayout::isNull() const {
388 return m_uncutOutline.isEmpty();
389 }
390
type() const391 PageLayout::Type PageLayout::type() const {
392 return m_type;
393 }
394
uncutOutline() const395 const QPolygonF& PageLayout::uncutOutline() const {
396 return m_uncutOutline;
397 }
398 } // namespace page_split