diff -Nru diffpdf-0.4.0/CHANGES diffpdf-1.1.4/CHANGES --- diffpdf-0.4.0/CHANGES 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/CHANGES 2010-08-04 06:50:47.000000000 +0000 @@ -0,0 +1,39 @@ +1.1.4 + +Improved the reporting of files that are the same. + +1.1.3 + +Tiny bugfix for Debian. + +1.1.2 + +It is now possible for the user to control whether highlighting is +combined in text mode. + +Added a cache for page images. This makes it much quicker to flick back +and forth between pages. The cache size can be set by the user: the +bigger the cache the more pages that can be flicked through without +recalculating. + +1.1.1 + +Replaced QVariant::toReal() with QVariant::toDouble() to avoid breaking +compatibility with Qt 4.4. + +1.1.0 + +Added margin rules to indicate where changes are. (These can be turned +off by setting a rule width of 0.0.) + +1.0.0 + +Implemented the sequence matcher algorithm for text mode matching. + +0.6.0 + +Redid the user interface to use dock widgets. + +0.5.0 + +First release. diff -Nru diffpdf-0.4.0/debian/changelog diffpdf-1.1.4/debian/changelog --- diffpdf-0.4.0/debian/changelog 2010-11-19 21:36:42.000000000 +0000 +++ diffpdf-1.1.4/debian/changelog 2010-11-19 21:29:16.000000000 +0000 @@ -1,3 +1,61 @@ +diffpdf (1.1.4-1~lffl~lucid~ppa) lucid; urgency=low + + * Improved the reporting of files that are the same. + + -- Ferramosca Roberto Wed, 17 Nov 2010 19:03:44 +0100 + +diffpdf (1.1.3-1) unstable; urgency=low + + * New upstream version, merging 00-fix_casting_error.patch. + + -- David Paleino Fri, 16 Jul 2010 11:52:40 +0200 + +diffpdf (1.1.2-2) unstable; urgency=low + + * debian/patches/00-fix_casting_error.patch added, merged back + from Ubuntu. Patch by David Sugar. + * debian/control: bump Standards-Version to 3.9.0 + + -- David Paleino Wed, 14 Jul 2010 22:32:29 +0200 + +diffpdf (1.1.2-1) unstable; urgency=low + + * New upstream release + + -- David Paleino Sun, 06 Jun 2010 22:12:08 +0200 + +diffpdf (1.1.1-1) unstable; urgency=low + + * New upstream release + + -- David Paleino Mon, 31 May 2010 21:07:38 +0200 + +diffpdf (1.0.0-1) unstable; urgency=low + + * New upstream release + + -- David Paleino Sat, 22 May 2010 15:38:44 +0200 + +diffpdf (0.6.0-1) unstable; urgency=low + + * New upstream release + * debian/patches/00-fix_typos.patch removed, merged upstream + + -- David Paleino Thu, 13 May 2010 22:36:40 +0200 + +diffpdf (0.5.0-1) unstable; urgency=low + + * New upstream release + * debian/control: + - bumped Standards-Version to 3.8.4 (no changes needed) + - multi-line fields re-formatted + * debian/watch: removed spurious comments + * debian/source/format: using 3.0 (quilt) + * debian/patches/: + - 00-fix_typos.patch added, fixes typos in the sourcecode + + -- David Paleino Sat, 24 Apr 2010 21:26:55 +0200 + diffpdf (0.4.0-1) unstable; urgency=low * Initial release (Closes: #562070) diff -Nru diffpdf-0.4.0/debian/control diffpdf-1.1.4/debian/control --- diffpdf-0.4.0/debian/control 2010-11-19 21:36:42.000000000 +0000 +++ diffpdf-1.1.4/debian/control 2010-07-16 09:50:07.000000000 +0000 @@ -2,19 +2,21 @@ Section: utils Priority: optional Maintainer: David Paleino -Build-Depends: debhelper (>= 7.0.50), - qt4-qmake, - libqt4-dev, - libpoppler-qt4-dev -Standards-Version: 3.8.3 +Build-Depends: + debhelper (>= 7.0.50~) + , qt4-qmake + , libqt4-dev + , libpoppler-qt4-dev +Standards-Version: 3.9.0 Homepage: http://www.qtrac.eu/diffpdf.html Vcs-Git: git://git.debian.org/collab-maint/diffpdf.git Vcs-Browser: http://git.debian.org/?p=collab-maint/diffpdf.git Package: diffpdf Architecture: any -Depends: ${shlibs:Depends}, - ${misc:Depends} +Depends: + ${shlibs:Depends} + , ${misc:Depends} Description: compare two PDF files textually or visually DiffPDF is used to compare two PDF files. By default the comparison is of the text on each pair of pages, but diff -Nru diffpdf-0.4.0/debian/watch diffpdf-1.1.4/debian/watch --- diffpdf-0.4.0/debian/watch 2010-11-19 21:36:42.000000000 +0000 +++ diffpdf-1.1.4/debian/watch 2010-07-16 09:50:07.000000000 +0000 @@ -1,24 +1,2 @@ -# Example watch control file for uscan -# Rename this file to "watch" and then you can run the "uscan" command -# to check for upstream updates and more. -# See uscan(1) for format - -# Compulsory line, this is a version 3 file version=3 - -# Uncomment to examine a Webpage -# -#http://www.example.com/downloads.php diffpdf-(.*)\.tar\.gz - -# Uncomment to examine a Webserver directory -#http://www.example.com/pub/diffpdf-(.*)\.tar\.gz - -# Uncommment to examine a FTP server -#ftp://ftp.example.com/pub/diffpdf-(.*)\.tar\.gz debian uupdate - -# Uncomment to find new files on sourceforge, for devscripts >= 2.9 -# http://sf.net/diffpdf/diffpdf-(.*)\.tar\.gz - -# Uncomment to find new files on GooglePages -# http://example.googlepages.com/foo.html diffpdf-(.*)\.tar\.gz http://www.qtrac.eu/diffpdf.html diffpdf-(\d+.*)\.tar\.gz diff -Nru diffpdf-0.4.0/diffpdf.1 diffpdf-1.1.4/diffpdf.1 --- diffpdf-0.4.0/diffpdf.1 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/diffpdf.1 2010-08-04 06:50:47.000000000 +0000 @@ -1,4 +1,4 @@ -.TH DIFFPDF 1 "2009-12-23" "diffpdf v0.4.0" +.TH DIFFPDF 1 "2010-08-04" "diffpdf v1.1.4" .SH NAME diffpdf \- compare two PDF files textually or visually .SH SYNOPSIS @@ -11,14 +11,15 @@ \fBDiffPDF\fP is a GUI application used to compare two PDF files. .br By default the comparison is of the text on each pair of pages, but -comparing the appearance of pages is also supported (for example, if -a diagram is changed or a paragraph reformatted). It is also possible -to compare particular pages or page ranges. For example, if there are -two versions of a PDF file, one with pages 1-12 and the other with -pages 1-13 because of an extra page having been added as page 4, they -can be compared by specifying two page ranges, 1-12 for the first and 1-3, -5-13 for the second. This will make DiffPDF compare pages in the pairs -(1, 1), (2, 2), (3, 3), (4, 5), (5, 6), and so on, to (12, 13). +comparing the visual appearance of pages is also supported (for example, +if a diagram is changed or if a paragraph is reformatted). It is also +possible to compare particular pages or page ranges. For example, if +there are two versions of a PDF file, one with pages 1-12 and the other +with pages 1-13 because of an extra page having been added as page 4, +they can be compared by specifying two page ranges, 1-12 for the first +and 1-3, 5-13 for the second. This will make DiffPDF compare pages in +the pairs (1, 1), (2, 2), (3, 3), (4, 5), (5, 6), and so on, to (12, +13). .SH AUTHOR diffpdf was written by Mark Summerfield . .PP diff -Nru diffpdf-0.4.0/diffpdf.pro diffpdf-1.1.4/diffpdf.pro --- diffpdf-0.4.0/diffpdf.pro 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/diffpdf.pro 2010-08-04 06:50:47.000000000 +0000 @@ -1,10 +1,18 @@ -HEADERS += optionsform.hpp -SOURCES += optionsform.cpp HEADERS += mainwindow.hpp SOURCES += mainwindow.cpp +HEADERS += optionsform.hpp +SOURCES += optionsform.cpp HEADERS += generic.hpp SOURCES += generic.cpp +HEADERS += sequence_matcher.hpp +SOURCES += sequence_matcher.cpp SOURCES += main.cpp RESOURCES += resources.qrc LIBS += -lpoppler-qt4 -INCLUDEPATH += /usr/include/poppler/qt4 +exists($(HOME)/opt/poppler014/) { + message(Using locally built Poppler library) + INCLUDEPATH += $(HOME)/opt/poppler014/include/poppler/qt4 + LIBS += -Wl,-rpath -Wl,$(HOME)/opt/poppler014/lib -Wl,-L$(HOME)/opt/poppler014/lib +} else { + INCLUDEPATH += /usr/include/poppler/qt4 +} diff -Nru diffpdf-0.4.0/generic.cpp diffpdf-1.1.4/generic.cpp --- diffpdf-0.4.0/generic.cpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/generic.cpp 2010-08-04 06:50:47.000000000 +0000 @@ -12,6 +12,11 @@ #include "generic.hpp" #include +#include +#include +#include + +const QSize SwatchSize(24, 24); void scaleRect(int dpi, QRectF *rect) @@ -32,3 +37,86 @@ } +Ranges unorderedRange(int end, int start) +{ + Q_ASSERT(end >= start); + Ranges ranges; + while (start < end) + ranges.insert(start++); + return ranges; +} + + +QPixmap colorSwatch(const QColor &color) +{ + QString key = QString("COLORSWATCH:%1").arg(color.name()); + QPixmap pixmap(SwatchSize); +#if QT_VERSION >= 0x040600 + if (!QPixmapCache::find(key, &pixmap)) { +#else + if (!QPixmapCache::find(key, pixmap)) { +#endif + pixmap.fill(Qt::transparent); + { + QPainter painter(&pixmap); + painter.setRenderHints(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(0, 0, SwatchSize.width(), + SwatchSize.height()); + } + QPixmapCache::insert(key, pixmap); + } + return pixmap; +} + + +QPixmap brushSwatch(const Qt::BrushStyle style, const QColor &color) +{ + QString key = QString("BRUSHSTYLESWATCH:%1:%2") + .arg(static_cast(style)).arg(color.name()); + QPixmap pixmap(SwatchSize); +#if QT_VERSION >= 0x040600 + if (!QPixmapCache::find(key, &pixmap)) { +#else + if (!QPixmapCache::find(key, pixmap)) { +#endif + pixmap.fill(Qt::transparent); + { + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(QBrush(color, style)); + painter.drawRect(0, 0, SwatchSize.width(), + SwatchSize.height()); + } + QPixmapCache::insert(key, pixmap); + } + return pixmap; +} + + +QPixmap penStyleSwatch(const Qt::PenStyle style, const QColor &color) +{ + QString key = QString("PENSTYLESWATCH:%1:%2") + .arg(static_cast(style)).arg(color.name()); + QPixmap pixmap(SwatchSize); +#if QT_VERSION >= 0x040600 + if (!QPixmapCache::find(key, &pixmap)) { +#else + if (!QPixmapCache::find(key, pixmap)) { +#endif + pixmap.fill(Qt::transparent); + { + QPainter painter(&pixmap); + QPen pen(style); + pen.setColor(color); + pen.setWidth(2); + painter.setPen(pen); + const int Y = SwatchSize.height() / 2; + painter.drawLine(0, Y, SwatchSize.width(), Y); + } + QPixmapCache::insert(key, pixmap); + } + return pixmap; +} diff -Nru diffpdf-0.4.0/generic.hpp diffpdf-1.1.4/generic.hpp --- diffpdf-0.4.0/generic.hpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/generic.hpp 2010-08-04 06:50:47.000000000 +0000 @@ -1,3 +1,5 @@ +#ifndef GENERIC_HPP +#define GENERIC_HPP /* Copyright (c) 2008-10 Qtrac Ltd. All rights reserved. This program or module is free software: you can redistribute it @@ -10,33 +12,39 @@ for more details. */ -#ifndef GENERIC_HPP -#define GENERIC_HPP - #include +#include +#include +#include +class QColor; class QRectF; const int DPI_FACTOR = 72; +typedef QSet Ranges; +typedef QPair RangesPair; struct PagePair { PagePair(int l=-1, int r=-1, bool v=false) - : left(l), right(r), visual_difference(v) {} + : left(l), right(r), hasVisualDifference(v) {} bool isNull() { return left == -1 || right == -1; } const int left; const int right; - const bool visual_difference; + const bool hasVisualDifference; }; Q_DECLARE_METATYPE(PagePair) void scaleRect(int dpi, QRectF *rect); +Ranges unorderedRange(int end, int start=0); -#endif // GENERIC_HPP - +QPixmap colorSwatch(const QColor &color); +QPixmap brushSwatch(const Qt::BrushStyle style, const QColor &color); +QPixmap penStyleSwatch(const Qt::PenStyle style, const QColor &color); +#endif // GENERIC_HPP diff -Nru diffpdf-0.4.0/main.cpp diffpdf-1.1.4/main.cpp --- diffpdf-0.4.0/main.cpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/main.cpp 2010-08-04 06:50:47.000000000 +0000 @@ -25,6 +25,7 @@ app.setOrganizationDomain("qtrac.eu"); app.setApplicationName("DiffPDF"); app.setWindowIcon(QIcon(":/icon.png")); + QString filename1; QString filename2; if (argc > 1) { diff -Nru diffpdf-0.4.0/mainwindow.cpp diffpdf-1.1.4/mainwindow.cpp --- diffpdf-0.4.0/mainwindow.cpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/mainwindow.cpp 2010-08-04 06:50:47.000000000 +0000 @@ -9,11 +9,28 @@ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ - #include "generic.hpp" #include "optionsform.hpp" #include "mainwindow.hpp" -#include +#include "sequence_matcher.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include TextBoxList getTextBoxes(PdfPage page) @@ -29,28 +46,67 @@ MainWindow::MainWindow(const QString &filename1, const QString &filename2, QWidget *parent) - : QMainWindow(parent), current_path("."), cancel(false) + : QMainWindow(parent), currentPath("."), + controlDockArea(Qt::RightDockWidgetArea), + actionDockArea(Qt::RightDockWidgetArea), cancel(false) { QSettings settings; + pen.setStyle(Qt::NoPen); + pen.setColor(Qt::red); + pen = settings.value("Outline", pen).value(); + brush.setColor(pen.color()); + brush.setStyle(Qt::SolidPattern); + brush = settings.value("Fill", brush).value(); + showToolTips = settings.value("ShowToolTips", true).toBool(); + QPixmapCache::setCacheLimit(1000 * + qBound(1, settings.value("CacheSizeMB", 25).toInt(), 100)); + + createWidgets(filename1, filename2); + createCentralArea(); + createDockWidgets(); + createConnections(); + + restoreGeometry(settings.value("MainWindow/Geometry").toByteArray()); + controlDockLocationChanged(static_cast( + settings.value("MainWindow/ControlDockArea", + static_cast(controlDockArea)).toInt())); + actionDockLocationChanged(static_cast( + settings.value("MainWindow/ActionDockArea", + static_cast(actionDockArea)).toInt())); + restoreState(settings.value("MainWindow/State").toByteArray()); + controlDockWidget->resize(controlDockWidget->minimumSizeHint()); + actionDockWidget->resize(actionDockWidget->minimumSizeHint()); + + setWindowTitle(tr("DiffPDF")); + setWindowIcon(QIcon(":/icon.png")); + + QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection, + Q_ARG(QString, filename1), + Q_ARG(QString, filename2)); +} + - QPushButton *setFile1Button = new QPushButton(tr("File #&1...")); +void MainWindow::createWidgets(const QString &filename1, + const QString &filename2) +{ + setFile1Button = new QPushButton(tr("File #&1...")); setFile1Button->setToolTip(tr("

Choose the first (left hand) file " "to be compared.")); - file1Label = new QLabel(); - file1Label->setToolTip(tr("The first (left hand) file.")); - file1Label->setMinimumWidth(100); - file1Label->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken); - file1Label->setText(filename1); + filename1LineEdit = new QLineEdit; + filename1LineEdit->setToolTip(tr("The first (left hand) file.")); + filename1LineEdit->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + filename1LineEdit->setMinimumWidth(100); + filename1LineEdit->setText(filename1); setFile2Button = new QPushButton(tr("File #&2...")); setFile2Button->setToolTip(tr("

Choose the second (right hand) file " "to be compared.")); - file2Label = new QLabel(); - file2Label->setToolTip(tr("The second (right hand) file.")); - file2Label->setMinimumWidth(100); - file2Label->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken); - file2Label->setText(filename2); - QLabel *comparePages1Label = new QLabel(tr("&Pages:")); - pages1LineEdit = new QLineEdit(); + filename2LineEdit = new QLineEdit; + filename2LineEdit->setToolTip(tr("The second (right hand) file.")); + filename2LineEdit->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + filename2LineEdit->setMinimumWidth(100); + filename2LineEdit->setText(filename2); + comparePages1Label = new QLabel(tr("&Pages:")); + pages1LineEdit = new QLineEdit; comparePages1Label->setBuddy(pages1LineEdit); pages1LineEdit->setToolTip(tr("

Pages can be specified using ranges " "such as 1-10, and multiple ranges can be used, e.g., " @@ -61,108 +117,182 @@ "with the extra page being page 14, the two page ranges " "would be set to 1-30 for file1.pdf and 1-13, 15-31 for " "file2.pdf.")); - QLabel *comparePages2Label = new QLabel(tr("Pag&es:")); - pages2LineEdit = new QLineEdit(); + comparePages2Label = new QLabel(tr("Pa&ges:")); + pages2LineEdit = new QLineEdit; comparePages2Label->setBuddy(pages2LineEdit); pages2LineEdit->setToolTip(pages1LineEdit->toolTip()); - compareAppearanceCheckBox = new QCheckBox(tr("Compare &Appearance")); - compareAppearanceCheckBox->setChecked( - settings.value("MainWindow/Appearance").toBool()); - compareAppearanceCheckBox->setToolTip(tr("

If this checkbox is " - "checked then each page's text is compared " - "and if no differences are detected then the pages' " - "appearance's are compared. If this checkbox is " - "unchecked only the text is compared. " - "Note that using this option can be slow for large " - "documents.")); compareButton = new QPushButton(tr("&Compare")); compareButton->setEnabled(false); compareButton->setToolTip(tr("

Click to compare (or re-compare) " - "the documents.")); - QLabel *viewDiffLabel = new QLabel(tr("&View Difference:")); + "the documents—or to cancel a comparison that's " + "in progress.")); + comparisonLabel = new QLabel(tr("Comparison &Mode:")); + comparisonComboBox = new QComboBox; + comparisonLabel->setBuddy(comparisonComboBox); + comparisonComboBox->addItem(tr("Text (Old)"), TextOld); + comparisonComboBox->addItem(tr("Text"), Text); + comparisonComboBox->addItem(tr("Appearance"), Appearance); + comparisonComboBox->setToolTip(tr("

If the Text comparison " + "mode is chosen, then each page's text is compared. " + "If the Appearance comparison mode is chosen " + "then each page's visual appearance is compared. " + "Comparing appearance can be slow for large documents " + "and can also produce false positives." + "

The Text (Old) mode is kept purely for " + "debugging; always use the Text or " + "Appearance modes instead.")); + comparisonComboBox->setCurrentIndex(Text); + viewDiffLabel = new QLabel(tr("&View:")); viewDiffLabel->setToolTip(tr("

Shows each pair of pages which " "are different. The comparison is textual unless the " - "Compare Appearance checkbox is checked, in " + "Appearance comparison mode is chosen, in " "which case the comparison is done visually. " "Visual differences can occur if a paragraph is " "formated differently or if an embedded diagram or " "image has changed.")); - viewDiffComboBox = new QComboBox(); + viewDiffComboBox = new QComboBox; viewDiffComboBox->addItem(tr("(Not viewing)")); viewDiffLabel->setBuddy(viewDiffComboBox); viewDiffComboBox->setToolTip(viewDiffLabel->toolTip()); - QLabel *zoomLabel = new QLabel(tr("&Zoom:")); + zoomLabel = new QLabel(tr("&Zoom:")); zoomLabel->setToolTip(tr("

Determines the scale at which the " "pages are shown.")); - zoomSpinBox = new QSpinBox(); + zoomSpinBox = new QSpinBox; zoomLabel->setBuddy(zoomSpinBox); zoomSpinBox->setRange(25, 400); zoomSpinBox->setSuffix(tr(" %")); zoomSpinBox->setSingleStep(25); - zoomSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + QSettings settings; zoomSpinBox->setValue(settings.value("Zoom", 100).toInt()); zoomSpinBox->setToolTip(zoomLabel->toolTip()); - QPushButton *optionsButton = new QPushButton(tr("&Options...")); + statusLabel = new QLabel(tr("Choose files...")); + statusLabel->setFrameStyle(QFrame::StyledPanel|QFrame::Sunken); + statusLabel->setMaximumHeight(statusLabel->minimumSizeHint().height()); + optionsButton = new QPushButton(tr("&Options...")); optionsButton->setToolTip(tr("Click to customize the application.")); - QPushButton *aboutButton = new QPushButton(tr("A&bout")); - aboutButton->setToolTip(tr("Click for copyright and credits.")); - QPushButton *quitButton = new QPushButton(tr("&Quit")); + aboutButton = new QPushButton(tr("&About")); + aboutButton->setToolTip(tr("Click for copyright, credits and " + "bare bones help.")); + quitButton = new QPushButton(tr("&Quit")); quitButton->setToolTip(tr("Click to terminate the application.")); - page1Label = new QLabel(); + page1Label = new QLabel; page1Label->setAlignment(Qt::AlignCenter); page1Label->setToolTip(tr("

Shows the first (left hand) document's " - "page that correponds to the page shown in the " + "page that corresponds to the page shown in the " "View Difference combobox.")); - page2Label = new QLabel(); + page2Label = new QLabel; page2Label->setAlignment(Qt::AlignCenter); page2Label->setToolTip(tr("

Shows the second (right hand) " - "document's page that correponds to the page shown in " + "document's page that corresponds to the page shown in " "the View Difference combobox.")); - resultsBrowser = new QTextBrowser(); - resultsBrowser->setToolTip(tr("

Shows a log of information about " - "the documents and the progress of comparisons.")); - - QGridLayout *topLayout = new QGridLayout(); - topLayout->addWidget(setFile1Button, 0, 0); - topLayout->addWidget(file1Label, 0, 1); - topLayout->addWidget(comparePages1Label, 0, 2); - topLayout->addWidget(pages1LineEdit, 0, 3); - topLayout->addWidget(setFile2Button, 1, 0); - topLayout->addWidget(file2Label, 1, 1); - topLayout->addWidget(comparePages2Label, 1, 2); - topLayout->addWidget(pages2LineEdit, 1, 3); - topLayout->addWidget(compareAppearanceCheckBox, 0, 4); - topLayout->addWidget(viewDiffLabel, 1, 4); - topLayout->addWidget(viewDiffComboBox, 1, 5, 1, 2); - topLayout->addWidget(zoomLabel, 0, 5); - topLayout->addWidget(zoomSpinBox, 0, 6); - topLayout->addWidget(compareButton, 0, 7); - topLayout->addWidget(optionsButton, 0, 8); - topLayout->addWidget(aboutButton, 1, 7); - topLayout->addWidget(quitButton, 1, 8); - QVBoxLayout *layout = new QVBoxLayout(); - layout->addLayout(topLayout); - topSplitter = new QSplitter(Qt::Horizontal); - QScrollArea *area1 = new QScrollArea(); + logEdit = new QPlainTextEdit; + + foreach (QWidget *widget, QList() << setFile1Button + << filename1LineEdit << pages1LineEdit << page1Label + << setFile2Button << filename2LineEdit << pages2LineEdit + << page2Label << comparisonComboBox << compareButton + << viewDiffLabel << viewDiffComboBox << zoomLabel + << zoomSpinBox << optionsButton << aboutButton << quitButton + << logEdit) + if (!widget->toolTip().isEmpty()) + widget->installEventFilter(this); +} + + +void MainWindow::createCentralArea() +{ + QHBoxLayout *topLeftLayout = new QHBoxLayout; + topLeftLayout->addWidget(setFile1Button); + topLeftLayout->addWidget(filename1LineEdit, 3); + topLeftLayout->addWidget(comparePages1Label); + topLeftLayout->addWidget(pages1LineEdit, 2); + area1 = new QScrollArea; area1->setWidget(page1Label); area1->setWidgetResizable(true); - topSplitter->addWidget(area1); - QScrollArea *area2 = new QScrollArea(); + QVBoxLayout *leftLayout = new QVBoxLayout; + leftLayout->addLayout(topLeftLayout); + leftLayout->addWidget(area1, 1); + QWidget *leftWidget = new QWidget; + leftWidget->setLayout(leftLayout); + + QHBoxLayout *topRightLayout = new QHBoxLayout; + topRightLayout->addWidget(setFile2Button); + topRightLayout->addWidget(filename2LineEdit, 3); + topRightLayout->addWidget(comparePages2Label); + topRightLayout->addWidget(pages2LineEdit, 2); + area2 = new QScrollArea; area2->setWidget(page2Label); area2->setWidgetResizable(true); - topSplitter->addWidget(area2); - topSplitter->restoreState(settings.value("MainWindow/TopSplitter") - .toByteArray()); - bottomSplitter = new QSplitter(Qt::Vertical); - bottomSplitter->addWidget(topSplitter); - bottomSplitter->addWidget(resultsBrowser); - bottomSplitter->restoreState(settings.value( - "MainWindow/BottomSplitter").toByteArray()); - layout->addWidget(bottomSplitter); - QWidget *widget = new QWidget(); - widget->setLayout(layout); - setCentralWidget(widget); + QVBoxLayout *rightLayout = new QVBoxLayout; + rightLayout->addLayout(topRightLayout); + rightLayout->addWidget(area2, 1); + QWidget *rightWidget = new QWidget; + rightWidget->setLayout(rightLayout); + + splitter = new QSplitter(Qt::Horizontal); + splitter->addWidget(leftWidget); + splitter->addWidget(rightWidget); + QSettings settings; + splitter->restoreState(settings.value("MainWindow/ViewSplitter") + .toByteArray()); + + setCentralWidget(splitter); +} + + +void MainWindow::createDockWidgets() +{ + setDockOptions(QMainWindow::AnimatedDocks); + QDockWidget::DockWidgetFeatures features = + QDockWidget::DockWidgetMovable| + QDockWidget::DockWidgetFloatable; + + controlDockWidget = new QDockWidget(tr("Controls"), this); + controlDockWidget->setObjectName("Controls"); + controlDockWidget->setFeatures(features); + controlLayout = new QBoxLayout(QBoxLayout::TopToBottom); + controlLayout->addWidget(compareButton); + controlLayout->addWidget(comparisonLabel); + controlLayout->addWidget(comparisonComboBox); + QHBoxLayout *viewLayout = new QHBoxLayout; + viewLayout->addWidget(viewDiffLabel); + viewLayout->addWidget(viewDiffComboBox); + controlLayout->addLayout(viewLayout); + QHBoxLayout *zoomLayout = new QHBoxLayout; + zoomLayout->addWidget(zoomLabel); + zoomLayout->addWidget(zoomSpinBox); + controlLayout->addLayout(zoomLayout); + controlLayout->addWidget(statusLabel); + controlLayout->addStretch(); + QWidget *widget = new QWidget; + widget->setLayout(controlLayout); + controlDockWidget->setWidget(widget); + addDockWidget(controlDockArea, controlDockWidget); + + actionDockWidget = new QDockWidget(tr("Actions"), this); + actionDockWidget->setObjectName("Actions"); + actionDockWidget->setFeatures(features); + actionLayout = new QBoxLayout(QBoxLayout::TopToBottom); + actionLayout->addWidget(compareButton); + actionLayout->addWidget(optionsButton); + actionLayout->addWidget(aboutButton); + actionLayout->addWidget(quitButton); + actionLayout->addStretch(); + widget = new QWidget; + widget->setLayout(actionLayout); + actionDockWidget->setWidget(widget); + addDockWidget(actionDockArea, actionDockWidget); + + logDockWidget = new QDockWidget(tr("Log"), this); + logDockWidget->setObjectName("Log"); + logDockWidget->setFeatures(features|QDockWidget::DockWidgetClosable); + logDockWidget->setWidget(logEdit); + addDockWidget(Qt::RightDockWidgetArea, logDockWidget); +} + +void MainWindow::createConnections() +{ connect(area1->verticalScrollBar(), SIGNAL(valueChanged(int)), area2->verticalScrollBar(), SLOT(setValue(int))); connect(area2->verticalScrollBar(), SIGNAL(valueChanged(int)), @@ -171,6 +301,12 @@ area2->horizontalScrollBar(), SLOT(setValue(int))); connect(area2->horizontalScrollBar(), SIGNAL(valueChanged(int)), area1->horizontalScrollBar(), SLOT(setValue(int))); + + connect(filename1LineEdit, SIGNAL(textEdited(const QString&)), + this, SLOT(updateUi())); + connect(filename2LineEdit, SIGNAL(textEdited(const QString&)), + this, SLOT(updateUi())); + connect(viewDiffComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(updateViews(int))); connect(setFile1Button, SIGNAL(clicked()), this, SLOT(setFile1())); @@ -182,15 +318,30 @@ connect(aboutButton, SIGNAL(clicked()), this, SLOT(about())); connect(quitButton, SIGNAL(clicked()), this, SLOT(close())); - restoreGeometry(settings.value("MainWindow/Geometry").toByteArray()); - setWindowTitle(tr("DiffPDF")); - setWindowIcon(QIcon(":/icon.png")); + connect(controlDockWidget, + SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), + this, SLOT(controlDockLocationChanged(Qt::DockWidgetArea))); + connect(actionDockWidget, + SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), + this, SLOT(actionDockLocationChanged(Qt::DockWidgetArea))); + connect(controlDockWidget, SIGNAL(topLevelChanged(bool)), + this, SLOT(controlTopLevelChanged(bool))); + connect(actionDockWidget, SIGNAL(topLevelChanged(bool)), + this, SLOT(actionTopLevelChanged(bool))); + connect(logDockWidget, SIGNAL(topLevelChanged(bool)), + this, SLOT(logTopLevelChanged(bool))); +} + + +void MainWindow::initialize(const QString &filename1, + const QString &filename2) +{ if (!filename1.isEmpty()) { setFile1(filename1); setFile2Button->setFocus(); if (!filename2.isEmpty()) { setFile2(filename2); - QTimer::singleShot(0, this, SLOT(compare())); + compare(); } } else @@ -200,8 +351,61 @@ void MainWindow::updateUi() { - compareButton->setEnabled(!file1Label->text().isEmpty() && - !file2Label->text().isEmpty()); + compareButton->setEnabled(!filename1LineEdit->text().isEmpty() && + !filename2LineEdit->text().isEmpty()); +} + + +void MainWindow::controlDockLocationChanged(Qt::DockWidgetArea area) +{ + if (area == Qt::TopDockWidgetArea || + area == Qt::BottomDockWidgetArea) + controlLayout->setDirection(QBoxLayout::LeftToRight); + else + controlLayout->setDirection(QBoxLayout::TopToBottom); + controlDockArea = area; +} + + +void MainWindow::actionDockLocationChanged(Qt::DockWidgetArea area) +{ + if (area == Qt::TopDockWidgetArea || + area == Qt::BottomDockWidgetArea) + actionLayout->setDirection(QBoxLayout::LeftToRight); + else + actionLayout->setDirection(QBoxLayout::TopToBottom); + actionDockArea = area; +} + + +void MainWindow::controlTopLevelChanged(bool floating) +{ + controlLayout->setDirection(floating ? QBoxLayout::TopToBottom + : QBoxLayout::LeftToRight); + if (QWidget *widget = static_cast(controlLayout->parent())) + widget->setFixedSize(floating ? widget->minimumSizeHint() + : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); + controlDockWidget->setWindowTitle(floating ? tr("DiffPDF - Controls") + : tr("Controls")); +} + + +void MainWindow::actionTopLevelChanged(bool floating) +{ + actionLayout->setDirection(floating ? QBoxLayout::TopToBottom + : QBoxLayout::LeftToRight); + if (QWidget *widget = static_cast(actionLayout->parent())) + widget->setFixedSize(floating ? widget->minimumSizeHint() + : QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); + actionDockWidget->setWindowTitle(floating ? tr("DiffPDF - Actions") + : tr("Actions")); +} + + +void MainWindow::logTopLevelChanged(bool floating) +{ + logDockWidget->setWindowTitle(floating ? tr("DiffPDF - Log") + : tr("Log")); } @@ -214,44 +418,144 @@ } else if (index == -1) index = viewDiffComboBox->currentIndex(); - PagePair pp = viewDiffComboBox->itemData(index).value(); - if (pp.isNull()) + PagePair pair = viewDiffComboBox->itemData(index).value(); + if (pair.isNull()) return; - QString filename1 = file1Label->text(); + QString filename1 = filename1LineEdit->text(); PdfDocument pdf1 = getPdf(filename1); if (!pdf1) return; - PdfPage page1(pdf1->page(pp.left)); + PdfPage page1(pdf1->page(pair.left)); if (!page1) return; - QString filename2 = file2Label->text(); + QString filename2 = filename2LineEdit->text(); PdfDocument pdf2 = getPdf(filename2); if (!pdf2) return; - PdfPage page2(pdf2->page(pp.right)); + PdfPage page2(pdf2->page(pair.right)); if (!page2) return; - int dpi = static_cast(DPI_FACTOR * - (zoomSpinBox->value() / 100.0)); - QImage plainImage1; - QImage plainImage2; - if (pp.visual_difference || compareAppearanceCheckBox->isChecked()) { - plainImage1 = page1->renderToImage(dpi, dpi); - plainImage2 = page2->renderToImage(dpi, dpi); - } - pdf1->setRenderHint(Poppler::Document::Antialiasing); - pdf1->setRenderHint(Poppler::Document::TextAntialiasing); - pdf2->setRenderHint(Poppler::Document::Antialiasing); - pdf2->setRenderHint(Poppler::Document::TextAntialiasing); - QImage image1 = page1->renderToImage(dpi, dpi); - QImage image2 = page2->renderToImage(dpi, dpi); - - QRegion highlighted1; - QRegion highlighted2; - // Always show textual differences + QString key = QString("%1:%2:%3").arg(index).arg(zoomSpinBox->value()) + .arg(comparisonComboBox->currentIndex()); + QString key1 = QString("1:%1:%2:%3").arg(key).arg(pair.left) + .arg(filename1); + QString key2 = QString("2:%1:%2:%3").arg(key).arg(pair.right) + .arg(filename2); + updateViews(pdf1, page1, pdf2, page2, pair.hasVisualDifference, + key1, key2); +} + + +void MainWindow::updateViews(const PdfDocument &pdf1, + const PdfPage &page1, const PdfDocument &pdf2, + const PdfPage &page2, bool hasVisualDifference, + const QString &key1, const QString &key2) +{ + QPixmap pixmap1; + QPixmap pixmap2; +#if QT_VERSION >= 0x040600 + if (!QPixmapCache::find(key1, &pixmap1) || + !QPixmapCache::find(key2, &pixmap2)) { +#else + if (!QPixmapCache::find(key1, pixmap1) || + !QPixmapCache::find(key2, pixmap2)) { +#endif + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + const int DPI = static_cast(DPI_FACTOR * + (zoomSpinBox->value() / 100.0)); + Comparison comparison = static_cast( + comparisonComboBox->itemData( + comparisonComboBox->currentIndex()).toInt()); + QImage plainImage1; + QImage plainImage2; + if (hasVisualDifference || comparison == Appearance) { + plainImage1 = page1->renderToImage(DPI, DPI); + plainImage2 = page2->renderToImage(DPI, DPI); + } + pdf1->setRenderHint(Poppler::Document::Antialiasing); + pdf1->setRenderHint(Poppler::Document::TextAntialiasing); + pdf2->setRenderHint(Poppler::Document::Antialiasing); + pdf2->setRenderHint(Poppler::Document::TextAntialiasing); + QImage image1 = page1->renderToImage(DPI, DPI); + QImage image2 = page2->renderToImage(DPI, DPI); + + QPainterPath highlighted1; + QPainterPath highlighted2; + if (hasVisualDifference || comparison == Appearance) + computeVisualHighlights(&highlighted1, &highlighted2, + plainImage1, plainImage2); + else if (comparison == Text) + computeTextHighlights(&highlighted1, &highlighted2, page1, + page2, DPI); + else if (comparison == TextOld) + computeTextHighlightsOld(&highlighted1, &highlighted2, page1, + page2, DPI); + if (!highlighted1.isEmpty()) + paintOnImage(highlighted1, &image1); + if (!highlighted2.isEmpty()) + paintOnImage(highlighted2, &image2); + if (highlighted1.isEmpty() && highlighted2.isEmpty()) { + QFont font("Helvetica", 14); + font.setOverline(true); + font.setUnderline(true); + highlighted1.addText(DPI / 4, DPI / 4, font, + tr("DiffPDF: False Positive")); + paintOnImage(highlighted1, &image1); + } + pixmap1 = QPixmap::fromImage(image1); + pixmap2 = QPixmap::fromImage(image2); + QPixmapCache::insert(key1, pixmap1); + QPixmapCache::insert(key2, pixmap2); + QApplication::restoreOverrideCursor(); + } + page1Label->setPixmap(pixmap1); + page2Label->setPixmap(pixmap2); +} + + +void MainWindow::computeTextHighlights(QPainterPath *highlighted1, + QPainterPath *highlighted2, const PdfPage &page1, + const PdfPage &page2, const int DPI) +{ + QRectF rect1; + QRectF rect2; + QSettings settings; + const int OVERLAP = settings.value("Overlap", 5).toInt(); + const bool COMBINE = settings.value("CombineTextHighlighting", true) + .toBool(); + TextBoxList list1 = getTextBoxes(page1); + TextBoxList list2 = getTextBoxes(page2); + QStringList words1; + QStringList words2; + foreach (const PdfTextBox &box, list1) + words1 << box->text(); + foreach (const PdfTextBox &box, list2) + words2 << box->text(); + SequenceMatcher matcher(words1, words2); + RangesPair rangesPair = computeRanges(&matcher); + rangesPair = invertRanges(rangesPair.first, words1.count(), + rangesPair.second, words2.count()); + foreach (int index, rangesPair.first) + addHighlighting(&rect1, highlighted1, list1[index], OVERLAP, + DPI, COMBINE); + if (!rect1.isNull() && !rangesPair.first.isEmpty()) + highlighted1->addRect(rect1); + foreach (int index, rangesPair.second) + addHighlighting(&rect2, highlighted2, list2[index], OVERLAP, + DPI, COMBINE); + if (!rect2.isNull() && !rangesPair.second.isEmpty()) + highlighted2->addRect(rect2); +} + + +void MainWindow::computeTextHighlightsOld(QPainterPath *highlighted1, + QPainterPath *highlighted2, const PdfPage &page1, + const PdfPage &page2, const int DPI) +{ + // Textual #1: Highlight every word that differs between the two pages QRectF rect1; QRectF rect2; QSettings settings; @@ -261,135 +565,169 @@ while (!list1.isEmpty() && !list2.isEmpty()) { PdfTextBox box1 = list1.takeFirst(); PdfTextBox box2 = list2.takeFirst(); - if (box1->text().simplified() != box2->text().simplified()) { - QRectF rect = box1->boundingBox(); - scaleRect(dpi, &rect); - if (rect.adjusted(-OVERLAP, -OVERLAP, OVERLAP, OVERLAP) - .intersects(rect1)) - rect1 = rect1.united(rect); - else { - highlighted1 = highlighted1.united(rect1.toRect()); - rect1 = rect; - } - rect = box2->boundingBox(); - scaleRect(dpi, &rect); - if (rect.adjusted(-OVERLAP, -OVERLAP, OVERLAP, OVERLAP) - .intersects(rect2)) - rect2 = rect2.united(rect); - else { - highlighted2 = highlighted2.united(rect2.toRect()); - rect2 = rect; - } - } - if (!rect1.isNull()) { - highlighted1 = highlighted1.united(rect1.toRect()); - } - if (!rect2.isNull()) { - highlighted2 = highlighted2.united(rect2.toRect()); + if (box1->text() != box2->text()) { + addHighlighting(&rect1, highlighted1, box1, OVERLAP, DPI); + addHighlighting(&rect2, highlighted2, box2, OVERLAP, DPI); } + if (!rect1.isNull()) + highlighted1->addRect(rect1); + if (!rect2.isNull()) + highlighted2->addRect(rect2); + } + + // Textual #2: Highlight text at the end of one page but not the other + TextBoxList list = list1.isEmpty() ? list2 : list1; + QPainterPath *highlighted = list1.isEmpty() ? highlighted2 + : highlighted1; + QRectF rect; + while (!list.isEmpty()) { + PdfTextBox box = list.takeFirst(); + addHighlighting(&rect, highlighted, box, OVERLAP, DPI); + if (!rect.isNull()) + highlighted->addRect(rect1); + } +} + + +void MainWindow::addHighlighting(QRectF *bigRect, + QPainterPath *highlighted, const PdfTextBox &box, + const int OVERLAP, const int DPI, const bool COMBINE) +{ + QRectF rect = box->boundingBox(); + scaleRect(DPI, &rect); + if (COMBINE && rect.adjusted(-OVERLAP, -OVERLAP, OVERLAP, OVERLAP) + .intersects(*bigRect)) + *bigRect = bigRect->united(rect); + else { + highlighted->addRect(*bigRect); + *bigRect = rect; } +} + + +void MainWindow::computeVisualHighlights(QPainterPath *highlighted1, + QPainterPath *highlighted2, const QImage &plainImage1, + const QImage &plainImage2) +{ + QSettings settings; const int SQUARE_SIZE = settings.value("SquareSize", 10).toInt(); - if (pp.visual_difference || compareAppearanceCheckBox->isChecked()) { - QRect target; - for (int x = 0; x < plainImage1.width(); x += SQUARE_SIZE) { - for (int y = 0; y < plainImage1.height(); y += SQUARE_SIZE) { - QRect rect(x, y, SQUARE_SIZE, SQUARE_SIZE); - QImage temp1 = plainImage1.copy(rect); - QImage temp2 = plainImage2.copy(rect); - if (temp1 != temp2) { - if (rect.adjusted(-1, -1, 1, 1).intersects(target)) - target = target.united(rect); - else { - highlighted1 = highlighted1.united(target); - highlighted2 = highlighted2.united(target); - target = rect; - } + QRect target; + for (int x = 0; x < plainImage1.width(); x += SQUARE_SIZE) { + for (int y = 0; y < plainImage1.height(); y += SQUARE_SIZE) { + QRect rect(x, y, SQUARE_SIZE, SQUARE_SIZE); + QImage temp1 = plainImage1.copy(rect); + QImage temp2 = plainImage2.copy(rect); + if (temp1 != temp2) { + if (rect.adjusted(-1, -1, 1, 1).intersects(target)) + target = target.united(rect); + else { + highlighted1->addRect(target); + highlighted2->addRect(target); + target = rect; } } } - if (!target.isNull()) { - highlighted1 = highlighted1.united(target); - highlighted2 = highlighted2.united(target); - } } - paintRectsOnImage(highlighted1, &image1); - paintRectsOnImage(highlighted2, &image2); - page1Label->setPixmap(QPixmap::fromImage(image1)); - page2Label->setPixmap(QPixmap::fromImage(image2)); + if (!target.isNull()) { + highlighted1->addRect(target); + highlighted2->addRect(target); + } } -void MainWindow::paintRectsOnImage(const QRegion ®ion, QImage *image) +void MainWindow::paintOnImage(const QPainterPath &path, QImage *image) { - QSettings settings; - QColor color = settings.value("HighlightColor", Qt::yellow) - .value(); - QPen pen(Qt::PenStyle(settings.value("LineStyle", Qt::SolidLine) - .toInt())); - pen.setColor(color); - color.setAlpha(32); // semi-transparent version of the color - QBrush brush(color); + QPen pen_(pen); + QBrush brush_(brush); + QColor color = pen.color(); + color.setAlpha(32); // semi-transparent for painting (stored as solid) + pen_.setColor(color); + brush_.setColor(color); + QPainter painter(image); painter.setRenderHint(QPainter::Antialiasing); - painter.setPen(pen); - painter.setBrush(brush); - foreach (QRect rect, region.rects()) - painter.drawRect(minimumSizedRect(rect)); - painter.end(); -} + painter.setPen(pen_); + painter.setBrush(brush_); - -QRect MainWindow::minimumSizedRect(const QRect &rect) -{ - const int MINIMUM_HEIGHT = 4; - const int MINIMUM_WIDTH = 8; - - QRect r(rect); - if (r.width() < MINIMUM_WIDTH) { - r.setWidth(MINIMUM_WIDTH); - r.moveLeft(MINIMUM_WIDTH / 2); + QSettings settings; + const int SQUARE_SIZE = settings.value("SquareSize", 10).toInt(); + const double RULE_WIDTH = settings.value("RuleWidth", 1.5).toDouble(); + QRectF rect = path.boundingRect(); + if (rect.width() < SQUARE_SIZE && rect.height() < SQUARE_SIZE) { + rect.setHeight(SQUARE_SIZE); + rect.setWidth(SQUARE_SIZE); + painter.drawRect(rect); + if (!qFuzzyCompare(RULE_WIDTH, 0.0)) { + painter.setPen(QPen(pen.color())); + painter.drawRect(0, rect.y(), RULE_WIDTH, rect.height()); + } } - if (r.height() < MINIMUM_HEIGHT) { - r.setHeight(MINIMUM_HEIGHT); - r.setY(r.y() - (MINIMUM_HEIGHT / 2)); + else { + QPainterPath path_(path); + path_.setFillRule(Qt::WindingFill); + painter.drawPath(path_); + if (!qFuzzyCompare(RULE_WIDTH, 0.0)) { + painter.setPen(QPen(pen.color())); + QList polygons = path_.toFillPolygons(); + foreach (const QPolygonF &polygon, polygons) { + const QRectF rect = polygon.boundingRect(); + painter.drawRect(0, rect.y(), RULE_WIDTH, rect.height()); + } + } } - return r; + painter.end(); } -void MainWindow::closeEvent(QCloseEvent *) +void MainWindow::closeEvent(QCloseEvent*) { QSettings settings; settings.setValue("MainWindow/Geometry", saveGeometry()); - settings.setValue("MainWindow/Appearance", - compareAppearanceCheckBox->isChecked()); - settings.setValue("MainWindow/TopSplitter", - topSplitter->saveState()); - settings.setValue("MainWindow/BottomSplitter", - bottomSplitter->saveState()); + settings.setValue("MainWindow/State", saveState()); + settings.setValue("MainWindow/ControlDockArea", + static_cast(controlDockArea)); + settings.setValue("MainWindow/ActionDockArea", + static_cast(actionDockArea)); + settings.setValue("MainWindow/ViewSplitter", splitter->saveState()); + settings.setValue("ShowToolTips", showToolTips); + settings.setValue("CombineTextHighlighting", combineTextHighlighting); settings.setValue("Zoom", zoomSpinBox->value()); + settings.setValue("Outline", pen); + settings.setValue("Fill", brush); QMainWindow::close(); } +bool MainWindow::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::ToolTip && !showToolTips) + return true; + return QMainWindow::eventFilter(object, event); +} + + void MainWindow::setFile1(QString filename) { if (filename.isEmpty()) filename = QFileDialog::getOpenFileName(this, - tr("DiffPDF - Choose File #1"), current_path, + tr("DiffPDF - Choose File #1"), currentPath, tr("PDF files (*.pdf)")); if (!filename.isEmpty()) { - if (filename == file2Label->text()) { + if (filename == filename2LineEdit->text()) { QMessageBox::warning(this, tr("DiffPDF - Error"), tr("Cannot compare a file to itself.")); return; } - file1Label->setText(filename); + filename1LineEdit->setText(filename); updateUi(); int page_count = writeFileInfo(filename); pages1LineEdit->setText(tr("1-%1").arg(page_count)); - current_path = QFileInfo(filename).canonicalPath(); + currentPath = QFileInfo(filename).canonicalPath(); setFile2Button->setFocus(); + if (filename2LineEdit->text().isEmpty()) + statusLabel->setText(tr("Choose second file")); + else + statusLabel->setText(tr("Ready to compare")); } } @@ -398,20 +736,24 @@ { if (filename.isEmpty()) filename = QFileDialog::getOpenFileName(this, - tr("DiffPDF - Choose File #2"), current_path, + tr("DiffPDF - Choose File #2"), currentPath, tr("PDF files (*.pdf)")); if (!filename.isEmpty()) { - if (filename == file1Label->text()) { + if (filename == filename1LineEdit->text()) { QMessageBox::warning(this, tr("DiffPDF - Error"), tr("Cannot compare a file to itself.")); return; } - file2Label->setText(filename); + filename2LineEdit->setText(filename); updateUi(); int page_count = writeFileInfo(filename); pages2LineEdit->setText(tr("1-%1").arg(page_count)); - current_path = QFileInfo(filename).canonicalPath(); + currentPath = QFileInfo(filename).canonicalPath(); compareButton->setFocus(); + if (filename1LineEdit->text().isEmpty()) + statusLabel->setText(tr("Choose first file")); + else + statusLabel->setText(tr("Ready to compare")); } } @@ -420,10 +762,16 @@ { PdfDocument pdf(Poppler::Document::load(filename)); if (!pdf) - writeError(tr("Failed to load '%1'").arg(filename)); + QMessageBox::warning(this, tr("DiffPDF - Error"), + tr("Cannot load '%1'.").arg(filename)); else if (pdf->isLocked()) { - writeError(tr("Cannot read a locked PDF")); + QMessageBox::warning(this, tr("DiffPDF - Error"), + tr("Cannot read a locked PDF ('%1').").arg(filename)); +#if QT_VERSION >= 0x040600 + pdf.clear(); +#else pdf.reset(); +#endif } return pdf; } @@ -451,21 +799,30 @@ writeLine(tr("Created: %1.").arg(created.toString())); page_count = pdf->numPages(); writeLine(tr("Page count: %1.").arg(page_count)); + if (page_count > 0) { + const double PointToMM = 0.3527777777; + PdfPage page1(pdf->page(0)); + QSize size = page1->pageSize(); + writeLine(tr("Page size: %1pt x %2pt (%3mm x %4mm).") + .arg(size.width()).arg(size.height()) + .arg(qRound(size.width() * PointToMM)) + .arg(qRound(size.height() * PointToMM))); + } return page_count; } void MainWindow::writeLine(const QString &text) { - resultsBrowser->append(text); - resultsBrowser->ensureCursorVisible(); + logEdit->appendHtml(text); + logEdit->ensureCursorVisible(); } void MainWindow::writeError(const QString &text) { - resultsBrowser->append(tr("%1").arg(text)); - resultsBrowser->ensureCursorVisible(); + logEdit->appendHtml(tr("%1").arg(text)); + logEdit->ensureCursorVisible(); } @@ -529,24 +886,43 @@ return; } cancel = false; - QString filename1 = file1Label->text(); + QString filename1 = filename1LineEdit->text(); PdfDocument pdf1 = getPdf(filename1); if (!pdf1) return; - QString filename2 = file2Label->text(); + QString filename2 = filename2LineEdit->text(); PdfDocument pdf2 = getPdf(filename2); if (!pdf2) { return; } + + comparePrepareUi(); + QPair pair = comparePages(filename1, pdf1, filename2, pdf2); + compareUpdateUi(pair); +} + + +void MainWindow::comparePrepareUi() +{ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); compareButton->setText(tr("&Cancel")); compareButton->setEnabled(true); compareButton->setFocus(); viewDiffComboBox->clear(); viewDiffComboBox->addItem(tr("(Not viewing)")); + statusLabel->setText(tr("Ready")); +} + + +QPair MainWindow::comparePages(const QString &filename1, + const PdfDocument &pdf1, const QString &filename2, + const PdfDocument &pdf2) +{ QList pages1 = getPageList(1, pdf1); QList pages2 = getPageList(2, pdf2); - int minimum = qMin(pages1.count(), pages2.count()); + int total = qMin(pages1.count(), pages2.count()); + int number = 0; + int index = 0; while (!pages1.isEmpty() && !pages2.isEmpty()) { int p1 = pages1.takeFirst(); PdfPage page1(pdf1->page(p1)); @@ -568,35 +944,53 @@ writeError(tr("Cancelled.")); break; } - Difference diff = comparePages(page1, page2); - if (diff != NoDifference) { + Difference difference = getTheDifference(page1, page2); + if (difference != NoDifference) { QVariant v; - v.setValue(PagePair(p1, p2, diff == VisualDifference)); - viewDiffComboBox->addItem(tr("%1 vs. %2").arg(p1 + 1) - .arg(p2 + 1), v); + v.setValue(PagePair(p1, p2, difference == VisualDifference)); + viewDiffComboBox->addItem(tr("%1 vs. %2 %3 %4") + .arg(p1 + 1).arg(p2 + 1).arg(QChar(0x2022)) + .arg(++index), v); } + statusLabel->setText(tr("Comparing %1/%2").arg(++number) + .arg(total)); } + return qMakePair(number, total); +} + + +void MainWindow::compareUpdateUi(const QPair &pair) +{ + int differ = viewDiffComboBox->count() - 1; if (!cancel) { if (viewDiffComboBox->count() > 1) { if (viewDiffComboBox->count() == 2) writeLine(tr("Files differ on 1 page " "(%1 page%2 compared).") - .arg(minimum) - .arg(minimum == 1 ? tr(" was") : tr("s were"))); + .arg(pair.first) + .arg(pair.first == 1 ? tr(" was") : tr("s were"))); else writeLine(tr("Files differ on %1 pages " "(%2 page%3 compared).") - .arg(viewDiffComboBox->count() - 1) - .arg(minimum) - .arg(minimum == 1 ? tr(" was") - : tr("s were"))); + .arg(differ).arg(pair.first) + .arg(pair.first == 1 ? tr(" was") + : tr("s were"))); viewDiffComboBox->setFocus(); viewDiffComboBox->setCurrentIndex(1); } - else - writeLine(tr("No differences detected.")); + else { + writeLine(tr("The PDFs appear to be the same.")); + const QString message("

" + "DiffPDF: The PDFs appear to be the same.

"); + page1Label->setText(message); + page2Label->setText(message); + } } + compareButton->setText(tr("&Compare")); + statusLabel->setText(tr("%1 differ%2 %3/%4 compared").arg(differ) + .arg(differ == 1 ? "s" : "").arg(pair.first).arg(pair.second)); updateUi(); if (!cancel) viewDiffComboBox->setFocus(); @@ -604,18 +998,19 @@ } -MainWindow::Difference MainWindow::comparePages(PdfPage page1, - PdfPage page2) +MainWindow::Difference MainWindow::getTheDifference(PdfPage page1, + PdfPage page2) { TextBoxList list1 = getTextBoxes(page1); TextBoxList list2 = getTextBoxes(page2); - while (!list1.isEmpty() && !list2.isEmpty()) { - PdfTextBox box1 = list1.takeFirst(); - PdfTextBox box2 = list2.takeFirst(); - if (box1->text().simplified() != box2->text().simplified()) + if (list1.count() != list2.count()) + return TextualDifference; + for (int i = 0; i < list1.count(); ++i) + if (list1[i]->text() != list2[i]->text()) return TextualDifference; - } - if (compareAppearanceCheckBox->isChecked()) { + + if (comparisonComboBox->itemData( + comparisonComboBox->currentIndex()) == Appearance) { QImage image1 = page1->renderToImage(); QImage image2 = page2->renderToImage(); if (image1 != image2) @@ -627,30 +1022,51 @@ void MainWindow::options() { - OptionsForm form(this); - form.exec(); + QSettings settings; + qreal ruleWidth = settings.value("RuleWidth", 1.5).toDouble(); + bool combineTextHighlighting = + settings.value("CombineTextHighlighting", true).toBool(); + int cacheSize = QPixmapCache::cacheLimit() / 1000; + OptionsForm form(&pen, &brush, &ruleWidth, &showToolTips, + &combineTextHighlighting, &cacheSize, this); + if (form.exec()) { + settings.setValue("RuleWidth", ruleWidth); + settings.setValue("CombineTextHighlighting", + combineTextHighlighting); + settings.setValue("CacheSizeMB", cacheSize); + QPixmapCache::clear(); + QPixmapCache::setCacheLimit(1000 * cacheSize); + updateViews(); + } } void MainWindow::about() { - static const QString version("0.4.0"); + static const QString version("1.1.4"); QMessageBox::about(this, tr("DiffPDF - About"), tr("

DiffPDF %1 by Mark Summerfield." - "

Copyright © 2008-10 Qtrac " - "Ltd. All rights reserved." - "

This program compares the text (and optionally the appearance) " - "of each page in two PDF files. It was inspired by an idea " - "from Jasmin Blanchette, and incorporates many of his suggestions." + "

Copyright © 2008-10 " + "Qtrac Ltd. All rights reserved." + "

This program compares the text or the visual appearance of " + "each page in two PDF files. It was inspired by an idea " + "from Jasmin Blanchette, and incorporates many of his suggestions. " + "Thanks also to David Paleino." "

To learn how to use the program click the File #1 button " "to choose one PDF file and then the File #2 button to choose " "another (ideally very similar) PDF file, then click the " - "Compare button to see the results! " - "You can also read the tooltips." + "Compare button to perform the comparison, and when " + "that's finished, navigate through the pairs of differing pages " + "using the View combobox. " + "

The Controls, Actions, and Log are in dock widgets—these " + "can be dragged into other dock areas (in which case they will " + "reshape themselves as necessary), or dragged to float free. " + "The Log can also be closed; right click a dock area and check " + "the Log checkbox to open it again. Hover the mouse for tooltips." "

Although a GUI program, if run from a console with two PDF " "files listed on the command line, DiffPDF will start up and " - "immediately compare them. " + "immediately compare them in Text mode. " "


This program is free software: you can redistribute it " "and/or modify it under the terms of the GNU General Public License " "as published by the Free Software Foundation, either version 2 of " @@ -661,4 +1077,3 @@ "See the GNU General Public License (in file gpl-2.0.txt) " "for more details.").arg(version)); } - diff -Nru diffpdf-0.4.0/mainwindow.hpp diffpdf-1.1.4/mainwindow.hpp --- diffpdf-0.4.0/mainwindow.hpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/mainwindow.hpp 2010-08-04 06:50:47.000000000 +0000 @@ -1,3 +1,5 @@ +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP /* Copyright (c) 2008-10 Qtrac Ltd. All rights reserved. This program or module is free software: you can redistribute it @@ -10,27 +12,36 @@ for more details. */ -#ifndef MAINWINDOW_HPP -#define MAINWINDOW_HPP - +#if QT_VERSION >= 0x040600 +#include +#else #include +#endif #include +#include #include #include +#include - -class QCheckBox; +class QBoxLayout; class QComboBox; class QLabel; class QLineEdit; +class QPlainTextEdit; class QPushButton; +class QScrollArea; class QSpinBox; class QSplitter; -class QTextBrowser; +#if QT_VERSION >= 0x040600 +typedef QSharedPointer PdfDocument; +typedef QSharedPointer PdfPage; +typedef QSharedPointer PdfTextBox; +#else typedef std::tr1::shared_ptr PdfDocument; typedef std::tr1::shared_ptr PdfPage; typedef std::tr1::shared_ptr PdfTextBox; +#endif typedef QList TextBoxList; @@ -47,6 +58,7 @@ protected: void closeEvent(QCloseEvent *event); + bool eventFilter(QObject *object, QEvent *event); private slots: void setFile1(QString filename=QString()); @@ -54,39 +66,91 @@ void compare(); void options(); void about(); - + void initialize(const QString &filename1, const QString &filename2); void updateUi(); void updateViews(int index=-1); + void controlDockLocationChanged(Qt::DockWidgetArea area); + void actionDockLocationChanged(Qt::DockWidgetArea area); + void controlTopLevelChanged(bool floating); + void actionTopLevelChanged(bool floating); + void logTopLevelChanged(bool floating); private: enum Difference {NoDifference, TextualDifference, VisualDifference}; + enum Comparison {TextOld, Text, Appearance}; + void createWidgets(const QString &filename1, const QString &filename2); + void createCentralArea(); + void createDockWidgets(); + void createConnections(); + QPair comparePages(const QString &filename1, + const PdfDocument &pdf1, const QString &filename2, + const PdfDocument &pdf2); + void comparePrepareUi(); + void compareUpdateUi(const QPair &pair); int writeFileInfo(const QString &filename); void writeLine(const QString &text); void writeError(const QString &text); PdfDocument getPdf(const QString &filename); QList getPageList(int which, PdfDocument pdf); - Difference comparePages(PdfPage page1, PdfPage page2); - QRect minimumSizedRect(const QRect &rect); - void paintRectsOnImage(const QRegion ®ion, QImage *image); - - QLabel *file1Label; - QLabel *file2Label; + Difference getTheDifference(PdfPage page1, PdfPage page2); + void paintOnImage(const QPainterPath &path, QImage *image); + void updateViews(const PdfDocument &pdf1, const PdfPage &page1, + const PdfDocument &pdf2, const PdfPage &page2, + bool hasVisualDifference, const QString &key1, + const QString &key2); + void computeTextHighlights(QPainterPath *highlighted1, + QPainterPath *highlighted2, const PdfPage &page1, + const PdfPage &page2, const int DPI); + void computeTextHighlightsOld(QPainterPath *highlighted1, + QPainterPath *highlighted2, const PdfPage &page1, + const PdfPage &page2, const int DPI); + void computeVisualHighlights(QPainterPath *highlighted1, + QPainterPath *highlighted2, const QImage &plainImage1, + const QImage &plainImage2); + void addHighlighting(QRectF *bigRect, QPainterPath *highlighted, + const PdfTextBox &box, const int OVERLAP, const int DPI, + const bool COMBINE=true); + + QPushButton *setFile1Button; + QLineEdit *filename1LineEdit; + QLabel *comparePages1Label; QLineEdit *pages1LineEdit; - QLineEdit *pages2LineEdit; - QCheckBox *compareAppearanceCheckBox; - QTextBrowser *resultsBrowser; + QLabel *page1Label; + QScrollArea *area1; QPushButton *setFile2Button; + QLineEdit *filename2LineEdit; + QLabel *comparePages2Label; + QLineEdit *pages2LineEdit; + QLabel *page2Label; + QScrollArea *area2; + QLabel *comparisonLabel; + QComboBox *comparisonComboBox; + QLabel *viewDiffLabel; QPushButton *compareButton; QComboBox *viewDiffComboBox; + QLabel *statusLabel; + QLabel *zoomLabel; QSpinBox *zoomSpinBox; - QLabel *page1Label; - QLabel *page2Label; - QSplitter *topSplitter; - QSplitter *bottomSplitter; - - QString current_path; + QPushButton *optionsButton; + QPushButton *aboutButton; + QPushButton *quitButton; + QPlainTextEdit *logEdit; + QSplitter *splitter; + QBoxLayout *controlLayout; + QDockWidget *controlDockWidget; + QBoxLayout *actionLayout; + QDockWidget *actionDockWidget; + QDockWidget *logDockWidget; + + QBrush brush; + QPen pen; + QString currentPath; + Qt::DockWidgetArea controlDockArea; + Qt::DockWidgetArea actionDockArea; bool cancel; + bool showToolTips; + bool combineTextHighlighting; }; #endif // MAINWINDOW_HPP diff -Nru diffpdf-0.4.0/optionsform.cpp diffpdf-1.1.4/optionsform.cpp --- diffpdf-0.4.0/optionsform.cpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/optionsform.cpp 2010-08-04 06:50:47.000000000 +0000 @@ -10,110 +10,217 @@ for more details. */ +#include "generic.hpp" #include "optionsform.hpp" -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +OptionsForm::OptionsForm(QPen *pen, QBrush *brush, qreal *ruleWidth, + bool *showToolTips, bool *combineTextHighlighting, + int *cacheSize, QWidget *parent) + : QDialog(parent), m_pen(pen), m_brush(brush), m_ruleWidth(ruleWidth), + m_showToolTips(showToolTips), + m_combineTextHighlighting(combineTextHighlighting), + m_cacheSize(cacheSize) +{ + this->pen = *m_pen; + this->brush = *m_brush; + + createWidgets(); + createLayout(); + createConnections(); + + updateSwatches(); + updateUi(); + setWindowTitle(tr("DiffPDF - Options")); +} -QPixmap penStyleSwatch(const Qt::PenStyle style, const QColor - &color=Qt::black, const QSize &size=QSize(32, 24)); -QPixmap penStyleSwatch(const Qt::PenStyle style, const QColor &color, - const QSize &size) +void OptionsForm::createWidgets() { - QPixmap pixmap(size); - pixmap.fill(Qt::transparent); - QPainter painter(&pixmap); - QPen pen(style); - pen.setColor(color); - pen.setWidth(3); - painter.setPen(pen); - const int Y = size.height() / 2; - painter.drawLine(0, Y, size.width(), Y); - painter.end(); - return pixmap; + colorComboBox = new QComboBox; + foreach (const QString &name, QColor::colorNames()) { + QColor color(name); + colorComboBox->addItem(colorSwatch(color), name, color); + } + colorComboBox->setCurrentIndex(colorComboBox->findData(pen.color())); + + QColor color = pen.color(); + color.setAlpha(32); // semi-transparent for painting (stored as solid) + + brushStyleComboBox = new QComboBox; + typedef QPair BrushPair; + foreach (const BrushPair &pair, QList() + << qMakePair(tr("No Brush"), Qt::NoBrush) + << qMakePair(tr("Solid"), Qt::SolidPattern) + << qMakePair(tr("Dense #1"), Qt::Dense1Pattern) + << qMakePair(tr("Dense #2"), Qt::Dense2Pattern) + << qMakePair(tr("Dense #3"), Qt::Dense3Pattern) + << qMakePair(tr("Dense #4"), Qt::Dense4Pattern) + << qMakePair(tr("Dense #5"), Qt::Dense5Pattern) + << qMakePair(tr("Dense #6"), Qt::Dense6Pattern) + << qMakePair(tr("Horizontal"), Qt::HorPattern) + << qMakePair(tr("Vertical"), Qt::VerPattern) + << qMakePair(tr("Cross"), Qt::CrossPattern) + << qMakePair(tr("Diagonal /"), Qt::BDiagPattern) + << qMakePair(tr("Diagonal \\"), Qt::FDiagPattern) + << qMakePair(tr("Diagonal Cross"), Qt::DiagCrossPattern)) + brushStyleComboBox->addItem(brushSwatch(pair.second, color), + pair.first, pair.second); + brushStyleComboBox->setCurrentIndex(brushStyleComboBox->findData( + brush.style())); + + penStyleComboBox = new QComboBox; + typedef QPair PenPair; + foreach (const PenPair &pair, QList() + << qMakePair(tr("No Pen"), Qt::NoPen) + << qMakePair(tr("Solid"), Qt::SolidLine) + << qMakePair(tr("Dashed"), Qt::DashLine) + << qMakePair(tr("Dotted"), Qt::DotLine) + << qMakePair(tr("Dash-Dotted"), Qt::DashDotLine) + << qMakePair(tr("Dash-Dot-Dotted"), Qt::DashDotDotLine)) + penStyleComboBox->addItem(penStyleSwatch(pair.second, color), + pair.first, pair.second); + penStyleComboBox->setCurrentIndex(penStyleComboBox->findData( + pen.style())); + + ruleWidthSpinBox = new QDoubleSpinBox; + ruleWidthSpinBox->setRange(0.0, 10.0); + ruleWidthSpinBox->setDecimals(2); + ruleWidthSpinBox->setSingleStep(0.25); + ruleWidthSpinBox->setAlignment(Qt::AlignVCenter|Qt::AlignRight); + ruleWidthSpinBox->setSpecialValueText(tr("No Rules")); + ruleWidthSpinBox->setValue(*m_ruleWidth); + + showToolTipsCheckBox = new QCheckBox(tr("Show &Tooltips in " + "the main window")); + showToolTipsCheckBox->setChecked(*m_showToolTips); + + combineTextHighlightingCheckBox = new QCheckBox( + tr("Combine &Highlighting in Text Mode")); + combineTextHighlightingCheckBox->setChecked( + *m_combineTextHighlighting); + + cacheSizeSpinBox = new QSpinBox; + cacheSizeSpinBox->setRange(1, 100); + cacheSizeSpinBox->setValue(*m_cacheSize); + cacheSizeSpinBox->setSuffix(tr(" MB")); + + buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok| + QDialogButtonBox::Cancel); } -OptionsForm::OptionsForm(QWidget *parent) - : QDialog(parent) -{ - QSettings settings; - - QPushButton *highlightColorButton = new QPushButton( - tr("Highlight &Color...")); - highlightColorLabel = new QLabel(); - QPixmap pixmap(60, 3); - highlight_color = settings.value("HighlightColor", Qt::yellow) - .value(); - pixmap.fill(highlight_color); - highlightColorLabel->setPixmap(pixmap); - QLabel *lineStyleLabel = new QLabel(tr("&Line Style:")); - lineStyleComboBox = new QComboBox(); - updateLineStyleCombobox(); - lineStyleLabel->setBuddy(lineStyleComboBox); - QDialogButtonBox *buttonBox = new QDialogButtonBox( - QDialogButtonBox::Ok|QDialogButtonBox::Cancel); - - QGridLayout *layout = new QGridLayout(); - layout->addWidget(highlightColorButton, 0, 0); - layout->addWidget(highlightColorLabel, 0, 1); - layout->addWidget(lineStyleLabel, 1, 0); - layout->addWidget(lineStyleComboBox, 1, 1); - layout->addWidget(buttonBox, 3, 0, 1, 2); +void OptionsForm::createLayout() +{ + QFormLayout *mainLayout = new QFormLayout; + mainLayout->addRow(tr("&Base Color:"), colorComboBox); + mainLayout->addRow(tr("Out&line:"), penStyleComboBox); + mainLayout->addRow(tr("&Fill:"), brushStyleComboBox); + mainLayout->addRow(tr("&Rule width:"), ruleWidthSpinBox); + mainLayout->addRow(combineTextHighlightingCheckBox); + QGroupBox *box = new QGroupBox(tr("Highlighting")); + box->setToolTip(tr("

The outline and fill are used to highlight " + "differences using a semi-transparent version of the base " + "color. The margin rules are painted using the base color " + "and indicate where changes are. Set the rule width to 0.0 " + "to switch the rules off.")); + box->setLayout(mainLayout); + QFormLayout *layout = new QFormLayout; + layout->addRow(box); + layout->addRow(showToolTipsCheckBox); + layout->addRow(tr("Cache &Size:"), cacheSizeSpinBox); + layout->addRow(buttonBox); setLayout(layout); - highlightColorButton->setFocus(); +} - connect(highlightColorButton, SIGNAL(clicked()), - this, SLOT(setColor())); + +void OptionsForm::createConnections() +{ + connect(colorComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateColor(int))); + connect(penStyleComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(updatePenStyle(int))); + connect(penStyleComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateUi())); + connect(brushStyleComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateBrushStyle(int))); + connect(brushStyleComboBox, SIGNAL(currentIndexChanged(int)), + this, SLOT(updateUi())); connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept())); connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject())); +} - setWindowTitle(tr("DiffPDF - Options")); + +void OptionsForm::updateColor(int index) +{ + QColor color = colorComboBox->itemData(index).value(); + brush.setColor(color); + pen.setColor(color); + updateSwatches(); } -void OptionsForm::updateLineStyleCombobox() +void OptionsForm::updatePenStyle(int index) { - int index = lineStyleComboBox->currentIndex(); - lineStyleComboBox->clear(); - lineStyleComboBox->addItem(QIcon(penStyleSwatch(Qt::SolidLine, - highlight_color)), tr("Solid Line"), Qt::SolidLine); - lineStyleComboBox->addItem(QIcon(penStyleSwatch(Qt::DashLine, - highlight_color)), tr("Dashed Line"), Qt::DashLine); - lineStyleComboBox->addItem(QIcon(penStyleSwatch(Qt::DotLine, - highlight_color)), tr("Dotted Line"), Qt::DotLine); - lineStyleComboBox->addItem(QIcon(penStyleSwatch(Qt::DashDotLine, - highlight_color)), tr("Dash-Dotted Line"), Qt::DashDotLine); - lineStyleComboBox->addItem(QIcon(penStyleSwatch(Qt::DashDotDotLine, - highlight_color)), tr("Dash-Dot-Dotted Line"), - Qt::DashDotDotLine); - if (index == -1) { - QSettings settings; - index = lineStyleComboBox->findData(settings.value("LineStyle")); - if (index == -1) - index = 0; - } - lineStyleComboBox->setCurrentIndex(index); + pen.setStyle(static_cast( + penStyleComboBox->itemData(index).toInt())); } -void OptionsForm::setColor() +void OptionsForm::updateBrushStyle(int index) { - QPixmap pixmap(60, 3); - QColor color = QColorDialog::getColor(highlight_color, this); - if (color.isValid()) { - highlight_color = color; - pixmap.fill(highlight_color); - highlightColorLabel->setPixmap(pixmap); - updateLineStyleCombobox(); - } + brush.setStyle(static_cast( + brushStyleComboBox->itemData(index).toInt())); +} + + +void OptionsForm::updateSwatches() +{ + QColor color = colorComboBox->itemData( + colorComboBox->currentIndex()).value(); + color.setAlpha(32); // semi-transparent for painting (stored as solid) + for (int i = 0; i < brushStyleComboBox->count(); ++i) + brushStyleComboBox->setItemIcon(i, brushSwatch( + static_cast( + brushStyleComboBox->itemData(i).toInt()), color)); + for (int i = 0; i < penStyleComboBox->count(); ++i) + penStyleComboBox->setItemIcon(i, penStyleSwatch( + static_cast( + penStyleComboBox->itemData(i).toInt()), color)); +} + + +void OptionsForm::updateUi() +{ + Qt::BrushStyle brushStyle = static_cast( + brushStyleComboBox->itemData( + brushStyleComboBox->currentIndex()).toInt()); + Qt::PenStyle penStyle = static_cast( + penStyleComboBox->itemData( + penStyleComboBox->currentIndex()).toInt()); + buttonBox->button(QDialogButtonBox::Ok)->setEnabled( + !(brushStyle == Qt::NoBrush && penStyle == Qt::NoPen)); } void OptionsForm::accept() { - QSettings settings; - settings.setValue("HighlightColor", highlight_color); - settings.setValue("LineStyle", lineStyleComboBox->itemData( - lineStyleComboBox->currentIndex())); + *m_pen = pen; + *m_brush = brush; + *m_ruleWidth = ruleWidthSpinBox->value(); + *m_showToolTips = showToolTipsCheckBox->isChecked(); + *m_combineTextHighlighting = + combineTextHighlightingCheckBox->isChecked(); + *m_cacheSize = cacheSizeSpinBox->value(); QDialog::accept(); } diff -Nru diffpdf-0.4.0/optionsform.hpp diffpdf-1.1.4/optionsform.hpp --- diffpdf-0.4.0/optionsform.hpp 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/optionsform.hpp 2010-08-04 06:50:47.000000000 +0000 @@ -1,3 +1,5 @@ +#ifndef OPTIONSFORM_HPP +#define OPTIONSFORM_HPP /* Copyright (c) 2008-10 Qtrac Ltd. All rights reserved. This program or module is free software: you can redistribute it @@ -10,14 +12,14 @@ for more details. */ -#ifndef OPTIONSFORM_HPP -#define OPTIONSFORM_HPP - +#include #include +#include -class QColor; +class QCheckBox; class QComboBox; -class QLabel; +class QDialogButtonBox; +class QDoubleSpinBox; class QSpinBox; @@ -26,20 +28,40 @@ Q_OBJECT public: - OptionsForm(QWidget *parent=0); - + OptionsForm(QPen *pen, QBrush *brush, qreal *ruleWidth, + bool *showToolTips, bool *combineTextHighlighting, + int *cacheSize, QWidget *parent=0); private slots: - void setColor(); + void updateColor(int index); + void updateBrushStyle(int index); + void updatePenStyle(int index); + void updateUi(); void accept(); private: - void updateLineStyleCombobox(); - - QComboBox *lineStyleComboBox; - QLabel *highlightColorLabel; - - QColor highlight_color; + void createWidgets(); + void createLayout(); + void createConnections(); + void updateSwatches(); + + QComboBox *colorComboBox; + QComboBox *brushStyleComboBox; + QComboBox *penStyleComboBox; + QDoubleSpinBox *ruleWidthSpinBox; + QCheckBox *showToolTipsCheckBox; + QCheckBox *combineTextHighlightingCheckBox; + QSpinBox *cacheSizeSpinBox; + QDialogButtonBox *buttonBox; + + QPen *m_pen; + QBrush *m_brush; + qreal *m_ruleWidth; + bool *m_showToolTips; + bool *m_combineTextHighlighting; + int *m_cacheSize; + QPen pen; + QBrush brush; }; #endif // OPTIONSFORM_HPP diff -Nru diffpdf-0.4.0/.pc/.quilt_patches diffpdf-1.1.4/.pc/.quilt_patches --- diffpdf-0.4.0/.pc/.quilt_patches 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/.pc/.quilt_patches 2010-11-19 21:23:55.000000000 +0000 @@ -0,0 +1 @@ +debian/patches diff -Nru diffpdf-0.4.0/.pc/.quilt_series diffpdf-1.1.4/.pc/.quilt_series --- diffpdf-0.4.0/.pc/.quilt_series 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/.pc/.quilt_series 2010-11-19 21:23:55.000000000 +0000 @@ -0,0 +1 @@ +series diff -Nru diffpdf-0.4.0/.pc/.version diffpdf-1.1.4/.pc/.version --- diffpdf-0.4.0/.pc/.version 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/.pc/.version 2010-11-19 21:23:55.000000000 +0000 @@ -0,0 +1 @@ +2 diff -Nru diffpdf-0.4.0/README diffpdf-1.1.4/README --- diffpdf-0.4.0/README 2009-12-23 12:45:25.000000000 +0000 +++ diffpdf-1.1.4/README 2010-08-04 06:50:47.000000000 +0000 @@ -1,34 +1,36 @@ DiffPDF =========== -DiffPDF is used to compare two PDF files. By default the comparison is -of the text on each pair of pages, but comparing the appearance of pages -is also supported (for example, if a diagram is changed or a paragraph -reformatted). It is also possible to compare particular pages or page -ranges. For example, if there are two versions of a PDF file, one with -pages 1-12 and the other with pages 1-13 because of an extra page having -been added as page 4, they can be compared by specifying two page -ranges, 1-12 for the first and 1-3, 5-13 for the second. This will make -DiffPDF compare pages in the pairs (1, 1), (2, 2), (3, 3), (4, 5), (5, -6), and so on, to (12, 13). - -A couple of example PDF files are provided so that you can try it out. -PDF files can be loaded from the GUI (by pressing the File #1 and File #2 -buttons), or by specifying them on the command line. More information is -available in the program's tooltips. +DiffPDF is used to compare two PDF files. + +By default the comparison is of the text on each pair of pages, but +comparing the appearance of pages is also supported (for example, if a +diagram is changed or a paragraph reformatted). It is also possible to +compare particular pages or page ranges. For example, if there are two +versions of a PDF file, one with pages 1-12 and the other with pages +1-13 because of an extra page having been added as page 4, they can be +compared by specifying two page ranges, 1-12 for the first and 1-3, 5-13 +for the second. This will make DiffPDF compare pages in the pairs (1, +1), (2, 2), (3, 3), (4, 5), (5, 6), and so on, to (12, 13). + +A couple of example PDF files are provided online so that you can try it +out. PDF files can be loaded from the GUI (by pressing the File #1 and +File #2 buttons), or by specifying them on the command line. More +information is available in the program's tooltips and About box. Compiling and Installing DiffPDF ================================ -Prerequisites: A C++ compiler that supports tr1, the Qt4 libraries, and -the Poppler libraries (including the Qt4 headers). +Prerequisites: A C++ compiler, the Qt 4 libraries (I test with Qt 4.6; +Qt 4.4 and 4.5 also work, but need a compiler with tr1 support), and the +Poppler libraries (including Poppler's Qt 4 headers). 1. Unpack the archive file, diffpdf-XXX.tar.gz 2. Change directory to diffpdf-XXX 3. Run qmake # On some systems, e.g., Fedora or Ubuntu, run qmake-qt4 4. Run make -5. Copy diffpdf to somewhere on your PATH +5. Copy or soft-link the diffpdf executable to somewhere on your PATH That's it! @@ -38,7 +40,7 @@ A pair of tiny example files are available: http://www.qtrac.eu/boson1.pdf and http://www.qtrac.eu/boson2.pdf. You -can use these to see the difference between textual and visual +can use these to see the difference between text and appearance comparisons and to get a feel for how DiffPDF works. If you hit a bug, please report it to mark@qtrac.eu. Be sure to include diff -Nru diffpdf-0.4.0/sequence_matcher.cpp diffpdf-1.1.4/sequence_matcher.cpp --- diffpdf-0.4.0/sequence_matcher.cpp 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/sequence_matcher.cpp 2010-08-04 06:50:47.000000000 +0000 @@ -0,0 +1,203 @@ +/* + Copyright (c) 2010 Qtrac Ltd. All rights reserved. + This program or module is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation, either version 2 of + the License, or (at your option) any later version. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. +*/ + +#include "sequence_matcher.hpp" +#include + + +RangesPair computeRanges(SequenceMatcher *matcher) +{ + Ranges ranges1; + Ranges ranges2; + QList matches = matcher->get_matching_blocks(); + foreach (const Match &match, matches) { + if (match.size == 0) + continue; + ranges1 |= unorderedRange(match.i + match.size, match.i); + ranges2 |= unorderedRange(match.j + match.size, match.j); + } + return qMakePair(ranges1, ranges2); +} + + +RangesPair invertRanges(const Ranges &ranges1, int length1, + const Ranges &ranges2, int length2) +{ + const Ranges newRanges1 = unorderedRange(length1) - ranges1; + const Ranges newRanges2 = unorderedRange(length2) - ranges2; + return qMakePair(newRanges1, newRanges2); +} + + +bool matchLessThan(const Match &a, const Match &b) +{ + if (a.i != b.i) + return a.i < b.i; + if (a.j != b.j) + return a.j < b.j; + return a.size < b.size; +} + + +struct Offsets +{ + Offsets(int a_low_=0, int a_high_=0, int b_low_=0, int b_high_=0) + : a_low(a_low_), a_high(a_high_), b_low(b_low_), b_high(b_high_) {} + + int a_low; + int a_high; + int b_low; + int b_high; +}; + + +SequenceMatcher::SequenceMatcher(const Sequence &a_, const Sequence &b_) + : a(a_), b(b_) +{ + set_sequences(a, b); +} + + +void SequenceMatcher::set_sequence1(const Sequence &sequence) +{ + a = sequence; + matching_blocks.clear(); +} + + +void SequenceMatcher::set_sequence2(const Sequence &sequence) +{ + b = sequence; + matching_blocks.clear(); + chain_b(); +} + + +void SequenceMatcher::chain_b() +{ + const int N = b.count(); + b2j.clear(); + QSet popular; + + for (int i = 0; i < N; ++i) { + const Element &element = b.at(i); + if (b2j.contains(element)) { + QList &indexes = b2j[element]; + if (N >= 200 && indexes.count() * 100 > N) { + popular.insert(element); + indexes.clear(); + } + else + indexes.append(i); + } + else + b2j[element].append(i); + } + + foreach (const Element &element, popular) + b2j.remove(element); +} + + +QList SequenceMatcher::get_matching_blocks() +{ + if (!matching_blocks.isEmpty()) + return matching_blocks; + + const int LengthA = a.count(); + const int LengthB = b.count(); + QList offsets; + offsets << Offsets(0, LengthA, 0, LengthB); + while (!offsets.isEmpty()) { + const Offsets &offset = offsets.takeLast(); + const int a_low = offset.a_low; + const int a_high = offset.a_high; + const int b_low = offset.b_low; + const int b_high = offset.b_high; + const Match match = find_longest_match(a_low, a_high, b_low, + b_high); + const int i = match.i; + const int j = match.j; + const int k = match.size; + if (k) { + matching_blocks.append(match); + if (a_low < i && b_low < j) + offsets.append(Offsets(a_low, i, b_low, j)); + if (i + k < a_high && j + k < b_high) + offsets.append(Offsets(i + k, a_high, j + k, b_high)); + } + } + qSort(matching_blocks.begin(), matching_blocks.end(), matchLessThan); + + int i1 = 0; + int j1 = 0; + int k1 = 0; + QList non_adjacent; + foreach (const Match match, matching_blocks) { + const int i2 = match.i; + const int j2 = match.j; + const int k2 = match.size; + if (i1 + k1 == i2 && j1 + k1 == j2) + k1 += k2; + else { + if (k1) + non_adjacent.append(Match(i1, j1, k1)); + i1 = i2; + j1 = j2; + k1 = k2; + } + } + if (k1) + non_adjacent.append(Match(i1, j1, k1)); + non_adjacent.append(Match(LengthA, LengthB, 0)); + matching_blocks = non_adjacent; + return matching_blocks; +} + + +Match SequenceMatcher::find_longest_match(int a_low, int a_high, + int b_low, int b_high) +{ + int best_i = a_low; + int best_j = b_low; + int best_size = 0; + QHash j2len; + for (int i = a_low; i < a_high; ++i) { + QHash newj2len; + foreach (int j, b2j.value(a[i])) { + if (j < b_low) + continue; + if (j >= b_high) + break; + const int k = j2len.value(j - 1, 0) + 1; + newj2len[j] = k; + if (k > best_size) { + best_i = i - k + 1; + best_j = j - k + 1; + best_size = k; + } + } + j2len = newj2len; + } + + while (best_i > a_low && best_j > b_low && + a[best_i - 1] == b[best_j - 1]) { + --best_i; + --best_j; + ++best_size; + } + while (best_i + best_size < a_high && best_j + best_size < b_high && + a[best_i + best_size] == b[best_j + best_size]) + ++best_size; + + return Match(best_i, best_j, best_size); +} diff -Nru diffpdf-0.4.0/sequence_matcher.hpp diffpdf-1.1.4/sequence_matcher.hpp --- diffpdf-0.4.0/sequence_matcher.hpp 1970-01-01 00:00:00.000000000 +0000 +++ diffpdf-1.1.4/sequence_matcher.hpp 2010-08-04 06:50:47.000000000 +0000 @@ -0,0 +1,64 @@ +#ifndef SEQUENCE_MATCHER_HPP +#define SEQUENCE_MATCHER_HPP +/* + Copyright (c) 2010 Qtrac Ltd. All rights reserved. + This program or module is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License + as published by the Free Software Foundation, either version 2 of + the License, or (at your option) any later version. This program is + distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. +*/ + +#include "generic.hpp" +#include +#include +#include +#include + +typedef QStringList Sequence; +typedef QString Element; + +class SequenceMatcher; + +RangesPair computeRanges(SequenceMatcher *matcher); +RangesPair invertRanges(const Ranges &ranges1, int length1, + const Ranges &ranges2, int length2); + +struct Match +{ + Match(int i_=0, int j_=0, int size_=0) : i(i_), j(j_), size(size_) {} + + int i; + int j; + int size; +}; + + +// A simplified C++ implementation of Python's difflib's SequenceMatcher +class SequenceMatcher +{ +public: + SequenceMatcher(const Sequence &a_=Sequence(), + const Sequence &b_=Sequence()); + + void set_sequences(const Sequence &a, const Sequence &b) + { set_sequence1(a); set_sequence2(b); } + void set_sequence1(const Sequence &sequence); + void set_sequence2(const Sequence &sequence); + + QList get_matching_blocks(); + Match find_longest_match(int a_low, int a_high, int b_low, int b_high); + +private: + void chain_b(); + + Sequence a; + Sequence b; + QHash > b2j; + QList matching_blocks; +}; + +#endif // SEQUENCE_MATCHER_HPP