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