Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
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(qLcWLink, "qt.pdf.widgets.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.pageGeometryAndScale.cbegin();
110 it != m_documentLayout.pageGeometryAndScale.cend(); ++it) {
111 const QRect pageGeometry = it.value().first;
112 if (pageGeometry.intersects(currentPageLine)) {
113 currentPage = it.key();
114 break;
115 }
116 }
117
118 if (currentPage != m_pageNavigator->currentPage()) {
119 m_blockPageScrolling = true;
120 // ΤODO give location on the page
121 m_pageNavigator->jump(currentPage, {}, m_zoomFactor);
122 m_blockPageScrolling = false;
123 }
124 }
125}
126
127void QPdfViewPrivate::updateScrollBars()
128{
129 Q_Q(QPdfView);
130
131 const QSize p = q->viewport()->size();
132 const QSize v = m_documentLayout.documentSize;
133
134 q->horizontalScrollBar()->setRange(0, v.width() - p.width());
135 q->horizontalScrollBar()->setPageStep(p.width());
136 q->verticalScrollBar()->setRange(0, v.height() - p.height());
137 q->verticalScrollBar()->setPageStep(p.height());
138}
139
140void QPdfViewPrivate::pageRendered(int pageNumber, QSize imageSize, const QImage &image, quint64 requestId)
141{
142 Q_Q(QPdfView);
143
144 Q_UNUSED(imageSize);
145 Q_UNUSED(requestId);
146
147 if (!m_cachedPagesLRU.contains(pageNumber)) {
148 if (m_cachedPagesLRU.size() > m_pageCacheLimit)
149 m_pageCache.remove(m_cachedPagesLRU.takeFirst());
150
151 m_cachedPagesLRU.append(pageNumber);
152 }
153
154 m_pageCache.insert(pageNumber, image);
155
156 q->viewport()->update();
157}
158
159void QPdfViewPrivate::invalidateDocumentLayout()
160{
161 updateDocumentLayout();
162 invalidatePageCache();
163}
164
165void QPdfViewPrivate::invalidatePageCache()
166{
167 Q_Q(QPdfView);
168
169 m_pageCache.clear();
170 q->viewport()->update();
171}
172
173QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const
174{
175 // The DocumentLayout describes a virtual layout where all pages are positioned inside
176 // - For SinglePage mode, this is just an area as large as the current page surrounded
177 // by the m_documentMargins.
178 // - For MultiPage mode, this is the area that is covered by all pages which are placed
179 // below each other, with m_pageSpacing inbetween and surrounded by m_documentMargins
180
181 DocumentLayout documentLayout;
182
183 if (!m_document || m_document->status() != QPdfDocument::Status::Ready)
184 return documentLayout;
185
186 QHash<int, QPair<QRect, qreal>> pageGeometryAndScale;
187
188 const int pageCount = m_document->pageCount();
189
190 int totalWidth = 0;
191
192 const int startPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() : 0);
193 const int endPage = (m_pageMode == QPdfView::PageMode::SinglePage ? m_pageNavigator->currentPage() + 1 : pageCount);
194
195 // calculate page sizes
196 for (int page = startPage; page < endPage; ++page) {
197 QSize pageSize;
198 qreal pageScale = m_zoomFactor;
199 if (m_zoomMode == QPdfView::ZoomMode::Custom) {
200 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution * m_zoomFactor).toSize();
201 } else if (m_zoomMode == QPdfView::ZoomMode::FitToWidth) {
202 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize();
203 pageScale = (qreal(m_viewport.width() - m_documentMargins.left() - m_documentMargins.right()) /
204 qreal(pageSize.width()));
205 pageSize *= pageScale;
206 } else if (m_zoomMode == QPdfView::ZoomMode::FitInView) {
207 const QSize viewportSize(m_viewport.size() +
208 QSize(-m_documentMargins.left() - m_documentMargins.right(), -m_pageSpacing));
209
210 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize();
211 QSize scaledSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio);
212 // because of KeepAspectRatio, the ratio of widths should be the same as the ratio of heights
213 pageScale = qreal(scaledSize.width()) / qreal(pageSize.width());
214 pageSize = scaledSize;
215 }
216
217 totalWidth = qMax(totalWidth, pageSize.width());
218
219 pageGeometryAndScale[page] = {QRect(QPoint(0, 0), pageSize), pageScale};
220 }
221
222 totalWidth += m_documentMargins.left() + m_documentMargins.right();
223
224 int pageY = m_documentMargins.top();
225
226 // calculate page positions
227 for (int page = startPage; page < endPage; ++page) {
228 const QSize pageSize = pageGeometryAndScale[page].first.size();
229
230 // center horizontal inside the viewport
231 const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2;
232
233 pageGeometryAndScale[page].first.moveTopLeft(QPoint(pageX, pageY));
234
235 pageY += pageSize.height() + m_pageSpacing;
236 }
237
238 pageY += m_documentMargins.bottom();
239
240 documentLayout.pageGeometryAndScale = pageGeometryAndScale;
241
242 // calculate overall document size
243 documentLayout.documentSize = QSize(totalWidth, pageY);
244
245 return documentLayout;
246}
247
248qreal QPdfViewPrivate::yPositionForPage(int pageNumber) const
249{
250 const auto it = m_documentLayout.pageGeometryAndScale.constFind(pageNumber);
251 if (it == m_documentLayout.pageGeometryAndScale.cend())
252 return 0.0;
253
254 return (*it).first.y();
255}
256
257QTransform QPdfViewPrivate::screenScaleTransform(int page) const
258{
259 qreal scale = m_screenResolution * m_zoomFactor;
260 switch (m_zoomMode) {
261 case QPdfView::ZoomMode::FitToWidth:
262 case QPdfView::ZoomMode::FitInView:
263 scale = m_screenResolution * m_documentLayout.pageGeometryAndScale[page].second;
264 break;
265 default:
266 break;
267 }
268
269 return QTransform::fromScale(scale, scale);
270}
271
272void QPdfViewPrivate::updateDocumentLayout()
273{
274 m_documentLayout = calculateDocumentLayout();
275
276 updateScrollBars();
277}
278
294QPdfView::QPdfView(QWidget *parent)
295 : QAbstractScrollArea(parent)
296 , d_ptr(new QPdfViewPrivate(this))
297{
298 Q_D(QPdfView);
299
300 d->init();
301
302 connect(d->m_pageNavigator, &QPdfPageNavigator::currentPageChanged, this,
303 [d](int page){ d->currentPageChanged(page); });
304
305 connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered, this,
306 [d](int pageNumber, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions, quint64 requestId) {
307 d->pageRendered(pageNumber, imageSize, image, requestId); });
308
309 verticalScrollBar()->setSingleStep(20);
310 horizontalScrollBar()->setSingleStep(20);
311
312 setMouseTracking(true);
313 d->calculateViewport();
314}
315
319QPdfView::~QPdfView()
320{
321}
322
328void QPdfView::setDocument(QPdfDocument *document)
329{
330 Q_D(QPdfView);
331
332 if (d->m_document == document)
333 return;
334
335 if (d->m_document)
336 disconnect(d->m_documentStatusChangedConnection);
337
338 d->m_document = document;
339 emit documentChanged(d->m_document);
340
341 if (d->m_document)
342 d->m_documentStatusChangedConnection =
343 connect(d->m_document.data(), &QPdfDocument::statusChanged, this,
344 [d](){ d->documentStatusChanged(); });
345
346 d->m_pageRenderer->setDocument(d->m_document);
347 d->m_linkModel.setDocument(d->m_document);
348
349 d->documentStatusChanged();
350}
351
352QPdfDocument *QPdfView::document() const
353{
354 Q_D(const QPdfView);
355
356 return d->m_document;
357}
358
367void QPdfView::setSearchModel(QPdfSearchModel *searchModel)
368{
369 Q_D(QPdfView);
370 if (d->m_searchModel == searchModel)
371 return;
372
373 if (d->m_searchModel)
374 d->m_searchModel->disconnect(this);
375
376 d->m_searchModel = searchModel;
377 emit searchModelChanged(searchModel);
378
379 if (searchModel) {
380 connect(searchModel, &QPdfSearchModel::dataChanged, this,
381 [this](const QModelIndex &, const QModelIndex &, const QList<int> &) { update(); });
382 }
383 setCurrentSearchResultIndex(-1);
384}
385
386QPdfSearchModel *QPdfView::searchModel() const
387{
388 Q_D(const QPdfView);
389 return d->m_searchModel;
390}
391
405void QPdfView::setCurrentSearchResultIndex(int currentResult)
406{
407 Q_D(QPdfView);
408 if (d->m_currentSearchResultIndex == currentResult)
409 return;
410
411 d->m_currentSearchResultIndex = currentResult;
412 emit currentSearchResultIndexChanged(currentResult);
413 viewport()->update(); //update();
414}
415
416int QPdfView::currentSearchResultIndex() const
417{
418 Q_D(const QPdfView);
419 return d->m_currentSearchResultIndex;
420}
421
425QPdfPageNavigator *QPdfView::pageNavigator() const
426{
427 Q_D(const QPdfView);
428
429 return d->m_pageNavigator;
430}
431
447QPdfView::PageMode QPdfView::pageMode() const
448{
449 Q_D(const QPdfView);
450
451 return d->m_pageMode;
452}
453
454void QPdfView::setPageMode(PageMode mode)
455{
456 Q_D(QPdfView);
457
458 if (d->m_pageMode == mode)
459 return;
460
461 d->m_pageMode = mode;
462 d->invalidateDocumentLayout();
463
464 emit pageModeChanged(d->m_pageMode);
465}
466
485QPdfView::ZoomMode QPdfView::zoomMode() const
486{
487 Q_D(const QPdfView);
488
489 return d->m_zoomMode;
490}
491
492void QPdfView::setZoomMode(ZoomMode mode)
493{
494 Q_D(QPdfView);
495
496 if (d->m_zoomMode == mode)
497 return;
498
499 d->m_zoomMode = mode;
500 d->invalidateDocumentLayout();
501
502 emit zoomModeChanged(d->m_zoomMode);
503}
504
511qreal QPdfView::zoomFactor() const
512{
513 Q_D(const QPdfView);
514
515 return d->m_zoomFactor;
516}
517
518void QPdfView::setZoomFactor(qreal factor)
519{
520 Q_D(QPdfView);
521
522 if (d->m_zoomFactor == factor)
523 return;
524
525 d->m_zoomFactor = factor;
526 d->invalidateDocumentLayout();
527
528 emit zoomFactorChanged(d->m_zoomFactor);
529}
530
537int QPdfView::pageSpacing() const
538{
539 Q_D(const QPdfView);
540
541 return d->m_pageSpacing;
542}
543
544void QPdfView::setPageSpacing(int spacing)
545{
546 Q_D(QPdfView);
547
548 if (d->m_pageSpacing == spacing)
549 return;
550
551 d->m_pageSpacing = spacing;
552 d->invalidateDocumentLayout();
553
554 emit pageSpacingChanged(d->m_pageSpacing);
555}
556
562QMargins QPdfView::documentMargins() const
563{
564 Q_D(const QPdfView);
565
566 return d->m_documentMargins;
567}
568
569void QPdfView::setDocumentMargins(QMargins margins)
570{
571 Q_D(QPdfView);
572
573 if (d->m_documentMargins == margins)
574 return;
575
576 d->m_documentMargins = margins;
577 d->invalidateDocumentLayout();
578
579 emit documentMarginsChanged(d->m_documentMargins);
580}
581
582void QPdfView::paintEvent(QPaintEvent *event)
583{
584 Q_D(QPdfView);
585
586 QPainter painter(viewport());
587 painter.fillRect(event->rect(), palette().brush(QPalette::Dark));
588 painter.translate(-d->m_viewport.x(), -d->m_viewport.y());
589
590 for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin();
591 it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) {
592 const QRect pageGeometry = it.value().first;
593 if (pageGeometry.intersects(d->m_viewport)) { // page needs to be painted
594 painter.fillRect(pageGeometry, Qt::white);
595
596 const int page = it.key();
597 const auto pageIt = d->m_pageCache.constFind(page);
598 if (pageIt != d->m_pageCache.cend()) {
599 const QImage &img = pageIt.value();
600 painter.drawImage(pageGeometry, img);
601 } else {
602 d->m_pageRenderer->requestPage(page, pageGeometry.size() * devicePixelRatioF());
603 }
604
605 const QTransform scaleTransform = d->screenScaleTransform(page);
606#ifdef DEBUG_LINKS
607 const QString fmt = u"page %1 @ %2, %3"_s;
608 d->m_linkModel.setPage(page);
609 const int linkCount = d->m_linkModel.rowCount({});
610 for (int i = 0; i < linkCount; ++i) {
611 const QRectF linkBounds = scaleTransform.mapRect(
612 d->m_linkModel.data(d->m_linkModel.index(i),
613 int(QPdfLinkModel::Role::Rect)).toRectF())
614 .translated(pageGeometry.topLeft());
615 painter.setPen(Qt::blue);
616 painter.drawRect(linkBounds);
617 painter.setPen(Qt::red);
618 const QPoint loc = d->m_linkModel.data(d->m_linkModel.index(i),
619 int(QPdfLinkModel::Role::Location)).toPoint();
620 // TODO maybe draw destination URL if that's what it is
621 painter.drawText(linkBounds.bottomLeft() + QPoint(2, -2),
622 fmt.arg(d->m_linkModel.data(d->m_linkModel.index(i),
623 int(QPdfLinkModel::Role::Page)).toInt())
624 .arg(loc.x()).arg(loc.y()));
625 }
626#endif
627 if (d->m_searchModel) {
628 for (const QPdfLink &result : d->m_searchModel->resultsOnPage(page)) {
629 for (const QRectF &rect : result.rectangles())
630 painter.fillRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft()), SearchResultHighlight);
631 }
632
633 if (d->m_currentSearchResultIndex >= 0 && d->m_currentSearchResultIndex < d->m_searchModel->rowCount({})) {
634 const QPdfLink &cur = d->m_searchModel->resultAtIndex(d->m_currentSearchResultIndex);
635 if (cur.page() == page) {
636 painter.setPen({CurrentSearchResultHighlight, CurrentSearchResultWidth});
637 for (const auto &rect : cur.rectangles())
638 painter.drawRect(scaleTransform.mapRect(rect).translated(pageGeometry.topLeft()));
639 }
640 }
641 }
642 }
643 }
644}
645
646void QPdfView::resizeEvent(QResizeEvent *event)
647{
648 Q_D(QPdfView);
649
650 QAbstractScrollArea::resizeEvent(event);
651
652 d->updateScrollBars();
653 d->calculateViewport();
654}
655
656void QPdfView::scrollContentsBy(int dx, int dy)
657{
658 Q_D(QPdfView);
659
660 QAbstractScrollArea::scrollContentsBy(dx, dy);
661
662 d->calculateViewport();
663}
664
665void QPdfView::mousePressEvent(QMouseEvent *event)
666{
667 Q_ASSERT(event->isAccepted());
668}
669
670void QPdfView::mouseMoveEvent(QMouseEvent *event)
671{
672 Q_D(QPdfView);
673 for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin();
674 it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) {
675 const int page = it.key();
676 const QTransform screenInvTransform = d->screenScaleTransform(page).inverted();
677 const QRect pageGeometry = it.value().first;
678 if (pageGeometry.contains(event->position().toPoint())) {
679 const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft());
680 d->m_linkModel.setPage(page);
681 auto dest = d->m_linkModel.linkAt(posInPoints);
682 setCursor(dest.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor);
683 if (dest.isValid())
684 qCDebug(qLcWLink) << event->position() << ":" << posInPoints << "pt ->" << dest;
685 }
686 }
687}
688
689void QPdfView::mouseReleaseEvent(QMouseEvent *event)
690{
691 Q_D(QPdfView);
692 for (auto it = d->m_documentLayout.pageGeometryAndScale.cbegin();
693 it != d->m_documentLayout.pageGeometryAndScale.cend(); ++it) {
694 const int page = it.key();
695 const QTransform screenInvTransform = d->screenScaleTransform(page).inverted();
696 const QRect pageGeometry = it.value().first;
697 if (pageGeometry.contains(event->position().toPoint())) {
698 const QPointF posInPoints = screenInvTransform.map(event->position() - pageGeometry.topLeft());
699 d->m_linkModel.setPage(page);
700 auto dest = d->m_linkModel.linkAt(posInPoints);
701 if (dest.isValid()) {
702 qCDebug(qLcWLink) << event << ": jumping to" << dest;
703 d->m_pageNavigator->jump(dest.page(), dest.location(), dest.zoom());
704 // TODO scroll and zoom to where the link tells us to
705 }
706 return;
707 }
708 }
709}
710
711QT_END_NAMESPACE
712
713#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")