diff --git a/PdfForQtLib/PdfForQtLib.pro b/PdfForQtLib/PdfForQtLib.pro index 3ec0d34..80e8095 100644 --- a/PdfForQtLib/PdfForQtLib.pro +++ b/PdfForQtLib/PdfForQtLib.pro @@ -45,7 +45,8 @@ SOURCES += \ sources/pdfencoding.cpp \ sources/pdfcatalog.cpp \ sources/pdfpage.cpp \ - sources/pdfstreamfilters.cpp + sources/pdfstreamfilters.cpp \ + sources/pdfdrawspacecontroller.cpp HEADERS += \ sources/pdfobject.h \ @@ -61,7 +62,8 @@ HEADERS += \ sources/pdfcatalog.h \ sources/pdfnumbertreeloader.h \ sources/pdfpage.h \ - sources/pdfstreamfilters.h + sources/pdfstreamfilters.h \ + sources/pdfdrawspacecontroller.h unix { target.path = /usr/lib diff --git a/PdfForQtLib/sources/pdfcatalog.h b/PdfForQtLib/sources/pdfcatalog.h index a799616..d559a5b 100644 --- a/PdfForQtLib/sources/pdfcatalog.h +++ b/PdfForQtLib/sources/pdfcatalog.h @@ -196,6 +196,12 @@ public: /// Returns viewer preferences of the application const PDFViewerPreferences* getViewerPreferences() const { return &m_viewerPreferences; } + /// Returns the page count + size_t getPageCount() const { return m_pages.size(); } + + /// Returns the page + const PDFPage* getPage(size_t index) const { return &m_pages.at(index); } + /// Parses catalog from catalog dictionary. If object cannot be parsed, or error occurs, /// then exception is thrown. static PDFCatalog parse(const PDFObject& catalog, const PDFDocument* document); diff --git a/PdfForQtLib/sources/pdfdocument.h b/PdfForQtLib/sources/pdfdocument.h index a82922e..8b5c820 100644 --- a/PdfForQtLib/sources/pdfdocument.h +++ b/PdfForQtLib/sources/pdfdocument.h @@ -173,6 +173,9 @@ public: /// is returned (no exception is thrown). const PDFObject& getObject(const PDFObject& object) const; + /// Returns the document catalog + const PDFCatalog* getCatalog() const { return &m_catalog; } + private: friend class PDFDocumentReader; diff --git a/PdfForQtLib/sources/pdfdrawspacecontroller.cpp b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp new file mode 100644 index 0000000..c74b931 --- /dev/null +++ b/PdfForQtLib/sources/pdfdrawspacecontroller.cpp @@ -0,0 +1,272 @@ +// Copyright (C) 2019 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt 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 +// (at your option) any later version. +// +// PdfForQt 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 PDFForQt. If not, see . + + +#include "pdfdrawspacecontroller.h" + +namespace pdf +{ + +PDFDrawSpaceController::PDFDrawSpaceController(QObject* parent) : + QObject(parent), + m_document(nullptr), + m_pageLayoutMode(PageLayout::SinglePage), + m_verticalSpacingMM(5.0), + m_horizontalSpacingMM(1.0) +{ + +} + +void PDFDrawSpaceController::recalculate() +{ + if (!m_document) + { + clear(true); + return; + } + + const PDFCatalog* catalog = m_document->getCatalog(); + size_t pageCount = catalog->getPageCount(); + + // First, preserve page rotations. We assume the count of pages is the same as the document. + // Document should not be changed while viewing. If a new document is setted, then the draw + // space is cleared first. + std::vector pageRotation(pageCount, PageRotation::None); + for (size_t i = 0; i < pageCount; ++i) + { + pageRotation[i] = catalog->getPage(i)->getPageRotation(); + } + for (const LayoutItem& layoutItem : m_layoutItems) + { + pageRotation[layoutItem.pageIndex] = layoutItem.pageRotation; + } + + static constexpr size_t INVALID_PAGE_INDEX = std::numeric_limits::max(); + + // Places the pages on the left/right sides. Pages can be nullptr, but not both of them. + // Updates bounding rectangle. + auto placePagesLeftRight = [this, catalog, &pageRotation](PDFInteger blockIndex, size_t leftIndex, size_t rightIndex, PDFReal& yPos, QRectF& boundingRect) + { + PDFReal yPosAdvance = 0.0; + + if (leftIndex != INVALID_PAGE_INDEX) + { + QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(leftIndex)->getMediaBoxMM(), pageRotation[leftIndex]).size(); + PDFReal xPos = -pageSize.width() - m_horizontalSpacingMM * 0.5; + QRectF rect(xPos, yPos, pageSize.width(), pageSize.height()); + m_layoutItems.emplace_back(blockIndex, leftIndex, pageRotation[leftIndex], rect); + yPosAdvance = qMax(yPosAdvance, pageSize.height()); + boundingRect = boundingRect.united(rect); + } + + if (rightIndex != INVALID_PAGE_INDEX) + { + QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(rightIndex)->getMediaBoxMM(), pageRotation[rightIndex]).size(); + PDFReal xPos = m_horizontalSpacingMM * 0.5; + QRectF rect(xPos, yPos, pageSize.width(), pageSize.height()); + m_layoutItems.emplace_back(blockIndex, rightIndex, pageRotation[rightIndex], rect); + yPosAdvance = qMax(yPosAdvance, pageSize.height()); + boundingRect = boundingRect.united(rect); + } + + if (yPosAdvance > 0.0) + { + yPos += yPosAdvance + m_verticalSpacingMM; + } + }; + + // Generates block with pages using page indices. If generateBlocks is true, then + // for each pair of pages, single block is generated, otherwise block containing all + // pages is generated. + auto placePagesLeftRightByIndices = [this, &placePagesLeftRight](const std::vector& indices, bool generateBlocks) + { + Q_ASSERT(indices.size() % 2 == 0); + + PDFReal yPos = 0.0; + PDFInteger blockIndex = 0; + QRectF boundingRectangle; + + size_t count = indices.size() / 2; + for (size_t i = 0; i < count; ++i) + { + const size_t leftPageIndex = indices[2 * i]; + const size_t rightPageIndex = indices[2 * i + 1]; + placePagesLeftRight(blockIndex, leftPageIndex, rightPageIndex, yPos, boundingRectangle); + + if (generateBlocks) + { + m_blockItems.emplace_back(boundingRectangle); + + // Clear the old data + yPos = 0.0; + ++blockIndex; + boundingRectangle = QRectF(); + } + } + + if (!generateBlocks) + { + // Generate single block for all layed out pages + m_blockItems.emplace_back(boundingRectangle); + } + }; + + switch (m_pageLayoutMode) + { + case PageLayout::SinglePage: + { + // Each block contains single page + m_layoutItems.reserve(pageCount); + m_blockItems.reserve(pageCount); + + // Pages can have different size, so we center them around the center. + // Block size will equal to the page size. + + for (size_t i = 0; i < pageCount; ++i) + { + QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(i)->getMediaBoxMM(), pageRotation[i]).size(); + QRectF rect(-pageSize.width() * 0.5, -pageSize.height() * 0.5, pageSize.width(), pageSize.height()); + m_layoutItems.emplace_back(i, i, pageRotation[i], rect); + m_blockItems.emplace_back(rect); + } + + break; + } + + case PageLayout::OneColumn: + { + // Single block, one column + m_layoutItems.reserve(pageCount); + m_blockItems.reserve(1); + + PDFReal yPos = 0.0; + QRectF boundingRectangle; + + for (size_t i = 0; i < pageCount; ++i) + { + // Top of current page is at yPos. + QSizeF pageSize = PDFPage::getRotatedBox(catalog->getPage(i)->getMediaBoxMM(), pageRotation[i]).size(); + QRectF rect(-pageSize.width() * 0.5, yPos, pageSize.width(), pageSize.height()); + m_layoutItems.emplace_back(0, i, pageRotation[i], rect); + yPos += pageSize.height() + m_verticalSpacingMM; + boundingRectangle = boundingRectangle.united(rect); + } + + // Insert the single block with union of bounding rectangles + m_blockItems.emplace_back(boundingRectangle); + + break; + } + + case PageLayout::TwoColumnLeft: + { + // Pages with number 1, 3, 5, ... are on the left, 2, 4, 6 are on the right. + // Page indices are numbered from 0, so pages 0, 2, 4 will be on the left, + // 1, 3, 5 will be on the right. + // For purposes or paging, "left" pages will be on the left side of y axis (negative x axis), + // the "right" pages will be on the right side of y axis (positive x axis). + + m_layoutItems.reserve(pageCount); + m_blockItems.reserve(1); + + std::vector pageIndices(pageCount, INVALID_PAGE_INDEX); + std::iota(pageIndices.begin(), pageIndices.end(), static_cast(0)); + + if (pageIndices.size() % 2 == 1) + { + pageIndices.push_back(INVALID_PAGE_INDEX); + } + + placePagesLeftRightByIndices(pageIndices, false); + break; + } + + case PageLayout::TwoColumnRight: + { + // Similar to previous case, but page sequence start on the right. + + m_layoutItems.reserve(pageCount); + m_blockItems.reserve(1); + + std::vector pageIndices(pageCount + 1, INVALID_PAGE_INDEX); + std::iota(std::next(pageIndices.begin()), pageIndices.end(), static_cast(0)); + + if (pageIndices.size() % 2 == 1) + { + pageIndices.push_back(INVALID_PAGE_INDEX); + } + + placePagesLeftRightByIndices(pageIndices, false); + break; + } + + case PageLayout::TwoPagesLeft: + { + m_layoutItems.reserve(pageCount); + m_blockItems.reserve((pageCount / 2) + (pageCount % 2)); + + std::vector pageIndices(pageCount, INVALID_PAGE_INDEX); + std::iota(pageIndices.begin(), pageIndices.end(), static_cast(0)); + + if (pageIndices.size() % 2 == 1) + { + pageIndices.push_back(INVALID_PAGE_INDEX); + } + + placePagesLeftRightByIndices(pageIndices, true); + break; + } + + case PageLayout::TwoPagesRight: + { + m_layoutItems.reserve(pageCount); + m_blockItems.reserve((pageCount / 2) + (pageCount % 2)); + + std::vector pageIndices(pageCount + 1, INVALID_PAGE_INDEX); + std::iota(std::next(pageIndices.begin()), pageIndices.end(), static_cast(0)); + + if (pageIndices.size() % 2 == 1) + { + pageIndices.push_back(INVALID_PAGE_INDEX); + } + + placePagesLeftRightByIndices(pageIndices, true); + break; + } + + default: + { + Q_ASSERT(false); + break; + } + } + + emit drawSpaceChanged(); +} + +void PDFDrawSpaceController::clear(bool emitSignal) +{ + m_layoutItems.clear(); + m_blockItems.clear(); + + if (emitSignal) + { + emit drawSpaceChanged(); + } +} + +} // namespace pdf diff --git a/PdfForQtLib/sources/pdfdrawspacecontroller.h b/PdfForQtLib/sources/pdfdrawspacecontroller.h new file mode 100644 index 0000000..998d82d --- /dev/null +++ b/PdfForQtLib/sources/pdfdrawspacecontroller.h @@ -0,0 +1,88 @@ +// Copyright (C) 2019 Jakub Melka +// +// This file is part of PdfForQt. +// +// PdfForQt 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 +// (at your option) any later version. +// +// PdfForQt 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 PDFForQt. If not, see . + +#ifndef PDFDRAWSPACECONTROLLER_H +#define PDFDRAWSPACECONTROLLER_H + +#include "pdfdocument.h" + +#include +#include +#include + +namespace pdf +{ + +/// This class controls draw space - page layout. Pages are divided into blocks +/// each block can contain one or multiple pages. Units are in milimeters. +/// Pages are layouted in zoom-independent mode. +class PDFDrawSpaceController : public QObject +{ + Q_OBJECT + +public: + explicit PDFDrawSpaceController(QObject* parent); + +signals: + void drawSpaceChanged(); + +private: + /// Recalculates the draw space. Preserves setted page rotation. + void recalculate(); + + /// Clears the draw space. Emits signal if desired. + void clear(bool emitSignal); + + /// Represents layouted page. This structure contains index of the block, index of the + /// page and page rectangle, in which the page is contained. + struct LayoutItem + { + constexpr inline explicit LayoutItem() : blockIndex(-1), pageIndex(-1), pageRotation(PageRotation::None) { } + constexpr inline explicit LayoutItem(PDFInteger blockIndex, PDFInteger pageIndex, PageRotation rotation, const QRectF& pageRectMM) : + blockIndex(blockIndex), pageIndex(pageIndex), pageRotation(rotation), pageRectMM(pageRectMM) { } + + PDFInteger blockIndex; + PDFInteger pageIndex; + PageRotation pageRotation; + QRectF pageRectMM; + }; + + using LayoutItems = std::vector; + + /// Represents data for the single block. Contains block size in milimeters. + struct LayoutBlock + { + constexpr inline explicit LayoutBlock() = default; + constexpr inline explicit LayoutBlock(const QRectF& blockRectMM) : blockRectMM(blockRectMM) { } + + QRectF blockRectMM; + }; + + using BlockItems = std::vector; + + const PDFDocument* m_document; + + PageLayout m_pageLayoutMode; + LayoutItems m_layoutItems; + BlockItems m_blockItems; + PDFReal m_verticalSpacingMM; + PDFReal m_horizontalSpacingMM; +}; + +} // namespace pdf + +#endif // PDFDRAWSPACECONTROLLER_H diff --git a/PdfForQtLib/sources/pdfglobal.h b/PdfForQtLib/sources/pdfglobal.h index 272a688..0b6667d 100644 --- a/PdfForQtLib/sources/pdfglobal.h +++ b/PdfForQtLib/sources/pdfglobal.h @@ -104,6 +104,15 @@ struct PDFTranslationContext Q_DECLARE_TR_FUNCTIONS(pdf::PDFTranslationContext) }; +constexpr PDFReal PDF_POINT_TO_INCH = 1.0 / 72.0; +constexpr PDFReal PDF_INT_TO_MM = 25.4; +constexpr PDFReal PDF_POINT_TO_MM = PDF_POINT_TO_INCH * PDF_INT_TO_MM; + +constexpr PDFReal convertPDFPointToMM(PDFReal point) +{ + return point * PDF_POINT_TO_MM; +} + } // namespace pdf #endif // PDFGLOBAL_H diff --git a/PdfForQtLib/sources/pdfpage.cpp b/PdfForQtLib/sources/pdfpage.cpp index 7330dd1..113d21e 100644 --- a/PdfForQtLib/sources/pdfpage.cpp +++ b/PdfForQtLib/sources/pdfpage.cpp @@ -108,6 +108,31 @@ std::vector PDFPage::parse(const PDFDocument* document, const PDFObject return result; } +QRectF PDFPage::getRectMM(const QRectF& rect) const +{ + return QRectF(convertPDFPointToMM(rect.left()), + convertPDFPointToMM(rect.top()), + convertPDFPointToMM(rect.width()), + convertPDFPointToMM(rect.height())); +} + +QRectF PDFPage::getRotatedBox(const QRectF& rect, PageRotation rotation) +{ + switch (rotation) + { + case PageRotation::None: + case PageRotation::Rotate180: + // Preserve rotation + break; + + case PageRotation::Rotate90: + case PageRotation::Rotate270: + return rect.transposed(); + } + + return rect; +} + void PDFPage::parseImpl(std::vector& pages, std::set& visitedReferences, const PDFPageInheritableAttributes& templateAttributes, @@ -165,7 +190,7 @@ void PDFPage::parseImpl(std::vector& pages, page.m_mediaBox = currentInheritableAttributes.getMediaBox(); page.m_cropBox = currentInheritableAttributes.getCropBox(); - page.m_resources = currentInheritableAttributes.getResources(); + page.m_resources = document->getObject(currentInheritableAttributes.getResources()); page.m_pageRotation = currentInheritableAttributes.getPageRotation(); if (!page.m_cropBox.isValid()) diff --git a/PdfForQtLib/sources/pdfpage.h b/PdfForQtLib/sources/pdfpage.h index a919270..59c64e2 100644 --- a/PdfForQtLib/sources/pdfpage.h +++ b/PdfForQtLib/sources/pdfpage.h @@ -76,14 +76,24 @@ public: /// \param root Root object of page tree static std::vector parse(const PDFDocument* document, const PDFObject& root); - const QRectF& getMediaBox() const { return m_mediaBox; } - const QRectF& getCropBox() const { return m_cropBox; } - const QRectF& getBleedBox() const { return m_bleedBox; } - const QRectF& getTrimBox() const { return m_trimBox; } - const QRectF& getArtBox() const { return m_artBox; } - PageRotation getPageRotation() const { return m_pageRotation; } - const PDFObject& getResources() const { return m_resources; } - const PDFObject& getContents() const { return m_contents; } + inline const QRectF& getMediaBox() const { return m_mediaBox; } + inline const QRectF& getCropBox() const { return m_cropBox; } + inline const QRectF& getBleedBox() const { return m_bleedBox; } + inline const QRectF& getTrimBox() const { return m_trimBox; } + inline const QRectF& getArtBox() const { return m_artBox; } + inline PageRotation getPageRotation() const { return m_pageRotation; } + inline const PDFObject& getResources() const { return m_resources; } + inline const PDFObject& getContents() const { return m_contents; } + + QRectF getRectMM(const QRectF& rect) const; + + inline QRectF getMediaBoxMM() const { return getRectMM(m_mediaBox); } + inline QRectF getCropBoxMM() const { return getRectMM(m_cropBox); } + inline QRectF getBleedBoxMM() const { return getRectMM(m_bleedBox); } + inline QRectF getTrimBoxMM() const { return getRectMM(m_trimBox); } + inline QRectF getArtBoxMM() const { return getRectMM(m_artBox); } + + static QRectF getRotatedBox(const QRectF& rect, PageRotation rotation); private: /// Parses the page tree (implementation). If error occurs, then exception is thrown. diff --git a/PdfForQtLib/sources/pdfstreamfilters.h b/PdfForQtLib/sources/pdfstreamfilters.h index 9438b32..f5747bb 100644 --- a/PdfForQtLib/sources/pdfstreamfilters.h +++ b/PdfForQtLib/sources/pdfstreamfilters.h @@ -26,7 +26,7 @@ namespace pdf { class PDFDocument; -class PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFStreamFilter { public: explicit PDFStreamFilter() = default; @@ -35,7 +35,7 @@ public: virtual QByteArray apply(const QByteArray& data, const PDFDocument* document, const PDFObject& parameters) const = 0; }; -class PDFAsciiHexDecodeFilter : public PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFAsciiHexDecodeFilter : public PDFStreamFilter { public: explicit PDFAsciiHexDecodeFilter() = default; @@ -44,7 +44,7 @@ public: virtual QByteArray apply(const QByteArray& data, const PDFDocument* document, const PDFObject& parameters) const override; }; -class PDFAscii85DecodeFilter : public PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFAscii85DecodeFilter : public PDFStreamFilter { public: explicit PDFAscii85DecodeFilter() = default; @@ -53,7 +53,7 @@ public: virtual QByteArray apply(const QByteArray& data, const PDFDocument* document, const PDFObject& parameters) const override; }; -class PDFLzwDecodeFilter : public PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFLzwDecodeFilter : public PDFStreamFilter { public: explicit PDFLzwDecodeFilter() = default; @@ -62,7 +62,7 @@ public: virtual QByteArray apply(const QByteArray& data, const PDFDocument* document, const PDFObject& parameters) const override; }; -class PDFFlateDecodeFilter : public PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFFlateDecodeFilter : public PDFStreamFilter { public: explicit PDFFlateDecodeFilter() = default; @@ -71,7 +71,7 @@ public: virtual QByteArray apply(const QByteArray& data, const PDFDocument* document, const PDFObject& parameters) const override; }; -class PDFRunLengthDecodeFilter : public PDFStreamFilter +class PDFFORQTLIBSHARED_EXPORT PDFRunLengthDecodeFilter : public PDFStreamFilter { public: explicit PDFRunLengthDecodeFilter() = default; diff --git a/PdfForQtViewer/pdfviewermainwindow.cpp b/PdfForQtViewer/pdfviewermainwindow.cpp index 15850a8..509d35e 100644 --- a/PdfForQtViewer/pdfviewermainwindow.cpp +++ b/PdfForQtViewer/pdfviewermainwindow.cpp @@ -3,6 +3,7 @@ #include "pdfdocumentreader.h" #include "pdfvisitor.h" +#include "pdfstreamfilters.h" #include #include @@ -42,6 +43,18 @@ void PDFViewerMainWindow::onActionOpenTriggered() { QMessageBox::information(this, tr("PDF Reader"), tr("Document read error: %1").arg(reader.getErrorMessage())); } + + const pdf::PDFCatalog* catalog = document.getCatalog(); + const pdf::PDFPage* page = catalog->getPage(0); + const pdf::PDFObject& contents = page->getContents(); + + if (contents.isStream()) + { + const pdf::PDFStream* stream = contents.getStream(); + const QByteArray* compressed = stream->getContent(); + pdf::PDFFlateDecodeFilter fd; + QByteArray uncompressed = fd.apply(*compressed, &document, pdf::PDFObject()); + } } } diff --git a/UnitTests/tst_lexicalanalyzertest.cpp b/UnitTests/tst_lexicalanalyzertest.cpp index db65659..6ebb693 100644 --- a/UnitTests/tst_lexicalanalyzertest.cpp +++ b/UnitTests/tst_lexicalanalyzertest.cpp @@ -22,6 +22,7 @@ #include "pdfparser.h" #include "pdfconstants.h" #include "pdfflatmap.h" +#include "pdfstreamfilters.h" #include @@ -44,6 +45,7 @@ private slots: void test_invalid_input(); void test_header_regexp(); void test_flat_map(); + void test_lzw_filter(); private: void scanWholeStream(const char* stream); @@ -295,6 +297,17 @@ void LexicalAnalyzerTest::test_flat_map() } } +void LexicalAnalyzerTest::test_lzw_filter() +{ + // This example is from PDF 1.7 Reference + QByteArray byteArray = QByteArray::fromHex("800B6050220C0C8501"); + pdf::PDFLzwDecodeFilter filter; + QByteArray decoded = filter.apply(byteArray, nullptr, pdf::PDFObject()); + QByteArray valid = "-----A---B"; + + QCOMPARE(decoded, valid); +} + void LexicalAnalyzerTest::scanWholeStream(const char* stream) { pdf::PDFLexicalAnalyzer analyzer(stream, stream + strlen(stream));