DocPage Organizer: Assemble documents

This commit is contained in:
Jakub Melka 2021-07-24 17:57:11 +02:00
parent b644ea48e3
commit b38bbf8468
11 changed files with 499 additions and 9 deletions

View File

@ -42,6 +42,7 @@ INSTALLS += application
SOURCES += \
aboutdialog.cpp \
assembleoutputsettingsdialog.cpp \
main.cpp \
mainwindow.cpp \
pageitemdelegate.cpp \
@ -49,10 +50,12 @@ SOURCES += \
FORMS += \
aboutdialog.ui \
assembleoutputsettingsdialog.ui \
mainwindow.ui
HEADERS += \
aboutdialog.h \
assembleoutputsettingsdialog.h \
mainwindow.h \
pageitemdelegate.h \
pageitemmodel.h

View File

@ -15,8 +15,8 @@
// You should have received a copy of the GNU Lesser General Public License
// along with Pdf4Qt. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFABOUTDIALOG_H
#define PDFABOUTDIALOG_H
#ifndef PDFDOCPAGEORGANIZER_PDFABOUTDIALOG_H
#define PDFDOCPAGEORGANIZER_PDFABOUTDIALOG_H
#include <QDialog>
@ -42,4 +42,4 @@ private:
} // namespace pdfdocpage
#endif // PDFABOUTDIALOG_H
#endif // PDFDOCPAGEORGANIZER_PDFABOUTDIALOG_H

View File

@ -0,0 +1,67 @@
// Copyright (C) 2021 Jakub Melka
//
// This file is part of Pdf4Qt.
//
// Pdf4Qt is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// Pdf4Qt 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Pdf4Qt. If not, see <https://www.gnu.org/licenses/>.
#include "assembleoutputsettingsdialog.h"
#include "ui_assembleoutputsettingsdialog.h"
#include "pdfwidgetutils.h"
#include <QFileDialog>
namespace pdfdocpage
{
AssembleOutputSettingsDialog::AssembleOutputSettingsDialog(QString directory, QWidget* parent) :
QDialog(parent),
ui(new Ui::AssembleOutputSettingsDialog)
{
ui->setupUi(this);
ui->directoryEdit->setText(directory);
pdf::PDFWidgetUtils::scaleWidget(this, QSize(450, 150));
}
AssembleOutputSettingsDialog::~AssembleOutputSettingsDialog()
{
delete ui;
}
QString AssembleOutputSettingsDialog::getDirectory() const
{
return ui->directoryEdit->text();
}
QString AssembleOutputSettingsDialog::getFileName() const
{
return ui->fileTemplateEdit->text();
}
bool AssembleOutputSettingsDialog::isOverwriteFiles() const
{
return ui->overwriteFilesCheckBox->isChecked();
}
void AssembleOutputSettingsDialog::on_selectDirectoryButton_clicked()
{
QString directory = QFileDialog::getExistingDirectory(this, tr("Select output directory"), ui->directoryEdit->text());
if (!directory.isEmpty())
{
ui->directoryEdit->setText(directory);
}
}
} // namespace pdfdocpage

View File

@ -0,0 +1,52 @@
// Copyright (C) 2021 Jakub Melka
//
// This file is part of Pdf4Qt.
//
// Pdf4Qt is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// Pdf4Qt 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Pdf4Qt. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFDOCPAGEORGANIZER_ASSEMBLEOUTPUTSETTINGSDIALOG_H
#define PDFDOCPAGEORGANIZER_ASSEMBLEOUTPUTSETTINGSDIALOG_H
#include <QDialog>
namespace Ui
{
class AssembleOutputSettingsDialog;
}
namespace pdfdocpage
{
class AssembleOutputSettingsDialog : public QDialog
{
Q_OBJECT
public:
explicit AssembleOutputSettingsDialog(QString directory, QWidget* parent);
virtual ~AssembleOutputSettingsDialog() override;
QString getDirectory() const;
QString getFileName() const;
bool isOverwriteFiles() const;
private slots:
void on_selectDirectoryButton_clicked();
private:
Ui::AssembleOutputSettingsDialog* ui;
};
} // namespace pdfdocpage
#endif // PDFDOCPAGEORGANIZER_ASSEMBLEOUTPUTSETTINGSDIALOG_H

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AssembleOutputSettingsDialog</class>
<widget class="QDialog" name="AssembleOutputSettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>614</width>
<height>213</height>
</rect>
</property>
<property name="windowTitle">
<string>Assemble Documents</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="assembleDocumentsGroupBox">
<property name="title">
<string>Assemble Documents</string>
</property>
<layout class="QGridLayout" name="assembleDocumentsGroupBoxLayout">
<item row="1" column="0">
<widget class="QLabel" name="fileTemplateLabel">
<property name="text">
<string>File template</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="selectDirectoryButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QLabel" name="infoLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In a template file name, you can use symbols '#' for output document number (means output document index, not input document) or '@' for page number of input document (if document contains more pages, it is a page number of a original document), or '%' for index of input document. Use more '#' or '@' or '%' for setting minimal number of digits (if number has less digits, the they are padded with zero).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="fileTemplateEdit">
<property name="text">
<string>doc-#.pdf</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="directoryLabel">
<property name="text">
<string>Generate into directory</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="directoryEdit"/>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="overwriteFilesCheckBox">
<property name="text">
<string>Overwrite existing files</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AssembleOutputSettingsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AssembleOutputSettingsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -19,6 +19,7 @@
#include "ui_mainwindow.h"
#include "aboutdialog.h"
#include "assembleoutputsettingsdialog.h"
#include "pdfwidgetutils.h"
#include "pdfdocumentreader.h"
@ -489,9 +490,102 @@ void MainWindow::performOperation(Operation operation)
break;
}
case Operation::ReplaceSelection:
Q_ASSERT(false);
case Operation::Unite:
case Operation::Separate:
case Operation::SeparateGrouped:
{
PageItemModel::AssembleMode assembleMode = PageItemModel::AssembleMode::Unite;
switch (operation)
{
case Operation::Unite:
assembleMode = PageItemModel::AssembleMode::Unite;
break;
case Operation::Separate:
assembleMode = PageItemModel::AssembleMode::Separate;
break;
case Operation::SeparateGrouped:
assembleMode = PageItemModel::AssembleMode::SeparateGrouped;
break;
default:
Q_ASSERT(false);
}
std::vector<std::vector<pdf::PDFDocumentManipulator::AssembledPage>> assembledDocuments = m_model->getAssembledPages(assembleMode);
// Check we have something to process
if (assembledDocuments.empty())
{
QMessageBox::critical(this, tr("Error"), tr("No documents to assemble."));
break;
}
AssembleOutputSettingsDialog dialog(m_settings.directory, this);
if (dialog.exec() == QDialog::Accepted)
{
pdf::PDFDocumentManipulator manipulator;
// Add documents and images
for (const auto& documentItem : m_model->getDocuments())
{
manipulator.addDocument(documentItem.first, &documentItem.second.document);
}
for (const auto& imageItem : m_model->getImages())
{
manipulator.addImage(imageItem.first, imageItem.second.image);
}
// Jakub Melka: create assembled documents
pdf::PDFOperationResult result(true);
std::vector<std::pair<QString, pdf::PDFDocument>> assembledDocumentStorage;
int sourceDocumentIndex = 1;
int assembledDocumentIndex = 1;
int sourcePageIndex = 1;
int documentCount = int(m_model->getDocuments().size());
QString directory = dialog.getDirectory();
QString fileNameTemplate = dialog.getFileName();
const bool isOverwriteEnabled = dialog.isOverwriteFiles();
for (const std::vector<pdf::PDFDocumentManipulator::AssembledPage>& assembledPages : assembledDocuments)
{
pdf::PDFOperationResult currentResult = manipulator.assemble(assembledPages);
if (!currentResult && result)
{
result = currentResult;
break;
}
pdf::PDFDocumentManipulator::AssembledPage samplePage = assembledPages.front();
sourceDocumentIndex = samplePage.documentIndex == -1 ? documentCount + samplePage.imageIndex : samplePage.documentIndex;
sourcePageIndex = qMax(samplePage.pageIndex + 1, 1);
QString fileName = fileNameTemplate;
if (!fileName.endsWith(".pdf"))
{
fileName += ".pdf";
}
assembledDocumentStorage.emplace_back(std::make_pair(std::move(fileName), manipulator.takeAssembledDocument()));
++assembledDocumentIndex;
}
if (!result)
{
QMessageBox::critical(this, tr("Error"), result.getErrorMessage());
break;
}
}
break;
}
case Operation::InsertImage:
{
@ -512,10 +606,9 @@ void MainWindow::performOperation(Operation operation)
}
case Operation::InsertPDF:
case Operation::Unite:
case Operation::Separate:
case Operation::SeparateGrouped:
case Operation::ReplaceSelection:
Q_ASSERT(false);
break;
default:
Q_ASSERT(false);

View File

@ -848,6 +848,97 @@ Qt::ItemFlags PageItemModel::flags(const QModelIndex& index) const
return flags;
}
std::vector<std::vector<pdf::PDFDocumentManipulator::AssembledPage>> PageItemModel::getAssembledPages(AssembleMode mode) const
{
std::vector<std::vector<pdf::PDFDocumentManipulator::AssembledPage>> result;
auto createAssembledPage = [this](const PageGroupItem::GroupItem& item)
{
pdf::PDFDocumentManipulator::AssembledPage assembledPage;
assembledPage.documentIndex = item.documentIndex;
assembledPage.imageIndex = item.imageIndex;
assembledPage.pageIndex = item.pageIndex;
if (assembledPage.pageIndex > 0)
{
--assembledPage.pageIndex;
}
pdf::PageRotation originalPageRotation = pdf::PageRotation::None;
if (item.pageType == PT_DocumentPage)
{
auto it = m_documents.find(item.documentIndex);
if (it != m_documents.cend())
{
const pdf::PDFPage* page = it->second.document.getCatalog()->getPage(item.pageIndex - 1);
originalPageRotation = page->getPageRotation();
}
}
assembledPage.pageRotation = pdf::getPageRotationCombined(originalPageRotation, item.pageAdditionalRotation);
assembledPage.pageSize = pdf::PDFPage::getRotatedSize(item.rotatedPageDimensionsMM, pdf::getPageRotationInversed(item.pageAdditionalRotation));
return assembledPage;
};
switch (mode)
{
case AssembleMode::Unite:
{
std::vector<pdf::PDFDocumentManipulator::AssembledPage> unitedDocument;
for (const auto& pageGroupItem : m_pageGroupItems)
{
for (const auto& groupItem : pageGroupItem.groups)
{
unitedDocument.emplace_back(createAssembledPage(groupItem));
}
}
result.emplace_back(std::move(unitedDocument));
break;
}
case AssembleMode::Separate:
{
for (const auto& pageGroupItem : m_pageGroupItems)
{
for (const auto& groupItem : pageGroupItem.groups)
{
result.emplace_back().emplace_back(createAssembledPage(groupItem));
}
}
break;
}
case AssembleMode::SeparateGrouped:
{
for (const auto& pageGroupItem : m_pageGroupItems)
{
std::vector<pdf::PDFDocumentManipulator::AssembledPage> groupDocument;
for (const auto& groupItem : pageGroupItem.groups)
{
groupDocument.emplace_back(createAssembledPage(groupItem));
}
result.emplace_back(std::move(groupDocument));
}
break;
}
default:
{
Q_ASSERT(false);
break;
}
}
// Remove empty documents
result.erase(std::remove_if(result.begin(), result.end(), [](const auto& pages) { return pages.empty(); }), result.end());
return result;
}
void PageItemModel::clear()
{
beginResetModel();

View File

@ -20,6 +20,7 @@
#include "pdfdocument.h"
#include "pdfutils.h"
#include "pdfdocumentmanipulator.h"
#include <QImage>
#include <QAbstractItemModel>
@ -100,6 +101,15 @@ public:
virtual Qt::DropActions supportedDragActions() const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
enum class AssembleMode
{
Unite,
Separate,
SeparateGrouped
};
std::vector<std::vector<pdf::PDFDocumentManipulator::AssembledPage>> getAssembledPages(AssembleMode mode) const;
/// Clear all data and undo/redo
void clear();
@ -153,6 +163,9 @@ public:
static QString getMimeDataType() { return QLatin1String("application/pagemodel.PDF4QtDocPageOrganizer"); }
const std::map<int, DocumentItem>& getDocuments() const { return m_documents; }
const std::map<int, ImageItem>& getImages() const { return m_images; }
private:
void createDocumentGroup(int index);
QString getGroupNameFromDocument(int index) const;

View File

@ -29,6 +29,10 @@ PDFOperationResult PDFDocumentManipulator::assemble(const AssembledPages& pages)
return tr("Empty page list.");
}
m_flags = None;
m_mergedObjects = { };
m_assembledDocument = PDFDocument();
try
{
classify(pages);

View File

@ -207,6 +207,23 @@ PDFObject PDFPage::getOutputIntents(const PDFObjectStorage* storage) const
return getObjectFromPageDictionary(storage, "OutputIntents");
}
QSizeF PDFPage::getRotatedSize(const QSizeF& size, PageRotation rotation)
{
switch (rotation)
{
case PageRotation::None:
case PageRotation::Rotate180:
// Preserve rotation
break;
case PageRotation::Rotate90:
case PageRotation::Rotate270:
return size.transposed();
}
return size;
}
QRectF PDFPage::getRotatedBox(const QRectF& rect, PageRotation rotation)
{
switch (rotation)

View File

@ -84,6 +84,34 @@ constexpr PageRotation getPageRotationRotatedLeft(PageRotation rotation)
return PageRotation::None;
}
constexpr PageRotation getPageRotationCombined(PageRotation r1, PageRotation r2)
{
while (r1 != PageRotation::None)
{
r2 = getPageRotationRotatedRight(r2);
r1 = getPageRotationRotatedLeft(r1);
}
return r2;
}
constexpr PageRotation getPageRotationInversed(PageRotation rotation)
{
switch (rotation)
{
case PageRotation::None:
return PageRotation::None;
case PageRotation::Rotate90:
return PageRotation::Rotate270;
case PageRotation::Rotate180:
return PageRotation::Rotate180;
case PageRotation::Rotate270:
return PageRotation::Rotate90;
}
return PageRotation::None;
}
/// This class represents attributes, which are inheritable. Also allows merging from
/// parents.
class PDFPageInheritableAttributes
@ -241,6 +269,7 @@ public:
/// of 1 / 72 inch. Default value is 1.0.
inline PDFReal getUserUnit() const { return m_userUnit; }
static QSizeF getRotatedSize(const QSizeF& size, PageRotation rotation);
static QRectF getRotatedBox(const QRectF& rect, PageRotation rotation);
private: