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 "ImageTransformation.h"
20 
ImageTransformation(const QRectF & orig_image_rect,const Dpi & orig_dpi)21 ImageTransformation::ImageTransformation(const QRectF& orig_image_rect, const Dpi& orig_dpi)
22     : m_postRotation(0.0), m_origRect(orig_image_rect), m_resultingRect(orig_image_rect), m_origDpi(orig_dpi) {
23   preScaleToEqualizeDpi();
24 }
25 
26 ImageTransformation::~ImageTransformation() = default;
27 
preScaleToDpi(const Dpi & dpi)28 void ImageTransformation::preScaleToDpi(const Dpi& dpi) {
29   if (m_origDpi.isNull() || dpi.isNull()) {
30     return;
31   }
32 
33   m_preScaledDpi = dpi;
34 
35   const double xscale = (double) dpi.horizontal() / m_origDpi.horizontal();
36   const double yscale = (double) dpi.vertical() / m_origDpi.vertical();
37 
38   const QSizeF new_pre_scaled_image_size(m_origRect.width() * xscale, m_origRect.height() * yscale);
39 
40   // Undo's for the specified steps.
41   const QTransform undo21(m_preRotateXform.inverted() * m_preScaleXform.inverted());
42   const QTransform undo4321(m_postRotateXform.inverted() * m_preCropXform.inverted() * undo21);
43 
44   // Update transform #1: pre-scale.
45   m_preScaleXform.reset();
46   m_preScaleXform.scale(xscale, yscale);
47 
48   // Update transform #2: pre-rotate.
49   m_preRotateXform = m_preRotation.transform(new_pre_scaled_image_size);
50 
51   // Update transform #3: pre-crop.
52   const QTransform redo12(m_preScaleXform * m_preRotateXform);
53   m_preCropArea = (undo21 * redo12).map(m_preCropArea);
54   m_preCropXform = calcCropXform(m_preCropArea);
55 
56   // Update transform #4: post-rotate.
57   m_postRotateXform = calcPostRotateXform(m_postRotation);
58 
59   // Update transform #5: post-crop.
60   const QTransform redo1234(redo12 * m_preCropXform * m_postRotateXform);
61   m_postCropArea = (undo4321 * redo1234).map(m_postCropArea);
62   m_postCropXform = calcCropXform(m_postCropArea);
63   // Update transform #6: post-scale.
64   m_postScaleXform = calcPostScaleXform(m_postScaledDpi);
65 
66   update();
67 }  // ImageTransformation::preScaleToDpi
68 
preScaleToEqualizeDpi()69 void ImageTransformation::preScaleToEqualizeDpi() {
70   const int min_dpi = std::min(m_origDpi.horizontal(), m_origDpi.vertical());
71   preScaleToDpi(Dpi(min_dpi, min_dpi));
72 }
73 
setPreRotation(const OrthogonalRotation rotation)74 void ImageTransformation::setPreRotation(const OrthogonalRotation rotation) {
75   m_preRotation = rotation;
76   m_preRotateXform = m_preRotation.transform(m_origRect.size());
77   resetPreCropArea();
78   resetPostRotation();
79   resetPostCrop();
80   resetPostScale();
81   update();
82 }
83 
setPreCropArea(const QPolygonF & area)84 void ImageTransformation::setPreCropArea(const QPolygonF& area) {
85   m_preCropArea = area;
86   m_preCropXform = calcCropXform(area);
87   resetPostRotation();
88   resetPostCrop();
89   resetPostScale();
90   update();
91 }
92 
setPostRotation(const double degrees)93 void ImageTransformation::setPostRotation(const double degrees) {
94   m_postRotateXform = calcPostRotateXform(degrees);
95   m_postRotation = degrees;
96   resetPostCrop();
97   resetPostScale();
98   update();
99 }
100 
setPostCropArea(const QPolygonF & area)101 void ImageTransformation::setPostCropArea(const QPolygonF& area) {
102   m_postCropArea = area;
103   m_postCropXform = calcCropXform(area);
104   resetPostScale();
105   update();
106 }
107 
postScaleToDpi(const Dpi & dpi)108 void ImageTransformation::postScaleToDpi(const Dpi& dpi) {
109   m_postScaledDpi = dpi;
110   m_postScaleXform = calcPostScaleXform(dpi);
111   update();
112 }
113 
calcCropXform(const QPolygonF & area)114 QTransform ImageTransformation::calcCropXform(const QPolygonF& area) {
115   const QRectF bounds(area.boundingRect());
116   QTransform xform;
117   xform.translate(-bounds.x(), -bounds.y());
118 
119   return xform;
120 }
121 
calcPostRotateXform(const double degrees)122 QTransform ImageTransformation::calcPostRotateXform(const double degrees) {
123   QTransform xform;
124   if (degrees != 0.0) {
125     const QPointF origin(m_preCropArea.boundingRect().center());
126     xform.translate(-origin.x(), -origin.y());
127     xform *= QTransform().rotate(degrees);
128     xform *= QTransform().translate(origin.x(), origin.y());
129 
130     // Calculate size changes.
131     const QPolygonF pre_rotate_poly(m_preCropXform.map(m_preCropArea));
132     const QRectF pre_rotate_rect(pre_rotate_poly.boundingRect());
133     const QPolygonF post_rotate_poly(xform.map(pre_rotate_poly));
134     const QRectF post_rotate_rect(post_rotate_poly.boundingRect());
135 
136     xform *= QTransform().translate(pre_rotate_rect.left() - post_rotate_rect.left(),
137                                     pre_rotate_rect.top() - post_rotate_rect.top());
138   }
139 
140   return xform;
141 }
142 
calcPostScaleXform(const Dpi & target_dpi)143 QTransform ImageTransformation::calcPostScaleXform(const Dpi& target_dpi) {
144   if (target_dpi.isNull()) {
145     return QTransform();
146   }
147 
148   // We are going to measure the effective DPI after the previous transforms.
149   // Normally m_preScaledDpi would be symmetric, so we could just
150   // use that, but just in case ...
151 
152   const QTransform to_orig(m_postScaleXform * m_transform.inverted());
153   // IMPORTANT: in the above line we assume post-scale is the last transform.
154 
155   const QLineF hor_unit(QPointF(0, 0), QPointF(1, 0));
156   const QLineF vert_unit(QPointF(0, 0), QPointF(0, 1));
157   const QLineF orig_hor_unit(to_orig.map(hor_unit));
158   const QLineF orig_vert_unit(to_orig.map(vert_unit));
159 
160   const double xscale = target_dpi.horizontal() * orig_hor_unit.length() / m_origDpi.horizontal();
161   const double yscale = target_dpi.vertical() * orig_vert_unit.length() / m_origDpi.vertical();
162   QTransform xform;
163   xform.scale(xscale, yscale);
164 
165   return xform;
166 }
167 
resetPreCropArea()168 void ImageTransformation::resetPreCropArea() {
169   m_preCropArea.clear();
170   m_preCropXform.reset();
171 }
172 
resetPostRotation()173 void ImageTransformation::resetPostRotation() {
174   m_postRotation = 0.0;
175   m_postRotateXform.reset();
176 }
177 
resetPostCrop()178 void ImageTransformation::resetPostCrop() {
179   m_postCropArea.clear();
180   m_postCropXform.reset();
181 }
182 
resetPostScale()183 void ImageTransformation::resetPostScale() {
184   m_postScaledDpi = Dpi();
185   m_postScaleXform.reset();
186 }
187 
update()188 void ImageTransformation::update() {
189   const QTransform pre_scale_then_pre_rotate(m_preScaleXform * m_preRotateXform);                // 12
190   const QTransform pre_crop_then_post_rotate(m_preCropXform * m_postRotateXform);                // 34
191   const QTransform post_crop_then_post_scale(m_postCropXform * m_postScaleXform);                // 56
192   const QTransform pre_crop_and_further(pre_crop_then_post_rotate * post_crop_then_post_scale);  // 3456
193   m_transform = pre_scale_then_pre_rotate * pre_crop_and_further;
194   m_invTransform = m_transform.inverted();
195   if (m_preCropArea.empty()) {
196     m_preCropArea = pre_scale_then_pre_rotate.map(m_origRect);
197   }
198   if (m_postCropArea.empty()) {
199     m_postCropArea = pre_crop_then_post_rotate.map(m_preCropArea);
200   }
201   m_resultingPreCropArea = pre_crop_and_further.map(m_preCropArea);
202   m_resultingPostCropArea = post_crop_then_post_scale.map(m_postCropArea);
203   m_resultingRect = m_resultingPostCropArea.boundingRect();
204 }
205