Qt 6.x
The Qt SDK
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Pages
qpdfview.cpp
Go to the documentation of this file.
1// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
2// Copyright (C) 2022 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qpdfview.h"
6#include "qpdfview_p.h"
7
8#include "qpdfpagerenderer.h"
9
10#include <QGuiApplication>
11#include <QLoggingCategory>
12#include <QPainter>
13#include <QPaintEvent>
14#include <QPdfDocument>
15#include <QPdfPageNavigator>
16#include <QPdfSearchModel>
17#include <QScreen>
18#include <QScrollBar>
19
21
22Q_LOGGING_CATEGORY(qLcLink, "qt.pdf.links")
23//#define DEBUG_LINKS
24
25static const QColor SearchResultHighlight("#80B0C4DE");
26static const QColor CurrentSearchResultHighlight(Qt::cyan);
27static const int CurrentSearchResultWidth(2);
28
29QPdfViewPrivate::QPdfViewPrivate(QPdfView *q)
30 : q_ptr(q)
31 , m_document(nullptr)
32 , m_pageNavigator(nullptr)
33 , m_pageRenderer(nullptr)
34 , m_pageMode(QPdfView::PageMode::SinglePage)
35 , m_zoomMode(QPdfView::ZoomMode::Custom)
36 , m_zoomFactor(1.0)
37 , m_pageSpacing(3)
38 , m_documentMargins(6, 6, 6, 6)
39 , m_blockPageScrolling(false)
40 , m_pageCacheLimit(20)
41 , m_screenResolution(QGuiApplication::primaryScreen()->logicalDotsPerInch() / 72.0)
42{
43}
44
45void QPdfViewPrivate::init()
46{
47 Q_Q(QPdfView);
48
49 m_pageNavigator = new QPdfPageNavigator(q);
50 m_pageRenderer = new QPdfPageRenderer(q);
51 m_pageRenderer->setRenderMode(QPdfPageRenderer::RenderMode::MultiThreaded);
52}
53
54void QPdfViewPrivate::documentStatusChanged()
55{
56 updateDocumentLayout();
57 invalidatePageCache();
58}
59
60void QPdfViewPrivate::currentPageChanged(int currentPage)
61{
62 Q_Q(QPdfView);
63
64 if (m_blockPageScrolling)
65 return;
66
67 q->verticalScrollBar()->setValue(yPositionForPage(currentPage));
68
69 if (m_pageMode == QPdfView::PageMode::SinglePage)
70 invalidateDocumentLayout();
71}
72
73void QPdfViewPrivate::calculateViewport()
74{
75 Q_Q(QPdfView);
76
77 const int x = q->horizontalScrollBar()->value();
78 const int y = q->verticalScrollBar()->value();
79 const int width = q->viewport()->width();
80 const int height = q->viewport()->height();
81
82 setViewport(QRect(x, y, width, height));
83}
84
85void QPdfViewPrivate::setViewport(QRect viewport)
86{
87 if (m_viewport == viewport)
88 return;
89
90 const QSize oldSize = m_viewport.size();
91
92 m_viewport = viewport;
93
94 if (oldSize != m_viewport.size()) {
95 updateDocumentLayout();
96
97 if (m_zoomMode != QPdfView::ZoomMode::Custom) {
98 invalidatePageCache();
99 }
100 }
101
102 if (m_pageMode == QPdfView::PageMode::MultiPage) {
103 // An imaginary, 2px height line at the upper half of the viewport, which is used to
104 // determine which page is currently located there -> we propagate that as 'current' page
105 // to the QPdfPageNavigator object
106 const QRect currentPageLine(m_viewport.x(), m_viewport.y() + m_viewport.height() * 0.4, m_viewport.width(), 2);
107
108 int currentPage = 0;
109 for (auto it = m_documentLayout.pageGeometries.cbegin(); it != m_documentLayout.pageGeometries.cend(); ++it) {
110 const QRect pageGeometry = it.value();
111 if (pageGeometry.intersects(currentPageLine)) {
112 currentPage = it.key();
113 break;
114 }
115 }
116
117 if (currentPage != m_pageNavigator->currentPage()) {
118 m_blockPageScrolling = true;
119 // ΤODO give location on the page
120 m_pageNavigator->jump(currentPage, {}, m_zoomFactor);
121 m_blockPageScrolling = false;
122 }
123 }
124}
125
126void QPdfViewPrivate::updateScrollBars()
127{
128 Q_Q(QPdfView);
129
130 const QSize p = q->viewport()->size();
131 const QSize v = m_documentLayout.documentSize;
132
133 q->horizontalScrollBar()->setRange(0, v.width() - p.width());
134 q->horizontalScrollBar()->setPageStep(p.width());
135 q->verticalScrollBar()->setRange(0, v.height() - p.height());
136 q->verticalScrollBar()->setPageStep(p.height());
137}
138
139void QPdfViewPrivate::pageRendered(int pageNumber, QSize imageSize, const QImage &image, quint64 requestId)
140{
141 Q_Q(QPdfView);
142
143 Q_UNUSED(imageSize);
144 Q_UNUSED(requestId);
145
146 if (!m_cachedPagesLRU.contains(pageNumber)) {
147 if (m_cachedPagesLRU.size() > m_pageCacheLimit)
148 m_pageCache.remove(m_cachedPagesLRU.takeFirst());
149
150 m_cachedPagesLRU.append(pageNumber);
151 }
152
153 m_pageCache.insert(pageNumber, image);
154
155 q->viewport()->update();
156}
157
158void QPdfViewPrivate::invalidateDocumentLayout()
159{
160 updateDocumentLayout();
161 invalidatePageCache();
162}
163
164void QPdfViewPrivate::invalidatePageCache()
165{
166 Q_Q(QPdfView);
167
168 m_pageCache.clear();
169 q->viewport()->update();
170}
171
172QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const
173{
174 // The DocumentLayout describes a virtual layout where all pages are positioned inside
175 // - For SinglePage mode, this is just an area as large as the current page surrounded
176 // by the m_documentMargins.
177 // - For MultiPage mode, this is the area that is covered by all pages which are placed
178 // below each other, with m_pageSpacing inbetween and surrounded by m_documentMargins
179
180 DocumentLayout documentLayout;
181
182 if (!m_document || m_document->status() != QPdfDocument::Status::Ready)
183 return documentLayout;
184
185 QHash<int, QRect> pageGeometries;
186
187 const int pageCount = m_document->pageCount();
188
189 int totalWidth = 0;
190
191 const int startPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() : 0);
192 const int endPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() + 1 : pageCount);
193
194 // calculate page sizes
195 for (int page = startPage; page < endPage; ++page) {
196 QSize pageSize;
197 if (m_zoomMode == QPdfView::ZoomMode::Custom) {
198 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution * m_zoomFactor).toSize();
199 } else if (m_zoomMode == QPdfView::ZoomMode::FitToWidth) {
200 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize();
201 const qreal factor = (qreal(m_viewport.width() - m_documentMargins.left() - m_documentMargins.right()) /
202 qreal(pageSize.width()));
203 pageSize *= factor;
204 } else if (m_zoomMode == QPdfView::ZoomMode::FitInView) {
205 const QSize viewportSize(m_viewport.size() +
206 QSize(-m_documentMargins.left() - m_documentMargins.right(), -m_pageSpacing));
207
208 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize();
209 pageSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio);
210 }
211
212 totalWidth = qMax(totalWidth, pageSize.width());
213
214 pageGeometries[page] = QRect(QPoint(0, 0), pageSize);
215 }
216
217 totalWidth += m_documentMargins.left() + m_documentMargins.right();
218
219 int pageY = m_documentMargins.top();
220
221 // calculate page positions
222 for (int page = startPage; page < endPage; ++page) {
223 const QSize pageSize = pageGeometries[page].size();
224
225 // center horizontal inside the viewport
226 const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2;
227
228 pageGeometries[page].moveTopLeft(QPoint(pageX, pageY));
229
230 pageY += pageSize.height() + m_pageSpacing;
231 }
232
233 pageY += m_documentMargins.bottom();
234
235 documentLayout.pageGeometries = pageGeometries;
236
237 // calculate overall document size
238 documentLayout.documentSize = QSize(totalWidth, pageY);
239
240 return documentLayout;
241}
242
243qreal QPdfViewPrivate::yPositionForPage(int pageNumber) const
244{
245 const auto it = m_documentLayout.pageGeometries.constFind(pageNumber);
246 if (it == m_documentLayout.pageGeometries.cend())
247 return 0.0;
248
249 return (*it).y();
250}
251
252QTransform QPdfViewPrivate::screenScaleTransform() const
253{
254 const qreal scale = m_screenResolution * m_zoomFactor;
255 return QTransform::fromScale(scale, scale);
256}
257
258void QPdfViewPrivate::updateDocumentLayout()
259{
260 m_documentLayout = calculateDocumentLayout();
261
262 updateScrollBars();
263}
264
280QPdfView::QPdfView(QWidget *parent)
281 : QAbstractScrollArea(parent)
282 , d_ptr(new QPdfViewPrivate(this))
283{
284 Q_D(QPdfView);
285
286 d->init();
287
288 connect(d->m_pageNavigator, &QPdfPageNavigator::currentPageChanged, this,
289 [d](int page){ d->currentPageChanged(page); });
290
291 connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered, this,
292 [d](int pageNumber, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions, quint64 requestId) {
293 d->pageRendered(pageNumber, imageSize, image, requestId); });
294
295 verticalScrollBar()->setSingleStep(20);
296 horizontalScrollBar()->setSingleStep(20);
297
298 setMouseTracking(true);
299 d->calculateViewport();
300}
301
305QPdfView::~QPdfView()
306{
307}
308
314void QPdfView::setDocument(QPdfDocument *document)
315{
316 Q_D(QPdfView);
317
318 if (d->m_document == document)
319 return;
320
321 if (d->m_document)
322 disconnect(d->m_documentStatusChangedConnection);
323
324 d->m_document = document;
325 emit documentChanged(d->m_document);
326
327 if (d->m_document)
328 d->m_documentStatusChangedConnection =
329 connect(d->m_document.data(), &QPdfDocument::statusChanged, this,
330 [d](){ d->documentStatusChanged(); });
331
332 d->m_pageRenderer->setDocument(d->m_document);
333 d->m_linkModel.setDocument(d->m_document);
334
335 d->documentStatusChanged();
336}
337
338QPdfDocument *QPdfView::document() const
339{
340 Q_D(const QPdfView);
341
342 return d->m_document;
343}
344
353void QPdfView::setSearchModel(QPdfSearchModel *searchModel)
354{
355 Q_D(QPdfView);
356 if (d->m_searchModel == searchModel)
357 return;
358
359 if (d->m_searchModel)
360 d->m_searchModel->disconnect(this);
361
362 d->m_searchModel = searchModel;
363 emit searchModelChanged(searchModel);
364
365 if (searchModel) {
366 connect(searchModel, &QPdfSearchModel::dataChanged, this,
367 [this](const QModelIndex &, const QModelIndex &, const QList<int> &) { update(); });
368 }
369 setCurrentSearchResult(-1);
370}
371
372QPdfSearchModel *QPdfView::searchModel() const
373{
374 Q_D(const QPdfView);
375 return d->m_searchModel;
376}
377
391void QPdfView::setCurrentSearchResult(int currentResult)
392{
393 Q_D(QPdfView);
394 if (d->m_currentSearchResult == currentResult)
395 return;
396
397 d->m_currentSearchResult = currentResult;
398 emit currentSearchResultChanged(currentResult);
399 viewport()->update(); //update();
400}
401
402int QPdfView::currentSearchResult() const
403{
404 Q_D(const QPdfView);
405 return d->m_currentSearchResult;
406}
407
411QPdfPageNavigator *QPdfView::pageNavigator() const
412{
413 Q_D(const QPdfView);
414
415 return d->m_pageNavigator;
416}
417
433QPdfView::PageMode QPdfView::pageMode() const
434{
435 Q_D(const QPdfView);
436
437 return d->m_pageMode;
438}
439
440void QPdfView::setPageMode(PageMode mode)
441{
442 Q_D(QPdfView);
443
444 if (d->m_pageMode == mode)
445 return;
446
447 d->m_pageMode = mode;
448 d->invalidateDocumentLayout();
449
450 emit pageModeChanged(d->m_pageMode);
451}
452
471QPdfView::ZoomMode QPdfView::zoomMode() const
472{
473 Q_D(const QPdfView);
474
475 return d->m_zoomMode;
476}
477
478void QPdfView::setZoomMode(ZoomMode mode)
479{
480 Q_D(QPdfView);
481
482 if (d->m_zoomMode == mode)
483 return;
484
485 d->m_zoomMode = mode;
486 d->invalidateDocumentLayout();
487
488 emit zoomModeChanged(d->m_zoomMode);
489}
490
497qreal QPdfView::zoomFactor() const
498{
499 Q_D(const QPdfView);
500
501 return d->m_zoomFactor;
502}
503
504void QPdfView::setZoomFactor(qreal factor)
505{
506 Q_D(QPdfView);
507
508 if (d->m_zoomFactor == factor)
509 return;
510
511 d->m_zoomFactor = factor;
512 d->invalidateDocumentLayout();
513
514 emit zoomFactorChanged(d->m_zoomFactor);
515}
516
523int QPdfView::pageSpacing() const
524{
525 Q_D(const QPdfView);
526
527 return d->m_pageSpacing;
528}
529
530void QPdfView::setPageSpacing(int spacing)
531{
532 Q_D(QPdfView);
533
534 if (d->m_pageSpacing == spacing)
535 return;
536
537 d->m_pageSpacing = spacing;
538 d->invalidateDocumentLayout();
539
540 emit pageSpacingChanged(d->m_pageSpacing);
541}
542
548QMargins QPdfView::documentMargins() const
549{
550 Q_D(const QPdfView);
551
552 return d->m_documentMargins;
553}
554
555void QPdfView::setDocumentMargins(QMargins margins)
556{
557 Q_D(QPdfView);
558
559 if (d->m_documentMargins == margins)
560 return;
561
562 d->m_documentMargins = margins;
563 d->invalidateDocumentLayout();
564
565 emit documentMarginsChanged(d->m_documentMargins);
566}
567
568void QPdfView::paintEvent(QPaintEvent *event)
569{
570 Q_D(QPdfView);
571
572 QPainter painter(viewport());
573 painter.fillRect(event->rect(), palette().brush(QPalette::Dark));
574 painter.translate(-d->m_viewport.x(), -d->m_viewport.y());
575
576 for (auto it = d->m_documentLayout.pageGeometries.cbegin();
577 it != d->m_documentLayout.pageGeometries.cend(); ++it) {
578 const QRect pageGeometry = it.value();
579 if (pageGeometry.intersects(d->m_viewport)) { // page needs to be painted
580 painter.fillRect(pageGeometry, Qt::white);
581
582 const int page = it.key();
583 const auto pageIt = d->m_pageCache.constFind(page);
584 if (pageIt != d->m_pageCache.cend()) {
585 const QImage &img = pageIt.value();
586 painter.drawImage(pageGeometry, img);
587 } else {
588 d->m_pageRenderer->requestPage(page, pageGeometry.size() * devicePixelRatioF());
589 }
590
591 const QTransform scaleTransform = d->screenScaleTransform();
592#ifdef DEBUG_LINKS
593 const QString fmt = u"page %1 @ %2, %3"_s;
594 d->m_linkModel.setPage(page);
595 const int linkCount = d->m_linkModel.rowCount({});
596 for (int i = 0; i < linkCount; ++i) {
597 const QRectF linkBounds = scaleTransform.mapRect(
598 d->m_linkModel.data(d->m_linkModel.index(i),
599 int(QPdfLinkModel::Role::Rect)).toRectF())
600 .translated(pageGeometry.topLeft());
601 painter.setPen(Qt::blue);
602 painter.drawRect(linkBounds);
603 painter.setPen(Qt::red);
604 const QPoint loc = d->m_linkModel.data(d->m_linkModel.index(i),
605 int(QPdfLinkModel::Role::Location)).toPoint();
606 // TODO maybe draw destination URL if that's what it is
607 painter.drawText(linkBounds.bottomLeft() + QPoint(2, -2),
608 fmt.arg(d->m_linkModel.data(d->m_linkModel.index(i),
609 int(QPdfLinkModel::Role::Page)).toInt())
610 .arg(loc.x()).arg(loc.y()));
611 }
612#endif
613 if (d->m_searchModel) {
614 for (const QPdfLink &result : d->m_searchModel->resultsOnPage(page)) {
615 for (const QRectF &rect : result.rectangles())
616 painter.fillRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft()), SearchResultHighlight);
617 }
618
619 if (d->m_currentSearchResult >= 0 && d->m_currentSearchResult < d->m_searchModel->rowCount({})) {
620 const QPdfLink &cur = d->m_searchModel->resultAtIndex(d->m_currentSearchResult);
621 if (cur.page() == page) {
622 painter.setPen({CurrentSearchResultHighlight, CurrentSearchResultWidth});
623 for (const auto &rect : cur.rectangles())
624 painter.drawRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft()));
625 }
626 }
627 }
628 }
629 }
630}
631
632void QPdfView::resizeEvent(QResizeEvent *event)
633{
634 Q_D(QPdfView);
635
636 QAbstractScrollArea::resizeEvent(event);
637
638 d->updateScrollBars();
639 d->calculateViewport();
640}
641
642void QPdfView::scrollContentsBy(int dx, int dy)
643{
644 Q_D(QPdfView);
645
646 QAbstractScrollArea::scrollContentsBy(dx, dy);
647
648 d->calculateViewport();
649}
650
651void QPdfView::mousePressEvent(QMouseEvent *event)
652{
653 Q_ASSERT(event->isAccepted());
654}
655
656void QPdfView::mouseMoveEvent(QMouseEvent *event)
657{
658 Q_D(QPdfView);
659 const QTransform screenInvTransform = d->screenScaleTransform().inverted();
660 for (auto it = d->m_documentLayout.pageGeometries.cbegin(); it != d->m_documentLayout.pageGeometries.cend(); ++it) {
661 const int page = it.key();
662 const QRect pageGeometry = it.value();
663 if (pageGeometry.contains(event->position().toPoint())) {
664 const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft());
665 d->m_linkModel.setPage(page);
666 auto dest = d->m_linkModel.linkAt(posInPoints);
667 setCursor(dest.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor);
668 if (dest.isValid())
669 qCDebug(qLcLink) << event->position() << ":" << posInPoints << "pt ->" << dest;
670 }
671 }
672}
673
674void QPdfView::mouseReleaseEvent(QMouseEvent *event)
675{
676 Q_D(QPdfView);
677 const QTransform screenInvTransform = d->screenScaleTransform().inverted();
678 for (auto it = d->m_documentLayout.pageGeometries.cbegin(); it != d->m_documentLayout.pageGeometries.cend(); ++it) {
679 const int page = it.key();
680 const QRect pageGeometry = it.value();
681 if (pageGeometry.contains(event->position().toPoint())) {
682 const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft());
683 d->m_linkModel.setPage(page);
684 auto dest = d->m_linkModel.linkAt(posInPoints);
685 if (dest.isValid()) {
686 qCDebug(qLcLink) << event << ": jumping to" << dest;
687 d->m_pageNavigator->jump(dest.page(), dest.location(), dest.zoom());
688 // TODO scroll and zoom to where the link tells us to
689 }
690 return;
691 }
692 }
693}
694
695QT_END_NAMESPACE
696
697#include "moc_qpdfview.cpp"
The QColor class provides colors based on RGB, HSV or CMYK values.
Definition qcolor.h:31
Combined button and popup list for selecting options.
#define Q_LOGGING_CATEGORY(name,...)
static QT_BEGIN_NAMESPACE const QColor SearchResultHighlight("#80B0C4DE")