10#include <QGuiApplication>
11#include <QLoggingCategory>
14#include <QPdfDocument>
15#include <QPdfPageNavigator>
16#include <QPdfSearchModel>
26static const QColor CurrentSearchResultHighlight(Qt::cyan);
27static const int CurrentSearchResultWidth(2);
29QPdfViewPrivate::QPdfViewPrivate(QPdfView *q)
32 , m_pageNavigator(nullptr)
33 , m_pageRenderer(nullptr)
34 , m_pageMode(QPdfView::PageMode::SinglePage)
35 , m_zoomMode(QPdfView::ZoomMode::Custom)
38 , m_documentMargins(6, 6, 6, 6)
39 , m_blockPageScrolling(false)
40 , m_pageCacheLimit(20)
41 , m_screenResolution(QGuiApplication::primaryScreen()->logicalDotsPerInch() / 72.0)
45void QPdfViewPrivate::init()
49 m_pageNavigator = new QPdfPageNavigator(q);
50 m_pageRenderer = new QPdfPageRenderer(q);
51 m_pageRenderer->setRenderMode(QPdfPageRenderer::RenderMode::MultiThreaded);
54void QPdfViewPrivate::documentStatusChanged()
56 updateDocumentLayout();
57 invalidatePageCache();
60void QPdfViewPrivate::currentPageChanged(int currentPage)
64 if (m_blockPageScrolling)
67 q->verticalScrollBar()->setValue(yPositionForPage(currentPage));
69 if (m_pageMode == QPdfView::PageMode::SinglePage)
70 invalidateDocumentLayout();
73void QPdfViewPrivate::calculateViewport()
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();
82 setViewport(QRect(x, y, width, height));
85void QPdfViewPrivate::setViewport(QRect viewport)
87 if (m_viewport == viewport)
90 const QSize oldSize = m_viewport.size();
92 m_viewport = viewport;
94 if (oldSize != m_viewport.size()) {
95 updateDocumentLayout();
97 if (m_zoomMode != QPdfView::ZoomMode::Custom) {
98 invalidatePageCache();
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);
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();
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;
126void QPdfViewPrivate::updateScrollBars()
130 const QSize p = q->viewport()->size();
131 const QSize v = m_documentLayout.documentSize;
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());
139void QPdfViewPrivate::pageRendered(int pageNumber, QSize imageSize, const QImage &image, quint64 requestId)
146 if (!m_cachedPagesLRU.contains(pageNumber)) {
147 if (m_cachedPagesLRU.size() > m_pageCacheLimit)
148 m_pageCache.remove(m_cachedPagesLRU.takeFirst());
150 m_cachedPagesLRU.append(pageNumber);
153 m_pageCache.insert(pageNumber, image);
155 q->viewport()->update();
158void QPdfViewPrivate::invalidateDocumentLayout()
160 updateDocumentLayout();
161 invalidatePageCache();
164void QPdfViewPrivate::invalidatePageCache()
169 q->viewport()->update();
172QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const
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
180 DocumentLayout documentLayout;
182 if (!m_document || m_document->status() != QPdfDocument::Status::Ready)
183 return documentLayout;
185 QHash<int, QRect> pageGeometries;
187 const int pageCount = m_document->pageCount();
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);
194 // calculate page sizes
195 for (int page = startPage; page < endPage; ++page) {
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()));
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));
208 pageSize = QSizeF(m_document->pagePointSize(page) * m_screenResolution).toSize();
209 pageSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio);
212 totalWidth = qMax(totalWidth, pageSize.width());
214 pageGeometries[page] = QRect(QPoint(0, 0), pageSize);
217 totalWidth += m_documentMargins.left() + m_documentMargins.right();
219 int pageY = m_documentMargins.top();
221 // calculate page positions
222 for (int page = startPage; page < endPage; ++page) {
223 const QSize pageSize = pageGeometries[page].size();
225 // center horizontal inside the viewport
226 const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2;
228 pageGeometries[page].moveTopLeft(QPoint(pageX, pageY));
230 pageY += pageSize.height() + m_pageSpacing;
233 pageY += m_documentMargins.bottom();
235 documentLayout.pageGeometries = pageGeometries;
237 // calculate overall document size
238 documentLayout.documentSize = QSize(totalWidth, pageY);
240 return documentLayout;
243qreal QPdfViewPrivate::yPositionForPage(int pageNumber) const
245 const auto it = m_documentLayout.pageGeometries.constFind(pageNumber);
246 if (it == m_documentLayout.pageGeometries.cend())
252QTransform QPdfViewPrivate::screenScaleTransform() const
254 const qreal scale = m_screenResolution * m_zoomFactor;
255 return QTransform::fromScale(scale, scale);
258void QPdfViewPrivate::updateDocumentLayout()
260 m_documentLayout = calculateDocumentLayout();
280QPdfView::QPdfView(QWidget *parent)
281 : QAbstractScrollArea(parent)
282 , d_ptr(new QPdfViewPrivate(this))
288 connect(d->m_pageNavigator, &QPdfPageNavigator::currentPageChanged, this,
289 [d](int page){ d->currentPageChanged(page); });
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); });
295 verticalScrollBar()->setSingleStep(20);
296 horizontalScrollBar()->setSingleStep(20);
298 setMouseTracking(true);
299 d->calculateViewport();
314void QPdfView::setDocument(QPdfDocument *document)
318 if (d->m_document == document)
322 disconnect(d->m_documentStatusChangedConnection);
324 d->m_document = document;
325 emit documentChanged(d->m_document);
328 d->m_documentStatusChangedConnection =
329 connect(d->m_document.data(), &QPdfDocument::statusChanged, this,
330 [d](){ d->documentStatusChanged(); });
332 d->m_pageRenderer->setDocument(d->m_document);
333 d->m_linkModel.setDocument(d->m_document);
335 d->documentStatusChanged();
338QPdfDocument *QPdfView::document() const
342 return d->m_document;
353void QPdfView::setSearchModel(QPdfSearchModel *searchModel)
356 if (d->m_searchModel == searchModel)
359 if (d->m_searchModel)
360 d->m_searchModel->disconnect(this);
362 d->m_searchModel = searchModel;
363 emit searchModelChanged(searchModel);
366 connect(searchModel, &QPdfSearchModel::dataChanged, this,
367 [this](const QModelIndex &, const QModelIndex &, const QList<int> &) { update(); });
369 setCurrentSearchResult(-1);
372QPdfSearchModel *QPdfView::searchModel() const
375 return d->m_searchModel;
391void QPdfView::setCurrentSearchResult(int currentResult)
394 if (d->m_currentSearchResult == currentResult)
397 d->m_currentSearchResult = currentResult;
398 emit currentSearchResultChanged(currentResult);
399 viewport()->update(); //update();
402int QPdfView::currentSearchResult() const
405 return d->m_currentSearchResult;
411QPdfPageNavigator *QPdfView::pageNavigator() const
415 return d->m_pageNavigator;
433QPdfView::PageMode QPdfView::pageMode() const
437 return d->m_pageMode;
440void QPdfView::setPageMode(PageMode mode)
444 if (d->m_pageMode == mode)
447 d->m_pageMode = mode;
448 d->invalidateDocumentLayout();
450 emit pageModeChanged(d->m_pageMode);
471QPdfView::ZoomMode QPdfView::zoomMode() const
475 return d->m_zoomMode;
478void QPdfView::setZoomMode(ZoomMode mode)
482 if (d->m_zoomMode == mode)
485 d->m_zoomMode = mode;
486 d->invalidateDocumentLayout();
488 emit zoomModeChanged(d->m_zoomMode);
497qreal QPdfView::zoomFactor() const
501 return d->m_zoomFactor;
504void QPdfView::setZoomFactor(qreal factor)
508 if (d->m_zoomFactor == factor)
511 d->m_zoomFactor = factor;
512 d->invalidateDocumentLayout();
514 emit zoomFactorChanged(d->m_zoomFactor);
523int QPdfView::pageSpacing() const
527 return d->m_pageSpacing;
530void QPdfView::setPageSpacing(int spacing)
534 if (d->m_pageSpacing == spacing)
537 d->m_pageSpacing = spacing;
538 d->invalidateDocumentLayout();
540 emit pageSpacingChanged(d->m_pageSpacing);
548QMargins QPdfView::documentMargins() const
552 return d->m_documentMargins;
555void QPdfView::setDocumentMargins(QMargins margins)
559 if (d->m_documentMargins == margins)
562 d->m_documentMargins = margins;
563 d->invalidateDocumentLayout();
565 emit documentMarginsChanged(d->m_documentMargins);
568void QPdfView::paintEvent(QPaintEvent *event)
572 QPainter painter(viewport());
573 painter.fillRect(event->rect(), palette().brush(QPalette::Dark));
574 painter.translate(-d->m_viewport.x(), -d->m_viewport.y());
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);
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);
588 d->m_pageRenderer->requestPage(page, pageGeometry.size() * devicePixelRatioF());
591 const QTransform scaleTransform = d->screenScaleTransform();
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()));
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);
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()));
632void QPdfView::resizeEvent(QResizeEvent *event)
636 QAbstractScrollArea::resizeEvent(event);
638 d->updateScrollBars();
639 d->calculateViewport();
642void QPdfView::scrollContentsBy(int dx, int dy)
646 QAbstractScrollArea::scrollContentsBy(dx, dy);
648 d->calculateViewport();
651void QPdfView::mousePressEvent(QMouseEvent *event)
653 Q_ASSERT(event->isAccepted());
656void QPdfView::mouseMoveEvent(QMouseEvent *event)
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);
669 qCDebug(qLcLink) << event->position() << ":" << posInPoints << "pt ->" << dest;
674void QPdfView::mouseReleaseEvent(QMouseEvent *event)
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
697#include "moc_qpdfview.cpp"
The QColor class provides colors based on RGB, HSV or CMYK values.
Combined button and popup list for selecting options.
#define Q_LOGGING_CATEGORY(name,...)
static QT_BEGIN_NAMESPACE const QColor SearchResultHighlight("#80B0C4DE")