blob: 35e3686337f94b16ad22ccd9fa23f10b5a288e3c [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtPDF module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL3$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qpdfview.h"
#include "qpdfview_p.h"
#include "qpdfpagerenderer.h"
#include <QGuiApplication>
#include <QPdfDocument>
#include <QPdfPageNavigation>
#include <QScreen>
#include <QScrollBar>
#include <QScroller>
QT_BEGIN_NAMESPACE
QPdfViewPrivate::QPdfViewPrivate()
: QAbstractScrollAreaPrivate()
, m_document(nullptr)
, m_pageNavigation(nullptr)
, m_pageRenderer(nullptr)
, m_pageMode(QPdfView::SinglePage)
, m_zoomMode(QPdfView::CustomZoom)
, m_zoomFactor(1.0)
, m_pageSpacing(3)
, m_documentMargins(6, 6, 6, 6)
, m_blockPageScrolling(false)
, m_pageCacheLimit(20)
, m_screenResolution(QGuiApplication::primaryScreen()->logicalDotsPerInch() / 72.0)
{
}
void QPdfViewPrivate::init()
{
Q_Q(QPdfView);
m_pageNavigation = new QPdfPageNavigation(q);
m_pageRenderer = new QPdfPageRenderer(q);
m_pageRenderer->setRenderMode(QPdfPageRenderer::RenderMode::MultiThreaded);
}
void QPdfViewPrivate::documentStatusChanged()
{
updateDocumentLayout();
invalidatePageCache();
}
void QPdfViewPrivate::currentPageChanged(int currentPage)
{
Q_Q(QPdfView);
if (m_blockPageScrolling)
return;
q->verticalScrollBar()->setValue(yPositionForPage(currentPage));
if (m_pageMode == QPdfView::SinglePage)
invalidateDocumentLayout();
}
void QPdfViewPrivate::calculateViewport()
{
Q_Q(QPdfView);
const int x = q->horizontalScrollBar()->value();
const int y = q->verticalScrollBar()->value();
const int width = q->viewport()->width();
const int height = q->viewport()->height();
setViewport(QRect(x, y, width, height));
}
void QPdfViewPrivate::setViewport(QRect viewport)
{
if (m_viewport == viewport)
return;
const QSize oldSize = m_viewport.size();
m_viewport = viewport;
if (oldSize != m_viewport.size()) {
updateDocumentLayout();
if (m_zoomMode != QPdfView::CustomZoom) {
invalidatePageCache();
}
}
if (m_pageMode == QPdfView::MultiPage) {
// An imaginary, 2px height line at the upper half of the viewport, which is used to
// determine which page is currently located there -> we propagate that as 'current' page
// to the QPdfPageNavigation object
const QRect currentPageLine(m_viewport.x(), m_viewport.y() + m_viewport.height() * 0.4, m_viewport.width(), 2);
int currentPage = 0;
for (auto it = m_documentLayout.pageGeometries.cbegin(); it != m_documentLayout.pageGeometries.cend(); ++it) {
const QRect pageGeometry = it.value();
if (pageGeometry.intersects(currentPageLine)) {
currentPage = it.key();
break;
}
}
if (currentPage != m_pageNavigation->currentPage()) {
m_blockPageScrolling = true;
m_pageNavigation->setCurrentPage(currentPage);
m_blockPageScrolling = false;
}
}
}
void QPdfViewPrivate::updateScrollBars()
{
Q_Q(QPdfView);
const QSize p = q->viewport()->size();
const QSize v = m_documentLayout.documentSize;
q->horizontalScrollBar()->setRange(0, v.width() - p.width());
q->horizontalScrollBar()->setPageStep(p.width());
q->verticalScrollBar()->setRange(0, v.height() - p.height());
q->verticalScrollBar()->setPageStep(p.height());
}
void QPdfViewPrivate::pageRendered(int pageNumber, QSize imageSize, const QImage &image, quint64 requestId)
{
Q_Q(QPdfView);
Q_UNUSED(imageSize)
Q_UNUSED(requestId)
if (!m_cachedPagesLRU.contains(pageNumber)) {
if (m_cachedPagesLRU.length() > m_pageCacheLimit)
m_pageCache.remove(m_cachedPagesLRU.takeFirst());
m_cachedPagesLRU.append(pageNumber);
}
m_pageCache.insert(pageNumber, image);
q->viewport()->update();
}
void QPdfViewPrivate::invalidateDocumentLayout()
{
updateDocumentLayout();
invalidatePageCache();
}
void QPdfViewPrivate::invalidatePageCache()
{
Q_Q(QPdfView);
m_pageCache.clear();
q->viewport()->update();
}
QPdfViewPrivate::DocumentLayout QPdfViewPrivate::calculateDocumentLayout() const
{
// The DocumentLayout describes a virtual layout where all pages are positioned inside
// - For SinglePage mode, this is just an area as large as the current page surrounded
// by the m_documentMargins.
// - For MultiPage mode, this is the area that is covered by all pages which are placed
// below each other, with m_pageSpacing inbetween and surrounded by m_documentMargins
DocumentLayout documentLayout;
if (!m_document || m_document->status() != QPdfDocument::Ready)
return documentLayout;
QHash<int, QRect> pageGeometries;
const int pageCount = m_document->pageCount();
int totalWidth = 0;
const int startPage = (m_pageMode == QPdfView::SinglePage ? m_pageNavigation->currentPage() : 0);
const int endPage = (m_pageMode == QPdfView::SinglePage ? m_pageNavigation->currentPage() + 1 : pageCount);
// calculate page sizes
for (int page = startPage; page < endPage; ++page) {
QSize pageSize;
if (m_zoomMode == QPdfView::CustomZoom) {
pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution * m_zoomFactor).toSize();
} else if (m_zoomMode == QPdfView::FitToWidth) {
pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution).toSize();
const qreal factor = (qreal(m_viewport.width() - m_documentMargins.left() - m_documentMargins.right()) / qreal(pageSize.width()));
pageSize *= factor;
} else if (m_zoomMode == QPdfView::FitInView) {
const QSize viewportSize(m_viewport.size() + QSize(-m_documentMargins.left() - m_documentMargins.right(), -m_pageSpacing));
pageSize = QSizeF(m_document->pageSize(page) * m_screenResolution).toSize();
pageSize = pageSize.scaled(viewportSize, Qt::KeepAspectRatio);
}
totalWidth = qMax(totalWidth, pageSize.width());
pageGeometries[page] = QRect(QPoint(0, 0), pageSize);
}
totalWidth += m_documentMargins.left() + m_documentMargins.right();
int pageY = m_documentMargins.top();
// calculate page positions
for (int page = startPage; page < endPage; ++page) {
const QSize pageSize = pageGeometries[page].size();
// center horizontal inside the viewport
const int pageX = (qMax(totalWidth, m_viewport.width()) - pageSize.width()) / 2;
pageGeometries[page].moveTopLeft(QPoint(pageX, pageY));
pageY += pageSize.height() + m_pageSpacing;
}
pageY += m_documentMargins.bottom();
documentLayout.pageGeometries = pageGeometries;
// calculate overall document size
documentLayout.documentSize = QSize(totalWidth, pageY);
return documentLayout;
}
qreal QPdfViewPrivate::yPositionForPage(int pageNumber) const
{
const auto it = m_documentLayout.pageGeometries.constFind(pageNumber);
if (it == m_documentLayout.pageGeometries.cend())
return 0.0;
return (*it).y();
}
void QPdfViewPrivate::updateDocumentLayout()
{
m_documentLayout = calculateDocumentLayout();
updateScrollBars();
}
QPdfView::QPdfView(QWidget *parent)
: QAbstractScrollArea(*new QPdfViewPrivate(), parent)
{
Q_D(QPdfView);
d->init();
connect(d->m_pageNavigation, &QPdfPageNavigation::currentPageChanged, this, [d](int page){ d->currentPageChanged(page); });
connect(d->m_pageRenderer, &QPdfPageRenderer::pageRendered,
this, [d](int pageNumber, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions, quint64 requestId){ d->pageRendered(pageNumber, imageSize, image, requestId); });
verticalScrollBar()->setSingleStep(20);
horizontalScrollBar()->setSingleStep(20);
QScroller::grabGesture(this);
d->calculateViewport();
}
/*!
\internal
*/
QPdfView::QPdfView(QPdfViewPrivate &dd, QWidget *parent)
: QAbstractScrollArea(dd, parent)
{
}
QPdfView::~QPdfView()
{
}
void QPdfView::setDocument(QPdfDocument *document)
{
Q_D(QPdfView);
if (d->m_document == document)
return;
if (d->m_document)
disconnect(d->m_documentStatusChangedConnection);
d->m_document = document;
emit documentChanged(d->m_document);
if (d->m_document)
d->m_documentStatusChangedConnection = connect(d->m_document.data(), &QPdfDocument::statusChanged, this, [d](){ d->documentStatusChanged(); });
d->m_pageNavigation->setDocument(d->m_document);
d->m_pageRenderer->setDocument(d->m_document);
d->documentStatusChanged();
}
QPdfDocument *QPdfView::document() const
{
Q_D(const QPdfView);
return d->m_document;
}
QPdfPageNavigation *QPdfView::pageNavigation() const
{
Q_D(const QPdfView);
return d->m_pageNavigation;
}
QPdfView::PageMode QPdfView::pageMode() const
{
Q_D(const QPdfView);
return d->m_pageMode;
}
void QPdfView::setPageMode(PageMode mode)
{
Q_D(QPdfView);
if (d->m_pageMode == mode)
return;
d->m_pageMode = mode;
d->invalidateDocumentLayout();
emit pageModeChanged(d->m_pageMode);
}
QPdfView::ZoomMode QPdfView::zoomMode() const
{
Q_D(const QPdfView);
return d->m_zoomMode;
}
void QPdfView::setZoomMode(ZoomMode mode)
{
Q_D(QPdfView);
if (d->m_zoomMode == mode)
return;
d->m_zoomMode = mode;
d->invalidateDocumentLayout();
emit zoomModeChanged(d->m_zoomMode);
}
qreal QPdfView::zoomFactor() const
{
Q_D(const QPdfView);
return d->m_zoomFactor;
}
void QPdfView::setZoomFactor(qreal factor)
{
Q_D(QPdfView);
if (d->m_zoomFactor == factor)
return;
d->m_zoomFactor = factor;
d->invalidateDocumentLayout();
emit zoomFactorChanged(d->m_zoomFactor);
}
int QPdfView::pageSpacing() const
{
Q_D(const QPdfView);
return d->m_pageSpacing;
}
void QPdfView::setPageSpacing(int spacing)
{
Q_D(QPdfView);
if (d->m_pageSpacing == spacing)
return;
d->m_pageSpacing = spacing;
d->invalidateDocumentLayout();
emit pageSpacingChanged(d->m_pageSpacing);
}
QMargins QPdfView::documentMargins() const
{
Q_D(const QPdfView);
return d->m_documentMargins;
}
void QPdfView::setDocumentMargins(QMargins margins)
{
Q_D(QPdfView);
if (d->m_documentMargins == margins)
return;
d->m_documentMargins = margins;
d->invalidateDocumentLayout();
emit documentMarginsChanged(d->m_documentMargins);
}
void QPdfView::paintEvent(QPaintEvent *event)
{
Q_D(QPdfView);
QPainter painter(viewport());
painter.fillRect(event->rect(), palette().brush(QPalette::Dark));
painter.translate(-d->m_viewport.x(), -d->m_viewport.y());
for (auto it = d->m_documentLayout.pageGeometries.cbegin(); it != d->m_documentLayout.pageGeometries.cend(); ++it) {
const QRect pageGeometry = it.value();
if (pageGeometry.intersects(d->m_viewport)) { // page needs to be painted
painter.fillRect(pageGeometry, Qt::white);
const int page = it.key();
const auto pageIt = d->m_pageCache.constFind(page);
if (pageIt != d->m_pageCache.cend()) {
const QImage &img = pageIt.value();
painter.drawImage(pageGeometry.topLeft(), img);
} else {
d->m_pageRenderer->requestPage(page, pageGeometry.size());
}
}
}
}
void QPdfView::resizeEvent(QResizeEvent *event)
{
Q_D(QPdfView);
QAbstractScrollArea::resizeEvent(event);
d->updateScrollBars();
d->calculateViewport();
}
void QPdfView::scrollContentsBy(int dx, int dy)
{
Q_D(QPdfView);
QAbstractScrollArea::scrollContentsBy(dx, dy);
d->calculateViewport();
}
QT_END_NAMESPACE
#include "moc_qpdfview.cpp"