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&mdash;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&mdash;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&mdash;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