diff options
Diffstat (limited to 'src/pdf/qpdfdocument.cpp')
-rw-r--r-- | src/pdf/qpdfdocument.cpp | 517 |
1 files changed, 355 insertions, 162 deletions
diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 8fd55dd57..17fdb29b9 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -1,38 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2020 The Qt Company Ltd. -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtPDF module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL3$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPLv3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or later as published by the Free -** Software Foundation and appearing in the file LICENSE.GPL included in -** the packaging of this file. Please review the following information to -** ensure the GNU General Public License version 2.0 requirements will be -** met: http://www.gnu.org/licenses/gpl-2.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2020 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qpdfdocument.h" #include "qpdfdocument_p.h" @@ -46,9 +13,13 @@ #include <QFile> #include <QHash> #include <QLoggingCategory> +#include <QMetaEnum> #include <QMutex> +#include <QPixmap> #include <QVector2D> +#include <QtCore/private/qtools_p.h> + QT_BEGIN_NAMESPACE Q_GLOBAL_STATIC(QRecursiveMutex, pdfMutex) @@ -61,12 +32,83 @@ QPdfMutexLocker::QPdfMutexLocker() { } +class Q_PDF_EXPORT QPdfPageModel : public QAbstractListModel +{ + Q_OBJECT +public: + QPdfPageModel(QPdfDocument *doc) : QAbstractListModel(doc) + { + m_roleNames = QAbstractItemModel::roleNames(); + QMetaEnum rolesMetaEnum = doc->metaObject()->enumerator(doc->metaObject()->indexOfEnumerator("PageModelRole")); + for (int r = Qt::UserRole; r < int(QPdfDocument::PageModelRole::NRoles); ++r) { + auto name = QByteArray(rolesMetaEnum.valueToKey(r)); + name[0] = QtMiscUtils::toAsciiLower(name[0]); + m_roleNames.insert(r, name); + } + connect(doc, &QPdfDocument::statusChanged, this, [this](QPdfDocument::Status s) { + if (s == QPdfDocument::Status::Loading) + beginResetModel(); + else if (s == QPdfDocument::Status::Ready) + endResetModel(); + }); + } + + QVariant data(const QModelIndex &index, int role) const override + { + if (!index.isValid()) + return QVariant(); + + switch (QPdfDocument::PageModelRole(role)) { + case QPdfDocument::PageModelRole::Label: + return document()->pageLabel(index.row()); + case QPdfDocument::PageModelRole::PointSize: + return document()->pagePointSize(index.row()); + case QPdfDocument::PageModelRole::NRoles: + break; + } + + switch (role) { + case Qt::DecorationRole: + return pageThumbnail(index.row()); + case Qt::DisplayRole: + return document()->pageLabel(index.row()); + } + + return QVariant(); + } + + int rowCount(const QModelIndex & = QModelIndex()) const override { return document()->pageCount(); } + + QHash<int, QByteArray> roleNames() const override { return m_roleNames; } + +private: + QPdfDocument *document() const { return static_cast<QPdfDocument *>(parent()); } + QPixmap pageThumbnail(int page) const + { + auto it = m_thumbnails.constFind(page); + if (it == m_thumbnails.constEnd()) { + auto doc = document(); + auto size = doc->pagePointSize(page); + size.scale(128, 128, Qt::KeepAspectRatio); + // TODO use QPdfPageRenderer for threading? + auto image = document()->render(page, size.toSize()); + QPixmap ret = QPixmap::fromImage(image); + m_thumbnails.insert(page, ret); + return ret; + } + return it.value(); + } + + QHash<int, QByteArray> m_roleNames; + mutable QHash<int, QPixmap> m_thumbnails; +}; + QPdfDocumentPrivate::QPdfDocumentPrivate() : avail(nullptr) , doc(nullptr) , loadComplete(false) - , status(QPdfDocument::Null) - , lastError(QPdfDocument::NoError) + , status(QPdfDocument::Status::Null) + , lastError(QPdfDocument::Error::None) , pageCount(0) { asyncBuffer.setData(QByteArray()); @@ -74,8 +116,12 @@ QPdfDocumentPrivate::QPdfDocumentPrivate() const QPdfMutexLocker lock; - if (libraryRefCount == 0) + if (libraryRefCount == 0) { + QElapsedTimer timer; + timer.start(); FPDF_InitLibrary(); + qCDebug(qLcDoc) << "FPDF_InitLibrary took" << timer.elapsed() << "ms"; + } ++libraryRefCount; // FPDF_FILEACCESS setup @@ -97,8 +143,10 @@ QPdfDocumentPrivate::~QPdfDocumentPrivate() const QPdfMutexLocker lock; - if (!--libraryRefCount) + if (!--libraryRefCount) { + qCDebug(qLcDoc) << "FPDF_DestroyLibrary"; FPDF_DestroyLibrary(); + } } void QPdfDocumentPrivate::clear() @@ -117,6 +165,7 @@ void QPdfDocumentPrivate::clear() if (pageCount != 0) { pageCount = 0; emit q->pageCountChanged(pageCount); + emit q->pageModelChanged(); } loadComplete = false; @@ -132,7 +181,7 @@ void QPdfDocumentPrivate::clear() void QPdfDocumentPrivate::updateLastError() { if (doc) { - lastError = QPdfDocument::NoError; + lastError = QPdfDocument::Error::None; return; } @@ -141,15 +190,17 @@ void QPdfDocumentPrivate::updateLastError() lock.unlock(); switch (error) { - case FPDF_ERR_SUCCESS: lastError = QPdfDocument::NoError; break; - case FPDF_ERR_UNKNOWN: lastError = QPdfDocument::UnknownError; break; - case FPDF_ERR_FILE: lastError = QPdfDocument::FileNotFoundError; break; - case FPDF_ERR_FORMAT: lastError = QPdfDocument::InvalidFileFormatError; break; - case FPDF_ERR_PASSWORD: lastError = QPdfDocument::IncorrectPasswordError; break; - case FPDF_ERR_SECURITY: lastError = QPdfDocument::UnsupportedSecuritySchemeError; break; + case FPDF_ERR_SUCCESS: lastError = QPdfDocument::Error::None; break; + case FPDF_ERR_UNKNOWN: lastError = QPdfDocument::Error::Unknown; break; + case FPDF_ERR_FILE: lastError = QPdfDocument::Error::FileNotFound; break; + case FPDF_ERR_FORMAT: lastError = QPdfDocument::Error::InvalidFileFormat; break; + case FPDF_ERR_PASSWORD: lastError = QPdfDocument::Error::IncorrectPassword; break; + case FPDF_ERR_SECURITY: lastError = QPdfDocument::Error::UnsupportedSecurityScheme; break; default: Q_UNREACHABLE(); } + if (lastError != QPdfDocument::Error::None) + qCDebug(qLcDoc) << "FPDF error" << error << "->" << lastError; } void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnership) @@ -165,19 +216,19 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi QNetworkReply *reply = qobject_cast<QNetworkReply*>(sequentialSourceDevice); if (!reply) { - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); qWarning() << "QPdfDocument: Loading from sequential devices only supported with QNetworkAccessManager."; return; } if (reply->isFinished() && reply->error() != QNetworkReply::NoError) { - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); return; } QObject::connect(reply, &QNetworkReply::finished, q, [this, reply](){ if (reply->error() != QNetworkReply::NoError || reply->bytesAvailable() == 0) { - this->setStatus(QPdfDocument::Error); + this->setStatus(QPdfDocument::Status::Error); } }); @@ -189,7 +240,7 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi device = newDevice; initiateAsyncLoadWithTotalSizeKnown(device->size()); if (!avail) { - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); return; } @@ -198,7 +249,7 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi if (!doc) { updateLastError(); - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); return; } @@ -208,15 +259,16 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi if (newPageCount != pageCount) { pageCount = newPageCount; emit q->pageCountChanged(pageCount); + emit q->pageModelChanged(); } // If it's a local file, and the first couple of pages are available, // probably the whole document is available. if (checkPageComplete(0) && (pageCount < 2 || checkPageComplete(1))) { - setStatus(QPdfDocument::Ready); + setStatus(QPdfDocument::Status::Ready); } else { updateLastError(); - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); } } } @@ -228,13 +280,13 @@ void QPdfDocumentPrivate::_q_tryLoadingWithSizeFromContentHeader() const QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(sequentialSourceDevice); if (!networkReply) { - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); return; } const QVariant contentLength = networkReply->header(QNetworkRequest::ContentLengthHeader); if (!contentLength.isValid()) { - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); return; } @@ -280,11 +332,10 @@ void QPdfDocumentPrivate::tryLoadDocument() break; case PDF_DATA_NOTAVAIL: qCDebug(qLcDoc) << "data not yet available"; - lastError = QPdfDocument::DataNotYetAvailableError; - setStatus(QPdfDocument::Error); + lastError = QPdfDocument::Error::DataNotYetAvailable; break; case PDF_DATA_AVAIL: - // all good + lastError = QPdfDocument::Error::None; break; } @@ -294,12 +345,14 @@ void QPdfDocumentPrivate::tryLoadDocument() lock.unlock(); updateLastError(); + if (lastError != QPdfDocument::Error::None) + setStatus(QPdfDocument::Status::Error); - if (lastError == QPdfDocument::IncorrectPasswordError) { + if (lastError == QPdfDocument::Error::IncorrectPassword) { FPDF_CloseDocument(doc); doc = nullptr; - setStatus(QPdfDocument::Error); + setStatus(QPdfDocument::Status::Error); emit q->passwordRequired(); } } @@ -336,9 +389,10 @@ void QPdfDocumentPrivate::checkComplete() if (newPageCount != pageCount) { pageCount = newPageCount; emit q->pageCountChanged(pageCount); + emit q->pageModelChanged(); } - setStatus(QPdfDocument::Ready); + setStatus(QPdfDocument::Status::Ready); } } @@ -392,7 +446,7 @@ void QPdfDocumentPrivate::fpdf_AddSegment(_FX_DOWNLOADHINTS *pThis, size_t offse Q_UNUSED(size); } -QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int count) +QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int count) const { QList<ushort> buf(count + 1); // TODO is that enough space in case one unicode character is more than one in utf-16? @@ -401,23 +455,73 @@ QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int return QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.constData()), len - 1); } -QPointF QPdfDocumentPrivate::getCharPosition(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex) +QPointF QPdfDocumentPrivate::getCharPosition(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const { double x, y; - int count = FPDFText_CountChars(textPage); - bool ok = FPDFText_GetCharOrigin(textPage, qMin(count - 1, charIndex), &x, &y); - if (!ok) - return QPointF(); - return QPointF(x, pageHeight - y); + const int count = FPDFText_CountChars(textPage); + if (FPDFText_GetCharOrigin(textPage, qMin(count - 1, charIndex), &x, &y)) + return mapPageToView(pdfPage, x, y); + return {}; } -QRectF QPdfDocumentPrivate::getCharBox(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex) +QRectF QPdfDocumentPrivate::getCharBox(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const { double l, t, r, b; - bool ok = FPDFText_GetCharBox(textPage, charIndex, &l, &r, &b, &t); - if (!ok) - return QRectF(); - return QRectF(l, pageHeight - t, r - l, t - b); + if (FPDFText_GetCharBox(textPage, charIndex, &l, &r, &b, &t)) + return mapPageToView(pdfPage, l, t, r, b); + return {}; +} + +/*! \internal + Convert the point \a x , \a y to the usual 1x (pixels = points) + 4th-quadrant "view" coordinate system relative to the top-left corner of + the rendered page. Some PDF files have internal transforms that make this + coordinate system different from "page coordinates", so we cannot just + subtract from page height to invert the y coordinates, in general. + */ +QPointF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double x, double y) const +{ + const auto pageHeight = FPDF_GetPageHeight(pdfPage); + const auto pageWidth = FPDF_GetPageWidth(pdfPage); + int rx, ry; + if (FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, x, y, &rx, &ry)) + return QPointF(rx, ry); + return {}; +} + +/*! \internal + Convert the bounding box defined by \a left \a top \a right and \a bottom + to the usual 1x (pixels = points) 4th-quadrant "view" coordinate system + that we use for rendering things on top of the page image. + Some PDF files have internal transforms that make this coordinate + system different from "page coordinates", so we cannot just + subtract from page height to invert the y coordinates, in general. + */ +QRectF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double left, double top, double right, double bottom) const +{ + const auto pageHeight = FPDF_GetPageHeight(pdfPage); + const auto pageWidth = FPDF_GetPageWidth(pdfPage); + int xfmLeft, xfmTop, xfmRight, xfmBottom; + if ( FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, left, top, &xfmLeft, &xfmTop) && + FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, right, bottom, &xfmRight, &xfmBottom) ) + return QRectF(xfmLeft, xfmTop, xfmRight - xfmLeft, xfmBottom - xfmTop); + return {}; +} + +/*! \internal + Convert the point \a x , \a y \a from the usual 1x (pixels = points) + 4th-quadrant "view" coordinate system relative to the top-left corner of + the rendered page, to "page coordinates" suited to the given \a pdfPage, + which may have arbitrary internal transforms. + */ +QPointF QPdfDocumentPrivate::mapViewToPage(FPDF_PAGE pdfPage, QPointF position) const +{ + const auto pageHeight = FPDF_GetPageHeight(pdfPage); + const auto pageWidth = FPDF_GetPageWidth(pdfPage); + double rx, ry; + if (FPDF_DeviceToPage(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, position.x(), position.y(), &rx, &ry)) + return QPointF(rx, ry); + return {}; } QPdfDocumentPrivate::TextPosition QPdfDocumentPrivate::hitTest(int page, QPointF position) @@ -426,14 +530,14 @@ QPdfDocumentPrivate::TextPosition QPdfDocumentPrivate::hitTest(int page, QPointF TextPosition result; FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page); - double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); - int hitIndex = FPDFText_GetCharIndexAtPos(textPage, position.x(), pageHeight - position.y(), + const QPointF pagePos = mapViewToPage(pdfPage, position); + int hitIndex = FPDFText_GetCharIndexAtPos(textPage, pagePos.x(), pagePos.y(), CharacterHitTolerance, CharacterHitTolerance); if (hitIndex >= 0) { - QPointF charPos = getCharPosition(textPage, pageHeight, hitIndex); + QPointF charPos = getCharPosition(pdfPage, textPage, hitIndex); if (!charPos.isNull()) { - QRectF charBox = getCharBox(textPage, pageHeight, hitIndex); + QRectF charBox = getCharBox(pdfPage, textPage, hitIndex); // If the given position is past the end of the line, i.e. if the right edge of the found character's // bounding box is closer to it than the left edge is, we say that we "hit" the next character index after if (qAbs(charBox.right() - position.x()) < qAbs(charPos.x() - position.x())) { @@ -476,24 +580,39 @@ QPdfDocument::~QPdfDocument() { } -QPdfDocument::DocumentError QPdfDocument::load(const QString &fileName) +/*! + Loads the document contents from \a fileName. +*/ +QPdfDocument::Error QPdfDocument::load(const QString &fileName) { qCDebug(qLcDoc) << "loading" << fileName; close(); - d->setStatus(QPdfDocument::Loading); + d->setStatus(QPdfDocument::Status::Loading); - QScopedPointer<QFile> f(new QFile(fileName)); + std::unique_ptr<QFile> f(new QFile(fileName)); if (!f->open(QIODevice::ReadOnly)) { - d->lastError = FileNotFoundError; - d->setStatus(QPdfDocument::Error); + d->lastError = Error::FileNotFound; + d->setStatus(QPdfDocument::Status::Error); } else { - d->load(f.take(), /*transfer ownership*/true); + d->load(f.release(), /*transfer ownership*/true); } return d->lastError; } +/*! \internal + Returns the filename of the document that has been opened, + or an empty string if no document is open. +*/ +QString QPdfDocument::fileName() const +{ + const QFile *f = qobject_cast<QFile *>(d->device.data()); + if (f) + return f->fileName(); + return QString(); +} + /*! \enum QPdfDocument::Status @@ -510,22 +629,35 @@ QPdfDocument::DocumentError QPdfDocument::load(const QString &fileName) */ /*! - Returns the current status of the document. + \property QPdfDocument::status + + This property holds the current status of the document. */ QPdfDocument::Status QPdfDocument::status() const { return d->status; } +/*! + Loads the document contents from \a device. +*/ void QPdfDocument::load(QIODevice *device) { close(); - d->setStatus(QPdfDocument::Loading); + d->setStatus(QPdfDocument::Status::Loading); d->load(device, /*transfer ownership*/false); } +/*! + \property QPdfDocument::password + + This property holds the document password. + + If the document is protected by a password, the user must provide it, and + the application must set this property. Otherwise, it's not needed. +*/ void QPdfDocument::setPassword(const QString &password) { const QByteArray newPassword = password.toUtf8(); @@ -570,53 +702,36 @@ QVariant QPdfDocument::metaData(MetaDataField field) const if (!d->doc) return QString(); + static QMetaEnum fieldsMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("MetaDataField")); QByteArray fieldName; switch (field) { - case Title: - fieldName = "Title"; - break; - case Subject: - fieldName = "Subject"; - break; - case Author: - fieldName = "Author"; - break; - case Keywords: - fieldName = "Keywords"; - break; - case Producer: - fieldName = "Producer"; - break; - case Creator: - fieldName = "Creator"; - break; - case CreationDate: - fieldName = "CreationDate"; - break; - case ModificationDate: + case MetaDataField::ModificationDate: fieldName = "ModDate"; break; + default: + fieldName = QByteArray(fieldsMetaEnum.valueToKey(int(field))); + break; } QPdfMutexLocker lock; const unsigned long len = FPDF_GetMetaText(d->doc, fieldName.constData(), nullptr, 0); QList<ushort> buf(len); - FPDF_GetMetaText(d->doc, fieldName.constData(), buf.data(), buf.length()); + FPDF_GetMetaText(d->doc, fieldName.constData(), buf.data(), buf.size()); lock.unlock(); QString text = QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.data())); switch (field) { - case Title: // fall through - case Subject: - case Author: - case Keywords: - case Producer: - case Creator: + case MetaDataField::Title: // fall through + case MetaDataField::Subject: + case MetaDataField::Author: + case MetaDataField::Keywords: + case MetaDataField::Producer: + case MetaDataField::Creator: return text; - case CreationDate: // fall through - case ModificationDate: + case MetaDataField::CreationDate: // fall through + case MetaDataField::ModificationDate: // convert a "D:YYYYMMDDHHmmSSOHH'mm'" into "YYYY-MM-DDTHH:mm:ss+HH:mm" if (text.startsWith(QLatin1String("D:"))) text = text.mid(2); @@ -635,20 +750,40 @@ QVariant QPdfDocument::metaData(MetaDataField field) const return QVariant(); } -QPdfDocument::DocumentError QPdfDocument::error() const +/*! + \enum QPdfDocument::Error + + This enum describes the error while attempting the last operation on the document. + + \value None No error occurred. + \value Unknown Unknown type of error. + \value DataNotYetAvailable The document is still loading, it's too early to attempt the operation. + \value FileNotFound The file given to load() was not found. + \value InvalidFileFormat The file given to load() is not a valid PDF file. + \value IncorrectPassword The password given to setPassword() is not correct for this file. + \value UnsupportedSecurityScheme QPdfDocument is not able to unlock this kind of PDF file. + + \sa QPdfDocument::error() +*/ + +/*! + Returns the type of error if \l status is \c Error, or \c NoError if there + is no error. +*/ +QPdfDocument::Error QPdfDocument::error() const { return d->lastError; } /*! - Closes the document. + Closes the document. */ void QPdfDocument::close() { if (!d->doc) return; - d->setStatus(Unloading); + d->setStatus(Status::Unloading); d->clear(); @@ -657,19 +792,24 @@ void QPdfDocument::close() emit passwordChanged(); } - d->setStatus(Null); + d->setStatus(Status::Null); } /*! - Returns the amount of pages for the loaded document or \c 0 if - no document is loaded. + \property QPdfDocument::pageCount + + This property holds the number of pages in the loaded document or \c 0 if + no document is loaded. */ int QPdfDocument::pageCount() const { return d->pageCount; } -QSizeF QPdfDocument::pageSize(int page) const +/*! + Returns the size of page \a page in points (1/72 of an inch). +*/ +QSizeF QPdfDocument::pagePointSize(int page) const { QSizeF result; if (!d->doc || !d->checkPageComplete(page)) @@ -682,6 +822,74 @@ QSizeF QPdfDocument::pageSize(int page) const } /*! + \enum QPdfDocument::PageModelRole + + Roles in pageModel(). + + \value Label The page number to be used for display purposes (QString). + \value PointSize The page size in points (1/72 of an inch) (QSizeF). + \omitvalue NRoles +*/ + +/*! + \property QPdfDocument::pageModel + + This property holds an instance of QAbstractListModel to provide + page-specific metadata, containing one row for each page in the document. + + \sa QPdfDocument::PageModelRole +*/ +QAbstractListModel *QPdfDocument::pageModel() +{ + if (!d->pageModel) + d->pageModel = new QPdfPageModel(this); + return d->pageModel; +} + +/*! + Returns the \a page number to be used for display purposes. + + For example, a document may have multiple sections with different numbering. + Perhaps the preface uses roman numerals, the body starts on page 1, and the + appendix starts at A1. Whenever a PDF viewer shows a page number, to avoid + confusing the user it should be the same "number" as is printed on the + corner of the page, rather than the zero-based page index that we use in + APIs (assuming the document author has made the page labels match the + printed numbers). + + If the document does not have custom page numbering, this function returns + \c {page + 1}. + + \sa pageIndexForLabel() +*/ +QString QPdfDocument::pageLabel(int page) +{ + const unsigned long len = FPDF_GetPageLabel(d->doc, page, nullptr, 0); + if (len == 0) + return QString::number(page + 1); + QList<char16_t> buf(len); + QPdfMutexLocker lock; + FPDF_GetPageLabel(d->doc, page, buf.data(), len); + lock.unlock(); + return QString::fromUtf16(buf.constData()); +} + +/*! + Returns the index of the page that has the \a label, or \c -1 if not found. + + \sa pageLabel() + \since 6.6 +*/ +int QPdfDocument::pageIndexForLabel(const QString &label) +{ + for (int i = 0; i < d->pageCount; ++i) { + if (pageLabel(i) == label) + return i; + } + return -1; +} + +/*! Renders the \a page into a QImage of size \a imageSize according to the provided \a renderOptions. @@ -709,37 +917,21 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions result.fill(Qt::transparent); FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(result.width(), result.height(), FPDFBitmap_BGRA, result.bits(), result.bytesPerLine()); - int rotation = 0; - switch (renderOptions.rotation()) { - case QPdf::Rotate0: - rotation = 0; - break; - case QPdf::Rotate90: - rotation = 1; - break; - case QPdf::Rotate180: - rotation = 2; - break; - case QPdf::Rotate270: - rotation = 3; - break; - } - - const QPdf::RenderFlags renderFlags = renderOptions.renderFlags(); + const QPdfDocumentRenderOptions::RenderFlags renderFlags = renderOptions.renderFlags(); int flags = 0; - if (renderFlags & QPdf::RenderAnnotations) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Annotations) flags |= FPDF_ANNOT; - if (renderFlags & QPdf::RenderOptimizedForLcd) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::OptimizedForLcd) flags |= FPDF_LCD_TEXT; - if (renderFlags & QPdf::RenderGrayscale) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Grayscale) flags |= FPDF_GRAYSCALE; - if (renderFlags & QPdf::RenderForceHalftone) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ForceHalftone) flags |= FPDF_RENDER_FORCEHALFTONE; - if (renderFlags & QPdf::RenderTextAliased) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::TextAliased) flags |= FPDF_RENDER_NO_SMOOTHTEXT; - if (renderFlags & QPdf::RenderImageAliased) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ImageAliased) flags |= FPDF_RENDER_NO_SMOOTHIMAGE; - if (renderFlags & QPdf::RenderPathAliased) + if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::PathAliased) flags |= FPDF_RENDER_NO_SMOOTHPATH; if (renderOptions.scaledClipRect().isValid()) { @@ -752,7 +944,7 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions float y1 = clipRect.bottom(); float x2 = clipRect.right(); float y2 = clipRect.top(); - QSizeF origSize = pageSize(page); + QSizeF origSize = pagePointSize(page); QVector2D pageScale(1, 1); if (!renderOptions.scaledSize().isNull()) { pageScale = QVector2D(renderOptions.scaledSize().width() / float(origSize.width()), @@ -770,6 +962,7 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions qCDebug(qLcDoc) << "page" << page << "region" << renderOptions.scaledClipRect() << "size" << imageSize << "took" << timer.elapsed() << "ms"; } else { + const auto rotation = QPdfDocumentPrivate::toFPDFRotation(renderOptions.rotation()); FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags); qCDebug(qLcDoc) << "page" << page << "size" << imageSize << "took" << timer.elapsed() << "ms"; } @@ -788,11 +981,12 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) { const QPdfMutexLocker lock; FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page); - double pageHeight = FPDF_GetPageHeight(pdfPage); + const QPointF pageStart = d->mapViewToPage(pdfPage, start); + const QPointF pageEnd = d->mapViewToPage(pdfPage, end); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); - int startIndex = FPDFText_GetCharIndexAtPos(textPage, start.x(), pageHeight - start.y(), + int startIndex = FPDFText_GetCharIndexAtPos(textPage, pageStart.x(), pageStart.y(), CharacterHitTolerance, CharacterHitTolerance); - int endIndex = FPDFText_GetCharIndexAtPos(textPage, end.x(), pageHeight - end.y(), + int endIndex = FPDFText_GetCharIndexAtPos(textPage, pageEnd.x(), pageEnd.y(), CharacterHitTolerance, CharacterHitTolerance); QPdfSelection result; @@ -803,7 +997,7 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) // If the given end position is past the end of the line, i.e. if the right edge of the last character's // bounding box is closer to it than the left edge is, then extend the char range by one - QRectF endCharBox = d->getCharBox(textPage, pageHeight, endIndex); + QRectF endCharBox = d->getCharBox(pdfPage, textPage, endIndex); if (qAbs(endCharBox.right() - end.x()) < qAbs(endCharBox.x() - end.x())) ++endIndex; @@ -815,7 +1009,7 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) for (int i = 0; i < rectCount; ++i) { double l, r, b, t; FPDFText_GetRect(textPage, i, &l, &t, &r, &b); - QRectF rect(l, pageHeight - t, r - l, t - b); + const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b); if (hull.isNull()) hull = rect; else @@ -836,7 +1030,7 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) /*! Returns information about the text on the given \a page that can be found - beginning at the given \a startIndex with at most \l maxLength characters. + beginning at the given \a startIndex with at most \a maxLength characters. */ QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int maxLength) { @@ -845,7 +1039,6 @@ QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int ma return {}; const QPdfMutexLocker lock; FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page); - double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); int pageCount = FPDFText_CountChars(textPage); if (startIndex >= pageCount) @@ -856,11 +1049,11 @@ QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int ma QString text; if (maxLength > 0) { text = d->getText(textPage, startIndex, maxLength); - rectCount = FPDFText_CountRects(textPage, startIndex, text.length()); + rectCount = FPDFText_CountRects(textPage, startIndex, text.size()); for (int i = 0; i < rectCount; ++i) { double l, r, b, t; FPDFText_GetRect(textPage, i, &l, &t, &r, &b); - QRectF rect(l, pageHeight - t, r - l, t - b); + const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b); if (hull.isNull()) hull = rect; else @@ -869,14 +1062,14 @@ QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int ma } } if (bounds.isEmpty()) - hull = QRectF(d->getCharPosition(textPage, pageHeight, startIndex), QSizeF()); + hull = QRectF(d->getCharPosition(pdfPage, textPage, startIndex), QSizeF()); qCDebug(qLcDoc) << "on page" << page << "at index" << startIndex << "maxLength" << maxLength - << "got" << text.length() << "chars," << rectCount << "rects within" << hull; + << "got" << text.size() << "chars," << rectCount << "rects within" << hull; FPDFText_ClosePage(textPage); FPDF_ClosePage(pdfPage); - return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.length()); + return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.size()); } /*! @@ -886,7 +1079,6 @@ QPdfSelection QPdfDocument::getAllText(int page) { const QPdfMutexLocker lock; FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page); - double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); int count = FPDFText_CountChars(textPage); if (count < 1) @@ -898,7 +1090,7 @@ QPdfSelection QPdfDocument::getAllText(int page) for (int i = 0; i < rectCount; ++i) { double l, r, b, t; FPDFText_GetRect(textPage, i, &l, &t, &r, &b); - QRectF rect(l, pageHeight - t, r - l, t - b); + const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b); if (hull.isNull()) hull = rect; else @@ -915,4 +1107,5 @@ QPdfSelection QPdfDocument::getAllText(int page) QT_END_NAMESPACE +#include "qpdfdocument.moc" #include "moc_qpdfdocument.cpp" |