Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qpdfdocument.cpp
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qpdfdocument.h"
7
8#include "third_party/pdfium/public/fpdf_doc.h"
9#include "third_party/pdfium/public/fpdf_text.h"
10
11#include <QDateTime>
12#include <QDebug>
13#include <QElapsedTimer>
14#include <QFile>
15#include <QHash>
16#include <QLoggingCategory>
17#include <QMetaEnum>
18#include <QMutex>
19#include <QPixmap>
20#include <QVector2D>
21
22#include <QtCore/private/qtools_p.h>
23
25
26Q_GLOBAL_STATIC(QRecursiveMutex, pdfMutex)
27static int libraryRefCount;
28static const double CharacterHitTolerance = 16.0;
29Q_PDF_LOGGING_CATEGORY(qLcDoc, "qt.pdf.document")
30
31QPdfMutexLocker::QPdfMutexLocker()
32 : std::unique_lock<QRecursiveMutex>(*pdfMutex())
33{
34}
35
36class Q_PDF_EXPORT QPdfPageModel : public QAbstractListModel
37{
38 Q_OBJECT
39public:
40 QPdfPageModel(QPdfDocument *doc) : QAbstractListModel(doc)
41 {
42 m_roleNames = QAbstractItemModel::roleNames();
43 QMetaEnum rolesMetaEnum = doc->metaObject()->enumerator(doc->metaObject()->indexOfEnumerator("PageModelRole"));
44 for (int r = Qt::UserRole; r < int(QPdfDocument::PageModelRole::NRoles); ++r) {
45 auto name = QByteArray(rolesMetaEnum.valueToKey(r));
46 name[0] = QtMiscUtils::toAsciiLower(name[0]);
47 m_roleNames.insert(r, name);
48 }
49 connect(doc, &QPdfDocument::statusChanged, this, [this](QPdfDocument::Status s) {
50 if (s == QPdfDocument::Status::Loading)
51 beginResetModel();
52 else if (s == QPdfDocument::Status::Ready)
53 endResetModel();
54 });
55 }
56
57 QVariant data(const QModelIndex &index, int role) const override
58 {
59 if (!index.isValid())
60 return QVariant();
61
62 switch (QPdfDocument::PageModelRole(role)) {
63 case QPdfDocument::PageModelRole::Label:
64 return document()->pageLabel(index.row());
65 case QPdfDocument::PageModelRole::PointSize:
66 return document()->pagePointSize(index.row());
67 case QPdfDocument::PageModelRole::NRoles:
68 break;
69 }
70
71 switch (role) {
72 case Qt::DecorationRole:
73 return pageThumbnail(index.row());
74 case Qt::DisplayRole:
75 return document()->pageLabel(index.row());
76 }
77
78 return QVariant();
79 }
80
81 int rowCount(const QModelIndex & = QModelIndex()) const override { return document()->pageCount(); }
82
83 QHash<int, QByteArray> roleNames() const override { return m_roleNames; }
84
85private:
86 QPdfDocument *document() const { return static_cast<QPdfDocument *>(parent()); }
87 QPixmap pageThumbnail(int page) const
88 {
89 auto it = m_thumbnails.constFind(page);
90 if (it == m_thumbnails.constEnd()) {
91 auto doc = document();
92 auto size = doc->pagePointSize(page);
93 size.scale(128, 128, Qt::KeepAspectRatio);
94 // TODO use QPdfPageRenderer for threading?
95 auto image = document()->render(page, size.toSize());
96 QPixmap ret = QPixmap::fromImage(image);
97 m_thumbnails.insert(page, ret);
98 return ret;
99 }
100 return it.value();
101 }
102
103 QHash<int, QByteArray> m_roleNames;
104 mutable QHash<int, QPixmap> m_thumbnails;
105};
106
107QPdfDocumentPrivate::QPdfDocumentPrivate()
108 : avail(nullptr)
109 , doc(nullptr)
110 , loadComplete(false)
111 , status(QPdfDocument::Status::Null)
112 , lastError(QPdfDocument::Error::None)
113 , pageCount(0)
114{
115 asyncBuffer.setData(QByteArray());
116 asyncBuffer.open(QIODevice::ReadWrite);
117
118 const QPdfMutexLocker lock;
119
120 if (libraryRefCount == 0) {
121 QElapsedTimer timer;
122 timer.start();
123 FPDF_InitLibrary();
124 qCDebug(qLcDoc) << "FPDF_InitLibrary took" << timer.elapsed() << "ms";
125 }
126 ++libraryRefCount;
127
128 // FPDF_FILEACCESS setup
129 m_Param = this;
130 m_GetBlock = fpdf_GetBlock;
131
132 // FX_FILEAVAIL setup
133 FX_FILEAVAIL::version = 1;
134 IsDataAvail = fpdf_IsDataAvail;
135
136 // FX_DOWNLOADHINTS setup
137 FX_DOWNLOADHINTS::version = 1;
138 AddSegment = fpdf_AddSegment;
139}
140
141QPdfDocumentPrivate::~QPdfDocumentPrivate()
142{
143 q->close();
144
145 const QPdfMutexLocker lock;
146
147 if (!--libraryRefCount) {
148 qCDebug(qLcDoc) << "FPDF_DestroyLibrary";
149 FPDF_DestroyLibrary();
150 }
151}
152
153void QPdfDocumentPrivate::clear()
154{
155 QPdfMutexLocker lock;
156
157 if (doc)
158 FPDF_CloseDocument(doc);
159 doc = nullptr;
160
161 if (avail)
162 FPDFAvail_Destroy(avail);
163 avail = nullptr;
164 lock.unlock();
165
166 if (pageCount != 0) {
167 pageCount = 0;
168 emit q->pageCountChanged(pageCount);
169 emit q->pageModelChanged();
170 }
171
172 loadComplete = false;
173
174 asyncBuffer.close();
175 asyncBuffer.setData(QByteArray());
176 asyncBuffer.open(QIODevice::ReadWrite);
177
178 if (sequentialSourceDevice)
179 sequentialSourceDevice->disconnect(q);
180}
181
182void QPdfDocumentPrivate::updateLastError()
183{
184 if (doc) {
185 lastError = QPdfDocument::Error::None;
186 return;
187 }
188
189 QPdfMutexLocker lock;
190 const unsigned long error = FPDF_GetLastError();
191 lock.unlock();
192
193 switch (error) {
194 case FPDF_ERR_SUCCESS: lastError = QPdfDocument::Error::None; break;
195 case FPDF_ERR_UNKNOWN: lastError = QPdfDocument::Error::Unknown; break;
196 case FPDF_ERR_FILE: lastError = QPdfDocument::Error::FileNotFound; break;
197 case FPDF_ERR_FORMAT: lastError = QPdfDocument::Error::InvalidFileFormat; break;
198 case FPDF_ERR_PASSWORD: lastError = QPdfDocument::Error::IncorrectPassword; break;
199 case FPDF_ERR_SECURITY: lastError = QPdfDocument::Error::UnsupportedSecurityScheme; break;
200 default:
201 Q_UNREACHABLE();
202 }
203 if (lastError != QPdfDocument::Error::None)
204 qCDebug(qLcDoc) << "FPDF error" << error << "->" << lastError;
205}
206
207void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnership)
208{
209 if (transferDeviceOwnership)
210 ownDevice.reset(newDevice);
211 else
212 ownDevice.reset();
213
214 if (newDevice->isSequential()) {
215 sequentialSourceDevice = newDevice;
216 device = &asyncBuffer;
217 QNetworkReply *reply = qobject_cast<QNetworkReply*>(sequentialSourceDevice);
218
219 if (!reply) {
220 setStatus(QPdfDocument::Status::Error);
221 qWarning() << "QPdfDocument: Loading from sequential devices only supported with QNetworkAccessManager.";
222 return;
223 }
224
225 if (reply->isFinished() && reply->error() != QNetworkReply::NoError) {
226 setStatus(QPdfDocument::Status::Error);
227 return;
228 }
229
230 QObject::connect(reply, &QNetworkReply::finished, q, [this, reply](){
231 if (reply->error() != QNetworkReply::NoError || reply->bytesAvailable() == 0) {
232 this->setStatus(QPdfDocument::Status::Error);
233 }
234 });
235
236 if (reply->header(QNetworkRequest::ContentLengthHeader).isValid())
237 _q_tryLoadingWithSizeFromContentHeader();
238 else
239 QObject::connect(reply, SIGNAL(metaDataChanged()), q, SLOT(_q_tryLoadingWithSizeFromContentHeader()));
240 } else {
241 device = newDevice;
242 initiateAsyncLoadWithTotalSizeKnown(device->size());
243 if (!avail) {
244 setStatus(QPdfDocument::Status::Error);
245 return;
246 }
247
248 if (!doc)
249 tryLoadDocument();
250
251 if (!doc) {
252 updateLastError();
253 setStatus(QPdfDocument::Status::Error);
254 return;
255 }
256
257 QPdfMutexLocker lock;
258 const int newPageCount = FPDF_GetPageCount(doc);
259 lock.unlock();
260 if (newPageCount != pageCount) {
261 pageCount = newPageCount;
262 emit q->pageCountChanged(pageCount);
263 emit q->pageModelChanged();
264 }
265
266 // If it's a local file, and the first couple of pages are available,
267 // probably the whole document is available.
268 if (checkPageComplete(0) && (pageCount < 2 || checkPageComplete(1))) {
269 setStatus(QPdfDocument::Status::Ready);
270 } else {
271 updateLastError();
272 setStatus(QPdfDocument::Status::Error);
273 }
274 }
275}
276
277void QPdfDocumentPrivate::_q_tryLoadingWithSizeFromContentHeader()
278{
279 if (avail)
280 return;
281
282 const QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(sequentialSourceDevice);
283 if (!networkReply) {
284 setStatus(QPdfDocument::Status::Error);
285 return;
286 }
287
288 const QVariant contentLength = networkReply->header(QNetworkRequest::ContentLengthHeader);
289 if (!contentLength.isValid()) {
290 setStatus(QPdfDocument::Status::Error);
291 return;
292 }
293
294 QObject::connect(sequentialSourceDevice, SIGNAL(readyRead()), q, SLOT(_q_copyFromSequentialSourceDevice()));
295
296 initiateAsyncLoadWithTotalSizeKnown(contentLength.toULongLong());
297
298 if (sequentialSourceDevice->bytesAvailable())
299 _q_copyFromSequentialSourceDevice();
300}
301
302void QPdfDocumentPrivate::initiateAsyncLoadWithTotalSizeKnown(quint64 totalSize)
303{
304 // FPDF_FILEACCESS setup
305 m_FileLen = totalSize;
306
307 const QPdfMutexLocker lock;
308
309 avail = FPDFAvail_Create(this, this);
310}
311
312void QPdfDocumentPrivate::_q_copyFromSequentialSourceDevice()
313{
314 if (loadComplete)
315 return;
316
317 const QByteArray data = sequentialSourceDevice->read(sequentialSourceDevice->bytesAvailable());
318 if (data.isEmpty())
319 return;
320
321 asyncBuffer.seek(asyncBuffer.size());
322 asyncBuffer.write(data);
323
324 checkComplete();
325}
326
327void QPdfDocumentPrivate::tryLoadDocument()
328{
329 QPdfMutexLocker lock;
330 switch (FPDFAvail_IsDocAvail(avail, this)) {
331 case PDF_DATA_ERROR:
332 qCDebug(qLcDoc) << "error loading";
333 break;
334 case PDF_DATA_NOTAVAIL:
335 qCDebug(qLcDoc) << "data not yet available";
336 lastError = QPdfDocument::Error::DataNotYetAvailable;
337 break;
338 case PDF_DATA_AVAIL:
339 lastError = QPdfDocument::Error::None;
340 break;
341 }
342
343 Q_ASSERT(!doc);
344
345 doc = FPDFAvail_GetDocument(avail, password);
346 lock.unlock();
347
348 updateLastError();
349 if (lastError != QPdfDocument::Error::None)
350 setStatus(QPdfDocument::Status::Error);
351
352 if (lastError == QPdfDocument::Error::IncorrectPassword) {
353 FPDF_CloseDocument(doc);
354 doc = nullptr;
355
356 setStatus(QPdfDocument::Status::Error);
357 emit q->passwordRequired();
358 }
359}
360
361void QPdfDocumentPrivate::checkComplete()
362{
363 if (!avail || loadComplete)
364 return;
365
366 if (!doc)
367 tryLoadDocument();
368
369 if (!doc)
370 return;
371
372 loadComplete = true;
373
374 QPdfMutexLocker lock;
375
376 const int newPageCount = FPDF_GetPageCount(doc);
377 for (int i = 0; i < newPageCount; ++i) {
378 int result = PDF_DATA_NOTAVAIL;
379 while (result == PDF_DATA_NOTAVAIL) {
380 result = FPDFAvail_IsPageAvail(avail, i, this);
381 }
382
383 if (result == PDF_DATA_ERROR)
384 loadComplete = false;
385 }
386
387 lock.unlock();
388
389 if (loadComplete) {
390 if (newPageCount != pageCount) {
391 pageCount = newPageCount;
392 emit q->pageCountChanged(pageCount);
393 emit q->pageModelChanged();
394 }
395
396 setStatus(QPdfDocument::Status::Ready);
397 }
398}
399
400bool QPdfDocumentPrivate::checkPageComplete(int page)
401{
402 if (page < 0 || page >= pageCount)
403 return false;
404
405 if (loadComplete)
406 return true;
407
408 QPdfMutexLocker lock;
409 int result = PDF_DATA_NOTAVAIL;
410 while (result == PDF_DATA_NOTAVAIL)
411 result = FPDFAvail_IsPageAvail(avail, page, this);
412 lock.unlock();
413
414 if (result == PDF_DATA_ERROR)
415 updateLastError();
416
417 return (result != PDF_DATA_ERROR);
418}
419
420void QPdfDocumentPrivate::setStatus(QPdfDocument::Status documentStatus)
421{
422 if (status == documentStatus)
423 return;
424
425 status = documentStatus;
426 emit q->statusChanged(status);
427}
428
429FPDF_BOOL QPdfDocumentPrivate::fpdf_IsDataAvail(_FX_FILEAVAIL *pThis, size_t offset, size_t size)
430{
431 QPdfDocumentPrivate *d = static_cast<QPdfDocumentPrivate*>(pThis);
432 return offset + size <= static_cast<quint64>(d->device->size());
433}
434
435int QPdfDocumentPrivate::fpdf_GetBlock(void *param, unsigned long position, unsigned char *pBuf, unsigned long size)
436{
437 QPdfDocumentPrivate *d = static_cast<QPdfDocumentPrivate*>(reinterpret_cast<FPDF_FILEACCESS*>(param));
438 d->device->seek(position);
439 return qMax(qint64(0), d->device->read(reinterpret_cast<char *>(pBuf), size));
440
441}
442
443void QPdfDocumentPrivate::fpdf_AddSegment(_FX_DOWNLOADHINTS *pThis, size_t offset, size_t size)
444{
445 Q_UNUSED(pThis);
446 Q_UNUSED(offset);
447 Q_UNUSED(size);
448}
449
450QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int count) const
451{
452 QList<ushort> buf(count + 1);
453 // TODO is that enough space in case one unicode character is more than one in utf-16?
454 int len = FPDFText_GetText(textPage, startIndex, count, buf.data());
455 Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator
456 return QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.constData()), len - 1);
457}
458
459QPointF QPdfDocumentPrivate::getCharPosition(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const
460{
461 double x, y;
462 const int count = FPDFText_CountChars(textPage);
463 if (FPDFText_GetCharOrigin(textPage, qMin(count - 1, charIndex), &x, &y))
464 return mapPageToView(pdfPage, x, y);
465 return {};
466}
467
468QRectF QPdfDocumentPrivate::getCharBox(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const
469{
470 double l, t, r, b;
471 if (FPDFText_GetCharBox(textPage, charIndex, &l, &r, &b, &t))
472 return mapPageToView(pdfPage, l, t, r, b);
473 return {};
474}
475
476/*! \internal
477 Convert the point \a x , \a y to the usual 1x (pixels = points)
478 4th-quadrant "view" coordinate system relative to the top-left corner of
479 the rendered page. Some PDF files have internal transforms that make this
480 coordinate system different from "page coordinates", so we cannot just
481 subtract from page height to invert the y coordinates, in general.
482 */
483QPointF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double x, double y) const
484{
485 const auto pageHeight = FPDF_GetPageHeight(pdfPage);
486 const auto pageWidth = FPDF_GetPageWidth(pdfPage);
487 int rx, ry;
488 if (FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, x, y, &rx, &ry))
489 return QPointF(rx, ry);
490 return {};
491}
492
493/*! \internal
494 Convert the bounding box defined by \a left \a top \a right and \a bottom
495 to the usual 1x (pixels = points) 4th-quadrant "view" coordinate system
496 that we use for rendering things on top of the page image.
497 Some PDF files have internal transforms that make this coordinate
498 system different from "page coordinates", so we cannot just
499 subtract from page height to invert the y coordinates, in general.
500 */
501QRectF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double left, double top, double right, double bottom) const
502{
503 const auto pageHeight = FPDF_GetPageHeight(pdfPage);
504 const auto pageWidth = FPDF_GetPageWidth(pdfPage);
505 int xfmLeft, xfmTop, xfmRight, xfmBottom;
506 if ( FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, left, top, &xfmLeft, &xfmTop) &&
507 FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, right, bottom, &xfmRight, &xfmBottom) )
508 return QRectF(xfmLeft, xfmTop, xfmRight - xfmLeft, xfmBottom - xfmTop);
509 return {};
510}
511
512/*! \internal
513 Convert the point \a x , \a y \a from the usual 1x (pixels = points)
514 4th-quadrant "view" coordinate system relative to the top-left corner of
515 the rendered page, to "page coordinates" suited to the given \a pdfPage,
516 which may have arbitrary internal transforms.
517 */
518QPointF QPdfDocumentPrivate::mapViewToPage(FPDF_PAGE pdfPage, QPointF position) const
519{
520 const auto pageHeight = FPDF_GetPageHeight(pdfPage);
521 const auto pageWidth = FPDF_GetPageWidth(pdfPage);
522 double rx, ry;
523 if (FPDF_DeviceToPage(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, position.x(), position.y(), &rx, &ry))
524 return QPointF(rx, ry);
525 return {};
526}
527
528QPdfDocumentPrivate::TextPosition QPdfDocumentPrivate::hitTest(int page, QPointF position)
529{
530 const QPdfMutexLocker lock;
531
532 TextPosition result;
533 FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page);
534 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
535 const QPointF pagePos = mapViewToPage(pdfPage, position);
536 int hitIndex = FPDFText_GetCharIndexAtPos(textPage, pagePos.x(), pagePos.y(),
537 CharacterHitTolerance, CharacterHitTolerance);
538 if (hitIndex >= 0) {
539 QPointF charPos = getCharPosition(pdfPage, textPage, hitIndex);
540 if (!charPos.isNull()) {
541 QRectF charBox = getCharBox(pdfPage, textPage, hitIndex);
542 // If the given position is past the end of the line, i.e. if the right edge of the found character's
543 // bounding box is closer to it than the left edge is, we say that we "hit" the next character index after
544 if (qAbs(charBox.right() - position.x()) < qAbs(charPos.x() - position.x())) {
545 charPos.setX(charBox.right());
546 ++hitIndex;
547 }
548 qCDebug(qLcDoc) << "on page" << page << "@" << position << "got char position" << charPos << "index" << hitIndex;
549 result = { charPos, charBox.height(), hitIndex };
550 }
551 }
552
553 FPDFText_ClosePage(textPage);
554 FPDF_ClosePage(pdfPage);
555
556 return result;
557}
558
559/*!
560 \class QPdfDocument
561 \since 5.10
562 \inmodule QtPdf
563
564 \brief The QPdfDocument class loads a PDF document and renders pages from it.
565*/
566
567/*!
568 Constructs a new document with parent object \a parent.
569*/
570QPdfDocument::QPdfDocument(QObject *parent)
571 : QObject(parent)
572 , d(new QPdfDocumentPrivate)
573{
574 d->q = this;
575}
576
577/*!
578 Destroys the document.
579*/
580QPdfDocument::~QPdfDocument()
581{
582}
583
584/*!
585 Loads the document contents from \a fileName.
586*/
587QPdfDocument::Error QPdfDocument::load(const QString &fileName)
588{
589 qCDebug(qLcDoc) << "loading" << fileName;
590
591 close();
592
593 d->setStatus(QPdfDocument::Status::Loading);
594
595 std::unique_ptr<QFile> f(new QFile(fileName));
596 if (!f->open(QIODevice::ReadOnly)) {
597 d->lastError = Error::FileNotFound;
598 d->setStatus(QPdfDocument::Status::Error);
599 } else {
600 d->load(f.release(), /*transfer ownership*/true);
601 }
602 return d->lastError;
603}
604
605/*! \internal
606 Returns the filename of the document that has been opened,
607 or an empty string if no document is open.
608*/
609QString QPdfDocument::fileName() const
610{
611 const QFile *f = qobject_cast<QFile *>(d->device.data());
612 if (f)
613 return f->fileName();
614 return QString();
615}
616
617/*!
618 \enum QPdfDocument::Status
619
620 This enum describes the current status of the document.
621
622 \value Null The initial status after the document has been created or after it has been closed.
623 \value Loading The status after load() has been called and before the document is fully loaded.
624 \value Ready The status when the document is fully loaded and its data can be accessed.
625 \value Unloading The status after close() has been called on an open document.
626 At this point the document is still valid and all its data can be accessed.
627 \value Error The status after Loading, if loading has failed.
628
629 \sa QPdfDocument::status()
630*/
631
632/*!
633 \property QPdfDocument::status
634
635 This property holds the current status of the document.
636*/
637QPdfDocument::Status QPdfDocument::status() const
638{
639 return d->status;
640}
641
642/*!
643 Loads the document contents from \a device.
644*/
645void QPdfDocument::load(QIODevice *device)
646{
647 close();
648
649 d->setStatus(QPdfDocument::Status::Loading);
650
651 d->load(device, /*transfer ownership*/false);
652}
653
654/*!
655 \property QPdfDocument::password
656
657 This property holds the document password.
658
659 If the document is protected by a password, the user must provide it, and
660 the application must set this property. Otherwise, it's not needed.
661*/
662void QPdfDocument::setPassword(const QString &password)
663{
664 const QByteArray newPassword = password.toUtf8();
665
666 if (d->password == newPassword)
667 return;
668
669 d->password = newPassword;
670 emit passwordChanged();
671}
672
673QString QPdfDocument::password() const
674{
675 return QString::fromUtf8(d->password);
676}
677
678/*!
679 \enum QPdfDocument::MetaDataField
680
681 This enum describes the available fields of meta data.
682
683 \value Title The document's title as QString.
684 \value Author The name of the person who created the document as QString.
685 \value Subject The subject of the document as QString.
686 \value Keywords Keywords associated with the document as QString.
687 \value Creator If the document was converted to PDF from another format,
688 the name of the conforming product that created the original document
689 from which it was converted as QString.
690 \value Producer If the document was converted to PDF from another format,
691 the name of the conforming product that converted it to PDF as QString.
692 \value CreationDate The date and time the document was created as QDateTime.
693 \value ModificationDate The date and time the document was most recently modified as QDateTime.
694
695 \sa QPdfDocument::metaData()
696*/
697
698/*!
699 Returns the meta data of the document for the given \a field.
700*/
701QVariant QPdfDocument::metaData(MetaDataField field) const
702{
703 if (!d->doc)
704 return QString();
705
706 static QMetaEnum fieldsMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("MetaDataField"));
707 QByteArray fieldName;
708 switch (field) {
709 case MetaDataField::ModificationDate:
710 fieldName = "ModDate";
711 break;
712 default:
713 fieldName = QByteArray(fieldsMetaEnum.valueToKey(int(field)));
714 break;
715 }
716
717 QPdfMutexLocker lock;
718 const unsigned long len = FPDF_GetMetaText(d->doc, fieldName.constData(), nullptr, 0);
719
720 QList<ushort> buf(len);
721 FPDF_GetMetaText(d->doc, fieldName.constData(), buf.data(), buf.size());
722 lock.unlock();
723
724 QString text = QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.data()));
725
726 switch (field) {
727 case MetaDataField::Title: // fall through
728 case MetaDataField::Subject:
729 case MetaDataField::Author:
730 case MetaDataField::Keywords:
731 case MetaDataField::Producer:
732 case MetaDataField::Creator:
733 return text;
734 case MetaDataField::CreationDate: // fall through
735 case MetaDataField::ModificationDate:
736 // convert a "D:YYYYMMDDHHmmSSOHH'mm'" into "YYYY-MM-DDTHH:mm:ss+HH:mm"
737 if (text.startsWith(QLatin1String("D:")))
738 text = text.mid(2);
739 text.insert(4, QLatin1Char('-'));
740 text.insert(7, QLatin1Char('-'));
741 text.insert(10, QLatin1Char('T'));
742 text.insert(13, QLatin1Char(':'));
743 text.insert(16, QLatin1Char(':'));
744 text.replace(QLatin1Char('\''), QLatin1Char(':'));
745 if (text.endsWith(QLatin1Char(':')))
746 text.chop(1);
747
748 return QDateTime::fromString(text, Qt::ISODate);
749 }
750
751 return QVariant();
752}
753
754/*!
755 \enum QPdfDocument::Error
756
757 This enum describes the error while attempting the last operation on the document.
758
759 \value None No error occurred.
760 \value Unknown Unknown type of error.
761 \value DataNotYetAvailable The document is still loading, it's too early to attempt the operation.
762 \value FileNotFound The file given to load() was not found.
763 \value InvalidFileFormat The file given to load() is not a valid PDF file.
764 \value IncorrectPassword The password given to setPassword() is not correct for this file.
765 \value UnsupportedSecurityScheme QPdfDocument is not able to unlock this kind of PDF file.
766
767 \sa QPdfDocument::error()
768*/
769
770/*!
771 Returns the type of error if \l status is \c Error, or \c NoError if there
772 is no error.
773*/
774QPdfDocument::Error QPdfDocument::error() const
775{
776 return d->lastError;
777}
778
779/*!
780 Closes the document.
781*/
782void QPdfDocument::close()
783{
784 if (!d->doc)
785 return;
786
787 d->setStatus(Status::Unloading);
788
789 d->clear();
790
791 if (!d->password.isEmpty()) {
792 d->password.clear();
793 emit passwordChanged();
794 }
795
796 d->setStatus(Status::Null);
797}
798
799/*!
800 \property QPdfDocument::pageCount
801
802 This property holds the number of pages in the loaded document or \c 0 if
803 no document is loaded.
804*/
805int QPdfDocument::pageCount() const
806{
807 return d->pageCount;
808}
809
810/*!
811 Returns the size of page \a page in points (1/72 of an inch).
812*/
813QSizeF QPdfDocument::pagePointSize(int page) const
814{
815 QSizeF result;
816 if (!d->doc || !d->checkPageComplete(page))
817 return result;
818
819 const QPdfMutexLocker lock;
820
821 FPDF_GetPageSizeByIndex(d->doc, page, &result.rwidth(), &result.rheight());
822 return result;
823}
824
825/*!
826 \enum QPdfDocument::PageModelRole
827
828 Roles in pageModel().
829
830 \value Label The page number to be used for display purposes (QString).
831 \value PointSize The page size in points (1/72 of an inch) (QSizeF).
832 \omitvalue NRoles
833*/
834
835/*!
836 \property QPdfDocument::pageModel
837
838 This property holds an instance of QAbstractListModel to provide
839 page-specific metadata, containing one row for each page in the document.
840
841 \sa QPdfDocument::PageModelRole
842*/
843QAbstractListModel *QPdfDocument::pageModel()
844{
845 if (!d->pageModel)
846 d->pageModel = new QPdfPageModel(this);
847 return d->pageModel;
848}
849
850/*!
851 Returns the \a page number to be used for display purposes.
852
853 For example, a document may have multiple sections with different numbering.
854 Perhaps the preface uses roman numerals, the body starts on page 1, and the
855 appendix starts at A1. Whenever a PDF viewer shows a page number, to avoid
856 confusing the user it should be the same "number" as is printed on the
857 corner of the page, rather than the zero-based page index that we use in
858 APIs (assuming the document author has made the page labels match the
859 printed numbers).
860
861 If the document does not have custom page numbering, this function returns
862 \c {page + 1}.
863
864 \sa pageIndexForLabel()
865*/
866QString QPdfDocument::pageLabel(int page)
867{
868 const unsigned long len = FPDF_GetPageLabel(d->doc, page, nullptr, 0);
869 if (len == 0)
870 return QString::number(page + 1);
871 QList<char16_t> buf(len);
872 QPdfMutexLocker lock;
873 FPDF_GetPageLabel(d->doc, page, buf.data(), len);
874 lock.unlock();
875 return QString::fromUtf16(buf.constData());
876}
877
878/*!
879 Returns the index of the page that has the \a label, or \c -1 if not found.
880
881 \sa pageLabel()
882 \since 6.6
883*/
884int QPdfDocument::pageIndexForLabel(const QString &label)
885{
886 for (int i = 0; i < d->pageCount; ++i) {
887 if (pageLabel(i) == label)
888 return i;
889 }
890 return -1;
891}
892
893/*!
894 Renders the \a page into a QImage of size \a imageSize according to the
895 provided \a renderOptions.
896
897 Returns the rendered page or an empty image in case of an error.
898
899 Note: If the \a imageSize does not match the aspect ratio of the page in the
900 PDF document, the page is rendered scaled, so that it covers the
901 complete \a imageSize.
902*/
903QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions)
904{
905 if (!d->doc || !d->checkPageComplete(page))
906 return QImage();
907
908 const QPdfMutexLocker lock;
909
910 QElapsedTimer timer;
911 if (Q_UNLIKELY(qLcDoc().isDebugEnabled()))
912 timer.start();
913 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
914 if (!pdfPage)
915 return QImage();
916
917 QImage result(imageSize, QImage::Format_ARGB32);
918 result.fill(Qt::transparent);
919 FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(result.width(), result.height(), FPDFBitmap_BGRA, result.bits(), result.bytesPerLine());
920
921 const QPdfDocumentRenderOptions::RenderFlags renderFlags = renderOptions.renderFlags();
922 int flags = 0;
923 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Annotations)
924 flags |= FPDF_ANNOT;
925 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::OptimizedForLcd)
926 flags |= FPDF_LCD_TEXT;
927 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Grayscale)
928 flags |= FPDF_GRAYSCALE;
929 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ForceHalftone)
930 flags |= FPDF_RENDER_FORCEHALFTONE;
931 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::TextAliased)
932 flags |= FPDF_RENDER_NO_SMOOTHTEXT;
933 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ImageAliased)
934 flags |= FPDF_RENDER_NO_SMOOTHIMAGE;
935 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::PathAliased)
936 flags |= FPDF_RENDER_NO_SMOOTHPATH;
937
938 if (renderOptions.scaledClipRect().isValid()) {
939 const QRect &clipRect = renderOptions.scaledClipRect();
940
941 // TODO take rotation into account, like cpdf_page.cpp lines 145-178
942 float x0 = clipRect.left();
943 float y0 = clipRect.top();
944 float x1 = clipRect.left();
945 float y1 = clipRect.bottom();
946 float x2 = clipRect.right();
947 float y2 = clipRect.top();
948 QSizeF origSize = pagePointSize(page);
949 QVector2D pageScale(1, 1);
950 if (!renderOptions.scaledSize().isNull()) {
951 pageScale = QVector2D(renderOptions.scaledSize().width() / float(origSize.width()),
952 renderOptions.scaledSize().height() / float(origSize.height()));
953 }
954 FS_MATRIX matrix {(x2 - x0) / result.width() * pageScale.x(),
955 (y2 - y0) / result.width() * pageScale.x(),
956 (x1 - x0) / result.height() * pageScale.y(),
957 (y1 - y0) / result.height() * pageScale.y(), -x0, -y0};
958
959 FS_RECTF clipRectF { 0, 0, float(imageSize.width()), float(imageSize.height()) };
960
961 FPDF_RenderPageBitmapWithMatrix(bitmap, pdfPage, &matrix, &clipRectF, flags);
962 qCDebug(qLcDoc) << "matrix" << matrix.a << matrix.b << matrix.c << matrix.d << matrix.e << matrix.f;
963 qCDebug(qLcDoc) << "page" << page << "region" << renderOptions.scaledClipRect()
964 << "size" << imageSize << "took" << timer.elapsed() << "ms";
965 } else {
966 const auto rotation = QPdfDocumentPrivate::toFPDFRotation(renderOptions.rotation());
967 FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags);
968 qCDebug(qLcDoc) << "page" << page << "size" << imageSize << "took" << timer.elapsed() << "ms";
969 }
970
971 FPDFBitmap_Destroy(bitmap);
972
973 FPDF_ClosePage(pdfPage);
974 return result;
975}
976
977/*!
978 Returns information about the text on the given \a page that can be found
979 between the given \a start and \a end points, if any.
980*/
981QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end)
982{
983 const QPdfMutexLocker lock;
984 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
985 const QPointF pageStart = d->mapViewToPage(pdfPage, start);
986 const QPointF pageEnd = d->mapViewToPage(pdfPage, end);
987 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
988 int startIndex = FPDFText_GetCharIndexAtPos(textPage, pageStart.x(), pageStart.y(),
989 CharacterHitTolerance, CharacterHitTolerance);
990 int endIndex = FPDFText_GetCharIndexAtPos(textPage, pageEnd.x(), pageEnd.y(),
991 CharacterHitTolerance, CharacterHitTolerance);
992
993 QPdfSelection result;
994
995 if (startIndex >= 0 && endIndex != startIndex) {
996 if (startIndex > endIndex)
997 qSwap(startIndex, endIndex);
998
999 // If the given end position is past the end of the line, i.e. if the right edge of the last character's
1000 // bounding box is closer to it than the left edge is, then extend the char range by one
1001 QRectF endCharBox = d->getCharBox(pdfPage, textPage, endIndex);
1002 if (qAbs(endCharBox.right() - end.x()) < qAbs(endCharBox.x() - end.x()))
1003 ++endIndex;
1004
1005 int count = endIndex - startIndex;
1006 QString text = d->getText(textPage, startIndex, count);
1007 QList<QPolygonF> bounds;
1008 QRectF hull;
1009 int rectCount = FPDFText_CountRects(textPage, startIndex, endIndex - startIndex);
1010 for (int i = 0; i < rectCount; ++i) {
1011 double l, r, b, t;
1012 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1013 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1014 if (hull.isNull())
1015 hull = rect;
1016 else
1017 hull = hull.united(rect);
1018 bounds << QPolygonF(rect);
1019 }
1020 qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text;
1021 result = QPdfSelection(text, bounds, hull, startIndex, endIndex);
1022 } else {
1023 qCDebug(qLcDoc) << page << start << "->" << end << "nothing found";
1024 }
1025
1026 FPDFText_ClosePage(textPage);
1027 FPDF_ClosePage(pdfPage);
1028
1029 return result;
1030}
1031
1032/*!
1033 Returns information about the text on the given \a page that can be found
1034 beginning at the given \a startIndex with at most \a maxLength characters.
1035*/
1036QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int maxLength)
1037{
1038
1039 if (page < 0 || startIndex < 0 || maxLength < 0)
1040 return {};
1041 const QPdfMutexLocker lock;
1042 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
1043 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
1044 int pageCount = FPDFText_CountChars(textPage);
1045 if (startIndex >= pageCount)
1046 return QPdfSelection();
1047 QList<QPolygonF> bounds;
1048 QRectF hull;
1049 int rectCount = 0;
1050 QString text;
1051 if (maxLength > 0) {
1052 text = d->getText(textPage, startIndex, maxLength);
1053 rectCount = FPDFText_CountRects(textPage, startIndex, text.size());
1054 for (int i = 0; i < rectCount; ++i) {
1055 double l, r, b, t;
1056 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1057 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1058 if (hull.isNull())
1059 hull = rect;
1060 else
1061 hull = hull.united(rect);
1062 bounds << QPolygonF(rect);
1063 }
1064 }
1065 if (bounds.isEmpty())
1066 hull = QRectF(d->getCharPosition(pdfPage, textPage, startIndex), QSizeF());
1067 qCDebug(qLcDoc) << "on page" << page << "at index" << startIndex << "maxLength" << maxLength
1068 << "got" << text.size() << "chars," << rectCount << "rects within" << hull;
1069
1070 FPDFText_ClosePage(textPage);
1071 FPDF_ClosePage(pdfPage);
1072
1073 return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.size());
1074}
1075
1076/*!
1077 Returns all the text and its bounds on the given \a page.
1078*/
1079QPdfSelection QPdfDocument::getAllText(int page)
1080{
1081 const QPdfMutexLocker lock;
1082 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
1083 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
1084 int count = FPDFText_CountChars(textPage);
1085 if (count < 1)
1086 return QPdfSelection();
1087 QString text = d->getText(textPage, 0, count);
1088 QList<QPolygonF> bounds;
1089 QRectF hull;
1090 int rectCount = FPDFText_CountRects(textPage, 0, count);
1091 for (int i = 0; i < rectCount; ++i) {
1092 double l, r, b, t;
1093 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1094 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1095 if (hull.isNull())
1096 hull = rect;
1097 else
1098 hull = hull.united(rect);
1099 bounds << QPolygonF(rect);
1100 }
1101 qCDebug(qLcDoc) << "on page" << page << "got" << count << "chars," << rectCount << "rects within" << hull;
1102
1103 FPDFText_ClosePage(textPage);
1104 FPDF_ClosePage(pdfPage);
1105
1106 return QPdfSelection(text, bounds, hull, 0, count);
1107}
1108
1109QT_END_NAMESPACE
1110
1111#include "qpdfdocument.moc"
1112#include "moc_qpdfdocument.cpp"
static const double CharacterHitTolerance
#define Q_PDF_LOGGING_CATEGORY(name,...)