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#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
895QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions)
896{
897 return render(page, imageSize, renderOptions, QImage::Format_ARGB32, Qt::transparent);
898}
899#endif
900
901/*!
902 Renders the \a page into a QImage of size \a imageSize with the given \a format
903 according to the provided \a renderOptions.
904
905 The \a format is \l {QImage::Format_ARGB32}{ARGB} by default.
906 Before rendering, the image is filled with the given \a fillColor, which is
907 \l {Qt::transparent}{transparent} by default. Thus the default result is
908 suitable for compositing onto any background; but you can specify a
909 different background color to get an image suitable for direct rendering.
910 Some PDF pages may include their own background: if the rendering is fully
911 opaque, it will cover up the background color.
912
913 Returns the rendered page or an empty image in case of an error.
914
915 Note: If the \a imageSize does not match the aspect ratio of the page in the
916 PDF document, the page is rendered scaled, so that it covers the
917 complete \a imageSize.
918*/
919QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions,
920 QImage::Format format, const QColor &fillColor)
921{
922 if (!d->doc || !d->checkPageComplete(page))
923 return QImage();
924
925 const QPdfMutexLocker lock;
926
927 QElapsedTimer timer;
928 if (Q_UNLIKELY(qLcDoc().isDebugEnabled()))
929 timer.start();
930 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
931 if (!pdfPage)
932 return QImage();
933
934 QImage result(imageSize, format);
935 result.fill(fillColor);
936 FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(result.width(), result.height(), FPDFBitmap_BGRA, result.bits(), result.bytesPerLine());
937
938 const QPdfDocumentRenderOptions::RenderFlags renderFlags = renderOptions.renderFlags();
939 int flags = 0;
940 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Annotations)
941 flags |= FPDF_ANNOT;
942 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::OptimizedForLcd)
943 flags |= FPDF_LCD_TEXT;
944 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Grayscale)
945 flags |= FPDF_GRAYSCALE;
946 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ForceHalftone)
947 flags |= FPDF_RENDER_FORCEHALFTONE;
948 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::TextAliased)
949 flags |= FPDF_RENDER_NO_SMOOTHTEXT;
950 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ImageAliased)
951 flags |= FPDF_RENDER_NO_SMOOTHIMAGE;
952 if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::PathAliased)
953 flags |= FPDF_RENDER_NO_SMOOTHPATH;
954
955 if (renderOptions.scaledClipRect().isValid()) {
956 const QRect &clipRect = renderOptions.scaledClipRect();
957
958 // TODO take rotation into account, like cpdf_page.cpp lines 145-178
959 float x0 = clipRect.left();
960 float y0 = clipRect.top();
961 float x1 = clipRect.left();
962 float y1 = clipRect.bottom();
963 float x2 = clipRect.right();
964 float y2 = clipRect.top();
965 QSizeF origSize = pagePointSize(page);
966 QVector2D pageScale(1, 1);
967 if (!renderOptions.scaledSize().isNull()) {
968 pageScale = QVector2D(renderOptions.scaledSize().width() / float(origSize.width()),
969 renderOptions.scaledSize().height() / float(origSize.height()));
970 }
971 FS_MATRIX matrix {(x2 - x0) / result.width() * pageScale.x(),
972 (y2 - y0) / result.width() * pageScale.x(),
973 (x1 - x0) / result.height() * pageScale.y(),
974 (y1 - y0) / result.height() * pageScale.y(), -x0, -y0};
975
976 FS_RECTF clipRectF { 0, 0, float(imageSize.width()), float(imageSize.height()) };
977
978 FPDF_RenderPageBitmapWithMatrix(bitmap, pdfPage, &matrix, &clipRectF, flags);
979 qCDebug(qLcDoc) << "matrix" << matrix.a << matrix.b << matrix.c << matrix.d << matrix.e << matrix.f;
980 qCDebug(qLcDoc) << "page" << page << "region" << renderOptions.scaledClipRect()
981 << "size" << imageSize << "took" << timer.elapsed() << "ms";
982 } else {
983 const auto rotation = QPdfDocumentPrivate::toFPDFRotation(renderOptions.rotation());
984 FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags);
985 qCDebug(qLcDoc) << "page" << page << "size" << imageSize << "took" << timer.elapsed() << "ms";
986 }
987
988 FPDFBitmap_Destroy(bitmap);
989
990 FPDF_ClosePage(pdfPage);
991 return result;
992}
993
994/*!
995 Returns information about the text on the given \a page that can be found
996 between the given \a start and \a end points, if any.
997*/
998QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end)
999{
1000 const QPdfMutexLocker lock;
1001 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
1002 const QPointF pageStart = d->mapViewToPage(pdfPage, start);
1003 const QPointF pageEnd = d->mapViewToPage(pdfPage, end);
1004 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
1005 int startIndex = FPDFText_GetCharIndexAtPos(textPage, pageStart.x(), pageStart.y(),
1006 CharacterHitTolerance, CharacterHitTolerance);
1007 int endIndex = FPDFText_GetCharIndexAtPos(textPage, pageEnd.x(), pageEnd.y(),
1008 CharacterHitTolerance, CharacterHitTolerance);
1009
1010 QPdfSelection result;
1011
1012 if (startIndex >= 0 && endIndex != startIndex) {
1013 if (startIndex > endIndex)
1014 qSwap(startIndex, endIndex);
1015
1016 // If the given end position is past the end of the line, i.e. if the right edge of the last character's
1017 // bounding box is closer to it than the left edge is, then extend the char range by one
1018 QRectF endCharBox = d->getCharBox(pdfPage, textPage, endIndex);
1019 if (qAbs(endCharBox.right() - end.x()) < qAbs(endCharBox.x() - end.x()))
1020 ++endIndex;
1021
1022 int count = endIndex - startIndex;
1023 QString text = d->getText(textPage, startIndex, count);
1024 QList<QPolygonF> bounds;
1025 QRectF hull;
1026 int rectCount = FPDFText_CountRects(textPage, startIndex, endIndex - startIndex);
1027 for (int i = 0; i < rectCount; ++i) {
1028 double l, r, b, t;
1029 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1030 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1031 if (hull.isNull())
1032 hull = rect;
1033 else
1034 hull = hull.united(rect);
1035 bounds << QPolygonF(rect);
1036 }
1037 qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text;
1038 result = QPdfSelection(text, bounds, hull, startIndex, endIndex);
1039 } else {
1040 qCDebug(qLcDoc) << page << start << "->" << end << "nothing found";
1041 }
1042
1043 FPDFText_ClosePage(textPage);
1044 FPDF_ClosePage(pdfPage);
1045
1046 return result;
1047}
1048
1049/*!
1050 Returns information about the text on the given \a page that can be found
1051 beginning at the given \a startIndex with at most \a maxLength characters.
1052*/
1053QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int maxLength)
1054{
1055
1056 if (page < 0 || startIndex < 0 || maxLength < 0)
1057 return {};
1058 const QPdfMutexLocker lock;
1059 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
1060 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
1061 int pageCount = FPDFText_CountChars(textPage);
1062 if (startIndex >= pageCount)
1063 return QPdfSelection();
1064 QList<QPolygonF> bounds;
1065 QRectF hull;
1066 int rectCount = 0;
1067 QString text;
1068 if (maxLength > 0) {
1069 text = d->getText(textPage, startIndex, maxLength);
1070 rectCount = FPDFText_CountRects(textPage, startIndex, text.size());
1071 for (int i = 0; i < rectCount; ++i) {
1072 double l, r, b, t;
1073 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1074 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1075 if (hull.isNull())
1076 hull = rect;
1077 else
1078 hull = hull.united(rect);
1079 bounds << QPolygonF(rect);
1080 }
1081 }
1082 if (bounds.isEmpty())
1083 hull = QRectF(d->getCharPosition(pdfPage, textPage, startIndex), QSizeF());
1084 qCDebug(qLcDoc) << "on page" << page << "at index" << startIndex << "maxLength" << maxLength
1085 << "got" << text.size() << "chars," << rectCount << "rects within" << hull;
1086
1087 FPDFText_ClosePage(textPage);
1088 FPDF_ClosePage(pdfPage);
1089
1090 return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.size());
1091}
1092
1093/*!
1094 Returns all the text and its bounds on the given \a page.
1095*/
1096QPdfSelection QPdfDocument::getAllText(int page)
1097{
1098 const QPdfMutexLocker lock;
1099 FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
1100 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
1101 int count = FPDFText_CountChars(textPage);
1102 if (count < 1)
1103 return QPdfSelection();
1104 QString text = d->getText(textPage, 0, count);
1105 QList<QPolygonF> bounds;
1106 QRectF hull;
1107 int rectCount = FPDFText_CountRects(textPage, 0, count);
1108 for (int i = 0; i < rectCount; ++i) {
1109 double l, r, b, t;
1110 FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
1111 const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
1112 if (hull.isNull())
1113 hull = rect;
1114 else
1115 hull = hull.united(rect);
1116 bounds << QPolygonF(rect);
1117 }
1118 qCDebug(qLcDoc) << "on page" << page << "got" << count << "chars," << rectCount << "rects within" << hull;
1119
1120 FPDFText_ClosePage(textPage);
1121 FPDF_ClosePage(pdfPage);
1122
1123 return QPdfSelection(text, bounds, hull, 0, count);
1124}
1125
1126QT_END_NAMESPACE
1127
1128#include "qpdfdocument.moc"
1129#include "moc_qpdfdocument.cpp"
Combined button and popup list for selecting options.
static const double CharacterHitTolerance
#define Q_PDF_LOGGING_CATEGORY(name,...)