diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml
index a2bccab445ca1a5b83bd3cc94d67ab79fc720220..777a9660bb7e452f30c8ec3d9561682395b51895 100644
--- a/examples/pdf/pdfviewer/viewer.qml
+++ b/examples/pdf/pdfviewer/viewer.qml
@@ -80,57 +80,57 @@ ApplicationWindow {
             ToolButton {
                 action: Action {
                     shortcut: StandardKey.ZoomIn
-                    enabled: pageView.sourceSize.width < 10000
+                    enabled: view.sourceSize.width < 10000
                     icon.source: "resources/zoom-in.svg"
-                    onTriggered: pageView.renderScale *= root.scaleStep
+                    onTriggered: view.renderScale *= root.scaleStep
                 }
             }
             ToolButton {
                 action: Action {
                     shortcut: StandardKey.ZoomOut
-                    enabled: pageView.sourceSize.width > 50
+                    enabled: view.sourceSize.width > 50
                     icon.source: "resources/zoom-out.svg"
-                    onTriggered: pageView.renderScale /= root.scaleStep
+                    onTriggered: view.renderScale /= root.scaleStep
                 }
             }
             ToolButton {
                 action: Action {
                     icon.source: "resources/zoom-fit-width.svg"
-                    onTriggered: pageView.scaleToWidth(root.contentItem.width, root.contentItem.height)
+                    onTriggered: view.scaleToWidth(root.contentItem.width, root.contentItem.height)
                 }
             }
             ToolButton {
                 action: Action {
                     icon.source: "resources/zoom-fit-best.svg"
-                    onTriggered: pageView.scaleToPage(root.contentItem.width, root.contentItem.height)
+                    onTriggered: view.scaleToPage(root.contentItem.width, root.contentItem.height)
                 }
             }
             ToolButton {
                 action: Action {
                     shortcut: "Ctrl+0"
                     icon.source: "resources/zoom-original.svg"
-                    onTriggered: pageView.resetScale()
+                    onTriggered: view.resetScale()
                 }
             }
             ToolButton {
                 action: Action {
                     shortcut: "Ctrl+L"
                     icon.source: "resources/rotate-left.svg"
-                    onTriggered: pageView.rotation -= 90
+                    onTriggered: view.pageRotation -= 90
                 }
             }
             ToolButton {
                 action: Action {
                     shortcut: "Ctrl+R"
                     icon.source: "resources/rotate-right.svg"
-                    onTriggered: pageView.rotation += 90
+                    onTriggered: view.pageRotation += 90
                 }
             }
             ToolButton {
                 action: Action {
                     icon.source: "resources/go-previous-view-page.svg"
-                    enabled: pageView.backEnabled
-                    onTriggered: pageView.back()
+                    enabled: view.backEnabled
+                    onTriggered: view.back()
                 }
                 ToolTip.visible: enabled && hovered
                 ToolTip.delay: 2000
@@ -141,22 +141,22 @@ ApplicationWindow {
                 from: 1
                 to: document.pageCount
                 editable: true
-                value: pageView.currentPage + 1
-                onValueModified: pageView.goToPage(value - 1)
+                value: view.currentPage + 1
+                onValueModified: view.goToPage(value - 1)
                 Shortcut {
                     sequence: StandardKey.MoveToPreviousPage
-                    onActivated: pageView.goToPage(currentPageSB.value - 2)
+                    onActivated: view.goToPage(currentPageSB.value - 2)
                 }
                 Shortcut {
                     sequence: StandardKey.MoveToNextPage
-                    onActivated: pageView.goToPage(currentPageSB.value)
+                    onActivated: view.goToPage(currentPageSB.value)
                 }
             }
             ToolButton {
                 action: Action {
                     icon.source: "resources/go-next-view-page.svg"
-                    enabled: pageView.forwardEnabled
-                    onTriggered: pageView.forward()
+                    enabled: view.forwardEnabled
+                    onTriggered: view.forward()
                 }
                 ToolTip.visible: enabled && hovered
                 ToolTip.delay: 2000
@@ -166,8 +166,8 @@ ApplicationWindow {
                 action: Action {
                     shortcut: StandardKey.Copy
                     icon.source: "resources/edit-copy.svg"
-                    enabled: pageView.selectedText !== ""
-                    onTriggered: pageView.copySelectionToClipboard()
+                    enabled: view.selectedText !== ""
+                    onTriggered: view.copySelectionToClipboard()
                 }
             }
             Shortcut {
@@ -199,9 +199,9 @@ ApplicationWindow {
         }
     }
 
-    PdfPageView {
-        id: pageView
-        x: searchDrawer.position * searchDrawer.width // TODO binding gets broken during centering
+    PdfScrollablePageView {
+        id: view
+        anchors.fill: parent
         document: PdfDocument {
             id: document
             source: Qt.resolvedUrl(root.source)
@@ -210,21 +210,6 @@ ApplicationWindow {
         searchString: searchField.text
     }
 
-    WheelHandler {
-        rotationScale: 15
-        target: pageView
-        property: "x"
-        orientation: Qt.Horizontal
-        acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
-    }
-    WheelHandler {
-        rotationScale: 15
-        target: pageView
-        property: "y"
-        orientation: Qt.Vertical
-        acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
-    }
-
     Drawer {
         id: searchDrawer
         edge: Qt.LeftEdge
@@ -249,7 +234,7 @@ ApplicationWindow {
                     action: Action {
                         icon.source: "resources/go-up-search.svg"
                         shortcut: StandardKey.FindPrevious
-                        onTriggered: pageView.searchBack()
+                        onTriggered: view.searchBack()
                     }
                     ToolTip.visible: enabled && hovered
                     ToolTip.delay: 2000
@@ -279,7 +264,7 @@ ApplicationWindow {
                     action: Action {
                         icon.source: "resources/go-down-search.svg"
                         shortcut: StandardKey.FindNext
-                        onTriggered: pageView.searchForward()
+                        onTriggered: view.searchForward()
                     }
                     ToolTip.visible: enabled && hovered
                     ToolTip.delay: 2000
@@ -291,7 +276,7 @@ ApplicationWindow {
                 Layout.fillWidth: true
                 Layout.fillHeight: true
                 clip: true
-                model: pageView.searchModel
+                model: view.searchModel
                 ScrollBar.vertical: ScrollBar { }
                 delegate: ItemDelegate {
                     width: parent ? parent.width : 0
@@ -299,8 +284,8 @@ ApplicationWindow {
                     highlighted: ListView.isCurrentItem
                     onClicked: {
                         searchResultsList.currentIndex = index
-                        pageView.goToLocation(page, location, 0)
-                        pageView.searchModel.currentResult = indexOnPage
+                        view.goToLocation(page, location, 0)
+                        view.searchModel.currentResult = indexOnPage
                     }
                 }
             }
@@ -308,9 +293,9 @@ ApplicationWindow {
     }
 
     footer: Label {
-        property size implicitPointSize: document.pagePointSize(pageView.currentPage)
-        text: "page " + (pageView.currentPage + 1) + " of " + document.pageCount +
-              " scale " + pageView.renderScale.toFixed(2) +
+        property size implicitPointSize: document.pagePointSize(view.currentPage)
+        text: "page " + (view.currentPage + 1) + " of " + document.pageCount +
+              " scale " + view.renderScale.toFixed(2) +
               " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + "pts"
         visible: document.status === PdfDocument.Ready
     }
diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp
index a831a09b63d280083643d49a49d92bf02f4d2ec4..bb68a817ea874670594e54ac826636bd3e69440d 100644
--- a/src/pdf/quick/plugin.cpp
+++ b/src/pdf/quick/plugin.cpp
@@ -90,6 +90,7 @@ public:
 
         qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfPageView.qml"), uri, 5, 15, "PdfPageView");
         qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfMultiPageView.qml"), uri, 5, 15, "PdfMultiPageView");
+        qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfScrollablePageView.qml"), uri, 5, 15, "PdfScrollablePageView");
     }
 };
 
diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml
new file mode 100644
index 0000000000000000000000000000000000000000..59bec04a23d75b199717a2d1d5b0dccab3c2fb59
--- /dev/null
+++ b/src/pdf/quick/qml/PdfScrollablePageView.qml
@@ -0,0 +1,249 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** 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$
+**
+****************************************************************************/
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtQuick.Pdf 5.15
+import QtQuick.Shapes 1.14
+import Qt.labs.animation 1.0
+
+Flickable {
+    // public API
+    // TODO 5.15: required property
+    property var document: undefined
+    property bool debug: false
+    property alias status: image.status
+
+    property alias selectedText: selection.text
+    function copySelectionToClipboard() {
+        selection.copyToClipboard()
+    }
+
+    // page navigation
+    property alias currentPage: navigationStack.currentPage
+    property alias backEnabled: navigationStack.backAvailable
+    property alias forwardEnabled: navigationStack.forwardAvailable
+    function back() { navigationStack.back() }
+    function forward() { navigationStack.forward() }
+    function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) }
+    function goToLocation(page, location, zoom) {
+        if (zoom > 0)
+            root.renderScale = zoom
+        navigationStack.push(page, location, zoom)
+    }
+
+    // page scaling
+    property real renderScale: 1
+    property real pageRotation: 0
+    property alias sourceSize: image.sourceSize
+    function resetScale() {
+        paper.scale = 1
+        root.renderScale = 1
+    }
+    function scaleToWidth(width, height) {
+        var pagePointSize = document.pagePointSize(navigationStack.currentPage)
+        root.renderScale = root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width)
+        if (debug)
+            console.log("scaling", pagePointSize, "to fit", root.width, "rotated?", paper.rot90, "scale", root.renderScale)
+        root.contentX = 0
+        root.contentY = 0
+    }
+    function scaleToPage(width, height) {
+
+        var pagePointSize = document.pagePointSize(navigationStack.currentPage)
+        root.renderScale = Math.min(
+                    root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width),
+                    root.height / (paper.rot90 ? pagePointSize.width : pagePointSize.height) )
+        root.contentX = 0
+        root.contentY = 0
+    }
+
+    // text search
+    property alias searchModel: searchModel
+    property alias searchString: searchModel.searchString
+    function searchBack() { --searchModel.currentResult }
+    function searchForward() { ++searchModel.currentResult }
+
+    // implementation
+    id: root
+    contentWidth: paper.width
+    contentHeight: paper.height
+    ScrollBar.vertical: ScrollBar { }
+    ScrollBar.horizontal: ScrollBar { }
+
+    onRenderScaleChanged: {
+        image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale
+        image.sourceSize.height = 0
+        paper.scale = 1
+    }
+
+    PdfSelection {
+        id: selection
+        document: root.document
+        page: navigationStack.currentPage
+        fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / image.pageScale,
+                            textSelectionDrag.centroid.pressPosition.y / image.pageScale)
+        toPoint: Qt.point(textSelectionDrag.centroid.position.x / image.pageScale,
+                          textSelectionDrag.centroid.position.y / image.pageScale)
+        hold: !textSelectionDrag.active && !tapHandler.pressed
+    }
+
+    PdfSearchModel {
+        id: searchModel
+        document: root.document === undefined ? null : root.document
+        onCurrentPageChanged: root.goToPage(currentPage)
+    }
+
+    PdfNavigationStack {
+        id: navigationStack
+        onCurrentPageChanged: searchModel.currentPage = currentPage
+        // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first!
+        onCurrentZoomChanged: root.renderScale = currentZoom
+        // TODO deal with horizontal location (need WheelHandler or Flickable probably)
+    }
+
+    Rectangle {
+        id: paper
+        width: rot90 ? image.height : image.width
+        height: rot90 ? image.width : image.height
+        property real rotationModulus: Math.abs(root.pageRotation % 180)
+        property bool rot90: rotationModulus > 45 && rotationModulus < 135
+
+        Image {
+            id: image
+            currentFrame: navigationStack.currentPage
+            source: document.status === PdfDocument.Ready ? document.source : ""
+            asynchronous: true
+            fillMode: Image.PreserveAspectFit
+            rotation: root.pageRotation
+            anchors.centerIn: parent
+            property real pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width
+        }
+
+        Shape {
+            anchors.fill: parent
+            opacity: 0.25
+            visible: image.status === Image.Ready
+            ShapePath {
+                strokeWidth: 1
+                strokeColor: "cyan"
+                fillColor: "steelblue"
+                scale: Qt.size(image.pageScale, image.pageScale)
+                PathMultiline {
+                    paths: searchModel.currentPageBoundingPolygons
+                }
+            }
+            ShapePath {
+                strokeWidth: 1
+                strokeColor: "orange"
+                fillColor: "cyan"
+                scale: Qt.size(image.pageScale, image.pageScale)
+                PathMultiline {
+                    paths: searchModel.currentResultBoundingPolygons
+                }
+            }
+            ShapePath {
+                fillColor: "orange"
+                scale: Qt.size(image.pageScale, image.pageScale)
+                PathMultiline {
+                    paths: selection.geometry
+                }
+            }
+        }
+
+        Repeater {
+            model: PdfLinkModel {
+                id: linkModel
+                document: root.document
+                page: navigationStack.currentPage
+            }
+            delegate: Rectangle {
+                color: "transparent"
+                border.color: "lightgrey"
+                x: rect.x * image.pageScale
+                y: rect.y * image.pageScale
+                width: rect.width * image.pageScale
+                height: rect.height * image.pageScale
+                MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15
+                    anchors.fill: parent
+                    cursorShape: Qt.PointingHandCursor
+                    onClicked: {
+                        if (page >= 0)
+                            navigationStack.push(page, Qt.point(0, 0), root.renderScale)
+                        else
+                            Qt.openUrlExternally(url)
+                    }
+                }
+            }
+        }
+
+        PinchHandler {
+            id: pinch
+            minimumScale: 0.1
+            maximumScale: root.renderScale < 4 ? 2 : 1
+            minimumRotation: 0
+            maximumRotation: 0
+            enabled: image.sourceSize.width < 5000
+            onActiveChanged:
+                if (!active) {
+                    var newSourceWidth = image.sourceSize.width * paper.scale
+                    var ratio = newSourceWidth / image.sourceSize.width
+                    if (ratio > 1.1 || ratio < 0.9) {
+                        paper.scale = 1
+                        root.renderScale *= ratio
+                    }
+                    // TODO adjust contentX/Y to position the page so the same region is visible
+                    paper.x = 0
+                    paper.y = 0
+                }
+            grabPermissions: PointerHandler.CanTakeOverFromAnything
+        }
+        DragHandler {
+            id: pageMovingMiddleMouseDrag
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
+            acceptedButtons: Qt.MiddleButton
+            snapMode: DragHandler.NoSnap
+        }
+        DragHandler {
+            id: textSelectionDrag
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
+            target: null
+        }
+        TapHandler {
+            id: tapHandler
+            acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
+        }
+    }
+}
diff --git a/src/pdf/quick/qquickpdflinkmodel.cpp b/src/pdf/quick/qquickpdflinkmodel.cpp
index a3f552d17df537ecdf213cf901e7a3588f8554ee..f2ff3fd22cf330589228260fd6c6b4cbbe83ab1b 100644
--- a/src/pdf/quick/qquickpdflinkmodel.cpp
+++ b/src/pdf/quick/qquickpdflinkmodel.cpp
@@ -96,7 +96,7 @@ QT_BEGIN_NAMESPACE
     \endqml
 
     \note General-purpose PDF viewing capabilities are provided by
-    \l PdfPageView and \l PdfMultiPageView. PdfLinkModel is only needed
+    \l PdfScrollablePageView and \l PdfMultiPageView. PdfLinkModel is only needed
     when building PDF view components from scratch.
 */
 
diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro
index a0a39d41459df004533d712c780b2641205a71fc..b62b80346ec62176f9a45084093d2bb579095d68 100644
--- a/src/pdf/quick/quick.pro
+++ b/src/pdf/quick/quick.pro
@@ -8,6 +8,7 @@ IMPORT_VERSION = 1.0
 PDF_QML_FILES = \
     qml/PdfMultiPageView.qml \
     qml/PdfPageView.qml \
+    qml/PdfScrollablePageView.qml \
 
 QML_FILES += $$PDF_QML_FILES qmldir
 
diff --git a/src/pdf/quick/resources.qrc b/src/pdf/quick/resources.qrc
index 282610d4c21659fa6f3d2a60aea3c73acad13404..20cac48270f51830e1aa419a5d34191c7831ddd7 100644
--- a/src/pdf/quick/resources.qrc
+++ b/src/pdf/quick/resources.qrc
@@ -2,5 +2,6 @@
   <qresource prefix="/qt-project.org/qtpdf">
     <file>qml/PdfMultiPageView.qml</file>
     <file>qml/PdfPageView.qml</file>
+    <file>qml/PdfScrollablePageView.qml</file>
   </qresource>
 </RCC>