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