Issue #82: Hot reload function

This commit is contained in:
Jakub Melka
2023-09-01 17:33:45 +02:00
parent 7a77bd796a
commit 7eb2f2a465
13 changed files with 150 additions and 12 deletions

View File

@ -452,14 +452,25 @@ public:
/// header. /// header.
QByteArray getVersion() const; QByteArray getVersion() const;
explicit PDFDocument(PDFObjectStorage&& storage, PDFVersion version) : explicit PDFDocument(PDFObjectStorage&& storage, PDFVersion version, QByteArray sourceDataHash) :
m_pdfObjectStorage(std::move(storage)) m_pdfObjectStorage(std::move(storage)),
m_sourceDataHash(std::move(sourceDataHash))
{ {
init(); init();
m_info.version = version; m_info.version = version;
} }
/**
* @brief Retrieves the hash of the source data.
*
* This function returns the hash derived from the source data
* from which the document was originally read.
*
* @return Hash value of the source data.
*/
const QByteArray& getSourceDataHash() const { return m_sourceDataHash; }
private: private:
friend class PDFDocumentReader; friend class PDFDocumentReader;
friend class PDFDocumentBuilder; friend class PDFDocumentBuilder;
@ -482,6 +493,10 @@ private:
/// Catalog object /// Catalog object
PDFCatalog m_catalog; PDFCatalog m_catalog;
/// Hash of the source byte array's data,
/// from which the document was created.
QByteArray m_sourceDataHash;
}; };
using PDFDocumentPointer = QSharedPointer<PDFDocument>; using PDFDocumentPointer = QSharedPointer<PDFDocument>;
@ -507,6 +522,7 @@ public:
Authorization = 0x0010, ///< Authorization has changed (for example, old document is granted user access, but for new, owner access) Authorization = 0x0010, ///< Authorization has changed (for example, old document is granted user access, but for new, owner access)
XFA_Pagination = 0x0020, ///< XFA pagination has been performed (this flag can be set only when Reset flag has been set and not any other flag) XFA_Pagination = 0x0020, ///< XFA pagination has been performed (this flag can be set only when Reset flag has been set and not any other flag)
PreserveUndoRedo = 0x0040, ///< Preserve undo/red even when Reset flag is being set PreserveUndoRedo = 0x0040, ///< Preserve undo/red even when Reset flag is being set
PreserveView = 0x0080, ///< Try to preserve view
}; };
Q_DECLARE_FLAGS(ModificationFlags, ModificationFlag) Q_DECLARE_FLAGS(ModificationFlags, ModificationFlag)
@ -555,6 +571,7 @@ public:
bool hasPageContentsChanged() const { return m_flags.testFlag(PageContents); } bool hasPageContentsChanged() const { return m_flags.testFlag(PageContents); }
bool hasPreserveUndoRedo() const { return m_flags.testFlag(PreserveUndoRedo); } bool hasPreserveUndoRedo() const { return m_flags.testFlag(PreserveUndoRedo); }
bool hasFlag(ModificationFlag flag) const { return m_flags.testFlag(flag); } bool hasFlag(ModificationFlag flag) const { return m_flags.testFlag(flag); }
bool hasPreserveView() const { return m_flags.testFlag(PreserveView); }
operator PDFDocument*() const { return m_document; } operator PDFDocument*() const { return m_document; }
operator PDFDocumentPointer() const { return m_documentPointer; } operator PDFDocumentPointer() const { return m_documentPointer; }

View File

@ -704,7 +704,7 @@ void PDFDocumentBuilder::setDocument(const PDFDocument* document)
PDFDocument PDFDocumentBuilder::build() PDFDocument PDFDocumentBuilder::build()
{ {
updateTrailerDictionary(m_storage.getObjects().size()); updateTrailerDictionary(m_storage.getObjects().size());
return PDFDocument(PDFObjectStorage(m_storage), m_version); return PDFDocument(PDFObjectStorage(m_storage), m_version, QByteArray());
} }
QByteArray PDFDocumentBuilder::getDecodedStream(const PDFStream* stream) const QByteArray PDFDocumentBuilder::getDecodedStream(const PDFStream* stream) const

View File

@ -26,6 +26,7 @@
#include "pdfdbgheap.h" #include "pdfdbgheap.h"
#include <QFile> #include <QFile>
#include <QCryptographicHash>
#include <regex> #include <regex>
#include <cctype> #include <cctype>
@ -582,7 +583,7 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
processObjectStreams(&xrefTable, objects); processObjectStreams(&xrefTable, objects);
PDFObjectStorage storage(std::move(objects), PDFObject(xrefTable.getTrailerDictionary()), qMove(m_securityHandler)); PDFObjectStorage storage(std::move(objects), PDFObject(xrefTable.getTrailerDictionary()), qMove(m_securityHandler));
return PDFDocument(std::move(storage), m_version); return PDFDocument(std::move(storage), m_version, hash(buffer));
} }
catch (const PDFException &parserException) catch (const PDFException &parserException)
{ {
@ -599,6 +600,11 @@ PDFDocument PDFDocumentReader::readFromBuffer(const QByteArray& buffer)
return PDFDocument(); return PDFDocument();
} }
QByteArray PDFDocumentReader::hash(const QByteArray& sourceData)
{
return QCryptographicHash::hash(sourceData, QCryptographicHash::Sha256);
}
std::vector<std::pair<int, int>> PDFDocumentReader::findObjectByteOffsets(const QByteArray& buffer) const std::vector<std::pair<int, int>> PDFDocumentReader::findObjectByteOffsets(const QByteArray& buffer) const
{ {
std::vector<std::pair<int, int>> offsets; std::vector<std::pair<int, int>> offsets;
@ -767,7 +773,7 @@ PDFDocument PDFDocumentReader::readDamagedDocumentFromBuffer(const QByteArray& b
} }
PDFObjectStorage storage(std::move(objects), PDFObject(trailerDictionaryObject), qMove(m_securityHandler)); PDFObjectStorage storage(std::move(objects), PDFObject(trailerDictionaryObject), qMove(m_securityHandler));
return PDFDocument(std::move(storage), m_version); return PDFDocument(std::move(storage), m_version, QByteArray());
} }
catch (const PDFException &parserException) catch (const PDFException &parserException)
{ {

View File

@ -81,6 +81,8 @@ public:
/// Returns warning messages /// Returns warning messages
const QStringList& getWarnings() const { return m_warnings; } const QStringList& getWarnings() const { return m_warnings; }
static QByteArray hash(const QByteArray& sourceData);
private: private:
static constexpr const int FIND_NOT_FOUND_RESULT = -1; static constexpr const int FIND_NOT_FOUND_RESULT = -1;

View File

@ -69,7 +69,7 @@ public:
/// Returns sanitized document. Object storage is cleared after /// Returns sanitized document. Object storage is cleared after
/// this function call. /// this function call.
PDFDocument takeSanitizedDocument() { return PDFDocument(qMove(m_storage), PDFVersion(2, 0)); } PDFDocument takeSanitizedDocument() { return PDFDocument(qMove(m_storage), PDFVersion(2, 0), QByteArray()); }
SanitizationFlags getFlags() const; SanitizationFlags getFlags() const;
void setFlags(SanitizationFlags flags); void setFlags(SanitizationFlags flags);

View File

@ -70,7 +70,7 @@ public:
/// Returns optimized document. Object storage is cleared after /// Returns optimized document. Object storage is cleared after
/// this function call. /// this function call.
PDFDocument takeOptimizedDocument() { return PDFDocument(qMove(m_storage), PDFVersion(2, 0)); } PDFDocument takeOptimizedDocument() { return PDFDocument(qMove(m_storage), PDFVersion(2, 0), QByteArray()); }
OptimizationFlags getFlags() const; OptimizationFlags getFlags() const;
void setFlags(OptimizationFlags flags); void setFlags(OptimizationFlags flags);

View File

@ -42,6 +42,7 @@ namespace pdfviewer
struct PDFFileInfo struct PDFFileInfo
{ {
QString originalFileName; QString originalFileName;
QString absoluteFilePath;
QString fileName; QString fileName;
QString path; QString path;
pdf::PDFInteger fileSize = 0; pdf::PDFInteger fileSize = 0;

View File

@ -335,7 +335,7 @@ PDFProgramController::PDFProgramController(QObject* parent) :
m_isFactorySettingsBeingRestored(false), m_isFactorySettingsBeingRestored(false),
m_progress(nullptr) m_progress(nullptr)
{ {
connect(&m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &PDFProgramController::onFileChanged);
} }
PDFProgramController::~PDFProgramController() PDFProgramController::~PDFProgramController()
@ -530,6 +530,10 @@ void PDFProgramController::initialize(Features features,
{ {
connect(action, &QAction::triggered, this, &PDFProgramController::onActionGetSource); connect(action, &QAction::triggered, this, &PDFProgramController::onActionGetSource);
} }
if (QAction* action = m_actionManager->getAction(PDFActionManager::AutomaticDocumentRefresh))
{
connect(action, &QAction::triggered, this, &PDFProgramController::onActionAutomaticDocumentRefresh);
}
if (m_recentFileManager) if (m_recentFileManager)
{ {
@ -1042,7 +1046,6 @@ void PDFProgramController::onActionRenderingOptionTriggered(bool checked)
void PDFProgramController::performSaveAs() void PDFProgramController::performSaveAs()
{ {
QFileInfo fileInfo(m_fileInfo.originalFileName); QFileInfo fileInfo(m_fileInfo.originalFileName);
QString saveFileName = QFileDialog::getSaveFileName(m_mainWindow, tr("Save As"), fileInfo.dir().absoluteFilePath(m_fileInfo.originalFileName), tr("Portable Document (*.pdf);;All files (*.*)")); QString saveFileName = QFileDialog::getSaveFileName(m_mainWindow, tr("Save As"), fileInfo.dir().absoluteFilePath(m_fileInfo.originalFileName), tr("Portable Document (*.pdf);;All files (*.*)"));
if (!saveFileName.isEmpty()) if (!saveFileName.isEmpty())
@ -1058,6 +1061,8 @@ void PDFProgramController::performSave()
void PDFProgramController::saveDocument(const QString& fileName) void PDFProgramController::saveDocument(const QString& fileName)
{ {
updateFileWatcher(true);
pdf::PDFDocumentWriter writer(nullptr); pdf::PDFDocumentWriter writer(nullptr);
pdf::PDFOperationResult result = writer.write(fileName, m_pdfDocument.data(), true); pdf::PDFOperationResult result = writer.write(fileName, m_pdfDocument.data(), true);
if (result) if (result)
@ -1079,6 +1084,8 @@ void PDFProgramController::saveDocument(const QString& fileName)
{ {
QMessageBox::critical(m_mainWindow, tr("Error"), result.getErrorMessage()); QMessageBox::critical(m_mainWindow, tr("Error"), result.getErrorMessage());
} }
updateFileWatcher();
} }
bool PDFProgramController::isFactorySettingsBeingRestored() const bool PDFProgramController::isFactorySettingsBeingRestored() const
@ -1266,7 +1273,7 @@ void PDFProgramController::onActionEncryptionTriggered()
pdf::PDFObjectStorage storage = m_pdfDocument->getStorage(); pdf::PDFObjectStorage storage = m_pdfDocument->getStorage();
storage.setSecurityHandler(qMove(clonedSecurityHandler)); storage.setSecurityHandler(qMove(clonedSecurityHandler));
pdf::PDFDocumentPointer pointer(new pdf::PDFDocument(qMove(storage), m_pdfDocument->getInfo()->version)); pdf::PDFDocumentPointer pointer(new pdf::PDFDocument(qMove(storage), m_pdfDocument->getInfo()->version, QByteArray()));
pdf::PDFModifiedDocument document(qMove(pointer), m_optionalContentActivity, pdf::PDFModifiedDocument::Authorization); pdf::PDFModifiedDocument document(qMove(pointer), m_optionalContentActivity, pdf::PDFModifiedDocument::Authorization);
onDocumentModified(qMove(document)); onDocumentModified(qMove(document));
} }
@ -1585,6 +1592,53 @@ void PDFProgramController::onColorManagementSystemChanged()
m_settings->setColorManagementSystemSettings(m_CMSManager->getSettings()); m_settings->setColorManagementSystemSettings(m_CMSManager->getSettings());
} }
void PDFProgramController::onFileChanged(const QString& fileName)
{
QAction* autoRefreshDocumentAction = m_actionManager->getAction(PDFActionManager::AutomaticDocumentRefresh);
if (!autoRefreshDocumentAction || // We do not have action
!autoRefreshDocumentAction->isChecked() || // Auto refresh is not enabled
m_fileInfo.originalFileName != fileName) // File is different
{
return;
}
if (m_undoRedoManager && !m_undoRedoManager->isCurrentSaved())
{
// If document is modified, we do not reload it
return;
}
QFile file(fileName);
if (file.open(QFile::ReadOnly))
{
QByteArray data = file.readAll();
file.close();
QByteArray hash = pdf::PDFDocumentReader::hash(data);
if (m_pdfDocument && m_pdfDocument->getSourceDataHash() != hash)
{
auto queryPassword = [this](bool* ok)
{
*ok = false;
return QString();
};
// Try to open a new document
pdf::PDFDocumentReader reader(m_progress, qMove(queryPassword), true, false);
pdf::PDFDocument document = reader.readFromFile(fileName);
if (reader.getReadingResult() == pdf::PDFDocumentReader::Result::OK)
{
pdf::PDFDocumentPointer pointer(new pdf::PDFDocument(std::move(document)));
pdf::PDFModifiedDocument modifiedDocument(std::move(pointer), m_optionalContentActivity, pdf::PDFModifiedDocument::ModificationFlags(pdf::PDFModifiedDocument::Reset | pdf::PDFModifiedDocument::PreserveView));
onDocumentModified(std::move(modifiedDocument));
m_undoRedoManager->setIsCurrentSaved();
}
}
}
}
void PDFProgramController::updateFileInfo(const QString& fileName) void PDFProgramController::updateFileInfo(const QString& fileName)
{ {
QFileInfo fileInfo(fileName); QFileInfo fileInfo(fileName);
@ -1596,6 +1650,27 @@ void PDFProgramController::updateFileInfo(const QString& fileName)
m_fileInfo.creationTime = fileInfo.birthTime(); m_fileInfo.creationTime = fileInfo.birthTime();
m_fileInfo.lastModifiedTime = fileInfo.lastModified(); m_fileInfo.lastModifiedTime = fileInfo.lastModified();
m_fileInfo.lastReadTime = fileInfo.lastRead(); m_fileInfo.lastReadTime = fileInfo.lastRead();
m_fileInfo.absoluteFilePath = fileInfo.absoluteFilePath();
updateFileWatcher(false);
}
void PDFProgramController::updateFileWatcher(bool forceDisable)
{
QStringList oldFiles = m_fileWatcher.files();
QStringList newFiles;
QAction* action = m_actionManager->getAction(PDFActionManager::AutomaticDocumentRefresh);
if (!forceDisable && !m_fileInfo.absoluteFilePath.isEmpty() && action && action->isChecked())
{
newFiles << m_fileInfo.absoluteFilePath;
}
if (oldFiles != newFiles)
{
m_fileWatcher.removePaths(oldFiles);
m_fileWatcher.addPaths(newFiles);
}
} }
void PDFProgramController::openDocument(const QString& fileName) void PDFProgramController::openDocument(const QString& fileName)
@ -1809,7 +1884,7 @@ void PDFProgramController::setDocument(pdf::PDFModifiedDocument document, bool i
plugin.second->setDocument(document); plugin.second->setDocument(document);
} }
if (m_pdfDocument && document.hasReset()) if (m_pdfDocument && document.hasReset() && !document.hasPreserveView())
{ {
const pdf::PDFCatalog* catalog = m_pdfDocument->getCatalog(); const pdf::PDFCatalog* catalog = m_pdfDocument->getCatalog();
setPageLayout(catalog->getPageLayout()); setPageLayout(catalog->getPageLayout());
@ -1831,6 +1906,7 @@ void PDFProgramController::closeDocument()
m_pdfDocument.reset(); m_pdfDocument.reset();
updateActionsAvailability(); updateActionsAvailability();
updateTitle(); updateTitle();
updateFileInfo(QString());
} }
void PDFProgramController::updateRenderingOptionActions() void PDFProgramController::updateRenderingOptionActions()
@ -2382,6 +2458,11 @@ void PDFProgramController::onActionGetSource()
QDesktopServices::openUrl(QUrl("https://github.com/JakubMelka/PDF4QT")); QDesktopServices::openUrl(QUrl("https://github.com/JakubMelka/PDF4QT"));
} }
void PDFProgramController::onActionAutomaticDocumentRefresh()
{
updateFileWatcher();
}
void PDFProgramController::onPageRenderingErrorsChanged(pdf::PDFInteger pageIndex, int errorsCount) void PDFProgramController::onPageRenderingErrorsChanged(pdf::PDFInteger pageIndex, int errorsCount)
{ {
if (errorsCount > 0) if (errorsCount > 0)

View File

@ -29,6 +29,7 @@
#include <QAction> #include <QAction>
#include <QToolButton> #include <QToolButton>
#include <QActionGroup> #include <QActionGroup>
#include <QFileSystemWatcher>
#include <array> #include <array>
@ -81,6 +82,7 @@ public:
Open, Open,
Close, Close,
Quit, Quit,
AutomaticDocumentRefresh,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
Find, Find,
@ -349,6 +351,7 @@ private:
void onActionCloseTriggered(); void onActionCloseTriggered();
void onActionDeveloperCreateInstaller(); void onActionDeveloperCreateInstaller();
void onActionGetSource(); void onActionGetSource();
void onActionAutomaticDocumentRefresh();
void onDrawSpaceChanged(); void onDrawSpaceChanged();
void onPageLayoutChanged(); void onPageLayoutChanged();
@ -359,6 +362,7 @@ private:
void onPageRenderingErrorsChanged(pdf::PDFInteger pageIndex, int errorsCount); void onPageRenderingErrorsChanged(pdf::PDFInteger pageIndex, int errorsCount);
void onViewerSettingsChanged(); void onViewerSettingsChanged();
void onColorManagementSystemChanged(); void onColorManagementSystemChanged();
void onFileChanged(const QString& fileName);
void updateMagnifierToolSettings(); void updateMagnifierToolSettings();
void updateUndoRedoSettings(); void updateUndoRedoSettings();
@ -369,6 +373,7 @@ private:
void setPageLayout(pdf::PageLayout pageLayout); void setPageLayout(pdf::PageLayout pageLayout);
void updateFileInfo(const QString& fileName); void updateFileInfo(const QString& fileName);
void updateFileWatcher(bool forceDisable = false);
enum SettingFlag enum SettingFlag
{ {
@ -408,6 +413,7 @@ private:
pdf::PDFFormManager* m_formManager; pdf::PDFFormManager* m_formManager;
PDFFileInfo m_fileInfo; PDFFileInfo m_fileInfo;
QFileSystemWatcher m_fileWatcher;
pdf::PDFCertificateStore m_certificateStore; pdf::PDFCertificateStore m_certificateStore;
std::vector<pdf::PDFSignatureVerificationResult> m_signatures; std::vector<pdf::PDFSignatureVerificationResult> m_signatures;

View File

@ -103,6 +103,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) :
// Initialize actions // Initialize actions
m_actionManager->setAction(PDFActionManager::Open, ui->actionOpen); m_actionManager->setAction(PDFActionManager::Open, ui->actionOpen);
m_actionManager->setAction(PDFActionManager::Close, ui->actionClose); m_actionManager->setAction(PDFActionManager::Close, ui->actionClose);
m_actionManager->setAction(PDFActionManager::AutomaticDocumentRefresh, ui->actionAutomaticDocumentRefresh);
m_actionManager->setAction(PDFActionManager::Quit, ui->actionQuit); m_actionManager->setAction(PDFActionManager::Quit, ui->actionQuit);
m_actionManager->setAction(PDFActionManager::ZoomIn, ui->actionZoom_In); m_actionManager->setAction(PDFActionManager::ZoomIn, ui->actionZoom_In);
m_actionManager->setAction(PDFActionManager::ZoomOut, ui->actionZoom_Out); m_actionManager->setAction(PDFActionManager::ZoomOut, ui->actionZoom_Out);

View File

@ -29,6 +29,7 @@
</property> </property>
<addaction name="actionOpen"/> <addaction name="actionOpen"/>
<addaction name="actionClose"/> <addaction name="actionClose"/>
<addaction name="actionAutomaticDocumentRefresh"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSave"/> <addaction name="actionSave"/>
<addaction name="actionSave_As"/> <addaction name="actionSave_As"/>
@ -936,6 +937,17 @@
<string>Sanitize document to remove sensitive information.</string> <string>Sanitize document to remove sensitive information.</string>
</property> </property>
</action> </action>
<action name="actionAutomaticDocumentRefresh">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Automatic Document Refresh</string>
</property>
<property name="statusTip">
<string>Automatically reloads the document if a change made by an external program is detected.</string>
</property>
</action>
</widget> </widget>
<layoutdefault spacing="6" margin="11"/> <layoutdefault spacing="6" margin="11"/>
<resources> <resources>

View File

@ -102,6 +102,7 @@ PDFViewerMainWindowLite::PDFViewerMainWindowLite(QWidget* parent) :
// Initialize actions // Initialize actions
m_actionManager->setAction(PDFActionManager::Open, ui->actionOpen); m_actionManager->setAction(PDFActionManager::Open, ui->actionOpen);
m_actionManager->setAction(PDFActionManager::Close, ui->actionClose); m_actionManager->setAction(PDFActionManager::Close, ui->actionClose);
m_actionManager->setAction(PDFActionManager::AutomaticDocumentRefresh, ui->actionAutomaticDocumentRefresh);
m_actionManager->setAction(PDFActionManager::Quit, ui->actionQuit); m_actionManager->setAction(PDFActionManager::Quit, ui->actionQuit);
m_actionManager->setAction(PDFActionManager::ZoomIn, ui->actionZoom_In); m_actionManager->setAction(PDFActionManager::ZoomIn, ui->actionZoom_In);
m_actionManager->setAction(PDFActionManager::ZoomOut, ui->actionZoom_Out); m_actionManager->setAction(PDFActionManager::ZoomOut, ui->actionZoom_Out);

View File

@ -29,7 +29,7 @@
</property> </property>
<addaction name="actionOpen"/> <addaction name="actionOpen"/>
<addaction name="actionClose"/> <addaction name="actionClose"/>
<addaction name="separator"/> <addaction name="actionAutomaticDocumentRefresh"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSend_by_E_Mail"/> <addaction name="actionSend_by_E_Mail"/>
<addaction name="actionPrint"/> <addaction name="actionPrint"/>
@ -458,6 +458,17 @@
<property name="text"> <property name="text">
<string>Certificates...</string> <string>Certificates...</string>
</property> </property>
</action>
<action name="actionAutomaticDocumentRefresh">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Automatic Document Refresh</string>
</property>
<property name="statusTip">
<string>Automatically reloads the document if a change made by an external program is detected.</string>
</property>
</action> </action>
</widget> </widget>
<layoutdefault spacing="6" margin="11"/> <layoutdefault spacing="6" margin="11"/>