diff options
author | Shawn Rutledge <shawn.rutledge@qt.io> | 2020-02-21 11:38:27 +0100 |
---|---|---|
committer | Shawn Rutledge <shawn.rutledge@qt.io> | 2020-02-21 11:40:49 +0100 |
commit | d9349a299f66fb154ad24f410451872a7ca253fb (patch) | |
tree | 2e8258ef3679707a2a9245c85bc8490251b3e256 | |
parent | 50bc8b124705c33c5e27f035b1eab756e14247ba (diff) | |
parent | c0aa9d794378846e4cc0b6fe94f2765bc31cefdd (diff) | |
download | qtwebengine-d9349a299f66fb154ad24f410451872a7ca253fb.tar.gz |
Merge remote-tracking branch 'origin/wip/qtpdf' into 5.15v5.15.0-beta1
The feature set is mostly in place (except for some known shortcomings)
and we need the merge to build it on iOS.
Task-number: QTBUG-69519
Change-Id: Ib1ac82a9a7e0830d98d1c4327a1b15d4d7f4d4c1
74 files changed, 4837 insertions, 210 deletions
diff --git a/examples/pdf/multipage/main.cpp b/examples/pdf/multipage/main.cpp new file mode 100644 index 000000000..35aaa8c64 --- /dev/null +++ b/examples/pdf/multipage/main.cpp @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char* argv[]) +{ + QCoreApplication::setApplicationName("Qt Quick Multi-page PDF Viewer Example"); + QCoreApplication::setOrganizationName("QtProject"); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:///pdfviewer/viewer.qml"))); + if (app.arguments().count() > 1) { + QUrl toLoad = QUrl::fromUserInput(app.arguments().at(1)); + engine.rootObjects().first()->setProperty("source", toLoad); + } else { + engine.rootObjects().first()->setProperty("source", QStringLiteral("resources/test.pdf")); + } + + + return app.exec(); +} diff --git a/examples/pdf/multipage/multipage.pro b/examples/pdf/multipage/multipage.pro new file mode 100644 index 000000000..5df9e653d --- /dev/null +++ b/examples/pdf/multipage/multipage.pro @@ -0,0 +1,14 @@ +TEMPLATE = app + +QT += qml quick pdf widgets svg + +SOURCES += main.cpp + +RESOURCES += \ + viewer.qrc +EXAMPLE_FILES = \ + viewer.qml + +target.path = $$[QT_INSTALL_EXAMPLES]/pdf/multipage +INSTALLS += target + diff --git a/examples/pdf/multipage/resources/document-open.svg b/examples/pdf/multipage/resources/document-open.svg new file mode 100644 index 000000000..bf23123a3 --- /dev/null +++ b/examples/pdf/multipage/resources/document-open.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="m4 4v24h24l-1-1h-22v-13h5l3-3h14v16l1 1v-21h-10l-3-3z" + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/edit-clear.svg b/examples/pdf/multipage/resources/edit-clear.svg new file mode 100644 index 000000000..1c35aaf04 --- /dev/null +++ b/examples/pdf/multipage/resources/edit-clear.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 8 3 L 0.94335938 10.056641 L 0 11 L 0.94335938 11.943359 L 8 19 L 20.333984 19 L 22 19 L 22 3 L 20.333984 3 L 8 3 z M 11.320312 7 L 14 9.6796875 L 16.679688 7 L 18 8.3203125 L 15.320312 11 L 18 13.679688 L 16.679688 15 L 14 12.320312 L 11.320312 15 L 10 13.679688 L 12.679688 11 L 10 8.3203125 L 11.320312 7 z " + class="ColorScheme-Text" + transform="translate(1,1)" + /> +</svg> diff --git a/examples/pdf/multipage/resources/edit-copy.svg b/examples/pdf/multipage/resources/edit-copy.svg new file mode 100644 index 000000000..9dd16877d --- /dev/null +++ b/examples/pdf/multipage/resources/edit-copy.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m4 3v1 13h1 2 1v1 1h6l4-4v-1-7-1h-2v-3h-1-10-1m1 1h10v2h-7v1 9h-1-2v-12m4 3h8v7h-3-1v1 3h-4v-11" + class="ColorScheme-Text" + transform="translate(1,1)" + /> +</svg> diff --git a/examples/pdf/multipage/resources/go-down-search.svg b/examples/pdf/multipage/resources/go-down-search.svg new file mode 100644 index 000000000..ae17ab93b --- /dev/null +++ b/examples/pdf/multipage/resources/go-down-search.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4.7070312 8 L 4 8.7070312 L 10.125 14.832031 L 12 16.707031 L 13.875 14.832031 L 20 8.7070312 L 19.292969 8 L 13.167969 14.125 L 12 15.292969 L 10.832031 14.125 L 4.7070312 8 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/go-next-view-page.svg b/examples/pdf/multipage/resources/go-next-view-page.svg new file mode 100644 index 000000000..e453ddbec --- /dev/null +++ b/examples/pdf/multipage/resources/go-next-view-page.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 8.7070312 4 L 8 4.7070312 L 14.125 10.832031 L 15.292969 12 L 14.125 13.167969 L 8 19.292969 L 8.7070312 20 L 14.832031 13.875 L 16.707031 12 L 14.832031 10.125 L 8.7070312 4 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/go-previous-view-page.svg b/examples/pdf/multipage/resources/go-previous-view-page.svg new file mode 100644 index 000000000..b032309e9 --- /dev/null +++ b/examples/pdf/multipage/resources/go-previous-view-page.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 15.292969 4 L 9.1679688 10.125 L 7.2929688 12 L 9.1679688 13.875 L 15.292969 20 L 16 19.292969 L 9.875 13.167969 L 8.7070312 12 L 9.875 10.832031 L 16 4.7070312 L 15.292969 4 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/go-up-search.svg b/examples/pdf/multipage/resources/go-up-search.svg new file mode 100644 index 000000000..5cc155873 --- /dev/null +++ b/examples/pdf/multipage/resources/go-up-search.svg @@ -0,0 +1,8 @@ +<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + <path d="M4.707 16L4 15.293l8-8 8 8-.707.707L12 8.707" class="ColorScheme-Text" fill="currentColor"/> +</svg> diff --git a/examples/pdf/multipage/resources/rotate-left.svg b/examples/pdf/multipage/resources/rotate-left.svg new file mode 100644 index 000000000..90ce53c9d --- /dev/null +++ b/examples/pdf/multipage/resources/rotate-left.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <g color="#000" font-weight="400" fill="#474747"> + <path d="M2 9v1c0 .265.093.53.281.719l3.72 3.719 3.718-3.72c.188-.187.281-.453.281-.718V9H9c-.265 0-.53.093-.719.281l-2.28 2.281-2.282-2.28A1.015 1.015 0 0 0 3 9z"/> + <path d="M8.5 3A3.515 3.515 0 0 0 5 6.5V12h2V6.5C7 5.66 7.66 5 8.5 5H13V3z"/> + </g> +</svg> diff --git a/examples/pdf/multipage/resources/rotate-right.svg b/examples/pdf/multipage/resources/rotate-right.svg new file mode 100644 index 000000000..7383d1c84 --- /dev/null +++ b/examples/pdf/multipage/resources/rotate-right.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <g font-weight="400" fill="#474747"> + <path d="M3 3v2h4.5C8.34 5 9 5.66 9 6.5V12h2V6.5C11 4.579 9.421 3 7.5 3z"/> + <path d="M6 9h1c.257 0 .529.13.719.313L10 11.592l2.281-2.28C12.471 9.13 12.743 9 13 9h1v1c0 .31-.09.552-.281.75L10 14.406 6.281 10.75C6.091 10.552 6 10.31 6 10z"/> + </g> +</svg> diff --git a/examples/pdf/multipage/resources/test.pdf b/examples/pdf/multipage/resources/test.pdf Binary files differnew file mode 100644 index 000000000..a9dc1bc29 --- /dev/null +++ b/examples/pdf/multipage/resources/test.pdf diff --git a/examples/pdf/multipage/resources/zoom-fit-best.svg b/examples/pdf/multipage/resources/zoom-fit-best.svg new file mode 100644 index 000000000..adf302621 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-fit-best.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 4 L 4 5 L 4 8 L 5 8 L 5 5 L 8 5 L 8 4 L 5 4 L 4 4 z M 12 4 L 10 6 L 14 6 L 12 4 z M 16 4 L 16 5 L 19 5 L 19 8 L 20 8 L 20 5 L 20 4 L 19 4 L 16 4 z M 7 7 L 7 17 L 17 17 L 17 7 L 7 7 z M 8 8 L 16 8 L 16 16 L 8 16 L 8 8 z M 6 10 L 4 12 L 6 14 L 6 10 z M 18 10 L 18 14 L 20 12 L 18 10 z M 4 16 L 4 19 L 4 20 L 8 20 L 8 19 L 5 19 L 5 16 L 4 16 z M 19 16 L 19 19 L 16 19 L 16 20 L 20 20 L 20 19 L 20 16 L 19 16 z M 10 18 L 12 20 L 14 18 L 10 18 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/zoom-fit-width.svg b/examples/pdf/multipage/resources/zoom-fit-width.svg new file mode 100644 index 000000000..985ee5205 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-fit-width.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 7 7 L 7 17 L 17 17 L 17 7 L 7 7 z M 8 8 L 16 8 L 16 16 L 8 16 L 8 8 z M 6 10 L 4 12 L 6 14 L 6 10 z M 18 10 L 18 14 L 20 12 L 18 10 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/zoom-in.svg b/examples/pdf/multipage/resources/zoom-in.svg new file mode 100644 index 000000000..efdc9f17d --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-in.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 4 L 4 6 L 5 6 L 5 5 L 6 5 L 6 4 L 4 4 z M 9 4 L 9 5 L 11 5 L 11 4 L 9 4 z M 13 4 L 13 5 L 15 5 L 15 4 L 13 4 z M 18 4 L 18 5 L 19 5 L 19 6 L 20 6 L 20 4 L 18 4 z M 12 8 L 12 9 L 14.292969 9 L 11 12.292969 L 11.707031 13 L 15 9.7070312 L 15 12 L 16 12 L 16 8 L 15 8 L 12 8 z M 4 9 L 4 11 L 5 11 L 5 9 L 4 9 z M 19 9 L 19 11 L 20 11 L 20 9 L 19 9 z M 19 13 L 19 15 L 20 15 L 20 13 L 19 13 z M 4 14 L 4 20 L 10 20 L 10 14 L 4 14 z M 5 15 L 9 15 L 9 19 L 5 19 L 5 15 z M 19 18 L 19 19 L 18 19 L 18 20 L 20 20 L 20 18 L 19 18 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/zoom-original.svg b/examples/pdf/multipage/resources/zoom-original.svg new file mode 100644 index 000000000..1b4080a03 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-original.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 4 L 4 5 L 4 7 L 5 7 L 5 5 L 7 5 L 7 4 L 5 4 L 4 4 z M 17 4 L 17 5 L 19 5 L 19 7 L 20 7 L 20 5 L 20 4 L 19 4 L 17 4 z M 6 6 L 6 18 L 18 18 L 18 6 L 6 6 z M 7 7 L 17 7 L 17 17 L 7 17 L 7 7 z M 4 17 L 4 19 L 4 20 L 7 20 L 7 19 L 5 19 L 5 17 L 4 17 z M 19 17 L 19 19 L 17 19 L 17 20 L 20 20 L 20 17 L 19 17 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/resources/zoom-out.svg b/examples/pdf/multipage/resources/zoom-out.svg new file mode 100644 index 000000000..fcde9e526 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-out.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 4 L 4 11 L 5 11 L 5 5 L 19 5 L 19 19 L 13 19 L 13 20 L 20 20 L 20 19 L 20 5 L 20 4 L 5 4 L 4 4 z M 15.292969 8 L 12 11.292969 L 12 9 L 11 9 L 11 13 L 12 13 L 15 13 L 15 12 L 12.707031 12 L 16 8.7070312 L 15.292969 8 z M 4 14 L 4 16 L 5 16 L 5 15 L 6 15 L 6 14 L 4 14 z M 8 14 L 8 15 L 9 15 L 9 16 L 10 16 L 10 14 L 8 14 z M 4 18 L 4 20 L 6 20 L 6 19 L 5 19 L 5 18 L 4 18 z M 9 18 L 9 19 L 8 19 L 8 20 L 10 20 L 10 18 L 9 18 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml new file mode 100644 index 000000000..9e5f92407 --- /dev/null +++ b/examples/pdf/multipage/viewer.qml @@ -0,0 +1,328 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 +import QtQuick.Window 2.14 +import Qt.labs.platform 1.1 as Platform + +ApplicationWindow { + id: root + width: 800 + height: 1024 + color: "lightgrey" + title: document.title + visible: true + property string source // for main.cpp + + header: ToolBar { + RowLayout { + anchors.fill: parent + anchors.rightMargin: 6 + ToolButton { + action: Action { + shortcut: StandardKey.Open + icon.source: "resources/document-open.svg" + onTriggered: fileDialog.open() + } + } + ToolButton { + action: Action { + shortcut: StandardKey.ZoomIn + enabled: view.renderScale < 10 + icon.source: "resources/zoom-in.svg" + onTriggered: view.renderScale *= Math.sqrt(2) + } + } + ToolButton { + action: Action { + shortcut: StandardKey.ZoomOut + enabled: view.renderScale > 0.1 + icon.source: "resources/zoom-out.svg" + onTriggered: view.renderScale /= Math.sqrt(2) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-width.svg" + onTriggered: view.scaleToWidth(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-best.svg" + onTriggered: view.scaleToPage(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+0" + icon.source: "resources/zoom-original.svg" + onTriggered: view.resetScale() + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+L" + icon.source: "resources/rotate-left.svg" + onTriggered: view.pageRotation -= 90 + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+R" + icon.source: "resources/rotate-right.svg" + onTriggered: view.pageRotation += 90 + } + } + ToolButton { + action: Action { + icon.source: "resources/go-previous-view-page.svg" + enabled: view.backEnbled + onTriggered: view.back() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go back" + } + SpinBox { + id: currentPageSB + from: 1 + to: document.pageCount + editable: true + onValueModified: view.goToPage(value - 1) + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: currentPageSB.value-- + } + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: currentPageSB.value++ + } + } + ToolButton { + action: Action { + icon.source: "resources/go-next-view-page.svg" + enabled: view.forwardEnabled + onTriggered: view.forward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go forward" + } + ToolButton { + action: Action { + shortcut: StandardKey.Copy + icon.source: "resources/edit-copy.svg" + enabled: view.selectedText !== "" + onTriggered: view.copySelectionToClipboard() + } + } + Shortcut { + sequence: StandardKey.Find + onActivated: searchField.forceActiveFocus() + } + Shortcut { + sequence: StandardKey.Quit + onActivated: Qt.quit() + } + } + } + + Platform.FileDialog { + id: fileDialog + title: "Open a PDF file" + nameFilters: [ "PDF files (*.pdf)" ] + onAccepted: document.source = file + } + + Dialog { + id: passwordDialog + title: "Password" + standardButtons: Dialog.Ok | Dialog.Cancel + modal: true + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + width: 300 + + TextField { + id: passwordField + placeholderText: qsTr("Please provide the password") + echoMode: TextInput.Password + width: parent.width + onAccepted: passwordDialog.accept() + } + onAccepted: document.password = passwordField.text + } + + Dialog { + id: errorDialog + title: "Error loading " + document.source + standardButtons: Dialog.Ok + modal: true + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + width: 300 + + Label { + id: errorField + text: document.error + } + } + + PdfDocument { + id: document + source: Qt.resolvedUrl(root.source) + onStatusChanged: { + if (status === PdfDocument.Error) errorDialog.open() + view.document = (status === PdfDocument.Ready ? document : undefined) + } + onPasswordRequired: { + passwordDialog.open() + passwordField.forceActiveFocus() + } + } + + PdfMultiPageView { + id: view + anchors.fill: parent + anchors.leftMargin: searchDrawer.position * searchDrawer.width + document: root.document + searchString: searchField.text + onCurrentPageChanged: currentPageSB.value = view.currentPage + 1 + } + + Drawer { + id: searchDrawer + edge: Qt.LeftEdge + modal: false + width: 300 + y: root.header.height + height: view.height + dim: false + clip: true + ListView { + id: searchResultsList + anchors.fill: parent + anchors.margins: 2 + model: view.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage + } + } + } + } + + footer: ToolBar { + height: footerRow.implicitHeight + RowLayout { + id: footerRow + anchors.fill: parent + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: view.searchBack() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 150 + Layout.fillWidth: true + onAccepted: searchDrawer.open() + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } + } + } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: view.searchForward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + Label { + id: statusLabel + property size implicitPointSize: document.pagePointSize(view.currentPage) + text: "page " + (currentPageSB.value) + " of " + document.pageCount + + " scale " + view.renderScale.toFixed(2) + + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" + visible: document.pageCount > 0 + } + } + } +} diff --git a/examples/pdf/multipage/viewer.qrc b/examples/pdf/multipage/viewer.qrc new file mode 100644 index 000000000..1b6fa52f7 --- /dev/null +++ b/examples/pdf/multipage/viewer.qrc @@ -0,0 +1,20 @@ +<RCC> + <qresource prefix="/pdfviewer"> + <file>viewer.qml</file> + <file>resources/document-open.svg</file> + <file>resources/edit-clear.svg</file> + <file>resources/edit-copy.svg</file> + <file>resources/go-down-search.svg</file> + <file>resources/go-next-view-page.svg</file> + <file>resources/go-previous-view-page.svg</file> + <file>resources/go-up-search.svg</file> + <file>resources/rotate-left.svg</file> + <file>resources/rotate-right.svg</file> + <file>resources/test.pdf</file> + <file>resources/zoom-in.svg</file> + <file>resources/zoom-fit-best.svg</file> + <file>resources/zoom-fit-width.svg</file> + <file>resources/zoom-original.svg</file> + <file>resources/zoom-out.svg</file> + </qresource> +</RCC> diff --git a/examples/pdf/pdf.pro b/examples/pdf/pdf.pro index 45df33e46..7130f3560 100644 --- a/examples/pdf/pdf.pro +++ b/examples/pdf/pdf.pro @@ -1,3 +1,3 @@ TEMPLATE=subdirs -SUBDIRS += pdfviewer +SUBDIRS += pdfviewer multipage diff --git a/examples/pdf/pdfviewer/main.cpp b/examples/pdf/pdfviewer/main.cpp index 6b94a3de1..5f65e3061 100644 --- a/examples/pdf/pdfviewer/main.cpp +++ b/examples/pdf/pdfviewer/main.cpp @@ -53,17 +53,19 @@ int main(int argc, char* argv[]) { - QApplication app(argc, argv); QCoreApplication::setApplicationName("Qt Quick PDF Viewer Example"); QCoreApplication::setOrganizationName("QtProject"); QCoreApplication::setApplicationVersion(QT_VERSION_STR); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///pdfviewer/viewer.qml"))); if (app.arguments().count() > 1) { QUrl toLoad = QUrl::fromUserInput(app.arguments().at(1)); engine.rootObjects().first()->setProperty("source", toLoad); + } else { + engine.rootObjects().first()->setProperty("source", QStringLiteral("resources/test.pdf")); } return app.exec(); diff --git a/examples/pdf/pdfviewer/pdfviewer.pro b/examples/pdf/pdfviewer/pdfviewer.pro index 697349cee..b8817c9be 100644 --- a/examples/pdf/pdfviewer/pdfviewer.pro +++ b/examples/pdf/pdfviewer/pdfviewer.pro @@ -1,6 +1,6 @@ TEMPLATE = app -QT += qml quick pdf widgets +QT += qml quick pdf widgets svg SOURCES += main.cpp diff --git a/examples/pdf/pdfviewer/resources/edit-copy.svg b/examples/pdf/pdfviewer/resources/edit-copy.svg new file mode 100644 index 000000000..9dd16877d --- /dev/null +++ b/examples/pdf/pdfviewer/resources/edit-copy.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m4 3v1 13h1 2 1v1 1h6l4-4v-1-7-1h-2v-3h-1-10-1m1 1h10v2h-7v1 9h-1-2v-12m4 3h8v7h-3-1v1 3h-4v-11" + class="ColorScheme-Text" + transform="translate(1,1)" + /> +</svg> diff --git a/examples/pdf/pdfviewer/resources/go-down-search.svg b/examples/pdf/pdfviewer/resources/go-down-search.svg new file mode 100644 index 000000000..ae17ab93b --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-down-search.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4.7070312 8 L 4 8.7070312 L 10.125 14.832031 L 12 16.707031 L 13.875 14.832031 L 20 8.7070312 L 19.292969 8 L 13.167969 14.125 L 12 15.292969 L 10.832031 14.125 L 4.7070312 8 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/pdfviewer/resources/go-up-search.svg b/examples/pdf/pdfviewer/resources/go-up-search.svg new file mode 100644 index 000000000..5cc155873 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-up-search.svg @@ -0,0 +1,8 @@ +<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + <path d="M4.707 16L4 15.293l8-8 8 8-.707.707L12 8.707" class="ColorScheme-Text" fill="currentColor"/> +</svg> diff --git a/examples/pdf/pdfviewer/resources/test.pdf b/examples/pdf/pdfviewer/resources/test.pdf Binary files differnew file mode 100644 index 000000000..a9dc1bc29 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/test.pdf diff --git a/examples/pdf/pdfviewer/resources/zoom-fit-best.svg b/examples/pdf/pdfviewer/resources/zoom-fit-best.svg new file mode 100644 index 000000000..adf302621 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/zoom-fit-best.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 4 L 4 5 L 4 8 L 5 8 L 5 5 L 8 5 L 8 4 L 5 4 L 4 4 z M 12 4 L 10 6 L 14 6 L 12 4 z M 16 4 L 16 5 L 19 5 L 19 8 L 20 8 L 20 5 L 20 4 L 19 4 L 16 4 z M 7 7 L 7 17 L 17 17 L 17 7 L 7 7 z M 8 8 L 16 8 L 16 16 L 8 16 L 8 8 z M 6 10 L 4 12 L 6 14 L 6 10 z M 18 10 L 18 14 L 20 12 L 18 10 z M 4 16 L 4 19 L 4 20 L 8 20 L 8 19 L 5 19 L 5 16 L 4 16 z M 19 16 L 19 19 L 16 19 L 16 20 L 20 20 L 20 19 L 20 16 L 19 16 z M 10 18 L 12 20 L 14 18 L 10 18 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/pdfviewer/resources/zoom-fit-width.svg b/examples/pdf/pdfviewer/resources/zoom-fit-width.svg new file mode 100644 index 000000000..985ee5205 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/zoom-fit-width.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 7 7 L 7 17 L 17 17 L 17 7 L 7 7 z M 8 8 L 16 8 L 16 16 L 8 16 L 8 8 z M 6 10 L 4 12 L 6 14 L 6 10 z M 18 10 L 18 14 L 20 12 L 18 10 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index adc2a4b5b..e3bb4b474 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -47,22 +47,23 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 import QtQuick.Pdf 5.15 -import QtQuick.Shapes 1.15 -import QtQuick.Window 2.15 +import QtQuick.Shapes 1.14 +import QtQuick.Window 2.14 +import Qt.labs.animation 1.0 import Qt.labs.platform 1.1 as Platform ApplicationWindow { id: root width: 800 - height: 640 + height: 1024 color: "lightgrey" title: document.title visible: true - property alias source: document.source // for main.cpp + property string source // for main.cpp property real scaleStep: Math.sqrt(2) header: ToolBar { @@ -79,74 +80,94 @@ 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: view.scaleToWidth(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-best.svg" + onTriggered: view.scaleToPage(root.contentItem.width, root.contentItem.height) } } ToolButton { action: Action { shortcut: "Ctrl+0" icon.source: "resources/zoom-original.svg" - onTriggered: pageView.renderScale = 1 + 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 { - shortcut: StandardKey.MoveToPreviousPage icon.source: "resources/go-previous-view-page.svg" - enabled: pageView.currentPage > 0 - onTriggered: pageView.currentPage-- + enabled: view.backEnabled + onTriggered: view.back() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go back" + } + SpinBox { + id: currentPageSB + from: 1 + to: document.pageCount + editable: true + value: view.currentPage + 1 + onValueModified: view.goToPage(value - 1) + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: view.goToPage(currentPageSB.value - 2) + } + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: view.goToPage(currentPageSB.value) } } ToolButton { action: Action { - shortcut: StandardKey.MoveToNextPage icon.source: "resources/go-next-view-page.svg" - enabled: pageView.currentPage < pageView.pageCount - 1 - onTriggered: pageView.currentPage++ + enabled: view.forwardEnabled + onTriggered: view.forward() } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go forward" } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 - } - TapHandler { - onTapped: searchField.clear() - } + ToolButton { + action: Action { + shortcut: StandardKey.Copy + icon.source: "resources/edit-copy.svg" + enabled: view.selectedText !== "" + onTriggered: view.copySelectionToClipboard() } } Shortcut { @@ -182,21 +203,100 @@ ApplicationWindow { } } - PdfPageView { - id: pageView + PdfScrollablePageView { + id: view + anchors.fill: parent document: PdfDocument { id: document + source: Qt.resolvedUrl(root.source) onStatusChanged: if (status === PdfDocument.Error) errorDialog.open() } searchString: searchField.text } - footer: Label { - property size implicitPointSize: document.pagePointSize(pageView.currentPage) - text: "page " + (pageView.currentPage + 1) + " of " + pageView.pageCount + - " scale " + pageView.renderScale.toFixed(2) + - " sourceSize " + pageView.sourceSize.width.toFixed(1) + "x" + pageView.sourceSize.height.toFixed(1) + - " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) - visible: pageView.pageCount > 0 + Drawer { + id: searchDrawer + edge: Qt.LeftEdge + modal: false + width: 300 + y: root.header.height + height: view.height + dim: false + clip: true + ListView { + id: searchResultsList + anchors.fill: parent + anchors.margins: 2 + model: view.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage + } + } + } + } + + footer: ToolBar { + height: footerRow.implicitHeight + RowLayout { + id: footerRow + anchors.fill: parent + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: view.searchBack() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 150 + Layout.maximumWidth: 300 + Layout.fillWidth: true + onAccepted: searchDrawer.open() + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } + } + } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: view.searchForward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + Label { + Layout.fillWidth: true + 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/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc index 78f9c8d30..1b6fa52f7 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -1,14 +1,20 @@ <RCC> <qresource prefix="/pdfviewer"> <file>viewer.qml</file> + <file>resources/document-open.svg</file> <file>resources/edit-clear.svg</file> + <file>resources/edit-copy.svg</file> + <file>resources/go-down-search.svg</file> <file>resources/go-next-view-page.svg</file> <file>resources/go-previous-view-page.svg</file> + <file>resources/go-up-search.svg</file> <file>resources/rotate-left.svg</file> <file>resources/rotate-right.svg</file> + <file>resources/test.pdf</file> <file>resources/zoom-in.svg</file> + <file>resources/zoom-fit-best.svg</file> + <file>resources/zoom-fit-width.svg</file> <file>resources/zoom-original.svg</file> <file>resources/zoom-out.svg</file> - <file>resources/document-open.svg</file> </qresource> </RCC> diff --git a/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc b/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc new file mode 100644 index 000000000..9c7b3deed --- /dev/null +++ b/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example pdfviewer + \ingroup qtpdf-examples + + \title PDF Viewer Example + \brief Renders PDF documents. + + \image pdfviewer.png + + \e {PDF Viewer} demonstrates how to use the \l QPdfDocument class to render + PDF documents and the \l QPdfPageNavigation class to navigate them. + + Qt Creator and the integrated Qt Designer were used to create the example + UI and to connect it to the code. This affects the code, which might be + somewhat different to what you would typically write by hand. + For more information about using Qt Designer, see \l{Qt Designer Manual} + and \l{Qt Creator: Creating a Qt Widget Based Application}. + + \include examples-run.qdocinc + + \section1 Creating the Main Window + + The MainWindow class inherits the QMainWindow class: + + \quotefromfile pdfviewer/mainwindow.h + \skipto public QMainWindow + \printuntil ~MainWindow() + + The class declares public and private slots that match the actions of the + selectors: + + \printuntil on_actionContinuous_triggered() + + The actual layout of the main window is specified in a \c{.ui} file. The + widgets and actions are available at runtime in the \c ui member variable. + + \printuntil Ui:: + + The \c m_zoomSelector variable holds the zoom selector and the + \c m_pageSelector holds the page selector. The \c m_document + variable is an instance of the QPdfDocument class that contains + the PDF document. + + \printuntil } + + The actual setup of the different objects is done in the MainWindow + constructor: + + \quotefromfile pdfviewer/mainwindow.cpp + \skipto MainWindow:: + \printuntil { + + The constructor first calls \c setupUi() to construct the zoom and page + selectors according to the UI file. We set the maximum width of the + selectors. + + \printuntil addWidget(m_pageSelector) + + We use the \l QPdfPageNavigation class to handle the navigation through a + PDF document: + + \printuntil setPageNavigation + + We connect the \c zoomModeChanged and \c zoomFactor changed signals of the + PDF view to the functions that reset the zoom selector: + + \printuntil reset() + + We then load the PDF document to the viewer: + + \dots + \skipto pdfView + \printuntil ; + + Finally, we connect the \c zoomFactorChanged signal to the function that + sets the value of the zoom selector: + + \printuntil } + + \section1 Files and Attributions +*/ diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h new file mode 100644 index 000000000..325863226 --- /dev/null +++ b/src/pdf/api/qpdfdestination.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFDESTINATION_H +#define QPDFDESTINATION_H + +#include <QtPdf/qtpdfglobal.h> +#include <QtCore/qdebug.h> +#include <QtCore/qobject.h> +#include <QtCore/qpoint.h> +#include <QtCore/qshareddata.h> + +QT_BEGIN_NAMESPACE + +class QPdfDestinationPrivate; + +class Q_PDF_EXPORT QPdfDestination +{ + Q_GADGET + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(int page READ page) + Q_PROPERTY(QPointF location READ location) + Q_PROPERTY(qreal zoom READ zoom) + +public: + QPdfDestination(const QPdfDestination &other); + ~QPdfDestination(); + QPdfDestination &operator=(const QPdfDestination &other); + inline QPdfDestination &operator=(QPdfDestination &&other) noexcept { swap(other); return *this; } + void swap(QPdfDestination &other) noexcept { d.swap(other.d); } + bool isValid() const; + int page() const; + QPointF location() const; + qreal zoom() const; + +protected: + QPdfDestination(); + QPdfDestination(int page, QPointF location, qreal zoom); + QPdfDestination(QPdfDestinationPrivate *d); + friend class QPdfDocument; + friend class QQuickPdfNavigationStack; + +protected: + QExplicitlySharedDataPointer<QPdfDestinationPrivate> d; +}; + +Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfDestination &); + +QT_END_NAMESPACE + +#endif // QPDFDESTINATION_H diff --git a/src/pdf/api/qpdfdestination_p.h b/src/pdf/api/qpdfdestination_p.h new file mode 100644 index 000000000..3520fb795 --- /dev/null +++ b/src/pdf/api/qpdfdestination_p.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFDESTINATION_P_H +#define QPDFDESTINATION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QPointF> + +QT_BEGIN_NAMESPACE + +class QPdfDestinationPrivate : public QSharedData +{ +public: + QPdfDestinationPrivate() = default; + QPdfDestinationPrivate(int page, QPointF location, qreal zoom) + : page(page), + location(location), + zoom(zoom) { } + + int page = -1; + QPointF location; + qreal zoom = 1; +}; + +QT_END_NAMESPACE + +#endif // QPDFDESTINATION_P_H diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h index 1252fa6d2..40df181f4 100644 --- a/src/pdf/api/qpdfdocument.h +++ b/src/pdf/api/qpdfdocument.h @@ -41,6 +41,7 @@ #include <QImage> #include <QObject> #include <QtPdf/QPdfDocumentRenderOptions> +#include "qpdfselection.h" QT_BEGIN_NAMESPACE @@ -111,6 +112,8 @@ public: QImage render(int page, QSize imageSize, QPdfDocumentRenderOptions options = QPdfDocumentRenderOptions()); + Q_INVOKABLE QPdfSelection getSelection(int page, QPointF start, QPointF end); + Q_SIGNALS: void passwordChanged(); void passwordRequired(); @@ -119,7 +122,9 @@ Q_SIGNALS: private: friend class QPdfBookmarkModelPrivate; + friend class QPdfLinkModelPrivate; friend class QPdfSearchModel; + friend class QPdfSearchModelPrivate; Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader()) Q_PRIVATE_SLOT(d, void _q_copyFromSequentialSourceDevice()) diff --git a/src/pdf/api/qpdfdocument_p.h b/src/pdf/api/qpdfdocument_p.h index 928754210..15d8b8259 100644 --- a/src/pdf/api/qpdfdocument_p.h +++ b/src/pdf/api/qpdfdocument_p.h @@ -98,6 +98,7 @@ public: void _q_copyFromSequentialSourceDevice(); void tryLoadDocument(); void checkComplete(); + bool checkPageComplete(int page); void setStatus(QPdfDocument::Status status); static FPDF_BOOL fpdf_IsDataAvail(struct _FX_FILEAVAIL* pThis, size_t offset, size_t size); diff --git a/src/pdf/api/qpdfdocumentrenderoptions.h b/src/pdf/api/qpdfdocumentrenderoptions.h index 99a5db2e3..873be0085 100644 --- a/src/pdf/api/qpdfdocumentrenderoptions.h +++ b/src/pdf/api/qpdfdocumentrenderoptions.h @@ -39,6 +39,7 @@ #include <QtPdf/qpdfnamespace.h> #include <QtCore/QObject> +#include <QtCore/QRect> QT_BEGIN_NAMESPACE @@ -53,6 +54,12 @@ public: Q_DECL_CONSTEXPR QPdf::RenderFlags renderFlags() const Q_DECL_NOTHROW { return static_cast<QPdf::RenderFlags>(bits.renderFlags); } Q_DECL_RELAXED_CONSTEXPR void setRenderFlags(QPdf::RenderFlags _renderFlags) Q_DECL_NOTHROW { bits.renderFlags = _renderFlags; } + Q_DECL_CONSTEXPR QRect scaledClipRect() const Q_DECL_NOTHROW { return m_clipRect; } + Q_DECL_RELAXED_CONSTEXPR void setScaledClipRect(const QRect &r) Q_DECL_NOTHROW { m_clipRect = r; } + + Q_DECL_CONSTEXPR QSize scaledSize() const Q_DECL_NOTHROW { return m_scaledSize; } + Q_DECL_RELAXED_CONSTEXPR void setScaledSize(const QSize &s) Q_DECL_NOTHROW { m_scaledSize = s; } + private: friend Q_DECL_CONSTEXPR inline bool operator==(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW; @@ -68,13 +75,16 @@ private: Bits bits; quint64 data; }; + + QRect m_clipRect; + QSize m_scaledSize; }; Q_DECLARE_TYPEINFO(QPdfDocumentRenderOptions, Q_PRIMITIVE_TYPE); Q_DECL_CONSTEXPR inline bool operator==(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW { - return lhs.data == rhs.data; + return lhs.data == rhs.data && lhs.m_clipRect == rhs.m_clipRect && lhs.m_scaledSize == rhs.m_scaledSize; } Q_DECL_CONSTEXPR inline bool operator!=(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW diff --git a/src/pdf/api/qpdflinkmodel_p.h b/src/pdf/api/qpdflinkmodel_p.h new file mode 100644 index 000000000..cf9c0aad4 --- /dev/null +++ b/src/pdf/api/qpdflinkmodel_p.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFLINKMODEL_P_H +#define QPDFLINKMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qtpdfglobal.h" +#include "qpdfdocument.h" + +#include <QObject> +#include <QAbstractListModel> + +QT_BEGIN_NAMESPACE + +class QPdfLinkModelPrivate; + +class Q_PDF_EXPORT QPdfLinkModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) + +public: + enum class Role : int { + Rect = Qt::UserRole, + Url, + Page, + Location, + Zoom, + _Count + }; + Q_ENUM(Role) + explicit QPdfLinkModel(QObject *parent = nullptr); + ~QPdfLinkModel(); + + QPdfDocument *document() const; + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + int page() const; + +public Q_SLOTS: + void setDocument(QPdfDocument *document); + void setPage(int page); + +Q_SIGNALS: + void documentChanged(); + void pageChanged(int page); + +private Q_SLOTS: + void onStatusChanged(QPdfDocument::Status status); + +private: + QHash<int, QByteArray> m_roleNames; + Q_DECLARE_PRIVATE(QPdfLinkModel) +}; + +QT_END_NAMESPACE + +#endif // QPDFLINKMODEL_P_H diff --git a/src/pdf/api/qpdflinkmodel_p_p.h b/src/pdf/api/qpdflinkmodel_p_p.h new file mode 100644 index 000000000..3e44f1651 --- /dev/null +++ b/src/pdf/api/qpdflinkmodel_p_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFLINKMODEL_P_P_H +#define QPDFLINKMODEL_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qpdflinkmodel_p.h" +#include <private/qabstractitemmodel_p.h> + +#include "third_party/pdfium/public/fpdfview.h" + +#include <QUrl> + +QT_BEGIN_NAMESPACE + +class QPdfLinkModelPrivate: public QAbstractItemModelPrivate +{ + Q_DECLARE_PUBLIC(QPdfLinkModel) + +public: + QPdfLinkModelPrivate(); + + void update(); + + struct Link { + // where it is on the current page + QRectF rect; + int textStart = -1; + int textCharCount = 0; + // destination inside PDF + int page = -1; // -1 means look at the url instead + QPointF location; + qreal zoom = 1; + // web destination + QUrl url; + + QString toString() const; + }; + + QPdfDocument *document = nullptr; + QVector<Link> links; + int page = 0; +}; + +QT_END_NAMESPACE + +#endif // QPDFLINKMODEL_P_P_H diff --git a/src/pdf/api/qpdfsearchmodel.h b/src/pdf/api/qpdfsearchmodel.h index 02d2a20d5..cc91e214a 100644 --- a/src/pdf/api/qpdfsearchmodel.h +++ b/src/pdf/api/qpdfsearchmodel.h @@ -39,34 +39,57 @@ #include "qtpdfglobal.h" #include "qpdfdocument.h" +#include "qpdfsearchresult.h" -#include <QObject> +#include <QtCore/qabstractitemmodel.h> QT_BEGIN_NAMESPACE class QPdfSearchModelPrivate; -class Q_PDF_EXPORT QPdfSearchModel : public QObject // TODO QAIM? +class Q_PDF_EXPORT QPdfSearchModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) public: + enum class Role : int { + Page = Qt::UserRole, + IndexOnPage, + Location, + Context, + _Count + }; + Q_ENUM(Role) explicit QPdfSearchModel(QObject *parent = nullptr); ~QPdfSearchModel(); - QVector<QRectF> matches(int page, const QString &searchString); + QVector<QPdfSearchResult> resultsOnPage(int page) const; + QPdfSearchResult resultAtIndex(int index) const; QPdfDocument *document() const; + QString searchString() const; + + QHash<int, QByteArray> roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; public Q_SLOTS: + void setSearchString(QString searchString); void setDocument(QPdfDocument *document); Q_SIGNALS: void documentChanged(); + void searchStringChanged(); + +protected: + void updatePage(int page); + void timerEvent(QTimerEvent *event) override; private: - QScopedPointer<QPdfSearchModelPrivate> d; + QHash<int, QByteArray> m_roleNames; + Q_DECLARE_PRIVATE(QPdfSearchModel) }; QT_END_NAMESPACE diff --git a/src/pdf/api/qpdfsearchmodel_p.h b/src/pdf/api/qpdfsearchmodel_p.h new file mode 100644 index 000000000..2a23706b2 --- /dev/null +++ b/src/pdf/api/qpdfsearchmodel_p.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFSEARCHMODEL_P_H +#define QPDFSEARCHMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qpdfsearchmodel.h" +#include "qpdfsearchresult_p.h" +#include <private/qabstractitemmodel_p.h> + +#include "third_party/pdfium/public/fpdfview.h" + +QT_BEGIN_NAMESPACE + +class QPdfSearchModelPrivate : public QAbstractItemModelPrivate +{ + Q_DECLARE_PUBLIC(QPdfSearchModel) + +public: + QPdfSearchModelPrivate(); + void clearResults(); + bool doSearch(int page); + + struct PageAndIndex { + int page; + int index; + }; + PageAndIndex pageAndIndexForResult(int resultIndex); + int rowsBeforePage(int page); + + QPdfDocument *document = nullptr; + QString searchString; + QVector<bool> pagesSearched; + QVector<QVector<QPdfSearchResult>> searchResults; + int rowCountSoFar = 0; + int updateTimerId = -1; + int nextPageToUpdate = 0; +}; + +QT_END_NAMESPACE + +#endif // QPDFSEARCHMODEL_P_H diff --git a/src/pdf/api/qpdfsearchresult.h b/src/pdf/api/qpdfsearchresult.h new file mode 100644 index 000000000..db7af3dd9 --- /dev/null +++ b/src/pdf/api/qpdfsearchresult.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFSEARCHRESULT_H +#define QPDFSEARCHRESULT_H + +#include "qpdfdestination.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qrect.h> +#include <QtCore/qvector.h> + +QT_BEGIN_NAMESPACE + +class QPdfSearchResultPrivate; + +class Q_PDF_EXPORT QPdfSearchResult : public QPdfDestination +{ + Q_GADGET + Q_PROPERTY(QString context READ context) + Q_PROPERTY(QVector<QRectF> rectangles READ rectangles) + +public: + QPdfSearchResult(); + ~QPdfSearchResult() {} + + QString context() const; + QVector<QRectF> rectangles() const; + +private: + QPdfSearchResult(int page, QVector<QRectF> rects, QString context); + QPdfSearchResult(QPdfSearchResultPrivate *d); + friend class QPdfDocument; + friend class QPdfSearchModelPrivate; + friend class QQuickPdfNavigationStack; +}; + +Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfSearchResult &); + +QT_END_NAMESPACE + +#endif // QPDFSEARCHRESULT_H diff --git a/src/pdf/api/qpdfsearchresult_p.h b/src/pdf/api/qpdfsearchresult_p.h new file mode 100644 index 000000000..a0f8e4457 --- /dev/null +++ b/src/pdf/api/qpdfsearchresult_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFSEARCHRESULT_P_H +#define QPDFSEARCHRESULT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qpdfdestination_p.h" + +QT_BEGIN_NAMESPACE + +class QPdfSearchResultPrivate : public QPdfDestinationPrivate +{ +public: + QPdfSearchResultPrivate() = default; + QPdfSearchResultPrivate(int page, QVector<QRectF> rects, QString context) : + QPdfDestinationPrivate(page, rects.first().topLeft(), 0), + context(context), + rects(rects) {} + + QString context; + QVector<QRectF> rects; +}; + +QT_END_NAMESPACE + +#endif // QPDFSEARCHRESULT_P_H diff --git a/src/pdf/api/qpdfselection.h b/src/pdf/api/qpdfselection.h new file mode 100644 index 000000000..900b203cf --- /dev/null +++ b/src/pdf/api/qpdfselection.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QPDFSELECTION_H +#define QPDFSELECTION_H + +#include <QtPdf/qtpdfglobal.h> +#include <QClipboard> +#include <QExplicitlySharedDataPointer> +#include <QObject> +#include <QPolygonF> + +QT_BEGIN_NAMESPACE + +class QPdfSelectionPrivate; + +class Q_PDF_EXPORT QPdfSelection +{ + Q_GADGET + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(QVector<QPolygonF> bounds READ bounds) + Q_PROPERTY(QString text READ text) + +public: + QPdfSelection(const QPdfSelection &other); + ~QPdfSelection(); + QPdfSelection &operator=(const QPdfSelection &other); + inline QPdfSelection &operator=(QPdfSelection &&other) noexcept { swap(other); return *this; } + void swap(QPdfSelection &other) noexcept { d.swap(other.d); } + bool isValid() const; + QVector<QPolygonF> bounds() const; + QString text() const; +#if QT_CONFIG(clipboard) + void copyToClipboard(QClipboard::Mode mode = QClipboard::Clipboard) const; +#endif + +private: + QPdfSelection(); + QPdfSelection(const QString &text, QVector<QPolygonF> bounds); + QPdfSelection(QPdfSelectionPrivate *d); + friend class QPdfDocument; + friend class QQuickPdfSelection; + +private: + QExplicitlySharedDataPointer<QPdfSelectionPrivate> d; +}; + +QT_END_NAMESPACE + +#endif // QPDFSELECTION_H diff --git a/src/pdf/qpdfsearchmodel_p.h b/src/pdf/api/qpdfselection_p.h index 90490d8e5..37145f7f9 100644 --- a/src/pdf/qpdfsearchmodel_p.h +++ b/src/pdf/api/qpdfselection_p.h @@ -34,23 +34,26 @@ ** ****************************************************************************/ -#ifndef QPDFSEARCHMODEL_P_H -#define QPDFSEARCHMODEL_P_H +#ifndef QPDFSELECTION_P_H +#define QPDFSELECTION_P_H -#include "qpdfsearchmodel.h" - -#include "third_party/pdfium/public/fpdfview.h" +#include <QPolygonF> +#include <QVector> QT_BEGIN_NAMESPACE -class QPdfSearchModelPrivate +class QPdfSelectionPrivate : public QSharedData { public: - QPdfSearchModelPrivate(); + QPdfSelectionPrivate() = default; + QPdfSelectionPrivate(const QString &text, QVector<QPolygonF> bounds) + : text(text), + bounds(bounds) { } - QPdfDocument *document = nullptr; + QString text; + QVector<QPolygonF> bounds; }; QT_END_NAMESPACE -#endif // QPDFSEARCHMODEL_P_H +#endif // QPDFSELECTION_P_H diff --git a/src/pdf/doc/qtpdf.qdocconf b/src/pdf/doc/qtpdf.qdocconf index b55b25327..7a77105c9 100644 --- a/src/pdf/doc/qtpdf.qdocconf +++ b/src/pdf/doc/qtpdf.qdocconf @@ -37,7 +37,10 @@ depends += qtcore \ qtwidgets \ qtgui \ qtdoc \ - qmake + qmake \ + qtdesigner \ + qtquick \ + qtcmake headerdirs += ../api \ ../quick diff --git a/src/pdf/doc/snippets/qtpdf-build.cmake b/src/pdf/doc/snippets/qtpdf-build.cmake new file mode 100644 index 000000000..d46b9c3ee --- /dev/null +++ b/src/pdf/doc/snippets/qtpdf-build.cmake @@ -0,0 +1,2 @@ +find_package(Qt5 COMPONENTS Pdf REQUIRED) +target_link_libraries(mytarget Qt5::Pdf) diff --git a/src/pdf/doc/src/qtpdf-examples.qdoc b/src/pdf/doc/src/qtpdf-examples.qdoc new file mode 100644 index 000000000..9daa0e7f8 --- /dev/null +++ b/src/pdf/doc/src/qtpdf-examples.qdoc @@ -0,0 +1,37 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \group qtpdf-examples + \ingroup all-examples + + \title Qt PDF Examples + \brief Using the classes and types in the Qt PDF module. + + The following examples illustrate how to use the C++ classes and QML types + in the \l{Qt PDF} module to render PDF documents. +*/ diff --git a/src/pdf/doc/src/qtpdf-index.qdoc b/src/pdf/doc/src/qtpdf-index.qdoc index 6893272ac..b32787eb5 100644 --- a/src/pdf/doc/src/qtpdf-index.qdoc +++ b/src/pdf/doc/src/qtpdf-index.qdoc @@ -39,7 +39,12 @@ \l QPdfPageNavigation class handles the navigation through a PDF document. - \section1 Getting Started + \include module-use.qdocinc using qt module + \quotefile qtpdf-build.cmake + + See also the \l{Build with CMake} overview. + + \section2 Building with qmake To include the definitions of the module's classes, use the following directive: @@ -56,6 +61,12 @@ \li \l{Qt PDF Overview} \endlist + \section1 Examples + + \list + \li \l{Qt PDF Examples} + \endlist + \section1 API Reference \list diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index 812b7fc14..c87722b7e 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -58,24 +58,36 @@ ios: OBJECTS += $$NINJA_OBJECTS SOURCES += \ qpdfbookmarkmodel.cpp \ + qpdfdestination.cpp \ qpdfdocument.cpp \ + qpdflinkmodel.cpp \ qpdfpagenavigation.cpp \ qpdfpagerenderer.cpp \ qpdfsearchmodel.cpp \ + qpdfsearchresult.cpp \ + qpdfselection.cpp \ # all "public" headers must be in "api" for sync script and to hide auto generated headers # by Chromium in case of in-source build HEADERS += \ api/qpdfbookmarkmodel.h \ + api/qpdfdestination.h \ + api/qpdfdestination_p.h \ api/qpdfdocument.h \ api/qpdfdocument_p.h \ api/qpdfdocumentrenderoptions.h \ api/qtpdfglobal.h \ + api/qpdflinkmodel_p.h \ + api/qpdflinkmodel_p_p.h \ api/qpdfnamespace.h \ api/qpdfpagenavigation.h \ api/qpdfpagerenderer.h \ api/qpdfsearchmodel.h \ - qpdfsearchmodel_p.h \ + api/qpdfsearchmodel_p.h \ + api/qpdfsearchresult.h \ + api/qpdfsearchresult_p.h \ + api/qpdfselection.h \ + api/qpdfselection_p.h \ load(qt_module) diff --git a/src/pdf/qpdfdestination.cpp b/src/pdf/qpdfdestination.cpp new file mode 100644 index 000000000..b347445e9 --- /dev/null +++ b/src/pdf/qpdfdestination.cpp @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qpdfdestination.h" +#include "qpdfdestination_p.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QPdfDestination + \since 5.15 + \inmodule QtPdf + + \brief The QPdfDestination class defines a location on a page in a PDF + document, and a suggested zoom level at which it is intended to be viewed. +*/ + +/*! + Constructs an invalid Destination. + + \sa valid +*/ +QPdfDestination::QPdfDestination() + : d(new QPdfDestinationPrivate()) +{ +} + +QPdfDestination::QPdfDestination(int page, QPointF location, qreal zoom) + : d(new QPdfDestinationPrivate(page, location, zoom)) +{ +} + +QPdfDestination::QPdfDestination(QPdfDestinationPrivate *d) + : d(d) +{ +} + +QPdfDestination::QPdfDestination(const QPdfDestination &other) + : d(other.d) +{ +} + +QPdfDestination::~QPdfDestination() +{ +} + +QPdfDestination &QPdfDestination::operator=(const QPdfDestination &other) +{ + d = other.d; + return *this; +} + +/*! + \property QPdfDestination::valid + + This property holds whether the destination is valid. +*/ +bool QPdfDestination::isValid() const +{ + return d->page >= 0; +} + +/*! + \property QPdfDestination::page + + This property holds the page number. +*/ +int QPdfDestination::page() const +{ + return d->page; +} + +/*! + \property QPdfDestination::location + + This property holds the location on the page, in units of points. +*/ +QPointF QPdfDestination::location() const +{ + return d->location; +} + +/*! + \property QPdfDestination::zoom + + This property holds the suggested magnification level, where 1.0 means default scale + (1 pixel = 1 point). +*/ +qreal QPdfDestination::zoom() const +{ + return d->zoom; +} + +QDebug operator<<(QDebug dbg, const QPdfDestination& dest) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QPdfDestination(page=" << dest.page() + << " location=" << dest.location() + << " zoom=" << dest.zoom(); + dbg << ')'; + return dbg; +} + +QT_END_NAMESPACE + +#include "moc_qpdfdestination.cpp" diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 690953691..1e8a0f527 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -38,6 +38,7 @@ #include "qpdfdocument_p.h" #include "third_party/pdfium/public/fpdf_doc.h" +#include "third_party/pdfium/public/fpdf_text.h" #include <QDateTime> #include <QDebug> @@ -46,12 +47,14 @@ #include <QHash> #include <QLoggingCategory> #include <QMutex> +#include <QVector2D> QT_BEGIN_NAMESPACE // The library is not thread-safe at all, it has a lot of global variables. Q_GLOBAL_STATIC_WITH_ARGS(QMutex, pdfMutex, (QMutex::Recursive)); static int libraryRefCount; +static const double CharacterHitTolerance = 6.0; Q_LOGGING_CATEGORY(qLcDoc, "qt.pdf.document") QPdfMutexLocker::QPdfMutexLocker() @@ -186,7 +189,36 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi } else { device = newDevice; initiateAsyncLoadWithTotalSizeKnown(device->size()); - checkComplete(); + if (!avail) { + setStatus(QPdfDocument::Error); + return; + } + + if (!doc) + tryLoadDocument(); + + if (!doc) { + updateLastError(); + setStatus(QPdfDocument::Error); + return; + } + + QPdfMutexLocker lock; + const int newPageCount = FPDF_GetPageCount(doc); + lock.unlock(); + if (newPageCount != pageCount) { + pageCount = newPageCount; + emit q->pageCountChanged(pageCount); + } + + // If it's a local file, and the first couple of pages are available, + // probably the whole document is available. + if (checkPageComplete(0) && (pageCount < 2 || checkPageComplete(1))) { + setStatus(QPdfDocument::Ready); + } else { + updateLastError(); + setStatus(QPdfDocument::Error); + } } } @@ -311,6 +343,26 @@ void QPdfDocumentPrivate::checkComplete() } } +bool QPdfDocumentPrivate::checkPageComplete(int page) +{ + if (page < 0 || page >= pageCount) + return false; + + if (loadComplete) + return true; + + QPdfMutexLocker lock; + int result = PDF_DATA_NOTAVAIL; + while (result == PDF_DATA_NOTAVAIL) + result = FPDFAvail_IsPageAvail(avail, page, this); + lock.unlock(); + + if (result == PDF_DATA_ERROR) + updateLastError(); + + return (result != PDF_DATA_ERROR); +} + void QPdfDocumentPrivate::setStatus(QPdfDocument::Status documentStatus) { if (status == documentStatus) @@ -368,6 +420,8 @@ QPdfDocument::~QPdfDocument() QPdfDocument::DocumentError QPdfDocument::load(const QString &fileName) { + qCDebug(qLcDoc) << "loading" << fileName; + close(); d->setStatus(QPdfDocument::Loading); @@ -560,7 +614,7 @@ int QPdfDocument::pageCount() const QSizeF QPdfDocument::pageSize(int page) const { QSizeF result; - if (!d->doc) + if (!d->doc || !d->checkPageComplete(page)) return result; const QPdfMutexLocker lock; @@ -581,7 +635,7 @@ QSizeF QPdfDocument::pageSize(int page) const */ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions) { - if (!d->doc) + if (!d->doc || !d->checkPageComplete(page)) return QImage(); const QPdfMutexLocker lock; @@ -630,15 +684,89 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions if (renderFlags & QPdf::RenderPathAliased) flags |= FPDF_RENDER_NO_SMOOTHPATH; - FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags); + if (renderOptions.scaledClipRect().isValid()) { + const QRect &clipRect = renderOptions.scaledClipRect(); + + // TODO take rotation into account, like cpdf_page.cpp lines 145-178 + float x0 = clipRect.left(); + float y0 = clipRect.top(); + float x1 = clipRect.left(); + float y1 = clipRect.bottom(); + float x2 = clipRect.right(); + float y2 = clipRect.top(); + QSizeF origSize = pageSize(page); + QVector2D pageScale(1, 1); + if (!renderOptions.scaledSize().isNull()) { + pageScale = QVector2D(renderOptions.scaledSize().width() / float(origSize.width()), + renderOptions.scaledSize().height() / float(origSize.height())); + } + FS_MATRIX matrix {(x2 - x0) / result.width() * pageScale.x(), + (y2 - y0) / result.width() * pageScale.x(), + (x1 - x0) / result.height() * pageScale.y(), + (y1 - y0) / result.height() * pageScale.y(), -x0, -y0}; + + FS_RECTF clipRectF { 0, 0, float(imageSize.width()), float(imageSize.height()) }; + + FPDF_RenderPageBitmapWithMatrix(bitmap, pdfPage, &matrix, &clipRectF, flags); + qCDebug(qLcDoc) << "matrix" << matrix.a << matrix.b << matrix.c << matrix.d << matrix.e << matrix.f; + qCDebug(qLcDoc) << "page" << page << "region" << renderOptions.scaledClipRect() + << "size" << imageSize << "took" << timer.elapsed() << "ms"; + } else { + FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags); + qCDebug(qLcDoc) << "page" << page << "size" << imageSize << "took" << timer.elapsed() << "ms"; + } FPDFBitmap_Destroy(bitmap); FPDF_ClosePage(pdfPage); - qCDebug(qLcDoc) << "page" << page << imageSize << "took" << timer.elapsed() << "ms"; return result; } +/*! + Returns information about the text on the given \a page that can be found + between the given \a start and \a end points, if any. +*/ +QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) +{ + const QPdfMutexLocker lock; + FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page); + double pageHeight = FPDF_GetPageHeight(pdfPage); + FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); + int startIndex = FPDFText_GetCharIndexAtPos(textPage, start.x(), pageHeight - start.y(), + CharacterHitTolerance, CharacterHitTolerance); + int endIndex = FPDFText_GetCharIndexAtPos(textPage, end.x(), pageHeight - end.y(), + CharacterHitTolerance, CharacterHitTolerance); + if (startIndex >= 0 && endIndex != startIndex) { + QString text; + if (startIndex > endIndex) + qSwap(startIndex, endIndex); + int count = endIndex - startIndex + 1; + QVector<ushort> buf(count + 1); + // TODO is that enough space in case one unicode character is more than one in utf-16? + int len = FPDFText_GetText(textPage, startIndex, count, buf.data()); + Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator + text = QString::fromUtf16(buf.constData(), len - 1); + QVector<QPolygonF> bounds; + int rectCount = FPDFText_CountRects(textPage, startIndex, endIndex - startIndex); + for (int i = 0; i < rectCount; ++i) { + double l, r, b, t; + FPDFText_GetRect(textPage, i, &l, &t, &r, &b); + QPolygonF poly; + poly << QPointF(l, pageHeight - t); + poly << QPointF(r, pageHeight - t); + poly << QPointF(r, pageHeight - b); + poly << QPointF(l, pageHeight - b); + poly << QPointF(l, pageHeight - t); + bounds << poly; + } + qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text; + return QPdfSelection(text, bounds); + } + + qCDebug(qLcDoc) << page << start << "->" << end << "nothing found"; + return QPdfSelection(); +} + QT_END_NAMESPACE #include "moc_qpdfdocument.cpp" diff --git a/src/pdf/qpdfdocumentrenderoptions.qdoc b/src/pdf/qpdfdocumentrenderoptions.qdoc index cafb0afc1..cc5083f9d 100644 --- a/src/pdf/qpdfdocumentrenderoptions.qdoc +++ b/src/pdf/qpdfdocumentrenderoptions.qdoc @@ -87,6 +87,40 @@ QT_BEGIN_NAMESPACE */ /*! + \fn QRect QPdfDocumentRenderOptions::scaledClipRect() const + + Returns the rectangular region to be clipped from the page after having + been scaled to \l scaledSize(). + + \sa setScaledClipRect() +*/ + +/*! + \fn void QPdfDocumentRenderOptions::setScaledClipRect(QRect rect) + + Sets the region \a rect to be clipped from the page after having been + scaled to \l scaledSize(). + + \sa scaledClipRect() +*/ + +/*! + \fn QRect QPdfDocumentRenderOptions::scaledSize() const + + Returns the \a size of the page to be rendered, in pixels. + + \sa setScaledSize() +*/ + +/*! + \fn void QPdfDocumentRenderOptions::setScaledSize(QSize size) + + Sets the \a size of the page to be rendered, in pixels. + + \sa scaledSize() +*/ + +/*! \fn bool operator!=(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) \relates QPdfDocumentRenderOptions diff --git a/src/pdf/qpdflinkmodel.cpp b/src/pdf/qpdflinkmodel.cpp new file mode 100644 index 000000000..96e6ddd5c --- /dev/null +++ b/src/pdf/qpdflinkmodel.cpp @@ -0,0 +1,251 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qpdflinkmodel_p.h" +#include "qpdflinkmodel_p_p.h" +#include "qpdfdocument_p.h" + +#include "third_party/pdfium/public/fpdf_doc.h" +#include "third_party/pdfium/public/fpdf_text.h" + +#include <QLoggingCategory> +#include <QMetaEnum> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcLink, "qt.pdf.links") + +QPdfLinkModel::QPdfLinkModel(QObject *parent) + : QAbstractListModel(*(new QPdfLinkModelPrivate()), parent) +{ + QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role")); + for (int r = Qt::UserRole; r < int(Role::_Count); ++r) + m_roleNames.insert(r, QByteArray(rolesMetaEnum.valueToKey(r)).toLower()); +} + +QPdfLinkModel::~QPdfLinkModel() {} + +QHash<int, QByteArray> QPdfLinkModel::roleNames() const +{ + return m_roleNames; +} + +int QPdfLinkModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QPdfLinkModel); + Q_UNUSED(parent) + return d->links.count(); +} + +QVariant QPdfLinkModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QPdfLinkModel); + const QPdfLinkModelPrivate::Link &link = d->links.at(index.row()); + switch (Role(role)) { + case Role::Rect: + return link.rect; + case Role::Url: + return link.url; + case Role::Page: + return link.page; + case Role::Location: + return link.location; + case Role::Zoom: + return link.zoom; + case Role::_Count: + break; + } + if (role == Qt::DisplayRole) + return link.toString(); + return QVariant(); +} + +QPdfDocument *QPdfLinkModel::document() const +{ + Q_D(const QPdfLinkModel); + return d->document; +} + +void QPdfLinkModel::setDocument(QPdfDocument *document) +{ + Q_D(QPdfLinkModel); + if (d->document == document) + return; + disconnect(d->document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged); + connect(document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged); + d->document = document; + emit documentChanged(); + if (page()) + setPage(0); + else + d->update(); +} + +int QPdfLinkModel::page() const +{ + Q_D(const QPdfLinkModel); + return d->page; +} + +void QPdfLinkModel::setPage(int page) +{ + Q_D(QPdfLinkModel); + if (d->page == page) + return; + + d->page = page; + emit pageChanged(page); + d->update(); +} + +QPdfLinkModelPrivate::QPdfLinkModelPrivate() : QAbstractItemModelPrivate() +{ +} + +void QPdfLinkModelPrivate::update() +{ + Q_Q(QPdfLinkModel); + if (!document || !document->d->doc) + return; + auto doc = document->d->doc; + const QPdfMutexLocker lock; + FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page); + if (!pdfPage) { + qWarning() << "failed to load page" << page; + return; + } + double pageHeight = FPDF_GetPageHeight(pdfPage); + q->beginResetModel(); + links.clear(); + + // Iterate the ordinary links + int linkStart = 0; + bool ok = true; + while (ok) { + FPDF_LINK linkAnnot; + ok = FPDFLink_Enumerate(pdfPage, &linkStart, &linkAnnot); + if (!ok) + break; + FS_RECTF rect; + ok = FPDFLink_GetAnnotRect(linkAnnot, &rect); + if (!ok) + break; + Link linkData; + linkData.rect = QRectF(rect.left, pageHeight - rect.top, + rect.right - rect.left, rect.top - rect.bottom); + FPDF_DEST dest = FPDFLink_GetDest(doc, linkAnnot); + FPDF_ACTION action = FPDFLink_GetAction(linkAnnot); + if (FPDFAction_GetType(action) != PDFACTION_GOTO) { + qWarning() << "link action type" << FPDFAction_GetType(action) << "is not yet supported"; + continue; + } + linkData.page = FPDFDest_GetDestPageIndex(doc, dest); + FPDF_BOOL hasX, hasY, hasZoom; + FS_FLOAT x, y, zoom; + ok = FPDFDest_GetLocationInPage(dest, &hasX, &hasY, &hasZoom, &x, &y, &zoom); + if (!ok) + break; + if (hasX && hasY) + linkData.location = QPointF(x, pageHeight - y); + if (hasZoom) + linkData.zoom = zoom; + links << linkData; + } + + // Iterate the web links + FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); + if (textPage) { + FPDF_PAGELINK webLinks = FPDFLink_LoadWebLinks(textPage); + if (webLinks) { + int count = FPDFLink_CountWebLinks(webLinks); + for (int i = 0; i < count; ++i) { + Link linkData; + int len = FPDFLink_GetURL(webLinks, i, nullptr, 0); + if (len < 1) { + qWarning() << "URL" << i << "has length" << len; + } else { + QVector<unsigned short> buf(len); + int got = FPDFLink_GetURL(webLinks, i, buf.data(), len); + Q_ASSERT(got == len); + linkData.url = QString::fromUtf16(buf.data(), got - 1); + } + FPDFLink_GetTextRange(webLinks, i, &linkData.textStart, &linkData.textCharCount); + len = FPDFLink_CountRects(webLinks, i); + for (int r = 0; r < len; ++r) { + double left, top, right, bottom; + bool success = FPDFLink_GetRect(webLinks, i, r, &left, &top, &right, &bottom); + if (success) { + linkData.rect = QRectF(left, pageHeight - top, right - left, top - bottom); + links << linkData; + } + } + } + FPDFLink_CloseWebLinks(webLinks); + } + FPDFText_ClosePage(textPage); + } + + // All done + FPDF_ClosePage(pdfPage); + if (Q_UNLIKELY(qLcLink().isDebugEnabled())) { + for (const Link &l : links) + qCDebug(qLcLink) << l.rect << l.toString(); + } + q->endResetModel(); +} + +void QPdfLinkModel::onStatusChanged(QPdfDocument::Status status) +{ + Q_D(QPdfLinkModel); + qCDebug(qLcLink) << "sees document statusChanged" << status; + if (status == QPdfDocument::Ready) + d->update(); +} + +QString QPdfLinkModelPrivate::Link::toString() const +{ + QString ret; + if (page >= 0) + return QLatin1String("page ") + QString::number(page) + + QLatin1String(" location ") + QString::number(location.x()) + QLatin1Char(',') + QString::number(location.y()) + + QLatin1String(" zoom ") + QString::number(zoom); + else + return url.toString(); +} + +QT_END_NAMESPACE + +#include "moc_qpdflinkmodel_p.cpp" diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp index 9010d76d3..4129c7cb7 100644 --- a/src/pdf/qpdfsearchmodel.cpp +++ b/src/pdf/qpdfsearchmodel.cpp @@ -34,80 +34,275 @@ ** ****************************************************************************/ +#include "qpdfdestination.h" +#include "qpdfdocument_p.h" #include "qpdfsearchmodel.h" #include "qpdfsearchmodel_p.h" -#include "qpdfdocument_p.h" +#include "qpdfsearchresult_p.h" #include "third_party/pdfium/public/fpdf_doc.h" #include "third_party/pdfium/public/fpdf_text.h" -#include <QLoggingCategory> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/QMetaEnum> QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") +static const int UpdateTimerInterval = 100; +static const int ContextChars = 20; +static const double CharacterHitTolerance = 6.0; + QPdfSearchModel::QPdfSearchModel(QObject *parent) - : QObject(parent), - d(new QPdfSearchModelPrivate()) + : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent) { + QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role")); + for (int r = Qt::UserRole; r < int(Role::_Count); ++r) { + QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r)); + if (roleName.isEmpty()) + continue; + roleName[0] = QChar::toLower(roleName[0]); + m_roleNames.insert(r, roleName); + } } QPdfSearchModel::~QPdfSearchModel() {} -QVector<QRectF> QPdfSearchModel::matches(int page, const QString &searchString) +QHash<int, QByteArray> QPdfSearchModel::roleNames() const +{ + return m_roleNames; +} + +int QPdfSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QPdfSearchModel); + Q_UNUSED(parent) + return d->rowCountSoFar; +} + +QVariant QPdfSearchModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QPdfSearchModel); + const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index.row()); + if (pi.page < 0) + return QVariant(); + switch (Role(role)) { + case Role::Page: + return pi.page; + case Role::IndexOnPage: + return pi.index; + case Role::Location: + return d->searchResults[pi.page][pi.index].location(); + case Role::Context: + return d->searchResults[pi.page][pi.index].context(); + case Role::_Count: + break; + } + if (role == Qt::DisplayRole) + return d->searchResults[pi.page][pi.index].context(); + return QVariant(); +} + +void QPdfSearchModel::updatePage(int page) +{ + Q_D(QPdfSearchModel); + d->doSearch(page); +} + +QString QPdfSearchModel::searchString() const +{ + Q_D(const QPdfSearchModel); + return d->searchString; +} + +void QPdfSearchModel::setSearchString(QString searchString) +{ + Q_D(QPdfSearchModel); + if (d->searchString == searchString) + return; + + d->searchString = searchString; + emit searchStringChanged(); + beginResetModel(); + d->clearResults(); + endResetModel(); +} + +QVector<QPdfSearchResult> QPdfSearchModel::resultsOnPage(int page) const +{ + Q_D(const QPdfSearchModel); + const_cast<QPdfSearchModelPrivate *>(d)->doSearch(page); + if (d->searchResults.count() <= page) + return {}; + return d->searchResults[page]; +} + +QPdfSearchResult QPdfSearchModel::resultAtIndex(int index) const +{ + Q_D(const QPdfSearchModel); + const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index); + if (pi.page < 0) + return QPdfSearchResult(); + return d->searchResults[pi.page][pi.index]; +} + +QPdfDocument *QPdfSearchModel::document() const +{ + Q_D(const QPdfSearchModel); + return d->document; +} + +void QPdfSearchModel::setDocument(QPdfDocument *document) +{ + Q_D(QPdfSearchModel); + if (d->document == document) + return; + + d->document = document; + emit documentChanged(); + d->clearResults(); +} + +void QPdfSearchModel::timerEvent(QTimerEvent *event) +{ + Q_D(QPdfSearchModel); + if (event->timerId() != d->updateTimerId) + return; + if (!d->document || d->nextPageToUpdate >= d->document->pageCount()) { + if (d->document) + qCDebug(qLcS, "done updating search results on %d pages", d->searchResults.count()); + killTimer(d->updateTimerId); + d->updateTimerId = -1; + } + d->doSearch(d->nextPageToUpdate++); +} + +QPdfSearchModelPrivate::QPdfSearchModelPrivate() +{ +} + +void QPdfSearchModelPrivate::clearResults() +{ + Q_Q(QPdfSearchModel); + rowCountSoFar = 0; + searchResults.clear(); + pagesSearched.clear(); + if (document) { + searchResults.resize(document->pageCount()); + pagesSearched.resize(document->pageCount()); + } else { + searchResults.resize(0); + pagesSearched.resize(0); + } + nextPageToUpdate = 0; + updateTimerId = q->startTimer(UpdateTimerInterval); +} + +bool QPdfSearchModelPrivate::doSearch(int page) { + if (page < 0 || page >= pagesSearched.count() || searchString.isEmpty()) + return false; + if (pagesSearched[page]) + return true; + Q_Q(QPdfSearchModel); + const QPdfMutexLocker lock; - FPDF_PAGE pdfPage = FPDF_LoadPage(d->document->d->doc, page); + QElapsedTimer timer; + timer.start(); + FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page); if (!pdfPage) { qWarning() << "failed to load page" << page; - return {}; + return false; } double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); if (!textPage) { qWarning() << "failed to load text of page" << page; FPDF_ClosePage(pdfPage); - return {}; + return false; } - QVector<QRectF> ret; - if (searchString.isEmpty()) - return ret; FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0); + QVector<QPdfSearchResult> newSearchResults; while (FPDFText_FindNext(sh)) { int idx = FPDFText_GetSchResultIndex(sh); int count = FPDFText_GetSchCount(sh); int rectCount = FPDFText_CountRects(textPage, idx, count); - qCDebug(qLcS) << searchString << ": matched" << count << "chars @" << idx << "across" << rectCount << "rects"; + QVector<QRectF> rects; + int startIndex = -1; + int endIndex = -1; for (int r = 0; r < rectCount; ++r) { double left, top, right, bottom; FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom); - ret << QRectF(left, pageHeight - top, right - left, top - bottom); - qCDebug(qLcS) << ret.last(); + rects << QRectF(left, pageHeight - top, right - left, top - bottom); + if (r == 0) { + startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top, + CharacterHitTolerance, CharacterHitTolerance); + } + if (r == rectCount - 1) { + endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top, + CharacterHitTolerance, CharacterHitTolerance); + } + qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex; } + QString context; + if (startIndex >= 0 || endIndex >= 0) { + startIndex = qMax(0, startIndex - ContextChars); + endIndex += ContextChars; + int count = endIndex - startIndex + 1; + if (count > 0) { + QVector<ushort> buf(count + 1); + int len = FPDFText_GetText(textPage, startIndex, count, buf.data()); + Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator + context = QString::fromUtf16(buf.constData(), len - 1); + context = context.replace(QLatin1Char('\n'), QLatin1Char(' ')); + context = context.replace(searchString, + QLatin1String("<b>") + searchString + QLatin1String("</b>")); + } + } + newSearchResults << QPdfSearchResult(page, rects, context); } FPDFText_FindClose(sh); FPDFText_ClosePage(textPage); FPDF_ClosePage(pdfPage); + qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find" + << newSearchResults.count() << "results on page" << page; - return ret; -} - -QPdfDocument *QPdfSearchModel::document() const -{ - return d->document; + pagesSearched[page] = true; + searchResults[page] = newSearchResults; + if (newSearchResults.count() > 0) { + int rowsBefore = rowsBeforePage(page); + qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.count(); + rowCountSoFar += newSearchResults.count(); + q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.count() - 1); + q->endInsertRows(); + } + return true; } -void QPdfSearchModel::setDocument(QPdfDocument *document) +QPdfSearchModelPrivate::PageAndIndex QPdfSearchModelPrivate::pageAndIndexForResult(int resultIndex) { - if (d->document == document) - return; - d->document = document; - emit documentChanged(); + const int pageCount = document->pageCount(); + int totalSoFar = 0; + int previousTotalSoFar = 0; + for (int page = 0; page < pageCount; ++page) { + if (!pagesSearched[page]) + doSearch(page); + totalSoFar += searchResults[page].count(); + if (totalSoFar > resultIndex) + return {page, resultIndex - previousTotalSoFar}; + previousTotalSoFar = totalSoFar; + } + return {-1, -1}; } -QPdfSearchModelPrivate::QPdfSearchModelPrivate() +int QPdfSearchModelPrivate::rowsBeforePage(int page) { + int ret = 0; + for (int i = 0; i < page; ++i) + ret += searchResults[i].count(); + return ret; } QT_END_NAMESPACE diff --git a/src/pdf/qpdfsearchresult.cpp b/src/pdf/qpdfsearchresult.cpp new file mode 100644 index 000000000..1164a1d43 --- /dev/null +++ b/src/pdf/qpdfsearchresult.cpp @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qpdfsearchresult.h" +#include "qpdfsearchresult_p.h" + +QT_BEGIN_NAMESPACE + +QPdfSearchResult::QPdfSearchResult() : + QPdfSearchResult(new QPdfSearchResultPrivate()) { } + +QPdfSearchResult::QPdfSearchResult(int page, QVector<QRectF> rects, QString context) : + QPdfSearchResult(new QPdfSearchResultPrivate(page, rects, context)) { } + +QPdfSearchResult::QPdfSearchResult(QPdfSearchResultPrivate *d) : + QPdfDestination(static_cast<QPdfDestinationPrivate *>(d)) { } + +QString QPdfSearchResult::context() const +{ + return static_cast<QPdfSearchResultPrivate *>(d.data())->context; +} + +QVector<QRectF> QPdfSearchResult::rectangles() const +{ + return static_cast<QPdfSearchResultPrivate *>(d.data())->rects; +} + +QDebug operator<<(QDebug dbg, const QPdfSearchResult &searchResult) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QPdfSearchResult(page=" << searchResult.page() + << " context=" << searchResult.context() + << " rects=" << searchResult.rectangles(); + dbg << ')'; + return dbg; +} + +QT_END_NAMESPACE + +#include "moc_qpdfsearchresult.cpp" diff --git a/src/pdf/qpdfselection.cpp b/src/pdf/qpdfselection.cpp new file mode 100644 index 000000000..8c3d6fde0 --- /dev/null +++ b/src/pdf/qpdfselection.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qpdfselection.h" +#include "qpdfselection_p.h" +#include <QGuiApplication> + +QT_BEGIN_NAMESPACE + +/*! + \class QPdfSelection + \since 5.15 + \inmodule QtPdf + + \brief The QPdfSelection class defines a range of text that has been selected + on one page in a PDF document, and its geometric boundaries. + + \sa QPdfDocument::getSelection() +*/ + +/*! + Constructs an invalid selection. + + \sa valid +*/ +QPdfSelection::QPdfSelection() + : d(new QPdfSelectionPrivate()) +{ +} + +/*! + \internal + Constructs a selection including the range of characters that make up the + \a text string, and which take up space on the page within the polygon + regions given in \a bounds. +*/ +QPdfSelection::QPdfSelection(const QString &text, QVector<QPolygonF> bounds) + : d(new QPdfSelectionPrivate(text, bounds)) +{ +} + +QPdfSelection::QPdfSelection(QPdfSelectionPrivate *d) + : d(d) +{ +} + +QPdfSelection::QPdfSelection(const QPdfSelection &other) + : d(other.d) +{ +} + +QPdfSelection::~QPdfSelection() +{ +} + +/*! + \property QPdfSelection::valid + + This property holds whether the selection is valid. +*/ +bool QPdfSelection::isValid() const +{ + return !d->bounds.isEmpty(); +} + +/*! + \property QPdfSelection::bounds + + This property holds a set of regions that the selected text occupies on the + page, represented as polygons. The coordinate system for the polygons has + the origin at the upper-left corner of the page, and the units are + \l {https://en.wikipedia.org/wiki/Point_(typography)}{points}. + + \note For now, the polygons returned from \l QPdfDocument::getSelection() + are always rectangles; but in the future it may be possible to represent + more complex regions. +*/ +QVector<QPolygonF> QPdfSelection::bounds() const +{ + return d->bounds; +} + +/*! + \property QPdfSelection::text + + This property holds the selected text. +*/ +QString QPdfSelection::text() const +{ + return d->text; +} + +#if QT_CONFIG(clipboard) +/*! + Copies \l text to the \l {QGuiApplication::clipboard()}{system clipboard}. +*/ +void QPdfSelection::copyToClipboard(QClipboard::Mode mode) const +{ + QGuiApplication::clipboard()->setText(d->text, mode); +} +#endif + +QT_END_NAMESPACE + +#include "moc_qpdfselection.cpp" diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index 664ba51ab..bb68a817e 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -39,7 +39,10 @@ #include <QtQml/qqmlengine.h> #include <QtQml/qqmlextensionplugin.h> #include "qquickpdfdocument_p.h" +#include "qquickpdflinkmodel_p.h" +#include "qquickpdfnavigationstack_p.h" #include "qquickpdfsearchmodel_p.h" +#include "qquickpdfselection_p.h" QT_BEGIN_NAMESPACE @@ -80,9 +83,14 @@ public: qmlRegisterModule(uri, 2, QT_VERSION_MINOR); qmlRegisterType<QQuickPdfDocument>(uri, 5, 15, "PdfDocument"); + qmlRegisterType<QQuickPdfLinkModel>(uri, 5, 15, "PdfLinkModel"); + qmlRegisterType<QQuickPdfNavigationStack>(uri, 5, 15, "PdfNavigationStack"); qmlRegisterType<QQuickPdfSearchModel>(uri, 5, 15, "PdfSearchModel"); + qmlRegisterType<QQuickPdfSelection>(uri, 5, 15, "PdfSelection"); 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/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml new file mode 100644 index 000000000..e8eccaf3b --- /dev/null +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -0,0 +1,342 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** 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 https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 +import QtQuick.Window 2.14 + +Item { + // public API + // TODO 5.15: required property + property var document: undefined + property bool debug: false + + property string selectedText + function copySelectionToClipboard() { + var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) + if (debug) + console.log("currentItem", currentItem, "sel", currentItem.selection.text) + if (currentItem !== null) + currentItem.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) { + if (page === navigationStack.currentPage) + return + 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 + function resetScale() { root.renderScale = 1 } + function scaleToWidth(width, height) { + root.renderScale = width / (tableView.rot90 ? tableView.firstPagePointSize.height : tableView.firstPagePointSize.width) + } + function scaleToPage(width, height) { + var windowAspect = width / height + var pageAspect = tableView.firstPagePointSize.width / tableView.firstPagePointSize.height + if (tableView.rot90) { + if (windowAspect > pageAspect) { + root.renderScale = height / tableView.firstPagePointSize.width + } else { + root.renderScale = width / tableView.firstPagePointSize.height + } + } else { + if (windowAspect > pageAspect) { + root.renderScale = height / tableView.firstPagePointSize.height + } else { + root.renderScale = width / tableView.firstPagePointSize.width + } + } + } + + // text search + property alias searchModel: searchModel + property alias searchString: searchModel.searchString + function searchBack() { --searchModel.currentResult } + function searchForward() { ++searchModel.currentResult } + + id: root + TableView { + id: tableView + anchors.fill: parent + model: root.document === undefined ? 0 : root.document.pageCount + rowSpacing: 6 + property real rotationModulus: Math.abs(root.pageRotation % 180) + property bool rot90: rotationModulus > 45 && rotationModulus < 135 + onRot90Changed: forceLayout() + property size firstPagePointSize: document === undefined ? Qt.size(0, 0) : document.pagePointSize(0) + contentWidth: document === undefined ? 0 : document.maxPageWidth * root.renderScale + // workaround for missing function (see https://codereview.qt-project.org/c/qt/qtdeclarative/+/248464) + function itemAtPos(x, y, includeSpacing) { + // we don't care about x (assume col 0), and assume includeSpacing is true + var ret = null + for (var i = 0; i < contentItem.children.length; ++i) { + var child = contentItem.children[i]; + if (root.debug) + console.log(child, "@y", child.y) + if (child.y < y && (!ret || child.y > ret.y)) + ret = child + } + if (root.debug) + console.log("given y", y, "found", ret, "@", ret.y) + return ret // the delegate with the largest y that is less than the given y + } + rowHeightProvider: function(row) { return (rot90 ? document.pagePointSize(row).width : document.pagePointSize(row).height) * root.renderScale } + delegate: Rectangle { + id: pageHolder + color: root.debug ? "beige" : "transparent" + Text { + visible: root.debug + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + rotation: -90; text: pageHolder.width.toFixed(1) + "x" + pageHolder.height.toFixed(1) + "\n" + + image.width.toFixed(1) + "x" + image.height.toFixed(1) + } + implicitWidth: Math.max(root.width, (tableView.rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale) + implicitHeight: tableView.rot90 ? image.width : image.height + onImplicitWidthChanged: tableView.forceLayout() + objectName: "page " + index + property int delegateIndex: row // expose the context property for JS outside of the delegate + property alias selection: selection + Rectangle { + id: paper + width: image.width + height: image.height + rotation: root.pageRotation + anchors.centerIn: parent + property size pagePointSize: document.pagePointSize(index) + property real pageScale: image.paintedWidth / pagePointSize.width + Image { + id: image + source: document.source + currentFrame: index + asynchronous: true + fillMode: Image.PreserveAspectFit + width: paper.pagePointSize.width * root.renderScale + height: paper.pagePointSize.height * root.renderScale + property real renderScale: root.renderScale + property real oldRenderScale: 1 + onRenderScaleChanged: { + image.sourceSize.width = paper.pagePointSize.width * renderScale + image.sourceSize.height = 0 + paper.scale = 1 + } + } + Shape { + anchors.fill: parent + opacity: 0.25 + visible: image.status === Image.Ready + ShapePath { + strokeWidth: 1 + strokeColor: "cyan" + fillColor: "steelblue" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: searchModel.boundingPolygonsOnPage(index) + } + } + ShapePath { + fillColor: "orange" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } + } + } + Shape { + anchors.fill: parent + opacity: 0.5 + visible: image.status === Image.Ready && searchModel.currentPage === index + ShapePath { + strokeWidth: 1 + strokeColor: "blue" + fillColor: "cyan" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + } + PinchHandler { + id: pinch + minimumScale: 0.1 + maximumScale: root.renderScale < 4 ? 2 : 1 + minimumRotation: 0 + maximumRotation: 0 + enabled: image.sourceSize.width < 5000 + onActiveChanged: + if (active) { + paper.z = 10 + } else { + paper.z = 0 + 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 + } + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x * paper.pageScale + y: rect.y * paper.pageScale + width: rect.width * paper.pageScale + height: rect.height * paper.pageScale + MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + id: linkMA + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (page >= 0) + root.goToLocation(page, location, zoom) + else + Qt.openUrlExternally(url) + } + } + ToolTip { + visible: linkMA.containsMouse + delay: 1000 + text: page >= 0 ? + ("page " + (page + 1) + + " location " + location.x.toFixed(1) + ", " + location.y.toFixed(1) + + " zoom " + zoom) : url + } + } + } + } + PdfSelection { + id: selection + document: root.document + page: image.currentFrame + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.pageScale, + textSelectionDrag.centroid.pressPosition.y / paper.pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.pageScale, + textSelectionDrag.centroid.position.y / paper.pageScale) + hold: !textSelectionDrag.active && !tapHandler.pressed + onTextChanged: root.selectedText = text + } + } + ScrollBar.vertical: ScrollBar { + property bool moved: false + onPositionChanged: moved = true + onActiveChanged: { + var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) + var currentPage = currentItem.delegateIndex + var currentLocation = Qt.point((tableView.contentX - currentItem.x + root.width / 2) / root.renderScale, + (tableView.contentY - currentItem.y + root.height / 2) / root.renderScale) + if (active) { + moved = false + navigationStack.push(currentPage, currentLocation, root.renderScale) + } else if (moved) { + navigationStack.update(currentPage, currentLocation, root.renderScale) + } + } + } + ScrollBar.horizontal: ScrollBar { } + } + onRenderScaleChanged: { + tableView.forceLayout() + var currentItem = tableView.itemAtPos(tableView.contentX + root.width / 2, tableView.contentY + root.height / 2) + if (currentItem !== undefined) + navigationStack.update(currentItem.delegateIndex, Qt.point(currentItem.x / renderScale, currentItem.y / renderScale), renderScale) + } + PdfNavigationStack { + id: navigationStack + onJumped: { + root.renderScale = zoom + tableView.contentX = Math.max(0, location.x - root.width / 2) * root.renderScale + tableView.contentY = tableView.originY + root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale + if (root.debug) { + console.log("going to page", page, + "@y", root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale, + "ended up @", tableView.contentY, "originY is", tableView.originY) + } + } + } + PdfSearchModel { + id: searchModel + document: root.document === undefined ? null : root.document + onCurrentPageChanged: root.goToPage(currentPage) + } +} diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index b7f75f4c2..dfd00a1a8 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -33,53 +33,147 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick 2.14 +import QtQuick.Controls 2.14 import QtQuick.Pdf 5.15 -import QtQuick.Shapes 1.15 +import QtQuick.Shapes 1.14 +import Qt.labs.animation 1.0 Rectangle { - id: paper - width: image.width - height: image.height - // public API // TODO 5.15: required property - property var document: null + property var document: undefined + 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 alias sourceSize: image.sourceSize - property alias currentPage: image.currentFrame - property alias pageCount: image.frameCount + function resetScale() { + image.sourceSize.width = 0 + image.sourceSize.height = 0 + root.x = 0 + root.y = 0 + root.scale = 1 + } + function scaleToWidth(width, height) { + var halfRotation = Math.abs(root.rotation % 180) + image.sourceSize = Qt.size((halfRotation > 45 && halfRotation < 135) ? height : width, 0) + root.x = 0 + root.y = 0 + image.centerInSize = Qt.size(width, height) + image.centerOnLoad = true + image.vCenterOnLoad = (halfRotation > 45 && halfRotation < 135) + root.scale = 1 + } + function scaleToPage(width, height) { + var windowAspect = width / height + var halfRotation = Math.abs(root.rotation % 180) + var pagePointSize = document.pagePointSize(navigationStack.currentPage) + if (halfRotation > 45 && halfRotation < 135) { + // rotated 90 or 270º + var pageAspect = pagePointSize.height / pagePointSize.width + if (windowAspect > pageAspect) { + image.sourceSize = Qt.size(height, 0) + } else { + image.sourceSize = Qt.size(0, width) + } + } else { + var pageAspect = pagePointSize.width / pagePointSize.height + if (windowAspect > pageAspect) { + image.sourceSize = Qt.size(0, height) + } else { + image.sourceSize = Qt.size(width, 0) + } + } + image.centerInSize = Qt.size(width, height) + image.centerOnLoad = true + image.vCenterOnLoad = true + root.scale = 1 + } + + // text search + property alias searchModel: searchModel property alias searchString: searchModel.searchString - property alias status: image.status + function searchBack() { --searchModel.currentResult } + function searchForward() { ++searchModel.currentResult } + + // implementation + id: root + width: image.width + height: image.height - property real __pageScale: image.paintedWidth / document.pagePointSize(image.currentFrame).width + 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: paper.document - page: image.currentFrame + 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) } Image { id: image + currentFrame: navigationStack.currentPage source: document.status === PdfDocument.Ready ? document.source : "" asynchronous: true fillMode: Image.PreserveAspectFit - } - function reRenderIfNecessary() { - var newSourceWidth = image.sourceSize.width * paper.scale - var ratio = newSourceWidth / image.sourceSize.width - if (ratio > 1.1 || ratio < 0.9) { - image.sourceSize.width = newSourceWidth - image.sourceSize.height = 1 - paper.scale = 1 + property bool centerOnLoad: false + property bool vCenterOnLoad: false + property size centerInSize + property real pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width + function reRenderIfNecessary() { + var newSourceWidth = image.sourceSize.width * root.scale + var ratio = newSourceWidth / image.sourceSize.width + if (ratio > 1.1 || ratio < 0.9) { + image.sourceSize.width = newSourceWidth + image.sourceSize.height = 0 + root.scale = 1 + } } + onStatusChanged: + if (status == Image.Ready && centerOnLoad) { + root.x = (centerInSize.width - image.implicitWidth) / 2 + root.y = vCenterOnLoad ? (centerInSize.height - image.implicitHeight) / 2 : 0 + centerOnLoad = false + vCenterOnLoad = false + } } onRenderScaleChanged: { - image.sourceSize.width = document.pagePointSize(image.currentFrame).width * renderScale + image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale image.sourceSize.height = 0 - paper.scale = 1 + root.scale = 1 } Shape { @@ -88,22 +182,64 @@ Rectangle { visible: image.status === Image.Ready ShapePath { strokeWidth: 1 - strokeColor: "blue" + 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(paper.__pageScale, paper.__pageScale) + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + ShapePath { + fillColor: "orange" + scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { - id: searchResultBoundaries - paths: searchModel.matchGeometry + 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: 10 minimumRotation: 0 maximumRotation: 0 - onActiveChanged: if (!active) paper.reRenderIfNecessary() + onActiveChanged: if (!active) image.reRenderIfNecessary() grabPermissions: PinchHandler.TakeOverForbidden // don't allow takeover if pinch has started } DragHandler { @@ -116,4 +252,22 @@ Rectangle { acceptedButtons: Qt.MiddleButton snapMode: DragHandler.NoSnap } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + // prevent it from being scrolled out of view + BoundaryRule on x { + minimum: 100 - root.width + maximum: root.parent.width - 100 + } + BoundaryRule on y { + minimum: 100 - root.height + maximum: root.parent.height - 100 + } } diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml new file mode 100644 index 000000000..55aa44bbf --- /dev/null +++ b/src/pdf/quick/qml/PdfScrollablePageView.qml @@ -0,0 +1,274 @@ +/**************************************************************************** +** +** 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) { + if (page === navigationStack.currentPage) + return + 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 { + onActiveChanged: + if (!active ) { + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) + } + } + ScrollBar.horizontal: ScrollBar { + onActiveChanged: + if (!active ) { + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) + } + } + + onRenderScaleChanged: { + image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale + image.sourceSize.height = 0 + paper.scale = 1 + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) + } + + 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 + onJumped: { + root.renderScale = zoom + root.contentX = Math.max(0, location.x * root.renderScale - root.width / 2) + root.contentY = Math.max(0, location.y * root.renderScale - root.height / 2) + if (root.debug) + console.log("going to zoom", zoom, "loc", location, + "on page", page, "ended up @", root.contentX + ", " + root.contentY) + } + onCurrentPageChanged: searchModel.currentPage = currentPage + } + + 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/qquickpdfdocument.cpp b/src/pdf/quick/qquickpdfdocument.cpp index 73b3d4537..3d5f0fa10 100644 --- a/src/pdf/quick/qquickpdfdocument.cpp +++ b/src/pdf/quick/qquickpdfdocument.cpp @@ -43,14 +43,14 @@ QT_BEGIN_NAMESPACE /*! - \qmltype Document + \qmltype PdfDocument \instantiates QQuickPdfDocument \inqmlmodule QtQuick.Pdf \ingroup pdf \brief A representation of a PDF document. \since 5.15 - A Document provides access to PDF document meta-information. + PdfDocument provides access to PDF document meta-information. It is not necessary for rendering, as it is enough to use an \l Image with source set to the URL of the PDF. */ @@ -78,7 +78,7 @@ void QQuickPdfDocument::componentComplete() } /*! - \qmlproperty url Document::source + \qmlproperty url PdfDocument::source This property holds a URL pointing to the PDF file to be loaded. @@ -90,12 +90,16 @@ void QQuickPdfDocument::setSource(QUrl source) return; m_source = source; + m_maxPageWidthHeight = QSizeF(); emit sourceChanged(); - m_doc.load(source.path()); + if (source.scheme() == QLatin1String("qrc")) + m_doc.load(QLatin1Char(':') + source.path()); + else + m_doc.load(source.path()); } /*! - \qmlproperty string Document::error + \qmlproperty string PdfDocument::error This property holds a translated string representation of the current error, if any. @@ -130,7 +134,7 @@ QString QQuickPdfDocument::error() const } /*! - \qmlproperty bool Document::password + \qmlproperty bool PdfDocument::password This property holds the document password. If the passwordRequired() signal is emitted, the UI should prompt the user and then set this @@ -146,13 +150,13 @@ void QQuickPdfDocument::setPassword(const QString &password) } /*! - \qmlproperty int Document::pageCount + \qmlproperty int PdfDocument::pageCount This property holds the number of pages the PDF contains. */ /*! - \qmlsignal Document::passwordRequired() + \qmlsignal PdfDocument::passwordRequired() This signal is emitted when the PDF requires a password in order to open. The UI in a typical PDF viewer should prompt the user for the password @@ -160,7 +164,7 @@ void QQuickPdfDocument::setPassword(const QString &password) */ /*! - \qmlmethod size Document::pagePointSize(int page) + \qmlmethod size PdfDocument::pagePointSize(int page) Returns the size of the given \a page in points. */ @@ -169,60 +173,125 @@ QSizeF QQuickPdfDocument::pagePointSize(int page) const return m_doc.pageSize(page); } +qreal QQuickPdfDocument::maxPageWidth() const +{ + const_cast<QQuickPdfDocument *>(this)->updateMaxPageSize(); + return m_maxPageWidthHeight.width(); +} + +qreal QQuickPdfDocument::maxPageHeight() const +{ + const_cast<QQuickPdfDocument *>(this)->updateMaxPageSize(); + return m_maxPageWidthHeight.height(); +} + +/*! + \internal + \qmlmethod size PdfDocument::heightSumBeforePage(int page) + + Returns the sum of the heights, in points, of all sets of \a facingPages + pages from 0 to the given \a page, exclusive. + + That is, if the pages were laid out end-to-end in adjacent sets of + \a facingPages, what would be the distance in points from the top of the + first page to the top of the given page. +*/ +// Workaround for lack of something analogous to ListView.positionViewAtIndex() in TableView +qreal QQuickPdfDocument::heightSumBeforePage(int page, qreal spacing, int facingPages) const +{ + qreal ret = 0; + for (int i = 0; i < page; i+= facingPages) { + if (i + facingPages > page) + break; + qreal facingPagesHeight = 0; + for (int j = i; j < i + facingPages; ++j) + facingPagesHeight = qMax(facingPagesHeight, pagePointSize(j).height()); + ret += facingPagesHeight + spacing; + } + return ret; +} + +void QQuickPdfDocument::updateMaxPageSize() +{ + if (m_maxPageWidthHeight.isValid()) + return; + qreal w = 0; + qreal h = 0; + const int count = pageCount(); + for (int i = 0; i < count; ++i) { + auto size = pagePointSize(i); + w = qMax(w, size.width()); + h = qMax(w, size.height()); + } + m_maxPageWidthHeight = QSizeF(w, h); +} + +/*! + \qmlproperty real PdfDocument::maxPageWidth + + This property holds the width of the widest page in the document, in points. +*/ + +/*! + \qmlproperty real PdfDocument::maxPageHeight + + This property holds the height of the tallest page in the document, in points. +*/ + /*! - \qmlproperty string Document::title + \qmlproperty string PdfDocument::title This property holds the document's title. A typical viewer UI can bind this to the \c Window.title property. */ /*! - \qmlproperty string Document::author + \qmlproperty string PdfDocument::author This property holds the name of the person who created the document. */ /*! - \qmlproperty string Document::subject + \qmlproperty string PdfDocument::subject This property holds the subject of the document. */ /*! - \qmlproperty string Document::keywords + \qmlproperty string PdfDocument::keywords This property holds the keywords associated with the document. */ /*! - \qmlproperty string Document::creator + \qmlproperty string PdfDocument::creator If the document was converted to PDF from another format, this property holds the name of the software that created the original document. */ /*! - \qmlproperty string Document::producer + \qmlproperty string PdfDocument::producer If the document was converted to PDF from another format, this property holds the name of the software that converted it to PDF. */ /*! - \qmlproperty string Document::creationDate + \qmlproperty string PdfDocument::creationDate This property holds the date and time the document was created. */ /*! - \qmlproperty string Document::modificationDate + \qmlproperty string PdfDocument::modificationDate This property holds the date and time the document was most recently modified. */ /*! - \qmlproperty enum Document::status + \qmlproperty enum PdfDocument::status This property tells the current status of the document. The possible values are: diff --git a/src/pdf/quick/qquickpdfdocument_p.h b/src/pdf/quick/qquickpdfdocument_p.h index 1ec7edb1a..cefa4f756 100644 --- a/src/pdf/quick/qquickpdfdocument_p.h +++ b/src/pdf/quick/qquickpdfdocument_p.h @@ -62,6 +62,8 @@ class QQuickPdfDocument : public QObject, public QQmlParserStatus Q_OBJECT Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged FINAL) + Q_PROPERTY(qreal maxPageWidth READ maxPageWidth NOTIFY metaDataChanged) + Q_PROPERTY(qreal maxPageHeight READ maxPageHeight NOTIFY metaDataChanged) Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged FINAL) Q_PROPERTY(QPdfDocument::Status status READ status NOTIFY statusChanged FINAL) Q_PROPERTY(QString error READ error NOTIFY statusChanged FINAL) @@ -102,6 +104,9 @@ public: QDateTime modificationDate() { return m_doc.metaData(QPdfDocument::ModificationDate).toDateTime(); } Q_INVOKABLE QSizeF pagePointSize(int page) const; + qreal maxPageWidth() const; + qreal maxPageHeight() const; + Q_INVOKABLE qreal heightSumBeforePage(int page, qreal spacing = 0, int facingPages = 1) const; Q_SIGNALS: void sourceChanged(); @@ -113,11 +118,14 @@ Q_SIGNALS: private: QPdfDocument &document() { return m_doc; } + void updateMaxPageSize(); private: QUrl m_source; QPdfDocument m_doc; + QSizeF m_maxPageWidthHeight; + friend class QQuickPdfLinkModel; friend class QQuickPdfSearchModel; friend class QQuickPdfSelection; diff --git a/src/pdf/quick/qquickpdflinkmodel.cpp b/src/pdf/quick/qquickpdflinkmodel.cpp new file mode 100644 index 000000000..f2ff3fd22 --- /dev/null +++ b/src/pdf/quick/qquickpdflinkmodel.cpp @@ -0,0 +1,132 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qquickpdflinkmodel_p.h" +#include <QQuickItem> +#include <QQmlEngine> +#include <QStandardPaths> +#include <private/qguiapplication_p.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfLinkModel + \instantiates QQuickPdfLinkModel + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of links within a PDF document. + \since 5.15 + + PdfLinkModel provides the geometry and the destination for each link + that the specified \l page contains. + + The available model roles are: + + \value rect + Bounding rectangle around the link. + \value url + If the link is a web link, the URL for that; otherwise an empty URL. + \value page + If the link is an internal link, the page number to which the link should jump; otherwise \c {-1}. + \value location + If the link is an internal link, the location on the page to which the link should jump. + \value zoom + If the link is an internal link, the intended zoom level on the destination page. + + Normally it will be used with \l {QtQuick::Repeater}{Repeater} to visualize + the links and provide the ability to click them: + + \qml + Repeater { + model: PdfLinkModel { + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x + y: rect.y + width: rect.width + height: rect.height + HoverHandler { cursorShape: Qt.PointingHandCursor } + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } + \endqml + + \note General-purpose PDF viewing capabilities are provided by + \l PdfScrollablePageView and \l PdfMultiPageView. PdfLinkModel is only needed + when building PDF view components from scratch. +*/ + +QQuickPdfLinkModel::QQuickPdfLinkModel(QObject *parent) + : QPdfLinkModel(parent) +{ +} + +/*! + \qmlproperty PdfDocument PdfLinkModel::document + + This property holds the PDF document in which links are to be found. +*/ +QQuickPdfDocument *QQuickPdfLinkModel::document() const +{ + return m_quickDocument; +} + +void QQuickPdfLinkModel::setDocument(QQuickPdfDocument *document) +{ + if (document == m_quickDocument) + return; + m_quickDocument = document; + QPdfLinkModel::setDocument(&document->m_doc); +} + +/*! + \qmlproperty int PdfLinkModel::page + + This property holds the page number on which links are to be found. +*/ + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdflinkmodel_p.h b/src/pdf/quick/qquickpdflinkmodel_p.h new file mode 100644 index 000000000..23ad6c8c1 --- /dev/null +++ b/src/pdf/quick/qquickpdflinkmodel_p.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QQUICKPDFLINKMODEL_P_H +#define QQUICKPDFLINKMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickpdfdocument_p.h" +#include "../api/qpdflinkmodel_p.h" + +#include <QVariant> +#include <QtQml/qqml.h> + +QT_BEGIN_NAMESPACE + +class QQuickPdfLinkModel : public QPdfLinkModel +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + +public: + explicit QQuickPdfLinkModel(QObject *parent = nullptr); + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument *document); + +signals: + void documentChanged(); + +private: + void updateResults(); + +private: + QQuickPdfDocument *m_quickDocument; + QVector<QPolygonF> m_linksGeometry; + + Q_DISABLE_COPY(QQuickPdfLinkModel) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfLinkModel) + +#endif // QQUICKPDFLINKMODEL_P_H diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp new file mode 100644 index 000000000..7ba317557 --- /dev/null +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -0,0 +1,266 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qquickpdfnavigationstack_p.h" +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcNav, "qt.pdf.navigationstack") + +/*! + \qmltype PdfNavigationStack + \instantiates QQuickPdfNavigationStack + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief History of the destinations visited within a PDF Document. + \since 5.15 + + PdfNavigationStack remembers which destinations the user has visited in a PDF + document, and provides the ability to traverse backward and forward. +*/ + +QQuickPdfNavigationStack::QQuickPdfNavigationStack(QObject *parent) + : QObject(parent) +{ + push(0, QPointF(), 1); +} + +/*! + \qmlmethod void PdfNavigationStack::forward() + + Goes back to the page, location and zoom level that was being viewed before + back() was called, and then emits the \l jumped() signal. + + If a new destination was pushed since the last time \l back() was called, + the forward() function does nothing, because there is a branch in the + timeline which causes the "future" to be lost. +*/ +void QQuickPdfNavigationStack::forward() +{ + if (m_currentHistoryIndex >= m_pageHistory.count() - 1) + return; + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + ++m_currentHistoryIndex; + m_changing = true; + emit jumped(currentPage(), currentLocation(), currentZoom()); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); + emit currentPageChanged(); + if (currentLocationWas != currentLocation()) + emit currentLocationChanged(); + if (!backAvailableWas) + emit backAvailableChanged(); + if (forwardAvailableWas != forwardAvailable()) + emit forwardAvailableChanged(); + m_changing = false; +} + +/*! + \qmlmethod void PdfNavigationStack::back() + + Pops the stack, updates the \l currentPage, \l currentLocation and + \l currentZoom properties to the most-recently-viewed destination, and then + emits the \l jumped() signal. +*/ +void QQuickPdfNavigationStack::back() +{ + if (m_currentHistoryIndex <= 0) + return; + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + --m_currentHistoryIndex; + m_changing = true; + emit jumped(currentPage(), currentLocation(), currentZoom()); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); + emit currentPageChanged(); + if (currentLocationWas != currentLocation()) + emit currentLocationChanged(); + if (backAvailableWas != backAvailable()) + emit backAvailableChanged(); + if (!forwardAvailableWas) + emit forwardAvailableChanged(); + m_changing = false; +} + +/*! + \qmlproperty int PdfNavigationStack::currentPage + + This property holds the current page that is being viewed. + If there is no current page, it holds \c -1. +*/ +int QQuickPdfNavigationStack::currentPage() const +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return -1; + return m_pageHistory.at(m_currentHistoryIndex)->page; +} + +/*! + \qmlproperty point PdfNavigationStack::currentLocation + + This property holds the current location on the page that is being viewed. +*/ +QPointF QQuickPdfNavigationStack::currentLocation() const +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return QPointF(); + return m_pageHistory.at(m_currentHistoryIndex)->location; +} + +/*! + \qmlproperty real PdfNavigationStack::currentZoom + + This property holds the magnification scale on the page that is being viewed. +*/ +qreal QQuickPdfNavigationStack::currentZoom() const +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return 1; + return m_pageHistory.at(m_currentHistoryIndex)->zoom; +} + +/*! + \qmlmethod void PdfNavigationStack::push(int page, point location, qreal zoom) + + Adds the given destination, consisting of \a page, \a location and \a zoom, + to the history of visited locations. + + If forwardAvailable is \c true, calling this function represents a branch + in the timeline which causes the "future" to be lost, and therefore + forwardAvailable will change to \c false. +*/ +void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) +{ + if (page == currentPage() && location == currentLocation() && zoom == currentZoom()) + return; + if (qFuzzyIsNull(zoom)) + zoom = currentZoom(); + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + if (!m_changing) { + if (m_currentHistoryIndex >= 0 && forwardAvailableWas) + m_pageHistory.remove(m_currentHistoryIndex + 1, m_pageHistory.count() - m_currentHistoryIndex - 1); + m_pageHistory.append(QExplicitlySharedDataPointer<QPdfDestinationPrivate>(new QPdfDestinationPrivate(page, location, zoom))); + m_currentHistoryIndex = m_pageHistory.count() - 1; + } + emit currentZoomChanged(); + emit currentPageChanged(); + emit currentLocationChanged(); + if (m_changing) + return; + if (!backAvailableWas) + emit backAvailableChanged(); + if (forwardAvailableWas) + emit forwardAvailableChanged(); + emit jumped(page, location, zoom); + qCDebug(qLcNav) << "push: index" << m_currentHistoryIndex << "page" << page + << "@" << location << "zoom" << zoom << "-> history" << + [this]() { + QStringList ret; + for (auto d : m_pageHistory) + ret << QString::number(d->page); + return ret.join(','); + }(); +} + +/*! + \qmlmethod void PdfNavigationStack::update(int page, point location, qreal zoom) + + Modifies the current destination, consisting of \a page, \a location and \a zoom. + + This can be called periodically while the user is manually moving around + the document, so that after back() is called, forward() will jump back to + the most-recently-viewed destination rather than the destination that was + last specified by push(). + + The \c currentZoomChanged, \c currentPageChanged and \c currentLocationChanged + signals will be emitted if the respective properties are actually changed. + The \l jumped signal is not emitted, because this operation + represents smooth movement rather than a navigational jump. +*/ +void QQuickPdfNavigationStack::update(int page, QPointF location, qreal zoom) +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return; + int currentPageWas = currentPage(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + if (page == currentPageWas && location == currentLocationWas && zoom == currentZoomWas) + return; + m_pageHistory[m_currentHistoryIndex]->page = page; + m_pageHistory[m_currentHistoryIndex]->location = location; + m_pageHistory[m_currentHistoryIndex]->zoom = zoom; + if (currentZoomWas != zoom) + emit currentZoomChanged(); + if (currentPageWas != page) + emit currentPageChanged(); + if (currentLocationWas != location) + emit currentLocationChanged(); + qCDebug(qLcNav) << "update: index" << m_currentHistoryIndex << "page" << page + << "@" << location << "zoom" << zoom << "-> history" << + [this]() { + QStringList ret; + for (auto d : m_pageHistory) + ret << QString::number(d->page); + return ret.join(','); + }(); +} + +bool QQuickPdfNavigationStack::backAvailable() const +{ + return m_currentHistoryIndex > 0; +} + +bool QQuickPdfNavigationStack::forwardAvailable() const +{ + return m_currentHistoryIndex < m_pageHistory.count() - 1; +} + +/*! + \qmlsignal PdfNavigationStack::jumped(int page, point location, qreal zoom) + + This signal is emitted when forward(), back() or push() is called, but not + when update() is called. +*/ + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfnavigationstack_p.h b/src/pdf/quick/qquickpdfnavigationstack_p.h new file mode 100644 index 000000000..8d7102fb1 --- /dev/null +++ b/src/pdf/quick/qquickpdfnavigationstack_p.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QQUICKPDFNAVIGATIONSTACK_P_H +#define QQUICKPDFNAVIGATIONSTACK_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickpdfdocument_p.h" +#include "../api/qpdfdestination_p.h" + +#include <QtQml/qqml.h> + +QT_BEGIN_NAMESPACE + +class QQuickPdfNavigationStack : public QObject +{ + Q_OBJECT + Q_PROPERTY(int currentPage READ currentPage NOTIFY currentPageChanged) + Q_PROPERTY(QPointF currentLocation READ currentLocation NOTIFY currentLocationChanged) + Q_PROPERTY(qreal currentZoom READ currentZoom NOTIFY currentZoomChanged) + Q_PROPERTY(bool backAvailable READ backAvailable NOTIFY backAvailableChanged) + Q_PROPERTY(bool forwardAvailable READ forwardAvailable NOTIFY forwardAvailableChanged) + +public: + explicit QQuickPdfNavigationStack(QObject *parent = nullptr); + + Q_INVOKABLE void push(int page, QPointF location, qreal zoom); + Q_INVOKABLE void update(int page, QPointF location, qreal zoom); + Q_INVOKABLE void forward(); + Q_INVOKABLE void back(); + + int currentPage() const; + QPointF currentLocation() const; + qreal currentZoom() const; + + bool backAvailable() const; + bool forwardAvailable() const; + +Q_SIGNALS: + void currentPageChanged(); + void currentLocationChanged(); + void currentZoomChanged(); + void backAvailableChanged(); + void forwardAvailableChanged(); + void jumped(int page, QPointF location, qreal zoom); + +private: + QVector<QExplicitlySharedDataPointer<QPdfDestinationPrivate>> m_pageHistory; + int m_currentHistoryIndex = 0; + bool m_changing = false; + + Q_DISABLE_COPY(QQuickPdfNavigationStack) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfNavigationStack) + +#endif // QQUICKPDFNAVIGATIONSTACK_P_H diff --git a/src/pdf/quick/qquickpdfsearchmodel.cpp b/src/pdf/quick/qquickpdfsearchmodel.cpp index 8b0e88673..a4b457841 100644 --- a/src/pdf/quick/qquickpdfsearchmodel.cpp +++ b/src/pdf/quick/qquickpdfsearchmodel.cpp @@ -35,13 +35,12 @@ ****************************************************************************/ #include "qquickpdfsearchmodel_p.h" -#include <QQuickItem> -#include <QQmlEngine> -#include <QStandardPaths> -#include <private/qguiapplication_p.h> +#include <QtCore/qloggingcategory.h> QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") + /*! \qmltype PdfSearchModel \instantiates QQuickPdfSearchModel @@ -57,6 +56,8 @@ QT_BEGIN_NAMESPACE QQuickPdfSearchModel::QQuickPdfSearchModel(QObject *parent) : QPdfSearchModel(parent) { + connect(this, &QPdfSearchModel::searchStringChanged, + this, &QQuickPdfSearchModel::onResultsChanged); } QQuickPdfDocument *QQuickPdfSearchModel::document() const @@ -66,18 +67,21 @@ QQuickPdfDocument *QQuickPdfSearchModel::document() const void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) { - if (document == m_quickDocument) + if (document == m_quickDocument || !document) return; + m_quickDocument = document; QPdfSearchModel::setDocument(&document->m_doc); } /*! - \qmlproperty list<list<point>> PdfSearchModel::matchGeometry + \qmlproperty list<list<point>> PdfSearchModel::currentResultBoundingPolygons A set of paths in a form that can be bound to the \c paths property of a \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of - rectangles around all the locations where search results are found: + rectangles around the regions comprising the search result \l currentResult + on \l currentPage. This is normally used to highlight one search result + at a time, in a UI that allows stepping through the results: \qml PdfDocument { @@ -86,12 +90,13 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) PdfSearchModel { id: searchModel document: doc - page: doc.currentPage + currentPage: view.currentPage + currentResult: ... } Shape { ShapePath { PathMultiline { - paths: searchModel.matchGeometry + paths: searchModel.currentResultBoundingPolygons } } } @@ -99,67 +104,174 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) \sa PathMultiline */ -QVector<QPolygonF> QQuickPdfSearchModel::matchGeometry() const +QVector<QPolygonF> QQuickPdfSearchModel::currentResultBoundingPolygons() const { - return m_matchGeometry; + QVector<QPolygonF> ret; + const auto &results = const_cast<QQuickPdfSearchModel *>(this)->resultsOnPage(m_currentPage); + if (m_currentResult < 0 || m_currentResult >= results.count()) + return ret; + const auto result = results[m_currentResult]; + for (auto rect : result.rectangles()) + ret << QPolygonF(rect); + return ret; } -/*! - \qmlproperty string PdfSearchModel::searchString - - The string to search for. -*/ -QString QQuickPdfSearchModel::searchString() const +void QQuickPdfSearchModel::onResultsChanged() { - return m_searchString; + emit currentPageBoundingPolygonsChanged(); + emit currentResultBoundingPolygonsChanged(); } -void QQuickPdfSearchModel::setSearchString(QString searchString) -{ - if (m_searchString == searchString) - return; +/*! + \qmlproperty list<list<point>> PdfSearchModel::currentPageBoundingPolygons + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around all the regions where search results are found on + \l currentPage: + + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml - m_searchString = searchString; - emit searchStringChanged(); - updateResults(); + \sa PathMultiline +*/ +QVector<QPolygonF> QQuickPdfSearchModel::currentPageBoundingPolygons() const +{ + return const_cast<QQuickPdfSearchModel *>(this)->boundingPolygonsOnPage(m_currentPage); } /*! - \qmlproperty int PdfSearchModel::page + \qmlfunction list<list<point>> PdfSearchModel::boundingPolygonsOnPage(int page) - The page number on which to search. + Returns a set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around all the locations where search results are found: - \sa QtQuick::Image::currentFrame + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml + + \sa PathMultiline */ -int QQuickPdfSearchModel::page() const +QVector<QPolygonF> QQuickPdfSearchModel::boundingPolygonsOnPage(int page) { - return m_page; + if (!document() || searchString().isEmpty() || page < 0 || page > document()->pageCount()) + return {}; + + updatePage(page); + + QVector<QPolygonF> ret; + auto m = QPdfSearchModel::resultsOnPage(page); + for (auto result : m) { + for (auto rect : result.rectangles()) + ret << QPolygonF(rect); + } + + return ret; } -void QQuickPdfSearchModel::setPage(int page) +/*! + \qmlproperty int PdfSearchModel::currentPage + + The page on which \l currentMatchGeometry should provide filtered search results. +*/ +void QQuickPdfSearchModel::setCurrentPage(int currentPage) { - if (m_page == page) + if (m_currentPage == currentPage) return; - m_page = page; - emit pageChanged(); - updateResults(); + if (currentPage < 0) + currentPage = document()->pageCount() - 1; + else if (currentPage >= document()->pageCount()) + currentPage = 0; + + m_currentPage = currentPage; + if (!m_suspendSignals) { + emit currentPageChanged(); + onResultsChanged(); + } } -void QQuickPdfSearchModel::updateResults() +/*! + \qmlproperty int PdfSearchModel::currentResult + + The result index on \l currentPage for which \l currentResultBoundingPolygons + should provide the regions to highlight. +*/ +void QQuickPdfSearchModel::setCurrentResult(int currentResult) { - if (!document() || (m_searchString.isEmpty() && !m_matchGeometry.isEmpty()) || m_page < 0 || m_page > document()->pageCount()) { - m_matchGeometry.clear(); - emit matchGeometryChanged(); - } - QVector<QRectF> m = QPdfSearchModel::matches(m_page, m_searchString); - QVector<QPolygonF> matches; - for (QRectF r : m) - matches << QPolygonF(r); - if (matches != m_matchGeometry) { - m_matchGeometry = matches; - emit matchGeometryChanged(); + if (m_currentResult == currentResult) + return; + + int currentResultWas = currentResult; + int currentPageWas = m_currentPage; + if (currentResult < 0) { + setCurrentPage(m_currentPage - 1); + while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) { + m_suspendSignals = true; + setCurrentPage(m_currentPage - 1); + } + if (m_suspendSignals) { + emit currentPageChanged(); + m_suspendSignals = false; + } + const auto results = resultsOnPage(m_currentPage); + currentResult = results.count() - 1; + } else { + const auto results = resultsOnPage(m_currentPage); + if (currentResult >= results.count()) { + setCurrentPage(m_currentPage + 1); + while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) { + m_suspendSignals = true; + setCurrentPage(m_currentPage + 1); + } + if (m_suspendSignals) { + emit currentPageChanged(); + m_suspendSignals = false; + } + currentResult = 0; + } } + qCDebug(qLcS) << "currentResult was" << m_currentResult + << "requested" << currentResultWas << "on page" << currentPageWas + << "->" << currentResult << "on page" << m_currentPage; + + m_currentResult = currentResult; + emit currentResultChanged(); + emit currentResultBoundingPolygonsChanged(); } +/*! + \qmlproperty string PdfSearchModel::searchString + + The string to search for. +*/ + QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfsearchmodel_p.h b/src/pdf/quick/qquickpdfsearchmodel_p.h index 799ef825f..3e05f80e3 100644 --- a/src/pdf/quick/qquickpdfsearchmodel_p.h +++ b/src/pdf/quick/qquickpdfsearchmodel_p.h @@ -51,7 +51,7 @@ #include "qquickpdfdocument_p.h" #include "../api/qpdfsearchmodel.h" -#include <QVariant> +#include <QtCore/qvariant.h> #include <QtQml/qqml.h> QT_BEGIN_NAMESPACE @@ -60,9 +60,10 @@ class QQuickPdfSearchModel : public QPdfSearchModel { Q_OBJECT Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) - Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) - Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) - Q_PROPERTY(QVector<QPolygonF> matchGeometry READ matchGeometry NOTIFY matchGeometryChanged) + Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) + Q_PROPERTY(int currentResult READ currentResult WRITE setCurrentResult NOTIFY currentResultChanged) + Q_PROPERTY(QVector<QPolygonF> currentPageBoundingPolygons READ currentPageBoundingPolygons NOTIFY currentPageBoundingPolygonsChanged) + Q_PROPERTY(QVector<QPolygonF> currentResultBoundingPolygons READ currentResultBoundingPolygons NOTIFY currentResultBoundingPolygonsChanged) public: explicit QQuickPdfSearchModel(QObject *parent = nullptr); @@ -70,28 +71,33 @@ public: QQuickPdfDocument *document() const; void setDocument(QQuickPdfDocument * document); - int page() const; - void setPage(int page); + Q_INVOKABLE QVector<QPolygonF> boundingPolygonsOnPage(int page); - QString searchString() const; - void setSearchString(QString searchString); + int currentPage() const { return m_currentPage; } + void setCurrentPage(int currentPage); - QVector<QPolygonF> matchGeometry() const; + int currentResult() const { return m_currentResult; } + void setCurrentResult(int currentResult); + + QVector<QPolygonF> currentPageBoundingPolygons() const; + QVector<QPolygonF> currentResultBoundingPolygons() const; signals: void documentChanged(); - void pageChanged(); - void searchStringChanged(); - void matchGeometryChanged(); + void currentPageChanged(); + void currentResultChanged(); + void currentPageBoundingPolygonsChanged(); + void currentResultBoundingPolygonsChanged(); private: void updateResults(); + void onResultsChanged(); private: QQuickPdfDocument *m_quickDocument = nullptr; - QString m_searchString; - QVector<QPolygonF> m_matchGeometry; - int m_page; + int m_currentPage = 0; + int m_currentResult = 0; + bool m_suspendSignals = false; Q_DISABLE_COPY(QQuickPdfSearchModel) }; @@ -99,5 +105,6 @@ private: QT_END_NAMESPACE QML_DECLARE_TYPE(QQuickPdfSearchModel) +QML_DECLARE_TYPE(QPdfSelection) #endif // QQUICKPDFSEARCHMODEL_P_H diff --git a/src/pdf/quick/qquickpdfselection.cpp b/src/pdf/quick/qquickpdfselection.cpp new file mode 100644 index 000000000..d313820ba --- /dev/null +++ b/src/pdf/quick/qquickpdfselection.cpp @@ -0,0 +1,268 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#include "qquickpdfselection_p.h" +#include "qquickpdfdocument_p.h" +#include <QClipboard> +#include <QQuickItem> +#include <QQmlEngine> +#include <QStandardPaths> +#include <private/qguiapplication_p.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfSelection + \instantiates QQuickPdfSelection + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of a text selection within a PDF Document. + \since 5.15 + + PdfSelection provides the text string and its geometry within a bounding box + from one point to another. +*/ + +/*! + Constructs a SearchModel. +*/ +QQuickPdfSelection::QQuickPdfSelection(QObject *parent) + : QObject(parent) +{ +} + +QQuickPdfDocument *QQuickPdfSelection::document() const +{ + return m_document; +} + +void QQuickPdfSelection::setDocument(QQuickPdfDocument *document) +{ + if (m_document == document) + return; + + if (m_document) { + disconnect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); + } + m_document = document; + emit documentChanged(); + resetPoints(); + connect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); +} + +/*! + \qmlproperty list<list<point>> PdfSelection::geometry + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around the text regions that are included in the selection: + + \qml + PdfDocument { + id: doc + } + PdfSelection { + id: selection + document: doc + fromPoint: textSelectionDrag.centroid.pressPosition + toPoint: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active + } + Shape { + ShapePath { + PathMultiline { + paths: selection.geometry + } + } + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + \endqml + + \sa PathMultiline +*/ +QVector<QPolygonF> QQuickPdfSelection::geometry() const +{ + return m_geometry; +} + +void QQuickPdfSelection::resetPoints() +{ + bool wasHolding = m_hold; + m_hold = false; + setFromPoint(QPointF()); + setToPoint(QPointF()); + m_hold = wasHolding; +} + +/*! + \qmlproperty int PdfSelection::page + + The page number on which to search. + + \sa QtQuick::Image::currentFrame +*/ +int QQuickPdfSelection::page() const +{ + return m_page; +} + +void QQuickPdfSelection::setPage(int page) +{ + if (m_page == page) + return; + + m_page = page; + emit pageChanged(); + resetPoints(); +} + +/*! + \qmlproperty point PdfSelection::fromPoint + + The beginning location, in \l {https://en.wikipedia.org/wiki/Point_(typography)}{points} + from the upper-left corner of the page, from which to find selected text. + This can be bound to a scaled version of the \c centroid.pressPosition + of a \l DragHandler to begin selecting text from the position where the user + presses the mouse button and begins dragging, for example. +*/ +QPointF QQuickPdfSelection::fromPoint() const +{ + return m_fromPoint; +} + +void QQuickPdfSelection::setFromPoint(QPointF fromPoint) +{ + if (m_hold || m_fromPoint == fromPoint) + return; + + m_fromPoint = fromPoint; + emit fromPointChanged(); + updateResults(); +} + +/*! + \qmlproperty point PdfSelection::toPoint + + The ending location, in \l {https://en.wikipedia.org/wiki/Point_(typography)}{points} + from the upper-left corner of the page, from which to find selected text. + This can be bound to a scaled version of the \c centroid.position + of a \l DragHandler to end selection of text at the position where the user + is currently dragging the mouse, for example. +*/ +QPointF QQuickPdfSelection::toPoint() const +{ + return m_toPoint; +} + +void QQuickPdfSelection::setToPoint(QPointF toPoint) +{ + if (m_hold || m_toPoint == toPoint) + return; + + m_toPoint = toPoint; + emit toPointChanged(); + updateResults(); +} + +/*! + \qmlproperty bool PdfSelection::hold + + Controls whether to hold the existing selection regardless of changes to + \l fromPoint and \l toPoint. This property can be set to \c true when the mouse + or touchpoint is released, so that the selection is not lost due to the + point bindings changing. +*/ +bool QQuickPdfSelection::hold() const +{ + return m_hold; +} + +void QQuickPdfSelection::setHold(bool hold) +{ + if (m_hold == hold) + return; + + m_hold = hold; + emit holdChanged(); +} + +/*! + \qmlproperty string PdfSelection::string + + The string found. +*/ +QString QQuickPdfSelection::text() const +{ + return m_text; +} + +#if QT_CONFIG(clipboard) +/*! + \qmlmethod void PdfSelection::copyToClipboard() + + Copies plain text from the \l string property to the system clipboard. +*/ +void QQuickPdfSelection::copyToClipboard() const +{ + QGuiApplication::clipboard()->setText(m_text); +} +#endif + +void QQuickPdfSelection::updateResults() +{ + if (!m_document) + return; + QPdfSelection sel = m_document->document().getSelection(m_page, m_fromPoint, m_toPoint); + if (sel.text() != m_text) { + m_text = sel.text(); + if (QGuiApplication::clipboard()->supportsSelection()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + emit geometryChanged(); + } +} + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfselection_p.h b/src/pdf/quick/qquickpdfselection_p.h new file mode 100644 index 000000000..a0e6d1a8d --- /dev/null +++ b/src/pdf/quick/qquickpdfselection_p.h @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QQUICKPDFSELECTION_P_H +#define QQUICKPDFSELECTION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QPointF> +#include <QPolygonF> +#include <QVariant> +#include <QtQml/qqml.h> + +QT_BEGIN_NAMESPACE + +class QQuickPdfDocument; + +class QQuickPdfSelection : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) + Q_PROPERTY(QPointF fromPoint READ fromPoint WRITE setFromPoint NOTIFY fromPointChanged) + Q_PROPERTY(QPointF toPoint READ toPoint WRITE setToPoint NOTIFY toPointChanged) + Q_PROPERTY(bool hold READ hold WRITE setHold NOTIFY holdChanged) + + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QVector<QPolygonF> geometry READ geometry NOTIFY geometryChanged) + +public: + explicit QQuickPdfSelection(QObject *parent = nullptr); + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument * document); + int page() const; + void setPage(int page); + QPointF fromPoint() const; + void setFromPoint(QPointF fromPoint); + QPointF toPoint() const; + void setToPoint(QPointF toPoint); + bool hold() const; + void setHold(bool hold); + + QString text() const; + QVector<QPolygonF> geometry() const; + +#if QT_CONFIG(clipboard) + Q_INVOKABLE void copyToClipboard() const; +#endif + +signals: + void documentChanged(); + void pageChanged(); + void fromPointChanged(); + void toPointChanged(); + void holdChanged(); + void textChanged(); + void geometryChanged(); + +private: + void resetPoints(); + void updateResults(); + +private: + QQuickPdfDocument *m_document = nullptr; + QPointF m_fromPoint; + QPointF m_toPoint; + QString m_text; + QVector<QPolygonF> m_geometry; + int m_page = 0; + bool m_hold = false; + + Q_DISABLE_COPY(QQuickPdfSelection) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfSelection) +\ +#endif // QQUICKPDFSELECTION_P_H diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index cda768369..b62b80346 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -6,7 +6,9 @@ IMPORT_VERSION = 1.0 #QMAKE_DOCS = $$PWD/doc/qtquickpdf.qdocconf PDF_QML_FILES = \ + qml/PdfMultiPageView.qml \ qml/PdfPageView.qml \ + qml/PdfScrollablePageView.qml \ QML_FILES += $$PDF_QML_FILES qmldir @@ -15,11 +17,17 @@ RESOURCES += resources.qrc SOURCES += \ plugin.cpp \ qquickpdfdocument.cpp \ + qquickpdflinkmodel.cpp \ + qquickpdfnavigationstack.cpp \ qquickpdfsearchmodel.cpp \ + qquickpdfselection.cpp \ HEADERS += \ qquickpdfdocument_p.h \ + qquickpdflinkmodel_p.h \ + qquickpdfnavigationstack_p.h \ qquickpdfsearchmodel_p.h \ + qquickpdfselection_p.h \ QT += pdf quick-private gui gui-private core core-private qml qml-private diff --git a/src/pdf/quick/resources.qrc b/src/pdf/quick/resources.qrc index a3f34189c..20cac4827 100644 --- a/src/pdf/quick/resources.qrc +++ b/src/pdf/quick/resources.qrc @@ -1,5 +1,7 @@ <RCC> <qresource prefix="/qt-project.org/qtpdf"> + <file>qml/PdfMultiPageView.qml</file> <file>qml/PdfPageView.qml</file> + <file>qml/PdfScrollablePageView.qml</file> </qresource> </RCC> diff --git a/src/plugins/imageformats/pdf/qpdfiohandler.cpp b/src/plugins/imageformats/pdf/qpdfiohandler.cpp index 9df85cf08..739e8b34c 100644 --- a/src/plugins/imageformats/pdf/qpdfiohandler.cpp +++ b/src/plugins/imageformats/pdf/qpdfiohandler.cpp @@ -98,7 +98,8 @@ bool QPdfIOHandler::read(QImage *image) if (m_page < 0) m_page = 0; const bool xform = (m_clipRect.isValid() || m_scaledSize.isValid() || m_scaledClipRect.isValid()); - QSize finalSize = m_doc.pageSize(m_page).toSize(); + QSize pageSize = m_doc.pageSize(m_page).toSize(); + QSize finalSize = pageSize; QRectF bounds; if (xform && !finalSize.isEmpty()) { bounds = QRectF(QPointF(0,0), QSizeF(finalSize)); @@ -112,6 +113,7 @@ bool QPdfIOHandler::read(QImage *image) sc = QSizeF(qreal(m_scaledSize.width()) / finalSize.width(), qreal(m_scaledSize.height()) / finalSize.height()); finalSize = m_scaledSize; + pageSize = m_scaledSize; } if (m_scaledClipRect.isValid()) { tr2 = -m_scaledClipRect.topLeft(); @@ -133,9 +135,13 @@ bool QPdfIOHandler::read(QImage *image) } } if (!finalSize.isEmpty()) { + QPdfDocumentRenderOptions options; + if (m_scaledClipRect.isValid()) + options.setScaledClipRect(m_scaledClipRect); + options.setScaledSize(pageSize); image->fill(m_backColor.rgba()); QPainter p(image); - QImage pageImage = m_doc.render(m_page, finalSize); + QImage pageImage = m_doc.render(m_page, finalSize, options); p.drawImage(0, 0, pageImage); p.end(); } diff --git a/tests/manual/quick/pdf/withdoc.qml b/tests/manual/quick/pdf/withdoc.qml index 0fed5b16e..2d82a6abf 100644 --- a/tests/manual/quick/pdf/withdoc.qml +++ b/tests/manual/quick/pdf/withdoc.qml @@ -51,6 +51,7 @@ import QtQuick 2.14 import QtQuick.Controls 2.14 import Qt.labs.platform 1.1 as Platform import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 import QtQuick.Window 2.14 Window { @@ -72,6 +73,15 @@ Window { onAccepted: doc.source = file } + PdfSelection { + id: selection + document: doc + page: image.currentFrame + fromPoint: dragHandler.centroid.pressPosition + toPoint: dragHandler.centroid.position + hold: !dragHandler.active + } + Column { id: column anchors.fill: parent @@ -149,6 +159,43 @@ Window { onActivated: Qt.quit() } } + + Shape { + anchors.fill: parent + opacity: 0.25 + ShapePath { + fillColor: "cyan" + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } + } + } + + Repeater { + model: PdfLinkModel { + id: linkModel + document: doc + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x + y: rect.y + width: rect.width + height: rect.height +// HoverHandler { cursorShape: Qt.PointingHandCursor } // 5.15 onward (QTBUG-68073) + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } } } } |