1 /*
2 Copyright © 2008-13 Qtrac Ltd. All rights reserved.
3 This program or module is free software: you can redistribute it
4 and/or modify it under the terms of the GNU General Public License
5 as published by the Free Software Foundation, either version 2 of
6 the License, or (at your option) any later version. This program is
7 distributed in the hope that it will be useful, but WITHOUT ANY
8 WARRANTY; without even the implied warranty of MERCHANTABILITY or
9 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
10 for more details.
11 */
12 #include "aboutform.hpp"
13 #include "generic.hpp"
14 #include "helpform.hpp"
15 #include "label.hpp"
16 #include "lineedit.hpp"
17 #include "optionsform.hpp"
18 #include "mainwindow.hpp"
19 #include "sequence_matcher.hpp"
20 #include "textitem.hpp"
21 #ifdef DEBUG
22 #include <QtDebug>
23 #endif
24 #include <QApplication>
25 #include <QBoxLayout>
26 #include <QCheckBox>
27 #include <QComboBox>
28 #include <QDir>
29 #include <QDockWidget>
30 #include <QEvent>
31 #include <QFileDialog>
32 #include <QGroupBox>
33 #include <QLabel>
34 #include <QLineEdit>
35 #include <QMessageBox>
36 #include <QPainter>
37 #include <QPixmapCache>
38 #include <QPlainTextEdit>
39 #include <QPrinter>
40 #include <QPushButton>
41 #include <QRadioButton>
42 #include <QScrollArea>
43 #include <QScrollBar>
44 #include <QSettings>
45 #include <QSpinBox>
46 #include <QSplitter>
47
48
MainWindow(const Debug debug,const InitialComparisonMode comparisonMode,const QString & filename1,const QString & filename2,const QString & language,QWidget * parent)49 MainWindow::MainWindow(const Debug debug,
50 const InitialComparisonMode comparisonMode,
51 const QString &filename1, const QString &filename2,
52 const QString &language, QWidget *parent)
53 : QMainWindow(parent),
54 controlDockArea(Qt::RightDockWidgetArea),
55 actionDockArea(Qt::RightDockWidgetArea),
56 marginsDockArea(Qt::RightDockWidgetArea),
57 zoningDockArea(Qt::RightDockWidgetArea),
58 logDockArea(Qt::RightDockWidgetArea), cancel(false),
59 saveAll(true), savePages(SaveBothPages), language(language),
60 debug(debug), aboutForm(0), helpForm(0)
61 {
62 currentPath = QDir::homePath();
63 QSettings settings;
64 pen.setStyle(Qt::NoPen);
65 pen.setColor(Qt::red);
66 pen = settings.value("Outline", pen).value<QPen>();
67 brush.setColor(pen.color());
68 brush.setStyle(Qt::SolidPattern);
69 brush = settings.value("Fill", brush).value<QBrush>();
70 showToolTips = settings.value("ShowToolTips", true).toBool();
71 combineTextHighlighting = settings.value("CombineTextHighlighting",
72 true).toBool();
73 QPixmapCache::setCacheLimit(1000 *
74 qBound(1, settings.value("CacheSizeMB", 25).toInt(), 100));
75
76 createWidgets(filename1, filename2);
77 createCentralArea();
78 createDockWidgets();
79 createConnections();
80
81 restoreGeometry(settings.value("MainWindow/Geometry").toByteArray());
82 restoreState(settings.value("MainWindow/State").toByteArray());
83 controlDockLocationChanged(static_cast<Qt::DockWidgetArea>(
84 settings.value("MainWindow/ControlDockArea",
85 static_cast<int>(controlDockArea)).toInt()));
86 actionDockLocationChanged(static_cast<Qt::DockWidgetArea>(
87 settings.value("MainWindow/ActionDockArea",
88 static_cast<int>(actionDockArea)).toInt()));
89 zoningDockLocationChanged(static_cast<Qt::DockWidgetArea>(
90 settings.value("MainWindow/ZoningDockArea",
91 static_cast<int>(zoningDockArea)).toInt()));
92 marginsDockLocationChanged(static_cast<Qt::DockWidgetArea>(
93 settings.value("MainWindow/MarginsDockArea",
94 static_cast<int>(marginsDockArea)).toInt()));
95 controlDockWidget->resize(controlDockWidget->minimumSizeHint());
96 actionDockWidget->resize(actionDockWidget->minimumSizeHint());
97 zoningDockWidget->resize(zoningDockWidget->minimumSizeHint());
98 marginsDockWidget->resize(marginsDockWidget->minimumSizeHint());
99 //logDockWidget->resize(logDockWidget->minimumSizeHint());
100
101 setWindowTitle(tr("DiffPDF"));
102 setWindowIcon(QIcon(":/icon.png"));
103 compareComboBox->setCurrentIndex(comparisonMode);
104 QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection,
105 Q_ARG(QString, filename1),
106 Q_ARG(QString, filename2));
107 }
108
109
createWidgets(const QString & filename1,const QString & filename2)110 void MainWindow::createWidgets(const QString &filename1,
111 const QString &filename2)
112 {
113 setFile1Button = new QPushButton(tr("File #&1..."));
114 setFile1Button->setToolTip(tr("<p>Choose the first (left hand) file "
115 "to be compared."));
116 filename1LineEdit = new LineEdit;
117 filename1LineEdit->setToolTip(tr("The first (left hand) file."));
118 filename1LineEdit->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
119 filename1LineEdit->setMinimumWidth(100);
120 filename1LineEdit->setText(filename1);
121 setFile2Button = new QPushButton(tr("File #&2..."));
122 setFile2Button->setToolTip(tr("<p>Choose the second (right hand) file "
123 "to be compared."));
124 filename2LineEdit = new LineEdit;
125 filename2LineEdit->setToolTip(tr("The second (right hand) file."));
126 filename2LineEdit->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
127 filename2LineEdit->setMinimumWidth(100);
128 filename2LineEdit->setText(filename2);
129 comparePages1Label = new QLabel(tr("Pa&ges:"));
130 pages1LineEdit = new QLineEdit;
131 comparePages1Label->setBuddy(pages1LineEdit);
132 pages1LineEdit->setToolTip(tr("<p>Pages can be specified using ranges "
133 "such as 1-10, and multiple ranges can be used, e.g., "
134 "1-10, 12-15, 20, 22, 35-39. This makes it "
135 "straighforward to compare similar documents where one "
136 "has one or more additional pages.<p>For example, if "
137 "file1.pdf has pages 1-30 and file2.pdf has pages 1-31 "
138 "with the extra page being page 14, the two page ranges "
139 "would be set to 1-30 for file1.pdf and 1-13, 15-31 for "
140 "file2.pdf."));
141 comparePages2Label = new QLabel(tr("&Pages:"));
142 pages2LineEdit = new QLineEdit;
143 comparePages2Label->setBuddy(pages2LineEdit);
144 pages2LineEdit->setToolTip(pages1LineEdit->toolTip());
145 compareButton = new QPushButton(tr("&Compare"));
146 compareButton->setEnabled(false);
147 compareButton->setDefault(true);
148 compareButton->setAutoDefault(true);
149 compareButton->setToolTip(tr("<p>Click to compare (or re-compare) "
150 "the documents—or to cancel a comparison that's "
151 "in progress."));
152 compareComboBox = new QComboBox;
153 compareComboBox->addItems(QStringList() << tr("Appearance")
154 << tr("Characters") << tr("Words"));
155 compareComboBox->setToolTip(
156 tr("<p>If the <b>Words</b> comparison "
157 "mode is chosen, then each page's text is compared "
158 "word by word (best for alphabetic languages like "
159 "English). "
160 "If the <b>Characters</b> comparison mode is chosen, "
161 "then each page's text is compared character by "
162 "character (best for logographic languages like Chinese "
163 "and Japanese). "
164 "If the <b>Appearance</b> comparison mode is chosen "
165 "then each page's visual appearance is compared. "
166 "Comparing appearance can be slow for large documents "
167 "and can also produce false positives—but is "
168 "absolutely precise."));
169 compareLabel = new QLabel(tr("Co&mpare:"));
170 compareLabel->setBuddy(compareComboBox);
171 viewDiffLabel = new QLabel(tr("&View:"));
172 viewDiffLabel->setToolTip(tr("<p>Shows each pair of pages which "
173 "are different. The comparison is textual unless the "
174 "<b>Appearance</b> comparison mode is chosen, in "
175 "which case the comparison is done visually. "
176 "Visual differences can occur if a paragraph is "
177 "formated differently or if an embedded diagram or "
178 "image has changed."));
179 viewDiffComboBox = new QComboBox;
180 viewDiffComboBox->addItem(tr("(Not viewing)"));
181 viewDiffLabel->setBuddy(viewDiffComboBox);
182 viewDiffComboBox->setToolTip(viewDiffLabel->toolTip());
183 showLabel = new QLabel(tr("S&how:"));
184 showLabel->setToolTip(tr("<p>In show <b>Highlighting</b> mode the "
185 "pages are shown side by side with their differences "
186 "highlighted. All the other modes are composition "
187 "modes which show the first PDF as-is and the "
188 "composition (blend) of the two PDFs."));
189 showComboBox = new QComboBox;
190 showComboBox->addItem(tr("%1Highlighting%2")
191 .arg(QChar(0xAB)).arg(QChar(0xBB)), -1);
192 showComboBox->addItem(tr("Not Src Xor Dest"),
193 QPainter::RasterOp_NotSourceXorDestination);
194 showComboBox->addItem(tr("Difference"),
195 QPainter::CompositionMode_Difference);
196 showComboBox->addItem(tr("Exclusion"),
197 QPainter::CompositionMode_Exclusion);
198 showComboBox->addItem(tr("Src Xor Dest"),
199 QPainter::RasterOp_SourceXorDestination);
200 showLabel->setBuddy(showComboBox);
201 showComboBox->setToolTip(showLabel->toolTip());
202 previousButton = new QPushButton(tr("Previo&us"));
203 previousButton->setToolTip(
204 "<p>Navigate to the previous pair of pages.");
205 #if QT_VERSION >= 0x040600
206 previousButton->setIcon(QIcon(":/left.png"));
207 #endif
208 nextButton = new QPushButton(tr("Ne&xt"));
209 nextButton->setToolTip("<p>Navigate to the next pair of pages.");
210 #if QT_VERSION >= 0x040600
211 nextButton->setIcon(QIcon(":/right.png"));
212 #endif
213 zoomLabel = new QLabel(tr("&Zoom:"));
214 zoomLabel->setToolTip(tr("<p>Determines the scale at which the "
215 "pages are shown."));
216 zoomSpinBox = new QSpinBox;
217 zoomLabel->setBuddy(zoomSpinBox);
218 zoomSpinBox->setRange(20, 800);
219 zoomSpinBox->setSuffix(tr(" %"));
220 zoomSpinBox->setSingleStep(5);
221 QSettings settings;
222 zoomSpinBox->setValue(settings.value("Zoom", 100).toInt());
223 zoomSpinBox->setToolTip(zoomLabel->toolTip());
224 zoningGroupBox = new QGroupBox(tr("Zo&ning"));
225 zoningGroupBox->setToolTip(tr("<p>Zoning is a computationally "
226 "expensive experimental mode that can reduce or "
227 "eliminate false positives particularly for pages "
228 "that have tables or that mix alphabetic and "
229 "logographic languages—it can also increase "
230 "false positives! Zoning only applies to text "
231 "comparisons."));
232 zoningGroupBox->setCheckable(true);
233 zoningGroupBox->setChecked(false);
234 columnsLabel = new QLabel(tr("Co&lumns:"));
235 columnsSpinBox = new QSpinBox;
236 columnsSpinBox->setRange(1, 16);
237 columnsSpinBox->setValue(settings.value("Columns", 1).toInt());
238 columnsSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
239 columnsSpinBox->setToolTip(tr("<p>Use this to tell DiffPDF how "
240 "many columns the page has; this should improve the "
241 "zoning."));
242 columnsLabel->setBuddy(columnsSpinBox);
243 toleranceRLabel = new QLabel(tr("Tolerance/&R:"));
244 toleranceRSpinBox = new QSpinBox;
245 toleranceRSpinBox->setRange(4, 144);
246 toleranceRSpinBox->setValue(settings.value("Tolerance/R", 8).toInt());
247 toleranceRSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
248 toleranceRSpinBox->setToolTip(tr("<p>This is the maximum distance "
249 "between text (word) rectangles for the rectangles to "
250 "appear in the same zone."));
251 toleranceRLabel->setBuddy(toleranceRSpinBox);
252 toleranceYLabel = new QLabel(tr("Tolerance/&Y:"));
253 toleranceYSpinBox = new QSpinBox;
254 toleranceYSpinBox->setRange(0, 32);
255 toleranceYSpinBox->setValue(settings.value("Tolerance/Y", 10).toInt());
256 toleranceYSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
257 toleranceYSpinBox->setToolTip(tr("<p>Text position <i>y</i> "
258 "coordinates are rounded to the nearest Tolerance/Y "
259 "value when zoning."));
260 toleranceYLabel->setBuddy(toleranceYSpinBox);
261 showZonesCheckBox = new QCheckBox(tr("Sho&w Zones"));
262 showZonesCheckBox->setToolTip(tr("<p>This shows the zones that are "
263 "being used and may be helpful when adjusting "
264 "tolerances. (Its original purpose was for debugging.)"));
265 marginsGroupBox = new QGroupBox(tr("&Exclude Margins"));
266 marginsGroupBox->setToolTip(tr("<p>If this is checked, anything "
267 "outside non-zero margins is ignored when comparing."));
268 marginsGroupBox->setCheckable(true);
269 marginsGroupBox->setChecked(settings.value("Margins/Exclude",
270 false).toBool());
271 topMarginLabel = new QLabel(tr("&Top:"));
272 topMarginSpinBox = new QSpinBox;
273 topMarginSpinBox->setSuffix(" pt");
274 topMarginSpinBox->setValue(settings.value("Margins/Top", 0).toInt());
275 topMarginSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
276 topMarginSpinBox->setToolTip(tr("<p>The top margin in points. "
277 "Anything above this will be ignored."));
278 topMarginLabel->setBuddy(topMarginSpinBox);
279 bottomMarginLabel = new QLabel(tr("&Bottom:"));
280 bottomMarginSpinBox = new QSpinBox;
281 bottomMarginSpinBox->setSuffix(" pt");
282 bottomMarginSpinBox->setValue(settings.value("Margins/Bottom", 0)
283 .toInt());
284 bottomMarginSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
285 bottomMarginSpinBox->setToolTip(tr("<p>The bottom margin in points. "
286 "Anything below this will be ignored."));
287 bottomMarginLabel->setBuddy(bottomMarginSpinBox);
288 leftMarginLabel = new QLabel(tr("Le&ft:"));
289 leftMarginSpinBox = new QSpinBox;
290 leftMarginSpinBox->setSuffix(" pt");
291 leftMarginSpinBox->setValue(settings.value("Margins/Left", 0).toInt());
292 leftMarginSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
293 leftMarginSpinBox->setToolTip(tr("<p>The left margin in points. "
294 "Anything left of this will be ignored."));
295 leftMarginLabel->setBuddy(leftMarginSpinBox);
296 rightMarginLabel = new QLabel(tr("R&ight:"));
297 rightMarginSpinBox = new QSpinBox;
298 rightMarginSpinBox->setSuffix(" pt");
299 rightMarginSpinBox->setValue(settings.value("Margins/Right", 0)
300 .toInt());
301 rightMarginSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight);
302 rightMarginSpinBox->setToolTip(tr("<p>The right margin in points. "
303 "Anything right of this will be ignored."));
304 rightMarginLabel->setBuddy(rightMarginSpinBox);
305 statusLabel = new QLabel(tr("Choose files..."));
306 statusLabel->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken);
307 statusLabel->setMaximumHeight(statusLabel->minimumSizeHint().height());
308 optionsButton = new QPushButton(tr("&Options..."));
309 optionsButton->setToolTip(tr("Click to customize the application."));
310 saveButton = new QPushButton(tr("&Save As..."));
311 saveButton->setToolTip(tr("Save the differences."));
312 helpButton = new QPushButton(tr("Help"));
313 helpButton->setShortcut(tr("F1"));
314 helpButton->setToolTip(tr("Click for basic help."));
315 aboutButton = new QPushButton(tr("&About"));
316 aboutButton->setToolTip(tr("Click for copyright and credits."));
317 quitButton = new QPushButton(tr("&Quit"));
318 quitButton->setToolTip(tr("Click to terminate the application."));
319 page1Label = new Label;
320 page1Label->setAlignment(Qt::AlignTop|Qt::AlignLeft);
321 page1Label->setToolTip(tr("<p>Shows the first (left hand) document's "
322 "page that corresponds to the page shown in the "
323 "View Difference combobox."));
324 page2Label = new Label;
325 page2Label->setAlignment(Qt::AlignTop|Qt::AlignLeft);
326 page2Label->setToolTip(tr("<p>Shows the second (right hand) "
327 "document's page that corresponds to the page shown in "
328 "the View Difference combobox."));
329 logEdit = new QPlainTextEdit;
330
331 QList<QWidget*> widgets;
332 widgets << setFile1Button << filename1LineEdit << pages1LineEdit
333 << page1Label << setFile2Button << filename2LineEdit
334 << pages2LineEdit << page2Label << compareButton
335 << compareComboBox << viewDiffLabel << viewDiffComboBox
336 << showLabel << showComboBox << zoomLabel << zoomSpinBox
337 << optionsButton << zoningGroupBox << columnsLabel
338 << columnsSpinBox << toleranceRLabel << toleranceRSpinBox
339 << toleranceYLabel << toleranceYSpinBox << marginsGroupBox
340 << topMarginLabel << topMarginSpinBox << bottomMarginSpinBox
341 << bottomMarginLabel << leftMarginLabel << leftMarginSpinBox
342 << rightMarginLabel << rightMarginSpinBox << saveButton
343 << helpButton << aboutButton << quitButton << logEdit
344 << previousButton << nextButton << showZonesCheckBox;
345 foreach (QWidget *widget, widgets)
346 if (!widget->toolTip().isEmpty())
347 widget->installEventFilter(this);
348 }
349
350
createCentralArea()351 void MainWindow::createCentralArea()
352 {
353 QHBoxLayout *topLeftLayout = new QHBoxLayout;
354 topLeftLayout->addWidget(setFile1Button);
355 topLeftLayout->addWidget(filename1LineEdit, 3);
356 topLeftLayout->addWidget(comparePages1Label);
357 topLeftLayout->addWidget(pages1LineEdit, 2);
358 area1 = new QScrollArea;
359 area1->setWidget(page1Label);
360 area1->setWidgetResizable(true);
361 QVBoxLayout *leftLayout = new QVBoxLayout;
362 leftLayout->addLayout(topLeftLayout);
363 leftLayout->addWidget(area1, 1);
364 QWidget *leftWidget = new QWidget;
365 leftWidget->setLayout(leftLayout);
366
367 QHBoxLayout *topRightLayout = new QHBoxLayout;
368 topRightLayout->addWidget(setFile2Button);
369 topRightLayout->addWidget(filename2LineEdit, 3);
370 topRightLayout->addWidget(comparePages2Label);
371 topRightLayout->addWidget(pages2LineEdit, 2);
372 area2 = new QScrollArea;
373 area2->setWidget(page2Label);
374 area2->setWidgetResizable(true);
375 QVBoxLayout *rightLayout = new QVBoxLayout;
376 rightLayout->addLayout(topRightLayout);
377 rightLayout->addWidget(area2, 1);
378 QWidget *rightWidget = new QWidget;
379 rightWidget->setLayout(rightLayout);
380
381 splitter = new QSplitter(Qt::Horizontal);
382 splitter->addWidget(leftWidget);
383 splitter->addWidget(rightWidget);
384 QSettings settings;
385 splitter->restoreState(settings.value("MainWindow/ViewSplitter")
386 .toByteArray());
387
388 setCentralWidget(splitter);
389 }
390
391
createDockWidgets()392 void MainWindow::createDockWidgets()
393 {
394 setTabPosition(Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea,
395 QTabWidget::North);
396 setTabPosition(Qt::TopDockWidgetArea|Qt::BottomDockWidgetArea,
397 QTabWidget::West);
398 QDockWidget::DockWidgetFeatures features =
399 QDockWidget::DockWidgetMovable|
400 QDockWidget::DockWidgetFloatable;
401
402 controlDockWidget = new QDockWidget(tr("Controls"), this);
403 controlDockWidget->setObjectName("Controls");
404 controlDockWidget->setFeatures(features);
405 controlLayout = new QBoxLayout(QBoxLayout::TopToBottom);
406 compareLayout = new QHBoxLayout;
407 compareLayout->addWidget(compareLabel);
408 compareLayout->addWidget(compareComboBox, 1);
409 controlLayout->addLayout(compareLayout);
410 QHBoxLayout *viewLayout = new QHBoxLayout;
411 viewLayout->addWidget(viewDiffLabel);
412 viewLayout->addWidget(viewDiffComboBox, 1);
413 controlLayout->addLayout(viewLayout);
414 QHBoxLayout *showLayout = new QHBoxLayout;
415 showLayout->addWidget(showLabel);
416 showLayout->addWidget(showComboBox, 1);
417 controlLayout->addLayout(showLayout);
418 QHBoxLayout *navigationLayout = new QHBoxLayout;
419 navigationLayout->addWidget(previousButton);
420 navigationLayout->addWidget(nextButton);
421 controlLayout->addLayout(navigationLayout);
422 QHBoxLayout *zoomLayout = new QHBoxLayout;
423 zoomLayout->addWidget(zoomLabel);
424 zoomLayout->addWidget(zoomSpinBox);
425 controlLayout->addLayout(zoomLayout);
426 controlLayout->addWidget(statusLabel);
427 controlLayout->addStretch();
428 QWidget *widget = new QWidget;
429 widget->setLayout(controlLayout);
430 controlDockWidget->setWidget(widget);
431 addDockWidget(controlDockArea, controlDockWidget);
432
433 actionDockWidget = new QDockWidget(tr("Actions"), this);
434 actionDockWidget->setObjectName("Actions");
435 actionDockWidget->setFeatures(features);
436 actionLayout = new QBoxLayout(QBoxLayout::TopToBottom);
437 actionLayout->addWidget(compareButton);
438 QHBoxLayout *optionLayout = new QHBoxLayout;
439 optionLayout->addWidget(optionsButton);
440 optionLayout->addWidget(saveButton);
441 actionLayout->addLayout(optionLayout);
442 QHBoxLayout *helpLayout = new QHBoxLayout;
443 helpLayout->addWidget(helpButton);
444 helpLayout->addWidget(aboutButton);
445 actionLayout->addLayout(helpLayout);
446 actionLayout->addWidget(quitButton);
447 actionLayout->addStretch();
448 widget = new QWidget;
449 widget->setLayout(actionLayout);
450 actionDockWidget->setWidget(widget);
451 addDockWidget(actionDockArea, actionDockWidget);
452
453 marginsDockWidget = new QDockWidget(tr("Margins"), this);
454 marginsDockWidget->setObjectName("Margins");
455 marginsDockWidget->setFeatures(features|
456 QDockWidget::DockWidgetClosable);
457 marginsLayout = new QBoxLayout(QBoxLayout::TopToBottom);
458 QHBoxLayout *topMarginLayout = new QHBoxLayout;
459 topMarginLayout->addWidget(topMarginLabel);
460 topMarginLayout->addWidget(topMarginSpinBox);
461 marginsLayout->addLayout(topMarginLayout);
462 QHBoxLayout *bottomMarginLayout = new QHBoxLayout;
463 bottomMarginLayout->addWidget(bottomMarginLabel);
464 bottomMarginLayout->addWidget(bottomMarginSpinBox);
465 marginsLayout->addLayout(bottomMarginLayout);
466 QHBoxLayout *leftMarginLayout = new QHBoxLayout;
467 leftMarginLayout->addWidget(leftMarginLabel);
468 leftMarginLayout->addWidget(leftMarginSpinBox);
469 marginsLayout->addLayout(leftMarginLayout);
470 QHBoxLayout *rightMarginLayout = new QHBoxLayout;
471 rightMarginLayout->addWidget(rightMarginLabel);
472 rightMarginLayout->addWidget(rightMarginSpinBox);
473 marginsLayout->addLayout(rightMarginLayout);
474 marginsLayout->addStretch();
475 marginsGroupBox->setLayout(marginsLayout);
476 marginsDockWidget->setWidget(marginsGroupBox);
477 addDockWidget(marginsDockArea, marginsDockWidget);
478
479 zoningDockWidget = new QDockWidget(tr("Zoning"), this);
480 zoningDockWidget->setObjectName("Zoning");
481 zoningDockWidget->setFeatures(features|
482 QDockWidget::DockWidgetClosable);
483 zoningLayout = new QBoxLayout(QBoxLayout::TopToBottom);
484 QHBoxLayout *columnLayout = new QHBoxLayout;
485 columnLayout->addWidget(columnsLabel);
486 columnLayout->addWidget(columnsSpinBox);
487 zoningLayout->addLayout(columnLayout);
488 QHBoxLayout *toleranceLayout1 = new QHBoxLayout;
489 toleranceLayout1->addWidget(toleranceRLabel);
490 toleranceLayout1->addWidget(toleranceRSpinBox);
491 zoningLayout->addLayout(toleranceLayout1);
492 QHBoxLayout *toleranceLayout2 = new QHBoxLayout;
493 toleranceLayout2->addWidget(toleranceYLabel);
494 toleranceLayout2->addWidget(toleranceYSpinBox);
495 zoningLayout->addLayout(toleranceLayout2);
496 zoningLayout->addWidget(showZonesCheckBox);
497 zoningLayout->addStretch();
498 zoningGroupBox->setLayout(zoningLayout);
499 zoningDockWidget->setWidget(zoningGroupBox);
500 addDockWidget(zoningDockArea, zoningDockWidget);
501
502 logDockWidget = new QDockWidget(tr("Log"), this);
503 logDockWidget->setObjectName("Log");
504 logDockWidget->setFeatures(features|QDockWidget::DockWidgetClosable);
505 logDockWidget->setWidget(logEdit);
506 addDockWidget(Qt::RightDockWidgetArea, logDockWidget);
507
508 tabifyDockWidget(marginsDockWidget, controlDockWidget);
509 tabifyDockWidget(logDockWidget, zoningDockWidget);
510 tabifyDockWidget(zoningDockWidget, actionDockWidget);
511 }
512
513
createConnections()514 void MainWindow::createConnections()
515 {
516 connect(area1->verticalScrollBar(), SIGNAL(valueChanged(int)),
517 area2->verticalScrollBar(), SLOT(setValue(int)));
518 connect(area2->verticalScrollBar(), SIGNAL(valueChanged(int)),
519 area1->verticalScrollBar(), SLOT(setValue(int)));
520 connect(area1->horizontalScrollBar(), SIGNAL(valueChanged(int)),
521 area2->horizontalScrollBar(), SLOT(setValue(int)));
522 connect(area2->horizontalScrollBar(), SIGNAL(valueChanged(int)),
523 area1->horizontalScrollBar(), SLOT(setValue(int)));
524
525 connect(filename1LineEdit, SIGNAL(textEdited(const QString&)),
526 this, SLOT(updateUi()));
527 connect(filename1LineEdit,
528 SIGNAL(filenamesDropped(const QStringList&)),
529 this, SLOT(setFiles1(const QStringList&)));
530 connect(filename2LineEdit, SIGNAL(textEdited(const QString&)),
531 this, SLOT(updateUi()));
532 connect(filename2LineEdit,
533 SIGNAL(filenamesDropped(const QStringList&)),
534 this, SLOT(setFiles2(const QStringList&)));
535
536 connect(page1Label, SIGNAL(filenamesDropped(const QStringList&)),
537 this, SLOT(setFiles1(const QStringList&)));
538 connect(page2Label, SIGNAL(filenamesDropped(const QStringList&)),
539 this, SLOT(setFiles2(const QStringList&)));
540
541 connect(compareComboBox, SIGNAL(currentIndexChanged(int)),
542 this, SLOT(updateUi()));
543 connect(compareComboBox, SIGNAL(currentIndexChanged(int)),
544 this, SLOT(updateViews()));
545
546 connect(viewDiffComboBox, SIGNAL(currentIndexChanged(int)),
547 this, SLOT(updateViews(int)));
548 connect(viewDiffComboBox, SIGNAL(currentIndexChanged(int)),
549 this, SLOT(updateUi()));
550 connect(showComboBox, SIGNAL(currentIndexChanged(int)),
551 this, SLOT(updateViews()));
552 connect(previousButton, SIGNAL(clicked()),
553 this, SLOT(previousPages()));
554 connect(nextButton, SIGNAL(clicked()), this, SLOT(nextPages()));
555 connect(setFile1Button, SIGNAL(clicked()), this, SLOT(setFile1()));
556 connect(setFile2Button, SIGNAL(clicked()), this, SLOT(setFile2()));
557 connect(compareButton, SIGNAL(clicked()), this, SLOT(compare()));
558 connect(zoomSpinBox, SIGNAL(valueChanged(int)),
559 this, SLOT(updateViews()));
560 connect(zoningGroupBox, SIGNAL(toggled(bool)),
561 this, SLOT(updateUi()));
562 connect(zoningGroupBox, SIGNAL(toggled(bool)),
563 this, SLOT(updateViews()));
564 connect(columnsSpinBox, SIGNAL(valueChanged(int)),
565 this, SLOT(updateViews()));
566 connect(toleranceRSpinBox, SIGNAL(valueChanged(int)),
567 this, SLOT(updateViews()));
568 connect(toleranceYSpinBox, SIGNAL(valueChanged(int)),
569 this, SLOT(updateViews()));
570 connect(showZonesCheckBox, SIGNAL(toggled(bool)),
571 this, SLOT(updateViews()));
572 connect(marginsGroupBox, SIGNAL(toggled(bool)),
573 this, SLOT(updateUi()));
574 connect(marginsGroupBox, SIGNAL(toggled(bool)),
575 this, SLOT(updateViews()));
576 connect(leftMarginSpinBox, SIGNAL(valueChanged(int)),
577 this, SLOT(updateViews()));
578 connect(rightMarginSpinBox, SIGNAL(valueChanged(int)),
579 this, SLOT(updateViews()));
580 connect(topMarginSpinBox, SIGNAL(valueChanged(int)),
581 this, SLOT(updateViews()));
582 connect(bottomMarginSpinBox, SIGNAL(valueChanged(int)),
583 this, SLOT(updateViews()));
584 connect(page1Label, SIGNAL(clicked(const QPoint&)),
585 this, SLOT(setAMargin(const QPoint&)));
586 connect(page2Label, SIGNAL(clicked(const QPoint&)),
587 this, SLOT(setAMargin(const QPoint&)));
588
589 connect(optionsButton, SIGNAL(clicked()), this, SLOT(options()));
590 connect(saveButton, SIGNAL(clicked()), this, SLOT(save()));
591 connect(helpButton, SIGNAL(clicked()), this, SLOT(help()));
592 connect(aboutButton, SIGNAL(clicked()), this, SLOT(about()));
593 connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
594
595 connect(controlDockWidget,
596 SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
597 this, SLOT(controlDockLocationChanged(Qt::DockWidgetArea)));
598 connect(actionDockWidget,
599 SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
600 this, SLOT(actionDockLocationChanged(Qt::DockWidgetArea)));
601 connect(zoningDockWidget,
602 SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
603 this, SLOT(zoningDockLocationChanged(Qt::DockWidgetArea)));
604 connect(marginsDockWidget,
605 SIGNAL(dockLocationChanged(Qt::DockWidgetArea)),
606 this, SLOT(marginsDockLocationChanged(Qt::DockWidgetArea)));
607 connect(controlDockWidget, SIGNAL(topLevelChanged(bool)),
608 this, SLOT(controlTopLevelChanged(bool)));
609 connect(actionDockWidget, SIGNAL(topLevelChanged(bool)),
610 this, SLOT(actionTopLevelChanged(bool)));
611 connect(zoningDockWidget, SIGNAL(topLevelChanged(bool)),
612 this, SLOT(zoningTopLevelChanged(bool)));
613 connect(marginsDockWidget, SIGNAL(topLevelChanged(bool)),
614 this, SLOT(marginsTopLevelChanged(bool)));
615 connect(logDockWidget, SIGNAL(topLevelChanged(bool)),
616 this, SLOT(logTopLevelChanged(bool)));
617 }
618
619
initialize(const QString & filename1,const QString & filename2)620 void MainWindow::initialize(const QString &filename1,
621 const QString &filename2)
622 {
623 if (!filename1.isEmpty()) {
624 setFile1(filename1);
625 setFile2Button->setFocus();
626 if (!filename2.isEmpty()) {
627 setFile2(filename2);
628 compare();
629 }
630 }
631 else
632 updateUi();
633 }
634
635
updateUi()636 void MainWindow::updateUi()
637 {
638 compareButton->setEnabled(!filename1LineEdit->text().isEmpty() &&
639 !filename2LineEdit->text().isEmpty());
640 saveButton->setEnabled(viewDiffComboBox->count() > 1);
641 if (!showZonesCheckBox->isEnabled())
642 showZonesCheckBox->setChecked(false);
643 if (compareComboBox->currentIndex() != CompareAppearance)
644 showComboBox->setCurrentIndex(0);
645 showComboBox->setEnabled(compareComboBox->currentIndex() ==
646 CompareAppearance);
647 QPushButton *button = qobject_cast<QPushButton*>(focusWidget());
648 bool enableNavigationButton = (button == previousButton ||
649 button == nextButton);
650 previousButton->setEnabled(viewDiffComboBox->count() > 1 &&
651 viewDiffComboBox->currentIndex() > 0);
652 nextButton->setEnabled(viewDiffComboBox->count() > 1 &&
653 viewDiffComboBox->currentIndex() + 1 <
654 viewDiffComboBox->count());
655 if (enableNavigationButton && !(previousButton->isEnabled() &&
656 nextButton->isEnabled())) {
657 if (previousButton->isEnabled())
658 previousButton->setFocus();
659 else
660 nextButton->setFocus();
661 }
662 if (marginsGroupBox->isChecked()) {
663 page1Label->setCursor(Qt::PointingHandCursor);
664 page2Label->setCursor(Qt::PointingHandCursor);
665 }
666 else {
667 page1Label->setCursor(Qt::ArrowCursor);
668 page2Label->setCursor(Qt::ArrowCursor);
669 }
670 }
671
672
controlDockLocationChanged(Qt::DockWidgetArea area)673 void MainWindow::controlDockLocationChanged(Qt::DockWidgetArea area)
674 {
675 if (area == Qt::TopDockWidgetArea ||
676 area == Qt::BottomDockWidgetArea) {
677 controlLayout->setDirection(QBoxLayout::LeftToRight);
678 }
679 else {
680 controlLayout->setDirection(QBoxLayout::TopToBottom);
681 }
682 controlDockArea = area;
683 }
684
685
actionDockLocationChanged(Qt::DockWidgetArea area)686 void MainWindow::actionDockLocationChanged(Qt::DockWidgetArea area)
687 {
688 if (area == Qt::TopDockWidgetArea ||
689 area == Qt::BottomDockWidgetArea)
690 actionLayout->setDirection(QBoxLayout::LeftToRight);
691 else
692 actionLayout->setDirection(QBoxLayout::TopToBottom);
693 actionDockArea = area;
694 }
695
696
zoningDockLocationChanged(Qt::DockWidgetArea area)697 void MainWindow::zoningDockLocationChanged(Qt::DockWidgetArea area)
698 {
699 if (area == Qt::TopDockWidgetArea ||
700 area == Qt::BottomDockWidgetArea)
701 zoningLayout->setDirection(QBoxLayout::LeftToRight);
702 else
703 zoningLayout->setDirection(QBoxLayout::TopToBottom);
704 zoningDockArea = area;
705 }
706
707
marginsDockLocationChanged(Qt::DockWidgetArea area)708 void MainWindow::marginsDockLocationChanged(Qt::DockWidgetArea area)
709 {
710 if (area == Qt::TopDockWidgetArea ||
711 area == Qt::BottomDockWidgetArea)
712 marginsLayout->setDirection(QBoxLayout::LeftToRight);
713 else
714 marginsLayout->setDirection(QBoxLayout::TopToBottom);
715 marginsDockArea = area;
716 }
717
718
controlTopLevelChanged(bool floating)719 void MainWindow::controlTopLevelChanged(bool floating)
720 {
721 controlLayout->setDirection(floating ? QBoxLayout::TopToBottom
722 : QBoxLayout::LeftToRight);
723 if (QWidget *widget = static_cast<QWidget*>(controlLayout->parent()))
724 widget->setFixedSize(floating ? widget->minimumSizeHint()
725 : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
726 controlDockWidget->setWindowTitle(floating ? tr("DiffPDF — Controls")
727 : tr("Controls"));
728 }
729
730
actionTopLevelChanged(bool floating)731 void MainWindow::actionTopLevelChanged(bool floating)
732 {
733 actionLayout->setDirection(floating ? QBoxLayout::TopToBottom
734 : QBoxLayout::LeftToRight);
735 if (QWidget *widget = static_cast<QWidget*>(actionLayout->parent()))
736 widget->setFixedSize(floating ? widget->minimumSizeHint()
737 : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
738 actionDockWidget->setWindowTitle(floating ? tr("DiffPDF — Actions")
739 : tr("Actions"));
740 }
741
742
zoningTopLevelChanged(bool floating)743 void MainWindow::zoningTopLevelChanged(bool floating)
744 {
745 zoningLayout->setDirection(floating ? QBoxLayout::TopToBottom
746 : QBoxLayout::LeftToRight);
747 if (QWidget *widget = static_cast<QWidget*>(zoningLayout->parent()))
748 widget->setFixedSize(floating ? widget->minimumSizeHint()
749 : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
750 zoningDockWidget->setWindowTitle(floating ? tr("DiffPDF — Zoning")
751 : tr("Zoning"));
752 }
753
754
marginsTopLevelChanged(bool floating)755 void MainWindow::marginsTopLevelChanged(bool floating)
756 {
757 marginsLayout->setDirection(floating ? QBoxLayout::TopToBottom
758 : QBoxLayout::LeftToRight);
759 if (QWidget *widget = static_cast<QWidget*>(marginsLayout->parent()))
760 widget->setFixedSize(floating ? widget->minimumSizeHint()
761 : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
762 marginsDockWidget->setWindowTitle(floating ? tr("DiffPDF — Margins")
763 : tr("Margins"));
764 }
765
766
logTopLevelChanged(bool floating)767 void MainWindow::logTopLevelChanged(bool floating)
768 {
769 logDockWidget->setWindowTitle(floating ? tr("DiffPDF — Log")
770 : tr("Log"));
771 }
772
773
previousPages()774 void MainWindow::previousPages()
775 {
776 int i = viewDiffComboBox->currentIndex();
777 if (i > 0)
778 viewDiffComboBox->setCurrentIndex(i - 1);
779 }
780
781
nextPages()782 void MainWindow::nextPages()
783 {
784 int i = viewDiffComboBox->currentIndex();
785 if (i + 1 < viewDiffComboBox->count())
786 viewDiffComboBox->setCurrentIndex(i + 1);
787 }
788
789
updateViews(int index)790 void MainWindow::updateViews(int index)
791 {
792 if (index == 0) {
793 page1Label->clear();
794 page2Label->clear();
795 return;
796 }
797 else if (index == -1)
798 index = viewDiffComboBox->currentIndex();
799 PagePair pair = viewDiffComboBox->itemData(index).value<PagePair>();
800 if (pair.isNull())
801 return;
802
803 QString filename1 = filename1LineEdit->text();
804 PdfDocument pdf1 = getPdf(filename1);
805 if (!pdf1)
806 return;
807 PdfPage page1(pdf1->page(pair.left));
808 if (!page1)
809 return;
810
811 QString filename2 = filename2LineEdit->text();
812 PdfDocument pdf2 = getPdf(filename2);
813 if (!pdf2)
814 return;
815 PdfPage page2(pdf2->page(pair.right));
816 if (!page2)
817 return;
818
819 const QPair<QString, QString> keys = cacheKeys(index, pair);
820 const QPair<QPixmap, QPixmap> pixmaps = populatePixmaps(pdf1, page1,
821 pdf2, page2, pair.hasVisualDifference, keys.first,
822 keys.second);
823 page1Label->setPixmap(pixmaps.first);
824 page2Label->setPixmap(pixmaps.second);
825 if (showZonesCheckBox->isChecked())
826 showZones();
827 if (marginsGroupBox->isChecked())
828 showMargins();
829 }
830
831
cacheKeys(const int index,const PagePair & pair) const832 const QPair<QString, QString> MainWindow::cacheKeys(const int index,
833 const PagePair &pair) const
834 {
835 int comparisonMode;
836 if (compareComboBox->currentIndex() == CompareAppearance)
837 comparisonMode = showComboBox->currentIndex();
838 else
839 comparisonMode = -compareComboBox->currentIndex();
840 QString zoning;
841 if (zoningGroupBox->isChecked())
842 zoning = QString("%1:%2:%3").arg(columnsSpinBox->value())
843 .arg(toleranceRSpinBox->value())
844 .arg(toleranceYSpinBox->value());
845 QString margins;
846 if (marginsGroupBox->isChecked())
847 margins = QString("%1:%2:%3:%4").arg(topMarginSpinBox->value())
848 .arg(bottomMarginSpinBox->value())
849 .arg(leftMarginSpinBox->value())
850 .arg(rightMarginSpinBox->value());
851 const QString key = QString("%1:%2:%3:%4:%5").arg(index)
852 .arg(zoomSpinBox->value()).arg(comparisonMode).arg(zoning)
853 .arg(margins);
854 const QString key1 = QString("1:%1:%2:%3").arg(key).arg(pair.left)
855 .arg(filename1LineEdit->text());
856 const QString key2 = QString("2:%1:%2:%3").arg(key).arg(pair.right)
857 .arg(filename2LineEdit->text());
858 return qMakePair(key1, key2);
859 }
860
861
populatePixmaps(const PdfDocument & pdf1,const PdfPage & page1,const PdfDocument & pdf2,const PdfPage & page2,bool hasVisualDifference,const QString & key1,const QString & key2)862 const QPair<QPixmap, QPixmap> MainWindow::populatePixmaps(
863 const PdfDocument &pdf1, const PdfPage &page1,
864 const PdfDocument &pdf2, const PdfPage &page2,
865 bool hasVisualDifference, const QString &key1,
866 const QString &key2)
867 {
868 QPixmap pixmap1;
869 QPixmap pixmap2;
870 #if QT_VERSION >= 0x040600
871 if (!QPixmapCache::find(key1, &pixmap1) ||
872 !QPixmapCache::find(key2, &pixmap2)) {
873 #else
874 if (!QPixmapCache::find(key1, pixmap1) ||
875 !QPixmapCache::find(key2, pixmap2)) {
876 #endif
877 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
878 const int DPI = static_cast<int>(POINTS_PER_INCH *
879 (zoomSpinBox->value() / 100.0));
880 const bool compareText = compareComboBox->currentIndex() !=
881 CompareAppearance;
882 QImage plainImage1;
883 QImage plainImage2;
884 if (hasVisualDifference || !compareText) {
885 plainImage1 = page1->renderToImage(DPI, DPI);
886 plainImage2 = page2->renderToImage(DPI, DPI);
887 }
888 pdf1->setRenderHint(Poppler::Document::Antialiasing);
889 pdf1->setRenderHint(Poppler::Document::TextAntialiasing);
890 pdf2->setRenderHint(Poppler::Document::Antialiasing);
891 pdf2->setRenderHint(Poppler::Document::TextAntialiasing);
892 QImage image1 = page1->renderToImage(DPI, DPI);
893 QImage image2 = page2->renderToImage(DPI, DPI);
894
895 if (compareComboBox->currentIndex() != CompareAppearance ||
896 showComboBox->currentIndex() == 0) {
897 QPainterPath highlighted1;
898 QPainterPath highlighted2;
899 if (hasVisualDifference || !compareText)
900 computeVisualHighlights(&highlighted1, &highlighted2,
901 plainImage1, plainImage2);
902 else
903 computeTextHighlights(&highlighted1, &highlighted2, page1,
904 page2, DPI);
905 if (!highlighted1.isEmpty())
906 paintOnImage(highlighted1, &image1);
907 if (!highlighted2.isEmpty())
908 paintOnImage(highlighted2, &image2);
909 if (highlighted1.isEmpty() && highlighted2.isEmpty()) {
910 QFont font("Helvetica", 14);
911 font.setOverline(true);
912 font.setUnderline(true);
913 highlighted1.addText(DPI / 4, DPI / 4, font,
914 tr("DiffPDF: False Positive"));
915 paintOnImage(highlighted1, &image1);
916 }
917 pixmap1 = QPixmap::fromImage(image1);
918 pixmap2 = QPixmap::fromImage(image2);
919 } else {
920 pixmap1 = QPixmap::fromImage(image1);
921 QImage composed(image1.size(), image1.format());
922 QPainter painter(&composed);
923 painter.setCompositionMode(QPainter::CompositionMode_Source);
924 painter.fillRect(composed.rect(), Qt::transparent);
925 painter.setCompositionMode(
926 QPainter::CompositionMode_SourceOver);
927 painter.drawImage(0, 0, image1);
928 painter.setCompositionMode(
929 static_cast<QPainter::CompositionMode>(
930 showComboBox->itemData(
931 showComboBox->currentIndex()).toInt()));
932 painter.drawImage(0, 0, image2);
933 painter.setCompositionMode(
934 QPainter::CompositionMode_DestinationOver);
935 painter.fillRect(composed.rect(), Qt::white);
936 painter.end();
937 pixmap2 = QPixmap::fromImage(composed);
938 }
939 QPixmapCache::insert(key1, pixmap1);
940 QPixmapCache::insert(key2, pixmap2);
941 QApplication::restoreOverrideCursor();
942 }
943 return qMakePair(pixmap1, pixmap2);
944 }
945
946
947 void MainWindow::computeTextHighlights(QPainterPath *highlighted1,
948 QPainterPath *highlighted2, const PdfPage &page1,
949 const PdfPage &page2, const int DPI)
950 {
951 const bool ComparingWords = compareComboBox->currentIndex() ==
952 CompareWords;
953 QRectF rect1;
954 QRectF rect2;
955 QSettings settings;
956 const int OVERLAP = settings.value("Overlap", 5).toInt();
957 const bool COMBINE = settings.value("CombineTextHighlighting", true)
958 .toBool();
959 QRectF rect;
960 if (marginsGroupBox->isChecked())
961 rect = pointRectForMargins(page1->pageSize());
962 const TextBoxList list1 = getTextBoxes(page1, rect);
963 const TextBoxList list2 = getTextBoxes(page2, rect);
964 TextItems items1 = ComparingWords ? getWords(list1)
965 : getCharacters(list1);
966 TextItems items2 = ComparingWords ? getWords(list2)
967 : getCharacters(list2);
968 const int ToleranceY = toleranceYSpinBox->value();
969 if (zoningGroupBox->isChecked()) {
970 const int ToleranceR = toleranceRSpinBox->value();
971 const int Columns = columnsSpinBox->value();
972 items1.columnZoneYxOrder(page1->pageSize().width(), ToleranceR,
973 ToleranceY, Columns);
974 items2.columnZoneYxOrder(page2->pageSize().width(), ToleranceR,
975 ToleranceY, Columns);
976 }
977
978 if (debug >= DebugShowTexts) {
979 const bool Yx = debug == DebugShowTextsAndYX;
980 items1.debug(1, ToleranceY, ComparingWords, Yx);
981 items2.debug(2, ToleranceY, ComparingWords, Yx);
982 }
983
984 SequenceMatcher matcher(items1.texts(), items2.texts());
985 RangesPair rangesPair = computeRanges(&matcher);
986 rangesPair = invertRanges(rangesPair.first, items1.count(),
987 rangesPair.second, items2.count());
988
989 foreach (int index, rangesPair.first)
990 addHighlighting(&rect1, highlighted1, items1.at(index).rect,
991 OVERLAP, DPI, COMBINE);
992 if (!rect1.isNull() && !rangesPair.first.isEmpty())
993 highlighted1->addRect(rect1);
994 foreach (int index, rangesPair.second)
995 addHighlighting(&rect2, highlighted2, items2.at(index).rect,
996 OVERLAP, DPI, COMBINE);
997 if (!rect2.isNull() && !rangesPair.second.isEmpty())
998 highlighted2->addRect(rect2);
999 }
1000
1001
1002 void MainWindow::addHighlighting(QRectF *bigRect,
1003 QPainterPath *highlighted, const QRectF wordOrCharRect,
1004 const int OVERLAP, const int DPI, const bool COMBINE)
1005 {
1006 QRectF rect = wordOrCharRect;
1007 scaleRect(DPI, &rect);
1008 if (COMBINE && rect.adjusted(-OVERLAP, -OVERLAP, OVERLAP, OVERLAP)
1009 .intersects(*bigRect))
1010 *bigRect = bigRect->united(rect);
1011 else {
1012 highlighted->addRect(*bigRect);
1013 *bigRect = rect;
1014 }
1015 }
1016
1017
1018 void MainWindow::computeVisualHighlights(QPainterPath *highlighted1,
1019 QPainterPath *highlighted2, const QImage &plainImage1,
1020 const QImage &plainImage2)
1021 {
1022 QSettings settings;
1023 const int SQUARE_SIZE = settings.value("SquareSize", 10).toInt();
1024 QRect box;
1025 if (marginsGroupBox->isChecked())
1026 box = pixelRectForMargins(plainImage1.size());
1027 QRect target;
1028 for (int x = 0; x < plainImage1.width(); x += SQUARE_SIZE) {
1029 for (int y = 0; y < plainImage1.height(); y += SQUARE_SIZE) {
1030 const QRect rect(x, y, SQUARE_SIZE, SQUARE_SIZE);
1031 if (!box.isEmpty() && !box.contains(rect))
1032 continue;
1033 QImage temp1 = plainImage1.copy(rect);
1034 QImage temp2 = plainImage2.copy(rect);
1035 if (temp1 != temp2) {
1036 if (rect.adjusted(-1, -1, 1, 1).intersects(target))
1037 target = target.united(rect);
1038 else {
1039 highlighted1->addRect(target);
1040 highlighted2->addRect(target);
1041 target = rect;
1042 }
1043 }
1044 }
1045 }
1046 if (!target.isNull()) {
1047 highlighted1->addRect(target);
1048 highlighted2->addRect(target);
1049 }
1050 }
1051
1052
1053 QRect MainWindow::pixelRectForMargins(const QSize &size)
1054 {
1055 const int DPI = static_cast<int>(POINTS_PER_INCH *
1056 (zoomSpinBox->value() / 100.0));
1057 int top = pixelOffsetForPointValue(DPI, topMarginSpinBox->value());
1058 int left = pixelOffsetForPointValue(DPI, leftMarginSpinBox->value());
1059 int right = pixelOffsetForPointValue(DPI, rightMarginSpinBox->value());
1060 int bottom = pixelOffsetForPointValue(DPI,
1061 bottomMarginSpinBox->value());
1062 return QRect(QPoint(left, top),
1063 QPoint(size.width() - right, size.height() - bottom));
1064 }
1065
1066
1067 void MainWindow::paintOnImage(const QPainterPath &path, QImage *image)
1068 {
1069 QPen pen_(pen);
1070 QBrush brush_(brush);
1071 QColor color = pen.color();
1072 QSettings settings;
1073 const qreal Alpha = settings.value("Opacity", 13).toInt() / 100.0;
1074 color.setAlphaF(Alpha);
1075 pen_.setColor(color);
1076 brush_.setColor(color);
1077
1078 QPainter painter(image);
1079 painter.setRenderHint(QPainter::Antialiasing);
1080 painter.setPen(pen_);
1081 painter.setBrush(brush_);
1082
1083 const int SQUARE_SIZE = settings.value("SquareSize", 10).toInt();
1084 const double RULE_WIDTH = settings.value("RuleWidth", 1.5).toDouble();
1085 QRectF rect = path.boundingRect();
1086 if (rect.width() < SQUARE_SIZE && rect.height() < SQUARE_SIZE) {
1087 rect.setHeight(SQUARE_SIZE);
1088 rect.setWidth(SQUARE_SIZE);
1089 painter.drawRect(rect);
1090 if (!qFuzzyCompare(RULE_WIDTH, 0.0)) {
1091 painter.setPen(QPen(pen.color()));
1092 painter.drawRect(0, rect.y(), RULE_WIDTH, rect.height());
1093 }
1094 }
1095 else {
1096 QPainterPath path_(path);
1097 path_.setFillRule(Qt::WindingFill);
1098 painter.drawPath(path_);
1099 if (!qFuzzyCompare(RULE_WIDTH, 0.0)) {
1100 painter.setPen(QPen(pen.color()));
1101 QList<QPolygonF> polygons = path_.toFillPolygons();
1102 foreach (const QPolygonF &polygon, polygons) {
1103 const QRectF rect = polygon.boundingRect();
1104 painter.drawRect(0, rect.y(), RULE_WIDTH, rect.height());
1105 }
1106 }
1107 }
1108 painter.end();
1109 }
1110
1111
1112 void MainWindow::closeEvent(QCloseEvent*)
1113 {
1114 QSettings settings;
1115 settings.setValue("MainWindow/Geometry", saveGeometry());
1116 settings.setValue("MainWindow/State", saveState());
1117 settings.setValue("MainWindow/ControlDockArea",
1118 static_cast<int>(controlDockArea));
1119 settings.setValue("MainWindow/ActionDockArea",
1120 static_cast<int>(actionDockArea));
1121 settings.setValue("MainWindow/ZoningDockArea",
1122 static_cast<int>(zoningDockArea));
1123 settings.setValue("MainWindow/MarginsDockArea",
1124 static_cast<int>(marginsDockArea));
1125 settings.setValue("MainWindow/LogDockArea",
1126 static_cast<int>(logDockArea));
1127 settings.setValue("MainWindow/ViewSplitter", splitter->saveState());
1128 settings.setValue("ShowToolTips", showToolTips);
1129 settings.setValue("CombineTextHighlighting", combineTextHighlighting);
1130 settings.setValue("Zoom", zoomSpinBox->value());
1131 settings.setValue("Columns", columnsSpinBox->value());
1132 settings.setValue("Tolerance/R", toleranceRSpinBox->value());
1133 settings.setValue("Tolerance/Y", toleranceYSpinBox->value());
1134 settings.setValue("Outline", pen);
1135 settings.setValue("Fill", brush);
1136 settings.setValue("InitialComparisonMode",
1137 compareComboBox->currentIndex());
1138 settings.setValue("Margins/Exclude", marginsGroupBox->isChecked());
1139 settings.setValue("Margins/Left", leftMarginSpinBox->value());
1140 settings.setValue("Margins/Right", rightMarginSpinBox->value());
1141 settings.setValue("Margins/Top", topMarginSpinBox->value());
1142 settings.setValue("Margins/Bottom", bottomMarginSpinBox->value());
1143 QMainWindow::close();
1144 }
1145
1146
1147 bool MainWindow::eventFilter(QObject *object, QEvent *event)
1148 {
1149 if (event->type() == QEvent::ToolTip && !showToolTips)
1150 return true;
1151 return QMainWindow::eventFilter(object, event);
1152 }
1153
1154
1155 void MainWindow::setFiles1(const QStringList &filenames)
1156 {
1157 if (filenames.count() && !filenames.at(0).isEmpty()) {
1158 setFile1(filenames.at(0));
1159 if (filenames.count() > 1 && !filenames.at(1).isEmpty())
1160 setFile2(filenames.at(1));
1161 }
1162 }
1163
1164
1165 void MainWindow::setFiles2(const QStringList &filenames)
1166 {
1167 if (filenames.count() && !filenames.at(0).isEmpty()) {
1168 setFile2(filenames.at(0));
1169 if (filenames.count() > 1 && !filenames.at(1).isEmpty())
1170 setFile1(filenames.at(1));
1171 }
1172 }
1173
1174
1175 void MainWindow::setFile1(QString filename)
1176 {
1177 if (filename.isEmpty())
1178 filename = QFileDialog::getOpenFileName(this,
1179 tr("DiffPDF — Choose File #1"), currentPath,
1180 tr("PDF files (*.pdf)"));
1181 if (!filename.isEmpty()) {
1182 if (filename == filename2LineEdit->text()) {
1183 QMessageBox::warning(this, tr("DiffPDF — Error"),
1184 tr("Cannot compare a file to itself."));
1185 return;
1186 }
1187 filename1LineEdit->setText(filename);
1188 if (!filename2LineEdit->text().isEmpty())
1189 page1Label->setText(tr("<p style='font-size: xx-large;"
1190 "color: darkgreen'>DiffPDF: Click Compare<br>"
1191 "or change File #2.</p>"));
1192 else
1193 page1Label->setText(tr("<p style='font-size: xx-large;"
1194 "color: darkgreen'>DiffPDF: Choose File #2.</p>"));
1195 page2Label->clear();
1196 updateUi();
1197 int page_count = writeFileInfo(filename);
1198 pages1LineEdit->setText(tr("1-%1").arg(page_count));
1199 currentPath = QFileInfo(filename).canonicalPath();
1200 setFile2Button->setFocus();
1201 if (filename2LineEdit->text().isEmpty())
1202 statusLabel->setText(tr("Choose second file"));
1203 else
1204 statusLabel->setText(tr("Ready to compare"));
1205 }
1206 }
1207
1208
1209 void MainWindow::setFile2(QString filename)
1210 {
1211 if (filename.isEmpty())
1212 filename = QFileDialog::getOpenFileName(this,
1213 tr("DiffPDF — Choose File #2"), currentPath,
1214 tr("PDF files (*.pdf)"));
1215 if (!filename.isEmpty()) {
1216 if (filename == filename1LineEdit->text()) {
1217 QMessageBox::warning(this, tr("DiffPDF — Error"),
1218 tr("Cannot compare a file to itself."));
1219 return;
1220 }
1221 filename2LineEdit->setText(filename);
1222 if (!filename1LineEdit->text().isEmpty())
1223 page2Label->setText(tr("<p style='font-size: xx-large;"
1224 "color: darkgreen'>DiffPDF: Click Compare<br>"
1225 "or change File #1.</p>"));
1226 else
1227 page2Label->setText(tr("<p style='font-size: xx-large;"
1228 "color: darkgreen'>DiffPDF: Choose File #1.</p>"));
1229 page1Label->clear();
1230 updateUi();
1231 int page_count = writeFileInfo(filename);
1232 pages2LineEdit->setText(tr("1-%1").arg(page_count));
1233 currentPath = QFileInfo(filename).canonicalPath();
1234 compareButton->setFocus();
1235 if (filename1LineEdit->text().isEmpty())
1236 statusLabel->setText(tr("Choose first file"));
1237 else
1238 statusLabel->setText(tr("Ready to compare"));
1239 }
1240 }
1241
1242
1243 PdfDocument MainWindow::getPdf(const QString &filename)
1244 {
1245 PdfDocument pdf(Poppler::Document::load(filename));
1246 if (!pdf)
1247 QMessageBox::warning(this, tr("DiffPDF — Error"),
1248 tr("Cannot load '%1'.").arg(filename));
1249 else if (pdf->isLocked()) {
1250 QMessageBox::warning(this, tr("DiffPDF — Error"),
1251 tr("Cannot read a locked PDF ('%1').").arg(filename));
1252 #if QT_VERSION >= 0x040600
1253 pdf.clear();
1254 #else
1255 pdf.reset();
1256 #endif
1257 }
1258 return pdf;
1259 }
1260
1261
1262 int MainWindow::writeFileInfo(const QString &filename)
1263 {
1264 int page_count = 0;
1265 PdfDocument pdf = getPdf(filename);
1266 if (!pdf)
1267 return page_count;
1268 writeLine(tr("<b>%1</b>").arg(filename));
1269 foreach (const QString &key, pdf->infoKeys()) {
1270 if (key == "CreationDate" || key == "ModDate")
1271 continue;
1272 writeLine(tr("%1: %2.").arg(key).arg(pdf->info(key)));
1273 }
1274 QDateTime created = pdf->date("CreationDate");
1275 QDateTime modified = pdf->date("ModDate");
1276 if (created != modified)
1277 writeLine(tr("Created: %1, last modified %2.")
1278 .arg(created.toString())
1279 .arg(modified.toString()));
1280 else
1281 writeLine(tr("Created: %1.").arg(created.toString()));
1282 page_count = pdf->numPages();
1283 writeLine(tr("Page count: %1.").arg(page_count));
1284 if (page_count > 0) {
1285 const double PointToMM = 0.3527777777;
1286 PdfPage page1(pdf->page(0));
1287 QSize size = page1->pageSize();
1288 writeLine(tr("Page size: %1pt x %2pt (%3mm x %4mm).")
1289 .arg(size.width()).arg(size.height())
1290 .arg(qRound(size.width() * PointToMM))
1291 .arg(qRound(size.height() * PointToMM)));
1292 topMarginSpinBox->setRange(0, (size.height() / 2) - 10);
1293 bottomMarginSpinBox->setRange(0, (size.height() / 2) - 10);
1294 leftMarginSpinBox->setRange(0, (size.width() / 2) - 10);
1295 rightMarginSpinBox->setRange(0, (size.width() / 2) - 10);
1296 }
1297 return page_count;
1298 }
1299
1300
1301 void MainWindow::writeLine(const QString &text)
1302 {
1303 logEdit->appendHtml(text);
1304 logEdit->ensureCursorVisible();
1305 }
1306
1307
1308 void MainWindow::writeError(const QString &text)
1309 {
1310 logEdit->appendHtml(tr("<font color=red>%1</font>").arg(text));
1311 logEdit->ensureCursorVisible();
1312 }
1313
1314
1315 QList<int> MainWindow::getPageList(int which, PdfDocument pdf)
1316 {
1317 // Poppler has 0-based page numbers; the UI has 1-based page numbers
1318 QLineEdit *pagesEdit = (which == 1 ? pages1LineEdit : pages2LineEdit);
1319 bool error = false;
1320 QList<int> pages;
1321 QString page_string = pagesEdit->text();
1322 page_string = page_string.replace(QRegExp("\\s+"), "");
1323 QStringList page_list = page_string.split(",");
1324 bool ok;
1325 foreach (const QString &page, page_list) {
1326 int hyphen = page.indexOf("-");
1327 if (hyphen > -1) {
1328 int p1 = page.left(hyphen).toInt(&ok);
1329 if (!ok || p1 < 1) {
1330 error = true;
1331 break;
1332 }
1333 int p2 = page.mid(hyphen + 1).toInt(&ok);
1334 if (!ok || p2 < 1 || p2 < p1) {
1335 error = true;
1336 break;
1337 }
1338 if (p1 == p2)
1339 pages.append(p1 - 1);
1340 else {
1341 for (int p = p1; p <= p2; ++p) {
1342 if (p > pdf->numPages())
1343 break;
1344 pages.append(p - 1);
1345 }
1346 }
1347 }
1348 else {
1349 int p = page.toInt(&ok);
1350 if (ok && p > 0 && p <= pdf->numPages())
1351 pages.append(p - 1);
1352 else {
1353 error = true;
1354 break;
1355 }
1356 }
1357 }
1358 if (error) {
1359 pages.clear();
1360 writeError(tr("Failed to understand page range '%1'.")
1361 .arg(pagesEdit->text()));
1362 pagesEdit->setText(tr("1-%1").arg(pdf->numPages()));
1363 for (int page = 0; page < pdf->numPages(); ++page)
1364 pages.append(page);
1365 }
1366 return pages;
1367 }
1368
1369
1370 void MainWindow::compare()
1371 {
1372 if (compareButton->text() == tr("&Cancel")) {
1373 cancel = true;
1374 compareButton->setText(tr("&Compare"));
1375 compareButton->setEnabled(true);
1376 return;
1377 }
1378 cancel = false;
1379 QString filename1 = filename1LineEdit->text();
1380 PdfDocument pdf1 = getPdf(filename1);
1381 if (!pdf1)
1382 return;
1383 QString filename2 = filename2LineEdit->text();
1384 PdfDocument pdf2 = getPdf(filename2);
1385 if (!pdf2) {
1386 return;
1387 }
1388
1389 comparePrepareUi();
1390 QTime time;
1391 time.start();
1392 const QPair<int, int> pair = comparePages(filename1, pdf1, filename2,
1393 pdf2);
1394 compareUpdateUi(pair, time.elapsed());
1395 }
1396
1397
1398 void MainWindow::comparePrepareUi()
1399 {
1400 QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
1401 compareButton->setText(tr("&Cancel"));
1402 compareButton->setEnabled(true);
1403 compareButton->setFocus();
1404 viewDiffComboBox->clear();
1405 viewDiffComboBox->addItem(tr("(Not viewing)"));
1406 saveButton->setEnabled(false);
1407 statusLabel->setText(tr("Ready"));
1408 }
1409
1410
1411 const QPair<int, int> MainWindow::comparePages(const QString &filename1,
1412 const PdfDocument &pdf1, const QString &filename2,
1413 const PdfDocument &pdf2)
1414 {
1415 QList<int> pages1 = getPageList(1, pdf1);
1416 QList<int> pages2 = getPageList(2, pdf2);
1417 int total = qMin(pages1.count(), pages2.count());
1418 int number = 0;
1419 int index = 0;
1420 while (!pages1.isEmpty() && !pages2.isEmpty()) {
1421 int p1 = pages1.takeFirst();
1422 PdfPage page1(pdf1->page(p1));
1423 if (!page1) {
1424 writeError(tr("Failed to read page %1 from '%2'.")
1425 .arg(p1 + 1).arg(filename1));
1426 continue;
1427 }
1428 int p2 = pages2.takeFirst();
1429 PdfPage page2(pdf2->page(p2));
1430 if (!page2) {
1431 writeError(tr("Failed to read page %1 from '%2'.")
1432 .arg(p2 + 1).arg(filename2));
1433 continue;
1434 }
1435 writeLine(tr("Comparing: %1 vs. %2.").arg(p1 + 1).arg(p2 + 1));
1436 QApplication::processEvents();
1437 if (cancel) {
1438 writeError(tr("Cancelled."));
1439 break;
1440 }
1441 Difference difference = getTheDifference(page1, page2);
1442 if (difference != NoDifference) {
1443 QVariant v;
1444 v.setValue(PagePair(p1, p2, difference == VisualDifference));
1445 viewDiffComboBox->addItem(tr("%1 vs. %2 %3 %4")
1446 .arg(p1 + 1).arg(p2 + 1).arg(QChar(0x2022))
1447 .arg(++index), v);
1448 }
1449 statusLabel->setText(tr("Comparing %1/%2").arg(++number)
1450 .arg(total));
1451 }
1452 return qMakePair(number, total);
1453 }
1454
1455
1456 void MainWindow::compareUpdateUi(const QPair<int, int> &pair,
1457 const int millisec)
1458 {
1459 const int differ = viewDiffComboBox->count() - 1;
1460 if (!cancel) {
1461 if (millisec > 1000)
1462 writeLine(tr("Completed in %1 seconds.")
1463 .arg(millisec / 1000.0, 0, 'f', 2));
1464 if (viewDiffComboBox->count() > 1) {
1465 if (viewDiffComboBox->count() == 2)
1466 writeLine(tr("<font color=brown>Files differ on 1 page "
1467 "(%1 page%2 compared).</font>")
1468 .arg(pair.first)
1469 .arg(pair.first == 1 ? tr(" was") : tr("s were")));
1470 else
1471 writeLine(tr("<font color=brown>Files differ on %1 pages "
1472 "(%2 page%3 compared).</font>")
1473 .arg(differ).arg(pair.first)
1474 .arg(pair.first == 1 ? tr(" was")
1475 : tr("s were")));
1476 viewDiffComboBox->setFocus();
1477 viewDiffComboBox->setCurrentIndex(1);
1478 }
1479 else {
1480 writeLine(tr("The PDFs appear to be the same."));
1481 const QString message(tr("<p style='font-size: x-large;"
1482 "color: darkgreen'>"
1483 "DiffPDF: The PDFs appear to be the same.</p>"));
1484 page1Label->setText(message);
1485 page2Label->setText(message);
1486 }
1487 }
1488
1489 compareButton->setText(tr("&Compare"));
1490 if (differ == 1) // Separated the cases for ease of translation
1491 statusLabel->setText(tr("1 differs %1/%2 compared").arg(pair.first)
1492 .arg(pair.second));
1493 else
1494 statusLabel->setText(tr("%1 differ %2/%3 compared").arg(differ)
1495 .arg(pair.first).arg(pair.second));
1496 saveButton->setEnabled(true);
1497 updateUi();
1498 if (!cancel)
1499 viewDiffComboBox->setFocus();
1500 QApplication::restoreOverrideCursor();
1501 }
1502
1503
1504 MainWindow::Difference MainWindow::getTheDifference(PdfPage page1,
1505 PdfPage page2)
1506 {
1507 QRectF rect;
1508 if (marginsGroupBox->isChecked())
1509 rect = pointRectForMargins(page1->pageSize());
1510 const TextBoxList list1 = getTextBoxes(page1, rect);
1511 const TextBoxList list2 = getTextBoxes(page2, rect);
1512 if (list1.count() != list2.count())
1513 return TextualDifference;
1514 for (int i = 0; i < list1.count(); ++i)
1515 if (list1[i]->text() != list2[i]->text())
1516 return TextualDifference;
1517
1518 if (compareComboBox->currentIndex() == CompareAppearance) {
1519 int x = -1;
1520 int y = -1;
1521 int width = -1;
1522 int height = -1;
1523 if (marginsGroupBox->isChecked())
1524 computeImageOffsets(page1->pageSize(), &x, &y, &width,
1525 &height);
1526 QImage image1 = page1->renderToImage(POINTS_PER_INCH,
1527 POINTS_PER_INCH, x, y, width, height);
1528 QImage image2 = page2->renderToImage(POINTS_PER_INCH,
1529 POINTS_PER_INCH, x, y, width, height);
1530 if (image1 != image2)
1531 return VisualDifference;
1532 }
1533 return NoDifference;
1534 }
1535
1536
1537 QRectF MainWindow::pointRectForMargins(const QSize &size)
1538 {
1539 return rectForMargins(size.width(), size.height(),
1540 topMarginSpinBox->value(), bottomMarginSpinBox->value(),
1541 leftMarginSpinBox->value(), rightMarginSpinBox->value());
1542 }
1543
1544
1545 void MainWindow::computeImageOffsets(const QSize &size, int *x, int *y,
1546 int *width, int *height)
1547 {
1548 const int DPI = static_cast<int>(POINTS_PER_INCH *
1549 (zoomSpinBox->value() / 100.0));
1550 *y = pixelOffsetForPointValue(DPI, topMarginSpinBox->value());
1551 *x = pixelOffsetForPointValue(DPI, leftMarginSpinBox->value());
1552 *width = pixelOffsetForPointValue(DPI, size.width() -
1553 (leftMarginSpinBox->value() + rightMarginSpinBox->value()));
1554 *height = pixelOffsetForPointValue(DPI, size.height() -
1555 (topMarginSpinBox->value() + bottomMarginSpinBox->value()));
1556 }
1557
1558
1559 void MainWindow::options()
1560 {
1561 QSettings settings;
1562 qreal ruleWidth = settings.value("RuleWidth", 1.5).toDouble();
1563 int cacheSize = QPixmapCache::cacheLimit() / 1000;
1564 int alpha = settings.value("Opacity", 13).toInt();
1565 int squareSize = settings.value("SquareSize", 10).toInt();
1566 OptionsForm form(&pen, &brush, &ruleWidth, &showToolTips,
1567 &combineTextHighlighting, &cacheSize, &alpha, &squareSize,
1568 this);
1569 if (form.exec()) {
1570 settings.setValue("RuleWidth", ruleWidth);
1571 settings.setValue("CombineTextHighlighting",
1572 combineTextHighlighting);
1573 settings.setValue("CacheSizeMB", cacheSize);
1574 settings.setValue("Opacity", alpha);
1575 settings.setValue("SquareSize", squareSize);
1576 QPixmapCache::clear();
1577 QPixmapCache::setCacheLimit(1000 * cacheSize);
1578 updateViews();
1579 }
1580 }
1581
1582
1583 void MainWindow::save()
1584 {
1585 SaveForm form(currentPath, &saveFilename, &saveAll, &savePages, this);
1586 if (form.exec()) {
1587 QString filename1 = filename1LineEdit->text();
1588 PdfDocument pdf1 = getPdf(filename1);
1589 if (!pdf1)
1590 return;
1591 QString filename2 = filename2LineEdit->text();
1592 PdfDocument pdf2 = getPdf(filename2);
1593 if (!pdf2)
1594 return;
1595 saveButton->setEnabled(false);
1596 QApplication::processEvents();
1597 const int originalIndex = viewDiffComboBox->currentIndex();
1598 int start = originalIndex;
1599 int end = originalIndex + 1;
1600 if (saveAll) {
1601 start = 0;
1602 end = viewDiffComboBox->count();
1603 }
1604 QString header;
1605 const QChar bullet(0x2022);
1606 if (savePages == SaveLeftPages)
1607 header = tr("DiffPDF %1 %2 %1 %3").arg(bullet)
1608 .arg(filename1)
1609 .arg(QDate::currentDate().toString(Qt::ISODate));
1610 else if (savePages == SaveRightPages)
1611 header = tr("DiffPDF %1 %2 %1 %3").arg(bullet)
1612 .arg(filename2)
1613 .arg(QDate::currentDate().toString(Qt::ISODate));
1614 else
1615 header = tr("DiffPDF %1 %2 vs. %3 %1 %4").arg(bullet)
1616 .arg(filename1).arg(filename2)
1617 .arg(QDate::currentDate().toString(Qt::ISODate));
1618 if (saveFilename.toLower().endsWith(".pdf"))
1619 saveAsPdf(start, end, pdf1, pdf2, header);
1620 else
1621 saveAsImages(start, end, pdf1, pdf2, header);
1622 updateViews(originalIndex);
1623 if (saveFilename.toLower().endsWith(".pdf"))
1624 writeLine(tr("Saved %1").arg(saveFilename));
1625 saveButton->setEnabled(true);
1626 }
1627 }
1628
1629
1630 void MainWindow::saveAsImages(const int start, const int end,
1631 const PdfDocument &pdf1, const PdfDocument &pdf2,
1632 const QString &header)
1633 {
1634 PdfPage page1(pdf1->page(0));
1635 if (!page1)
1636 return;
1637 PdfPage page2(pdf2->page(0));
1638 if (!page2)
1639 return;
1640 int width = 2 * (savePages == SaveBothPages
1641 ? page1->pageSize().width() + page2->pageSize().width()
1642 : page1->pageSize().width());
1643 const int y = fontMetrics().lineSpacing();
1644 const int height = (2 * page1->pageSize().height()) - y;
1645 const int gap = 30;
1646 const QRect rect(0, 0, width, height);
1647 if (savePages == SaveBothPages)
1648 width = (width / 2) - gap;
1649 const QRect leftRect(0, y, width, height);
1650 const QRect rightRect(width + gap, y, width, height);
1651 int count = 0;
1652 QString imageFilename = saveFilename;
1653 int i = imageFilename.lastIndexOf(".");
1654 if (i > -1)
1655 imageFilename.insert(i, "-%1");
1656 else
1657 imageFilename += "-%1.png";
1658 for (int index = start; index < end; ++index) {
1659 QImage image(rect.size(), QImage::Format_ARGB32);
1660 QPainter painter(&image);
1661 painter.setRenderHints(QPainter::Antialiasing|
1662 QPainter::TextAntialiasing|QPainter::SmoothPixmapTransform);
1663 painter.setFont(QFont("Helvetica", 11));
1664 painter.setPen(Qt::darkCyan);
1665 painter.fillRect(rect, Qt::white);
1666 if (!paintSaveAs(&painter, index, pdf1, pdf2, header, rect,
1667 leftRect, rightRect))
1668 continue;
1669 QString filename = imageFilename;
1670 filename = filename.arg(++count);
1671 if (image.save(filename))
1672 writeLine(tr("Saved %1").arg(filename));
1673 else
1674 writeLine(tr("Failed to save %1").arg(filename));
1675 }
1676 }
1677
1678
1679 void MainWindow::saveAsPdf(const int start, const int end,
1680 const PdfDocument &pdf1, const PdfDocument &pdf2,
1681 const QString &header)
1682 {
1683 QPrinter printer(QPrinter::HighResolution);
1684 printer.setOutputFileName(saveFilename);
1685 printer.setOutputFormat(QPrinter::PdfFormat);
1686 printer.setColorMode(QPrinter::Color);
1687 printer.setCreator(tr("DiffPDF"));
1688 printer.setOrientation(savePages == SaveBothPages
1689 ? QPrinter::Landscape : QPrinter::Portrait);
1690 QPainter painter(&printer);
1691 painter.setRenderHints(QPainter::Antialiasing|
1692 QPainter::TextAntialiasing|QPainter::SmoothPixmapTransform);
1693 painter.setFont(QFont("Helvetica", 11));
1694 painter.setPen(Qt::darkCyan);
1695 const QRect rect(0, 0, painter.viewport().width(),
1696 painter.fontMetrics().height());
1697 const int y = painter.fontMetrics().lineSpacing();
1698 const int height = painter.viewport().height() - y;
1699 const int gap = 30;
1700 int width = (painter.viewport().width() / 2) - gap;
1701 if (savePages != SaveBothPages)
1702 width = painter.viewport().width();
1703 const QRect leftRect(0, y, width, height);
1704 const QRect rightRect(width + gap, y, width, height);
1705 for (int index = start; index < end; ++index) {
1706 if (!paintSaveAs(&painter, index, pdf1, pdf2, header, rect,
1707 leftRect, rightRect))
1708 continue;
1709 if (index + 1 < end)
1710 printer.newPage();
1711 }
1712 }
1713
1714
1715 bool MainWindow::paintSaveAs(QPainter *painter, const int index,
1716 const PdfDocument &pdf1, const PdfDocument &pdf2,
1717 const QString &header, const QRect &rect, const QRect &leftRect,
1718 const QRect &rightRect)
1719 {
1720 PagePair pair = viewDiffComboBox->itemData(index)
1721 .value<PagePair>();
1722 if (pair.isNull())
1723 return false;
1724 PdfPage page1(pdf1->page(pair.left));
1725 if (!page1)
1726 return false;
1727 PdfPage page2(pdf2->page(pair.right));
1728 if (!page2)
1729 return false;
1730 const QPair<QString, QString> keys = cacheKeys(index, pair);
1731 const QPair<QPixmap, QPixmap> pixmaps = populatePixmaps(pdf1,
1732 page1, pdf2, page2, pair.hasVisualDifference,
1733 keys.first, keys.second);
1734 painter->drawText(rect, header, QTextOption(Qt::AlignHCenter|
1735 Qt::AlignTop));
1736 if (savePages == SaveBothPages) {
1737 QRect rect = resizeRect(leftRect, pixmaps.first.size());
1738 painter->drawPixmap(rect, pixmaps.first);
1739 rect = resizeRect(rightRect, pixmaps.second.size());
1740 painter->drawPixmap(rect, pixmaps.second);
1741 painter->drawRect(rightRect.adjusted(2.5, 2.5, 2.5, 2.5));
1742 } else if (savePages == SaveLeftPages) {
1743 QRect rect = resizeRect(leftRect, pixmaps.first.size());
1744 painter->drawPixmap(rect, pixmaps.first);
1745 } else { // (savePages == SaveRightPages)
1746 QRect rect = resizeRect(leftRect, pixmaps.second.size());
1747 painter->drawPixmap(rect, pixmaps.second);
1748 }
1749 painter->drawRect(leftRect.adjusted(2.5, 2.5, 2.5, 2.5));
1750 return true;
1751 }
1752
1753
1754 void MainWindow::help()
1755 {
1756 if (!helpForm)
1757 helpForm = new HelpForm(language, this);
1758 helpForm->show();
1759 helpForm->raise();
1760 helpForm->activateWindow();
1761 }
1762
1763
1764 void MainWindow::about()
1765 {
1766 if (!aboutForm)
1767 aboutForm = new AboutForm(this);
1768 aboutForm->show();
1769 aboutForm->raise();
1770 aboutForm->activateWindow();
1771 }
1772
1773
1774 void MainWindow::showZones()
1775 {
1776 PagePair pair = viewDiffComboBox->itemData(
1777 viewDiffComboBox->currentIndex()).value<PagePair>();
1778 if (pair.isNull())
1779 return;
1780 QString filename1 = filename1LineEdit->text();
1781 PdfDocument pdf1 = getPdf(filename1);
1782 if (!pdf1)
1783 return;
1784 PdfPage page1(pdf1->page(pair.left));
1785 if (!page1)
1786 return;
1787 const TextBoxList list1 = getTextBoxes(page1);
1788 showZones(page1->pageSize().width(), list1, page1Label);
1789
1790 QString filename2 = filename2LineEdit->text();
1791 PdfDocument pdf2 = getPdf(filename2);
1792 if (!pdf2)
1793 return;
1794 PdfPage page2(pdf2->page(pair.right));
1795 if (!page2)
1796 return;
1797 const TextBoxList list2 = getTextBoxes(page2);
1798 showZones(page2->pageSize().width(), list2, page2Label);
1799 }
1800
1801
1802 void MainWindow::showZones(const int Width, const TextBoxList &list,
1803 QLabel *label)
1804 {
1805 if (!label || !label->pixmap() || label->pixmap()->isNull())
1806 return;
1807 const bool ComparingWords = compareComboBox->currentIndex() ==
1808 CompareWords;
1809 TextItems items = ComparingWords ? getWords(list)
1810 : getCharacters(list);
1811 items.columnYxOrder(Width, toleranceYSpinBox->value(),
1812 columnsSpinBox->value());
1813 QList<QPainterPath> paths = items.generateZones(Width,
1814 toleranceRSpinBox->value(), toleranceYSpinBox->value(),
1815 columnsSpinBox->value());
1816 const int DPI = static_cast<int>(POINTS_PER_INCH *
1817 (zoomSpinBox->value() / 100.0));
1818 QPixmap pixmap = label->pixmap()->copy();
1819 QPainter painter(&pixmap);
1820 painter.setPen(Qt::green);
1821 for (int i = 0; i < paths.count(); ++i) {
1822 const QPainterPath &path = paths.at(i);
1823 QRectF rect = path.boundingRect();
1824 scaleRect(DPI, &rect);
1825 painter.drawRect(rect);
1826 painter.drawText(rect.x(), rect.y(), QString("#%1").arg(i + 1));
1827 }
1828 painter.end();
1829 label->setPixmap(pixmap);
1830 }
1831
1832
1833 void MainWindow::showMargins()
1834 {
1835 if (leftMarginSpinBox->value() == 0 &&
1836 rightMarginSpinBox->value() == 0 &&
1837 topMarginSpinBox->value() == 0 &&
1838 bottomMarginSpinBox->value() == 0)
1839 return;
1840 showMargins(page1Label);
1841 showMargins(page2Label);
1842 }
1843
1844
1845 void MainWindow::showMargins(QLabel *label)
1846 {
1847 if (!label || !label->pixmap() || label->pixmap()->isNull())
1848 return;
1849 const int DPI = static_cast<int>(POINTS_PER_INCH *
1850 (zoomSpinBox->value() / 100.0));
1851 QPixmap pixmap = label->pixmap()->copy();
1852 QPainter painter(&pixmap);
1853 painter.setPen(Qt::cyan);
1854 int left = leftMarginSpinBox->value();
1855 if (left) {
1856 const int x = pixelOffsetForPointValue(DPI, left);
1857 painter.drawLine(x, 0, x, pixmap.height());
1858 }
1859 int right = rightMarginSpinBox->value();
1860 if (right) {
1861 const int x = pixmap.width() -
1862 pixelOffsetForPointValue(DPI, right);
1863 painter.drawLine(x, 0, x, pixmap.height());
1864 }
1865 int top = topMarginSpinBox->value();
1866 if (top) {
1867 const int y = pixelOffsetForPointValue(DPI, top);
1868 painter.drawLine(0, y, pixmap.width(), y);
1869 }
1870 int bottom = bottomMarginSpinBox->value();
1871 if (bottom) {
1872 const int y = pixmap.height() -
1873 pixelOffsetForPointValue(DPI, bottom);
1874 painter.drawLine(0, y, pixmap.width(), y);
1875 }
1876 painter.end();
1877 label->setPixmap(pixmap);
1878 }
1879
1880
1881 void MainWindow::setAMargin(const QPoint &pos)
1882 {
1883 if (!marginsGroupBox->isChecked() || !page1Label->pixmap() ||
1884 page1Label->pixmap()->isNull())
1885 return;
1886 const int DPI = static_cast<int>(POINTS_PER_INCH *
1887 (zoomSpinBox->value() / 100.0));
1888 const QSize &size = page1Label->pixmap()->size();
1889 int x = pos.x();
1890 int y = pos.y();
1891 const int HorizontalMiddle = size.width() / 2;
1892 const int TopOffset = size.height() / 3;
1893 const int BottomOffset = size.height() - TopOffset;
1894 const int VerticalMiddle = size.height() / 2;
1895 if (y > TopOffset && y < BottomOffset) { // Setting left or right
1896 if (x < HorizontalMiddle)
1897 leftMarginSpinBox->setValue(pointValueForPixelOffset(DPI, x));
1898 else
1899 rightMarginSpinBox->setValue(pointValueForPixelOffset(DPI,
1900 (size.width() - x)));
1901 } else { // Setting top or bottom
1902 if (y < VerticalMiddle)
1903 topMarginSpinBox->setValue(pointValueForPixelOffset(DPI, y));
1904 else
1905 bottomMarginSpinBox->setValue(pointValueForPixelOffset(DPI,
1906 (size.height() - y)));
1907 }
1908 }
1909