summaryrefslogtreecommitdiffstats
path: root/src/pdf
diff options
context:
space:
mode:
Diffstat (limited to 'src/pdf')
-rw-r--r--src/pdf/CMakeLists.txt259
-rw-r--r--src/pdf/configure.cmake54
-rw-r--r--src/pdf/configure/BUILD.root.gn.in73
-rw-r--r--src/pdf/doc/about_credits.tmpl1
-rw-r--r--src/pdf/doc/about_credits_entry.tmpl13
-rw-r--r--src/pdf/doc/images/multipageviewer.pngbin0 -> 39637 bytes
-rw-r--r--src/pdf/doc/images/pdfviewer.pngbin0 -> 264348 bytes
-rw-r--r--src/pdf/doc/images/search-results.pngbin0 -> 19718 bytes
-rw-r--r--src/pdf/doc/images/singlepageviewer.webpbin0 -> 57680 bytes
-rw-r--r--src/pdf/doc/images/wrapping-search-result.pngbin0 -> 39106 bytes
-rw-r--r--src/pdf/doc/qtpdf.qdocconf67
-rw-r--r--src/pdf/doc/snippets/multipageview.qml11
-rw-r--r--src/pdf/doc/snippets/pdfpageview.qml12
-rw-r--r--src/pdf/doc/snippets/qtpdf-build.cmake2
-rw-r--r--src/pdf/doc/snippets/qtpdf_build_snippet.qdoc6
-rw-r--r--src/pdf/doc/src/qtpdf-examples.qdoc12
-rw-r--r--src/pdf/doc/src/qtpdf-index.qdoc80
-rw-r--r--src/pdf/doc/src/qtpdf-licensing.qdoc18
-rw-r--r--src/pdf/doc/src/qtpdf-module.qdoc21
-rw-r--r--src/pdf/doc/src/qtpdf-platformnotes.qdoc11
-rw-r--r--src/pdf/plugins/imageformats/pdf/CMakeLists.txt13
-rw-r--r--src/pdf/plugins/imageformats/pdf/main.cpp41
-rw-r--r--src/pdf/plugins/imageformats/pdf/pdf.json4
-rw-r--r--src/pdf/plugins/imageformats/pdf/qpdfiohandler.cpp225
-rw-r--r--src/pdf/plugins/imageformats/pdf/qpdfiohandler_p.h57
-rw-r--r--src/pdf/qpdfbookmarkmodel.cpp388
-rw-r--r--src/pdf/qpdfbookmarkmodel.h60
-rw-r--r--src/pdf/qpdfdocument.cpp1111
-rw-r--r--src/pdf/qpdfdocument.h127
-rw-r--r--src/pdf/qpdfdocument_p.h123
-rw-r--r--src/pdf/qpdfdocumentrenderoptions.h81
-rw-r--r--src/pdf/qpdfdocumentrenderoptions.qdoc135
-rw-r--r--src/pdf/qpdffile.cpp28
-rw-r--r--src/pdf/qpdffile_p.h37
-rw-r--r--src/pdf/qpdflink.cpp189
-rw-r--r--src/pdf/qpdflink.h78
-rw-r--r--src/pdf/qpdflink_p.h53
-rw-r--r--src/pdf/qpdflinkmodel.cpp338
-rw-r--r--src/pdf/qpdflinkmodel.h67
-rw-r--r--src/pdf/qpdflinkmodel_p.h42
-rw-r--r--src/pdf/qpdfpagenavigator.cpp362
-rw-r--r--src/pdf/qpdfpagenavigator.h62
-rw-r--r--src/pdf/qpdfpagerenderer.cpp310
-rw-r--r--src/pdf/qpdfpagerenderer.h60
-rw-r--r--src/pdf/qpdfsearchmodel.cpp373
-rw-r--r--src/pdf/qpdfsearchmodel.h70
-rw-r--r--src/pdf/qpdfsearchmodel_p.h54
-rw-r--r--src/pdf/qpdfselection.cpp132
-rw-r--r--src/pdf/qpdfselection.h62
-rw-r--r--src/pdf/qpdfselection_p.h45
-rw-r--r--src/pdf/qtpdfglobal.h11
51 files changed, 5378 insertions, 0 deletions
diff --git a/src/pdf/CMakeLists.txt b/src/pdf/CMakeLists.txt
new file mode 100644
index 000000000..41018e7da
--- /dev/null
+++ b/src/pdf/CMakeLists.txt
@@ -0,0 +1,259 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.19)
+find_package(Ninja 1.7.2 REQUIRED)
+find_package(Nodejs 14.19 REQUIRED)
+find_package(PkgConfig)
+if(PkgConfig_FOUND)
+ create_pkg_config_host_wrapper(${CMAKE_CURRENT_BINARY_DIR})
+endif()
+
+set(buildDir "${CMAKE_CURRENT_BINARY_DIR}")
+
+##
+# PDF MODULE
+##
+
+qt_internal_add_module(Pdf
+ SOURCES
+ qpdfbookmarkmodel.cpp qpdfbookmarkmodel.h
+ qpdfdocument.cpp qpdfdocument.h qpdfdocument_p.h
+ qpdfdocumentrenderoptions.h
+ qpdffile.cpp qpdffile_p.h
+ qpdflink.cpp qpdflink.h qpdflink_p.h
+ qpdflinkmodel.cpp qpdflinkmodel.h qpdflinkmodel_p.h
+ qpdfpagenavigator.cpp qpdfpagenavigator.h
+ qpdfpagerenderer.cpp qpdfpagerenderer.h
+ qpdfsearchmodel.cpp qpdfsearchmodel.h qpdfsearchmodel_p.h
+ qpdfselection.cpp qpdfselection.h qpdfselection_p.h
+ qtpdfglobal.h
+ INCLUDE_DIRECTORIES
+ ../3rdparty/chromium
+ DEFINES
+ QT_BUILD_PDF_LIB
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Network
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::Gui
+)
+
+add_subdirectory(plugins/imageformats/pdf)
+
+get_install_config(config)
+get_architectures(archs)
+list(GET archs 0 arch)
+
+##
+# PDF DOCS
+##
+
+qt_internal_add_docs(Pdf
+ doc/qtpdf.qdocconf
+)
+
+add_code_attributions_target(
+ TARGET generate_pdf_attributions
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pdf_attributions.qdoc
+ GN_TARGET :QtPdf
+ FILE_TEMPLATE doc/about_credits.tmpl
+ ENTRY_TEMPLATE doc/about_credits_entry.tmpl
+ BUILDDIR ${buildDir}/${config}/${arch}
+)
+add_dependencies(generate_pdf_attributions run_pdf_GnDone)
+add_dependencies(prepare_docs_Pdf generate_pdf_attributions)
+
+##
+# TOOLCHAIN SETUP
+##
+
+if(LINUX OR MINGW OR ANDROID)
+ setup_toolchains()
+endif()
+
+##
+# GN BUILD SETUP
+##
+
+addSyncTargets(pdf)
+
+get_configs(configs)
+get_architectures(archs)
+foreach(arch ${archs})
+ foreach(config ${configs})
+
+ ##
+ # BULID.gn SETUP
+ ##
+
+ set(buildGn pdf_${config}_${arch})
+ add_gn_target(${buildGn} ${config} ${arch}
+ SOURCES DEFINES CXX_COMPILE_OPTIONS C_COMPILE_OPTIONS
+ INCLUDES MOC_PATH PNG_INCLUDES JPEG_INCLUDES HARFBUZZ_INCLUDES
+ FREETYPE_INCLUDES ZLIB_INCLUDES
+ )
+ resolve_target_includes(gnIncludes Pdf)
+ get_forward_declaration_macro(forwardDeclarationMacro)
+
+ extend_gn_target(${buildGn}
+ INCLUDES
+ ${gnIncludes}
+ )
+
+ ##
+ # GN PARAMETERS
+ ##
+
+ unset(gnArgArg)
+ append_build_type_setup(gnArgArg)
+ append_compiler_linker_sdk_setup(gnArgArg)
+ append_sanitizer_setup(gnArgArg)
+ append_toolchain_setup(gnArgArg)
+ append_pkg_config_setup(gnArgArg)
+
+ list(APPEND gnArgArg
+ qtwebengine_target="${buildDir}/${config}/${arch}:QtPdf"
+ qt_libpng_config="${buildDir}/${config}/${arch}:qt_libpng_config"
+ qt_libjpeg_config="${buildDir}/${config}/${arch}:qt_libjpeg_config"
+ qt_harfbuzz_config="${buildDir}/${config}/${arch}:qt_harfbuzz_config"
+ qt_freetype_config="${buildDir}/${config}/${arch}:qt_freetype_config"
+ enable_swiftshader=false
+ enable_swiftshader_vulkan=false
+ angle_enable_swiftshader=false
+ dawn_use_swiftshader=false
+ use_dawn=false
+ build_dawn_tests=false
+ enable_ipc_fuzzer=false
+ enable_remoting=false
+ enable_resource_allowlist_generation=false
+ enable_vr=false
+ enable_web_speech=false
+ chrome_pgo_phase=0
+ strip_absolute_paths_from_debug_symbols=false
+ use_perfetto_client_library=false
+ v8_enable_webassembly=false
+ )
+
+ if(LINUX OR ANDROID)
+ list(APPEND gnArgArg
+ is_cfi=false
+ ozone_auto_platforms=false
+ enable_arcore=false
+ use_ml_inliner=false
+ )
+ extend_gn_list(gnArgArg
+ ARGS use_system_icu
+ CONDITION QT_FEATURE_webengine_system_icu
+ )
+ extend_gn_list(gnArgArg
+ ARGS use_system_libopenjpeg2
+ CONDITION QT_FEATURE_webengine_system_libopenjpeg2
+ )
+ endif()
+ if(MACOS)
+ list(APPEND gnArgArg angle_enable_vulkan=false)
+ endif()
+ if(IOS)
+ list(APPEND gnArgArg enable_base_tracing=false)
+ extend_gn_list(gnArgArg
+ ARGS enable_ios_bitcode
+ CONDITION QT_FEATURE_pdf_bitcode
+ )
+ endif()
+ if(WIN32 OR ANDROID)
+ list(APPEND gnArgArg
+ ninja_use_custom_environment_files=false
+ safe_browsing_mode=0
+ )
+ extend_gn_list(gnArgArg
+ ARGS qt_uses_static_runtime
+ CONDITION QT_FEATURE_pdf_static_runtime
+ )
+ endif()
+
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_v8
+ CONDITION QT_FEATURE_pdf_v8
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_xfa
+ CONDITION QT_FEATURE_pdf_xfa
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_xfa_bmp
+ CONDITION QT_FEATURE_pdf_xfa_bmp
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_xfa_gif
+ CONDITION QT_FEATURE_pdf_xfa_gif
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_xfa_png
+ CONDITION QT_FEATURE_pdf_xfa_png
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdf_enable_xfa_tiff
+ CONDITION QT_FEATURE_pdf_xfa_tiff
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdfium_use_system_zlib use_system_zlib
+ CONDITION QT_FEATURE_webengine_system_zlib
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdfium_use_system_libpng use_system_libpng
+ CONDITION QT_FEATURE_webengine_system_libpng
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdfium_use_qt_libpng
+ CONDITION QT_FEATURE_webengine_qt_libpng
+ )
+ extend_gn_list(gnArgArg
+ ARGS pdfium_use_system_libtiff
+ CONDITION QT_FEATURE_webengine_system_libtiff
+ )
+ extend_gn_list(gnArgArg
+ ARGS use_qt_libjpeg
+ CONDITION QT_FEATURE_webengine_qt_libjpeg
+ )
+ extend_gn_list(gnArgArg
+ ARGS use_qt_harfbuzz
+ CONDITION QT_FEATURE_webengine_qt_harfbuzz
+ )
+ extend_gn_list(gnArgArg
+ ARGS use_qt_freetype
+ CONDITION QT_FEATURE_webengine_qt_freetype
+ )
+
+ add_gn_command(
+ CMAKE_TARGET Pdf
+ NINJA_TARGETS QtPdf
+ GN_TARGET ${buildGn}
+ GN_ARGS ${gnArgArg}
+ BUILDDIR ${buildDir}/${config}/${arch}
+ MODULE pdf
+ )
+
+ endforeach()
+ create_cxx_configs(Pdf ${arch})
+endforeach()
+
+
+##
+# PDF SETUP
+##
+
+get_architectures(archs)
+list(GET archs 0 arch)
+target_include_directories(Pdf PRIVATE ${buildDir}/$<CONFIG>/${arch}/gen)
+add_gn_build_artifacts_to_target(
+ CMAKE_TARGET Pdf
+ NINJA_TARGET QtPdf
+ MODULE pdf
+ BUILDDIR ${buildDir}
+ COMPLETE_STATIC TRUE
+ NINJA_STAMP QtPdf.stamp
+)
+add_dependencies(Pdf run_pdf_NinjaDone)
+
diff --git a/src/pdf/configure.cmake b/src/pdf/configure.cmake
new file mode 100644
index 000000000..ac4e4e25f
--- /dev/null
+++ b/src/pdf/configure.cmake
@@ -0,0 +1,54 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_feature("pdf-v8" PRIVATE
+ LABEL "Support V8"
+ PURPOSE "Enables javascript support."
+ AUTODETECT FALSE
+ CONDITION NOT IOS
+)
+qt_feature("pdf-xfa" PRIVATE
+ LABEL "Support XFA"
+ PURPOSE "Enables XFA support."
+ CONDITION QT_FEATURE_pdf_v8
+)
+qt_feature("pdf-xfa-bmp" PRIVATE
+ LABEL "Support XFA-BMP"
+ PURPOSE "Enables XFA-BMP support."
+ CONDITION QT_FEATURE_pdf_xfa
+)
+qt_feature("pdf-xfa-gif" PRIVATE
+ LABEL "Support XFA-GIF"
+ PURPOSE "Enables XFA-GIF support."
+ CONDITION QT_FEATURE_pdf_xfa
+)
+qt_feature("pdf-xfa-png" PRIVATE
+ LABEL "Support XFA-PNG"
+ PURPOSE "Enables XFA-PNG support."
+ CONDITION QT_FEATURE_pdf_xfa
+)
+qt_feature("pdf-xfa-tiff" PRIVATE
+ LABEL "Support XFA-TIFF"
+ PURPOSE "Enables XFA-TIFF support."
+ CONDITION QT_FEATURE_pdf_xfa
+)
+qt_feature("pdf-bitcode" PRIVATE
+ LABEL "Bitcode support"
+ PURPOSE "Enables bitcode"
+ CONDITION IOS
+)
+qt_feature("pdf-static-runtime" PRIVATE
+ LABEL "Use static runtime"
+ PURPOSE "Enables static runtime"
+ CONDITION WIN32 AND QT_FEATURE_static AND QT_FEATURE_static_runtime
+)
+qt_configure_add_summary_section(NAME "Qt PDF")
+qt_configure_add_summary_entry(ARGS "pdf-v8")
+qt_configure_add_summary_entry(ARGS "pdf-xfa")
+qt_configure_add_summary_entry(ARGS "pdf-xfa-bmp")
+qt_configure_add_summary_entry(ARGS "pdf-xfa-gif")
+qt_configure_add_summary_entry(ARGS "pdf-xfa-png")
+qt_configure_add_summary_entry(ARGS "pdf-xfa-tiff")
+qt_configure_add_summary_entry(ARGS "pdf-bitcode")
+qt_configure_add_summary_entry(ARGS "pdf-static-runtime")
+qt_configure_end_summary_section()
diff --git a/src/pdf/configure/BUILD.root.gn.in b/src/pdf/configure/BUILD.root.gn.in
new file mode 100644
index 000000000..e9f54ed6d
--- /dev/null
+++ b/src/pdf/configure/BUILD.root.gn.in
@@ -0,0 +1,73 @@
+import("//build/config/features.gni")
+
+config("qt_libpng_config") {
+ include_dirs = [ @GN_PNG_INCLUDES@ ]
+ defines = [ "USE_SYSTEM_LIBPNG" ]
+}
+config ("qt_libjpeg_config") {
+ include_dirs = [ @GN_JPEG_INCLUDES@ ]
+}
+config("qt_harfbuzz_config") {
+ visibility = [
+ "//third_party:freetype_harfbuzz",
+ "//third_party/freetype:freetype_source",
+ ]
+ include_dirs = [ @GN_HARFBUZZ_INCLUDES@ ]
+}
+config("qt_freetype_config") {
+ visibility = [
+ "//build/config/freetype:freetype",
+ "//third_party:freetype_harfbuzz",
+ "//third_party/harfbuzz-ng:harfbuzz_source",
+ ]
+ include_dirs = [ @GN_FREETYPE_INCLUDES@ ]
+}
+
+config("QtPdf_config") {
+ cflags = [
+ @GN_CFLAGS_C@,
+ ]
+ cflags_cc = [
+ @GN_CFLAGS_CC@,
+ ]
+ defines = [
+ @GN_DEFINES@,
+ ]
+ include_dirs = [
+ @GN_INCLUDE_DIRS@,
+ rebase_path("${target_gen_dir}/.moc/")
+ ]
+}
+
+config("cpp20_config") {
+ # Chromium headers now use concepts and requires c++20
+ if (is_win) {
+ cflags_cc = [ "/std:c++20" ]
+ } else {
+ cflags_cc = [ "-std=c++20" ]
+ }
+}
+
+static_library("QtPdf") {
+ complete_static_lib = true
+ rsp_types = [ "objects", "archives", "libs", "ldir" ]
+ configs += [
+ ":cpp20_config",
+ ":QtPdf_config"
+ ]
+ deps = [
+ "//third_party/pdfium"
+ ]
+ if (is_msvc) {
+ libs = [
+ "dloadhelper.lib",
+ "winmm.lib",
+ "usp10.lib",
+ ]
+ }
+ if (is_mingw) {
+ libs = [
+ "winmm",
+ ]
+ }
+}
diff --git a/src/pdf/doc/about_credits.tmpl b/src/pdf/doc/about_credits.tmpl
new file mode 100644
index 000000000..57fae9e78
--- /dev/null
+++ b/src/pdf/doc/about_credits.tmpl
@@ -0,0 +1 @@
+{{entries}}
diff --git a/src/pdf/doc/about_credits_entry.tmpl b/src/pdf/doc/about_credits_entry.tmpl
new file mode 100644
index 000000000..294198709
--- /dev/null
+++ b/src/pdf/doc/about_credits_entry.tmpl
@@ -0,0 +1,13 @@
+/*!
+\page qtpdf-3rdparty-{{name-sanitized}}.html
+\attribution
+\ingroup qtpdf-licensing
+\brief {{license-type}}
+\title {{name}}
+
+\l{{{url}}}{Project Homepage}
+
+\badcode
+{{license}}
+\endcode
+*/
diff --git a/src/pdf/doc/images/multipageviewer.png b/src/pdf/doc/images/multipageviewer.png
new file mode 100644
index 000000000..2f0bb62a2
--- /dev/null
+++ b/src/pdf/doc/images/multipageviewer.png
Binary files differ
diff --git a/src/pdf/doc/images/pdfviewer.png b/src/pdf/doc/images/pdfviewer.png
new file mode 100644
index 000000000..ac8a31ac0
--- /dev/null
+++ b/src/pdf/doc/images/pdfviewer.png
Binary files differ
diff --git a/src/pdf/doc/images/search-results.png b/src/pdf/doc/images/search-results.png
new file mode 100644
index 000000000..91ee53b83
--- /dev/null
+++ b/src/pdf/doc/images/search-results.png
Binary files differ
diff --git a/src/pdf/doc/images/singlepageviewer.webp b/src/pdf/doc/images/singlepageviewer.webp
new file mode 100644
index 000000000..e429cb818
--- /dev/null
+++ b/src/pdf/doc/images/singlepageviewer.webp
Binary files differ
diff --git a/src/pdf/doc/images/wrapping-search-result.png b/src/pdf/doc/images/wrapping-search-result.png
new file mode 100644
index 000000000..108ec0444
--- /dev/null
+++ b/src/pdf/doc/images/wrapping-search-result.png
Binary files differ
diff --git a/src/pdf/doc/qtpdf.qdocconf b/src/pdf/doc/qtpdf.qdocconf
new file mode 100644
index 000000000..d0340fe83
--- /dev/null
+++ b/src/pdf/doc/qtpdf.qdocconf
@@ -0,0 +1,67 @@
+include($QT_INSTALL_DOCS/global/qt-module-defaults.qdocconf)
+include($QT_INSTALL_DOCS/config/exampleurl-qtwebengine.qdocconf)
+
+project = QtPdf
+description = Qt Pdf Reference Documentation
+version = $QT_VERSION
+
+qhp.projects = QtPdf
+
+qhp.QtPdf.file = qtpdf.qhp
+qhp.QtPdf.namespace = org.qt-project.qtpdf.$QT_VERSION_TAG
+qhp.QtPdf.virtualFolder = qtpdf
+qhp.QtPdf.indexTitle = Qt PDF
+qhp.QtPdf.indexRoot =
+
+qhp.QtPdf.subprojects = classes qmltypes examples
+
+qhp.QtPdf.subprojects.classes.title = C++ Classes
+qhp.QtPdf.subprojects.classes.indexTitle = Qt PDF C++ Classes
+qhp.QtPdf.subprojects.classes.selectors = class fake:headerfile
+qhp.QtPdf.subprojects.classes.sortPages = true
+
+qhp.QtPdf.subprojects.qmltypes.title = QML Types
+qhp.QtPdf.subprojects.qmltypes.indexTitle = Qt Quick PDF QML Types
+qhp.QtPdf.subprojects.qmltypes.selectors = qmltype
+qhp.QtPdf.subprojects.qmltypes.sortPages = true
+
+qhp.QtPdf.subprojects.examples.title = Examples
+qhp.QtPdf.subprojects.examples.indexTitle = Qt PDF Examples
+qhp.QtPdf.subprojects.examples.selectors = doc:example
+qhp.QtPdf.subprojects.examples.sortPages = true
+
+manifestmeta.highlighted.names += "QtPdf/PDF Multipage Viewer Example"
+
+depends += qtcore \
+ qtwidgets \
+ qtgui \
+ qtdoc \
+ qmake \
+ qtdesigner \
+ qtquick \
+ qtquickcontrols \
+ qtcmake \
+ qtsvg
+
+headerdirs += ../ \
+ ../../pdfwidgets
+
+sourcedirs += ../ \
+ ../../pdfquick \
+ ../../pdfwidgets
+
+exampledirs += ../../../examples/pdfwidgets \
+ ../../../examples/pdf \
+ snippets/
+
+# add a generic thumbnail for an example that has no \image in its doc
+manifestmeta.thumbnail.names = "QtPdf/PDF Viewer Example"
+
+imagedirs += images
+
+navigation.landingpage = "Qt PDF"
+navigation.cppclassespage = "Qt PDF C++ Classes"
+navigation.qmltypespage = "Qt Quick PDF QML Types"
+
+# Enforce zero documentation warnings
+warninglimit = 0
diff --git a/src/pdf/doc/snippets/multipageview.qml b/src/pdf/doc/snippets/multipageview.qml
new file mode 100644
index 000000000..113444165
--- /dev/null
+++ b/src/pdf/doc/snippets/multipageview.qml
@@ -0,0 +1,11 @@
+// Copyright (C) 2016 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+//! [0]
+import QtQuick
+import QtQuick.Pdf
+
+PdfMultiPageView {
+ document: PdfDocument { source: "my.pdf" }
+}
+//! [0]
diff --git a/src/pdf/doc/snippets/pdfpageview.qml b/src/pdf/doc/snippets/pdfpageview.qml
new file mode 100644
index 000000000..5e233961a
--- /dev/null
+++ b/src/pdf/doc/snippets/pdfpageview.qml
@@ -0,0 +1,12 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+//! [0]
+import QtQuick
+import QtQuick.Pdf
+
+PdfPageView {
+ document: PdfDocument { source: "my.pdf" }
+}
+//! [0]
+
diff --git a/src/pdf/doc/snippets/qtpdf-build.cmake b/src/pdf/doc/snippets/qtpdf-build.cmake
new file mode 100644
index 000000000..b4372d411
--- /dev/null
+++ b/src/pdf/doc/snippets/qtpdf-build.cmake
@@ -0,0 +1,2 @@
+find_package(Qt6 REQUIRED COMPONENTS Pdf)
+target_link_libraries(mytarget Qt6::Pdf)
diff --git a/src/pdf/doc/snippets/qtpdf_build_snippet.qdoc b/src/pdf/doc/snippets/qtpdf_build_snippet.qdoc
new file mode 100644
index 000000000..7d30ccdfd
--- /dev/null
+++ b/src/pdf/doc/snippets/qtpdf_build_snippet.qdoc
@@ -0,0 +1,6 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+//! [0]
+QT += pdf
+//! [0]
diff --git a/src/pdf/doc/src/qtpdf-examples.qdoc b/src/pdf/doc/src/qtpdf-examples.qdoc
new file mode 100644
index 000000000..02dc23dc2
--- /dev/null
+++ b/src/pdf/doc/src/qtpdf-examples.qdoc
@@ -0,0 +1,12 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \group qtpdf-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
new file mode 100644
index 000000000..b72619fbf
--- /dev/null
+++ b/src/pdf/doc/src/qtpdf-index.qdoc
@@ -0,0 +1,80 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \page qtpdf-index.html
+ \title Qt PDF
+
+ \brief Renders pages from PDF documents.
+
+ The Qt PDF module contains classes and functions for rendering
+ PDF documents. The \l QPdfDocument class loads a PDF document
+ and renders pages from it according to the options provided by
+ the \l QPdfDocumentRenderOptions class. The \l QPdfPageRenderer
+ class manages a queue that collects all render requests. The
+ \l QPdfPageNavigator class handles the navigation through a
+ PDF document. The \l QPdfSearchModel class searches for a string
+ and holds the search results. The QPdfBookmarkModel class holds the
+ table of contents, if present. The QPdfLinkModel holds information
+ about hyperlinks on a page. The \l QPdfView widget is a complete
+ PDF viewer, and the \l {PDF Viewer Widget Example} shows how to use it.
+
+ For Qt Quick applications, three kinds of full-featured viewer
+ components are provided. \l PdfMultiPageView should be your
+ first choice for the most common user experience: flicking
+ through the pages in the entire document.
+ \l PdfScrollablePageView shows one page at a time, with scrolling;
+ and \l PdfPageView shows one full page at a time, without scrolling.
+
+ The full-featured viewer components are composed of lower-level
+ QML components that can be used separately if you need to write a
+ more customized PDF viewing application: \l PdfDocument,
+ \l PdfPageImage, \l PdfPageNavigator, \l PdfSelection,
+ \l PdfSearchModel, \l PdfLinkModel, and \l PdfBookmarkModel.
+
+ If you only need to render page images, without features such as
+ text selection, search and navigation, this module includes a
+ \l QImageIOHandler plugin that treats PDF as a scalable
+ \l {Qt Image Formats}{image format}, similar to \l {Qt SVG}{SVG}.
+ You can simply use \l Image, and set the
+ \l {Image::currentFrame}{currentFrame} property to the page index
+ that you wish to display. If the PDF file does not render its own
+ background, the image has a transparent background.
+
+ \include module-use.qdocinc using qt module
+ \quotefile qtpdf-build.cmake
+
+ See also the \l{Build with CMake} overview.
+
+ \section2 Building with qmake
+
+ To link against the module, add this line to your qmake project file:
+
+ \snippet qtpdf_build_snippet.qdoc 0
+
+ \section1 Examples
+
+ \list
+ \li \l{Qt PDF Examples}
+ \endlist
+
+ \section1 API Reference
+
+ \list
+ \li \l{Qt PDF C++ Classes}
+ \li \l{Qt Quick PDF QML Types}
+ \endlist
+
+ \section1 Articles and Guides
+ \list
+ \li {Qt PDF Platform Notes} {Platform Notes}
+ \endlist
+
+ \section1 Licenses and Attributions
+
+ Qt PDF is available under commercial licenses from \l{The Qt Company}.
+ In addition, it is available under the
+ \l{GNU Lesser General Public License, version 3}, or
+ the \l{GNU General Public License, version 2}.
+ See \l{Qt PDF Licensing} for further details about this module.
+*/
diff --git a/src/pdf/doc/src/qtpdf-licensing.qdoc b/src/pdf/doc/src/qtpdf-licensing.qdoc
new file mode 100644
index 000000000..190ee8331
--- /dev/null
+++ b/src/pdf/doc/src/qtpdf-licensing.qdoc
@@ -0,0 +1,18 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \group qtpdf-licensing
+ \title Qt PDF Licensing
+
+ Qt PDF is available under commercial licenses from \l{The Qt Company}.
+ In addition, it is available under the
+ \l{GNU Lesser General Public License, version 3}, or
+ the \l{GNU General Public License, version 2}.
+ See \l{Qt Licensing} for further details.
+
+ The module includes a snapshot of PDFium. As such, users need to respect
+ the licenses of PDFium and third-party code included in it.
+
+ Third party licenses included in the sources are:
+*/
diff --git a/src/pdf/doc/src/qtpdf-module.qdoc b/src/pdf/doc/src/qtpdf-module.qdoc
new file mode 100644
index 000000000..e2ca8e4ce
--- /dev/null
+++ b/src/pdf/doc/src/qtpdf-module.qdoc
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+
+/*!
+ \module QtPdf
+ \title Qt PDF C++ Classes
+ \brief Renders pages from PDF documents.
+ \since 5.14
+ \ingroup qtwebengine-modules
+ \ingroup modules
+
+ The Qt PDF module contains classes and functions for rendering
+ PDF documents.
+
+ \if !defined(qtforpython)
+ To link against the module, add this line to your qmake project file:
+
+ \snippet qtpdf_build_snippet.qdoc 0
+ \endif
+*/
diff --git a/src/pdf/doc/src/qtpdf-platformnotes.qdoc b/src/pdf/doc/src/qtpdf-platformnotes.qdoc
new file mode 100644
index 000000000..f50be120d
--- /dev/null
+++ b/src/pdf/doc/src/qtpdf-platformnotes.qdoc
@@ -0,0 +1,11 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \page qtpdf-platformnotes.html
+ \title Qt PDF Platform Notes
+
+ Building Qt PDF for Android is currently
+ \l{https://bugreports.qt.io/browse/QTBUG-83459} {not supported} on Windows host platforms.
+*/
+
diff --git a/src/pdf/plugins/imageformats/pdf/CMakeLists.txt b/src/pdf/plugins/imageformats/pdf/CMakeLists.txt
new file mode 100644
index 000000000..73a0b3144
--- /dev/null
+++ b/src/pdf/plugins/imageformats/pdf/CMakeLists.txt
@@ -0,0 +1,13 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+qt_internal_add_plugin(QPdfPlugin
+ OUTPUT_NAME qpdf
+ PLUGIN_TYPE imageformats
+ SOURCES
+ main.cpp
+ qpdfiohandler.cpp qpdfiohandler_p.h
+ LIBRARIES
+ Qt::PdfPrivate
+)
+
diff --git a/src/pdf/plugins/imageformats/pdf/main.cpp b/src/pdf/plugins/imageformats/pdf/main.cpp
new file mode 100644
index 000000000..cb69c4ca1
--- /dev/null
+++ b/src/pdf/plugins/imageformats/pdf/main.cpp
@@ -0,0 +1,41 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfiohandler_p.h"
+
+QT_BEGIN_NAMESPACE
+
+class QPdfPlugin : public QImageIOPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID QImageIOHandlerFactoryInterface_iid FILE "pdf.json")
+
+public:
+ Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
+ QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
+};
+
+QImageIOPlugin::Capabilities QPdfPlugin::capabilities(QIODevice *device, const QByteArray &format) const
+{
+ if (format == "pdf")
+ return Capabilities(CanRead);
+ if (!format.isEmpty())
+ return {};
+
+ Capabilities cap;
+ if (device->isReadable() && QPdfIOHandler::canRead(device))
+ cap |= CanRead;
+ return cap;
+}
+
+QImageIOHandler *QPdfPlugin::create(QIODevice *device, const QByteArray &format) const
+{
+ QPdfIOHandler *hand = new QPdfIOHandler();
+ hand->setDevice(device);
+ hand->setFormat(format);
+ return hand;
+}
+
+QT_END_NAMESPACE
+
+#include "main.moc"
diff --git a/src/pdf/plugins/imageformats/pdf/pdf.json b/src/pdf/plugins/imageformats/pdf/pdf.json
new file mode 100644
index 000000000..1f5268ca1
--- /dev/null
+++ b/src/pdf/plugins/imageformats/pdf/pdf.json
@@ -0,0 +1,4 @@
+{
+ "Keys": [ "pdf" ],
+ "MimeTypes": [ "application/pdf" ]
+}
diff --git a/src/pdf/plugins/imageformats/pdf/qpdfiohandler.cpp b/src/pdf/plugins/imageformats/pdf/qpdfiohandler.cpp
new file mode 100644
index 000000000..bb3e7c929
--- /dev/null
+++ b/src/pdf/plugins/imageformats/pdf/qpdfiohandler.cpp
@@ -0,0 +1,225 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfiohandler_p.h"
+#include <QLoggingCategory>
+#include <QPainter>
+#include <QtPdf/private/qpdffile_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(qLcPdf, "qt.imageformat.pdf")
+
+QPdfIOHandler::QPdfIOHandler()
+{
+}
+
+QPdfIOHandler::~QPdfIOHandler()
+{
+ if (m_ownsDocument)
+ delete m_doc;
+}
+
+bool QPdfIOHandler::canRead() const
+{
+ if (!device())
+ return false;
+ if (m_loaded)
+ return true;
+ if (QPdfIOHandler::canRead(device())) {
+ setFormat("pdf");
+ return true;
+ }
+ return false;
+}
+
+bool QPdfIOHandler::canRead(QIODevice *device)
+{
+ char buf[6];
+ device->peek(buf, 6);
+ return (!qstrncmp(buf, "%PDF-", 5) || Q_UNLIKELY(!qstrncmp(buf, "\012%PDF-", 6)));
+}
+
+int QPdfIOHandler::currentImageNumber() const
+{
+ return m_page;
+}
+
+QRect QPdfIOHandler::currentImageRect() const
+{
+ return QRect(QPoint(0, 0), m_doc->pagePointSize(m_page).toSize());
+}
+
+int QPdfIOHandler::imageCount() const
+{
+ int ret = 0;
+ if (const_cast<QPdfIOHandler *>(this)->load(device()))
+ ret = m_doc->pageCount();
+ qCDebug(qLcPdf) << ret;
+ return ret;
+}
+
+bool QPdfIOHandler::read(QImage *image)
+{
+ if (load(device())) {
+ if (m_doc.isNull() || m_page >= m_doc->pageCount())
+ return false;
+ if (m_page < 0)
+ m_page = 0;
+ const bool xform = (m_clipRect.isValid() || m_scaledSize.isValid() || m_scaledClipRect.isValid());
+ QSize pageSize = m_doc->pagePointSize(m_page).toSize();
+ QSize finalSize = pageSize;
+ QRectF bounds;
+ if (xform && !finalSize.isEmpty()) {
+ bounds = QRectF(QPointF(0,0), QSizeF(finalSize));
+ QPoint tr1, tr2;
+ QSizeF sc(1, 1);
+ if (m_clipRect.isValid()) {
+ tr1 = -m_clipRect.topLeft();
+ finalSize = m_clipRect.size();
+ }
+ if (m_scaledSize.isValid()) {
+ 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();
+ finalSize = m_scaledClipRect.size();
+ }
+ QTransform t;
+ t.translate(tr2.x(), tr2.y());
+ t.scale(sc.width(), sc.height());
+ t.translate(tr1.x(), tr1.y());
+ bounds = t.mapRect(bounds);
+ }
+ qCDebug(qLcPdf) << m_page << finalSize;
+ if (image->size() != finalSize || !image->reinterpretAsFormat(QImage::Format_ARGB32_Premultiplied)) {
+ *image = QImage(finalSize, QImage::Format_ARGB32_Premultiplied);
+ if (!finalSize.isEmpty() && image->isNull()) {
+ // avoid QTBUG-68229
+ qWarning("QPdfIOHandler: QImage allocation failed (size %i x %i)", finalSize.width(), finalSize.height());
+ return false;
+ }
+ }
+ if (!finalSize.isEmpty()) {
+ QPdfDocumentRenderOptions options;
+ if (m_scaledClipRect.isValid())
+ options.setScaledClipRect(m_scaledClipRect);
+ options.setScaledSize(pageSize);
+ image->fill(m_backColor.rgba());
+ QPainter p(image);
+ if (!m_doc.isNull()) {
+ QImage pageImage = m_doc->render(m_page, finalSize, options);
+ p.drawImage(0, 0, pageImage);
+ p.end();
+ }
+ }
+ return true;
+ }
+
+ return false;
+}
+
+QVariant QPdfIOHandler::option(ImageOption option) const
+{
+ switch (option) {
+ case ImageFormat:
+ return QImage::Format_ARGB32_Premultiplied;
+ case Size:
+ const_cast<QPdfIOHandler *>(this)->load(device());
+ return m_doc->pagePointSize(qMax(0, m_page));
+ case ClipRect:
+ return m_clipRect;
+ case ScaledSize:
+ return m_scaledSize;
+ case ScaledClipRect:
+ return m_scaledClipRect;
+ case BackgroundColor:
+ return m_backColor;
+ case Name:
+ return m_doc->metaData(QPdfDocument::MetaDataField::Title);
+ default:
+ break;
+ }
+ return QVariant();
+}
+
+void QPdfIOHandler::setOption(ImageOption option, const QVariant & value)
+{
+ switch (option) {
+ case ClipRect:
+ m_clipRect = value.toRect();
+ break;
+ case ScaledSize:
+ m_scaledSize = value.toSize();
+ break;
+ case ScaledClipRect:
+ m_scaledClipRect = value.toRect();
+ break;
+ case BackgroundColor:
+ m_backColor = value.value<QColor>();
+ break;
+ default:
+ break;
+ }
+}
+
+bool QPdfIOHandler::supportsOption(ImageOption option) const
+{
+ switch (option)
+ {
+ case ImageFormat:
+ case Size:
+ case ClipRect:
+ case ScaledSize:
+ case ScaledClipRect:
+ case BackgroundColor:
+ case Name:
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+bool QPdfIOHandler::jumpToImage(int frame)
+{
+ qCDebug(qLcPdf) << frame;
+ if (frame < 0 || frame >= imageCount())
+ return false;
+ m_page = frame;
+ return true;
+}
+
+bool QPdfIOHandler::jumpToNextImage()
+{
+ return jumpToImage(m_page + 1);
+}
+
+bool QPdfIOHandler::load(QIODevice *device)
+{
+ if (m_loaded)
+ return true;
+ if (format().isEmpty())
+ if (!canRead())
+ return false;
+
+ QPdfFile *pdfFile = qobject_cast<QPdfFile *>(device);
+ if (pdfFile) {
+ m_doc = pdfFile->document();
+ m_ownsDocument = false;
+ qCDebug(qLcPdf) << "loading via QPdfFile, reusing document instance" << m_doc;
+ } else {
+ m_doc = new QPdfDocument();
+ m_ownsDocument = true;
+ m_doc->load(device);
+ qCDebug(qLcPdf) << "loading via new document instance" << m_doc;
+ }
+ m_loaded = (m_doc->error() == QPdfDocument::Error::None);
+
+ return m_loaded;
+}
+
+QT_END_NAMESPACE
diff --git a/src/pdf/plugins/imageformats/pdf/qpdfiohandler_p.h b/src/pdf/plugins/imageformats/pdf/qpdfiohandler_p.h
new file mode 100644
index 000000000..c4d8e0f9a
--- /dev/null
+++ b/src/pdf/plugins/imageformats/pdf/qpdfiohandler_p.h
@@ -0,0 +1,57 @@
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFIOHANDLER_H
+#define QPDFIOHANDLER_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 <QtGui/qimageiohandler.h>
+#include <QtPdf/QPdfDocument>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfIOHandler : public QImageIOHandler
+{
+public:
+ QPdfIOHandler();
+ ~QPdfIOHandler() override;
+ bool canRead() const override;
+ static bool canRead(QIODevice *device);
+ int currentImageNumber() const override;
+ QRect currentImageRect() const override;
+ int imageCount() const override;
+ bool read(QImage *image) override;
+ QVariant option(ImageOption option) const override;
+ void setOption(ImageOption option, const QVariant & value) override;
+ bool supportsOption(ImageOption option) const override;
+ bool jumpToImage(int frame) override;
+ bool jumpToNextImage() override;
+
+private:
+ bool load(QIODevice *device);
+
+private:
+ QPointer<QPdfDocument> m_doc;
+ int m_page = -1;
+
+ QRect m_clipRect;
+ QSize m_scaledSize;
+ QRect m_scaledClipRect;
+ QColor m_backColor = Qt::transparent;
+ bool m_loaded = false;
+ bool m_ownsDocument = false;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFIOHANDLER_H
diff --git a/src/pdf/qpdfbookmarkmodel.cpp b/src/pdf/qpdfbookmarkmodel.cpp
new file mode 100644
index 000000000..93dbf5d1f
--- /dev/null
+++ b/src/pdf/qpdfbookmarkmodel.cpp
@@ -0,0 +1,388 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfbookmarkmodel.h"
+
+#include "qpdfdocument.h"
+#include "qpdfdocument_p.h"
+
+#include "third_party/pdfium/public/fpdf_doc.h"
+#include "third_party/pdfium/public/fpdfview.h"
+
+#include <QLoggingCategory>
+#include <QMetaEnum>
+#include <QPointer>
+#include <QScopedPointer>
+#include <private/qabstractitemmodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(qLcBM, "qt.pdf.bookmarks")
+
+class BookmarkNode
+{
+public:
+ explicit BookmarkNode(BookmarkNode *parentNode = nullptr)
+ : m_parentNode(parentNode)
+ {
+ }
+
+ ~BookmarkNode()
+ {
+ clear();
+ }
+
+ void clear()
+ {
+ qDeleteAll(m_childNodes);
+ m_childNodes.clear();
+ }
+
+ void appendChild(BookmarkNode *child)
+ {
+ m_childNodes.append(child);
+ }
+
+ BookmarkNode *child(int row) const
+ {
+ return m_childNodes.at(row);
+ }
+
+ int childCount() const
+ {
+ return m_childNodes.size();
+ }
+
+ int row() const
+ {
+ if (m_parentNode)
+ return m_parentNode->m_childNodes.indexOf(const_cast<BookmarkNode*>(this));
+
+ return 0;
+ }
+
+ BookmarkNode *parentNode() const
+ {
+ return m_parentNode;
+ }
+
+ QString title() const
+ {
+ return m_title;
+ }
+
+ void setTitle(const QString &title)
+ {
+ m_title = title;
+ }
+
+ int level() const
+ {
+ return m_level;
+ }
+
+ void setLevel(int level)
+ {
+ m_level = level;
+ }
+
+ int pageNumber() const
+ {
+ return m_pageNumber;
+ }
+
+ void setPageNumber(int pageNumber)
+ {
+ m_pageNumber = pageNumber;
+ }
+
+ QPointF location() const
+ {
+ return m_location;
+ }
+
+ void setLocation(qreal x, qreal y)
+ {
+ m_location = QPointF(x, y);
+ }
+
+ qreal zoom() const
+ {
+ return m_zoom;
+ }
+
+ void setZoom(qreal zoom)
+ {
+ m_zoom = zoom;
+ }
+
+private:
+ QList<BookmarkNode*> m_childNodes;
+ BookmarkNode *m_parentNode;
+
+ QString m_title;
+ int m_level = 0;
+ int m_pageNumber = 0;
+ QPointF m_location;
+ qreal m_zoom = 0;
+};
+
+
+struct QPdfBookmarkModelPrivate
+{
+ QPdfBookmarkModelPrivate()
+ : m_rootNode(new BookmarkNode(nullptr))
+ , m_document(nullptr)
+ {
+ }
+
+ void rebuild()
+ {
+ const bool documentAvailable = (m_document && m_document->status() == QPdfDocument::Status::Ready);
+
+ if (documentAvailable) {
+ q->beginResetModel();
+ m_rootNode->clear();
+ QPdfMutexLocker lock;
+ appendChildNode(m_rootNode.data(), nullptr, 0, m_document->d->doc);
+ lock.unlock();
+ q->endResetModel();
+ } else {
+ if (m_rootNode->childCount() == 0) {
+ return;
+ } else {
+ q->beginResetModel();
+ m_rootNode->clear();
+ q->endResetModel();
+ }
+ }
+ }
+
+ void appendChildNode(BookmarkNode *parentBookmarkNode, FPDF_BOOKMARK parentBookmark, int level, FPDF_DOCUMENT document)
+ {
+ FPDF_BOOKMARK bookmark = FPDFBookmark_GetFirstChild(document, parentBookmark);
+
+ while (bookmark) {
+ BookmarkNode *childBookmarkNode = nullptr;
+
+ childBookmarkNode = new BookmarkNode(parentBookmarkNode);
+ parentBookmarkNode->appendChild(childBookmarkNode);
+ Q_ASSERT(childBookmarkNode);
+
+ const int titleLength = int(FPDFBookmark_GetTitle(bookmark, nullptr, 0));
+
+ QList<char16_t> titleBuffer(titleLength);
+ FPDFBookmark_GetTitle(bookmark, titleBuffer.data(), quint32(titleBuffer.size()));
+
+ const FPDF_DEST dest = FPDFBookmark_GetDest(document, bookmark);
+ const int pageNumber = FPDFDest_GetDestPageIndex(document, dest);
+ const qreal pageHeight = m_document->pagePointSize(pageNumber).height();
+ FPDF_BOOL hasX, hasY, hasZoom;
+ FS_FLOAT x, y, zoom;
+ bool ok = FPDFDest_GetLocationInPage(dest, &hasX, &hasY, &hasZoom, &x, &y, &zoom);
+ if (ok) {
+ if (hasX && hasY)
+ childBookmarkNode->setLocation(x, pageHeight - y);
+ if (hasZoom)
+ childBookmarkNode->setZoom(zoom);
+ } else {
+ qCWarning(qLcBM) << "bookmark with invalid location and/or zoom" << x << y << zoom;
+ }
+
+ childBookmarkNode->setTitle(QString::fromUtf16(titleBuffer.data()));
+ childBookmarkNode->setLevel(level);
+ childBookmarkNode->setPageNumber(pageNumber);
+
+ // recurse down
+ appendChildNode(childBookmarkNode, bookmark, level + 1, document);
+
+ bookmark = FPDFBookmark_GetNextSibling(document, bookmark);
+ }
+ }
+
+ void _q_documentStatusChanged()
+ {
+ rebuild();
+ }
+
+ QPdfBookmarkModel *q = nullptr;
+
+ QScopedPointer<BookmarkNode> m_rootNode;
+ QPointer<QPdfDocument> m_document;
+ QHash<int, QByteArray> m_roleNames;
+};
+
+
+/*!
+ \class QPdfBookmarkModel
+ \since 5.10
+ \inmodule QtPdf
+ \inherits QAbstractItemModel
+
+ \brief The QPdfBookmarkModel class holds a tree of of links (anchors)
+ within a PDF document, such as the table of contents.
+
+ This is used in the \l {Model/View Programming} paradigm to display a
+ table of contents in the form of a tree or list.
+*/
+
+/*!
+ \enum QPdfBookmarkModel::Role
+
+ \value Title The name of the bookmark for display.
+ \value Level The level of indentation.
+ \value Page The page number of the destination (int).
+ \value Location The position of the destination (QPointF).
+ \value Zoom The suggested zoom level (qreal).
+ \omitvalue NRoles
+*/
+
+/*!
+ Constructs a new bookmark model with parent object \a parent.
+*/
+QPdfBookmarkModel::QPdfBookmarkModel(QObject *parent)
+ : QAbstractItemModel(parent), d(new QPdfBookmarkModelPrivate)
+{
+ d->q = this;
+ d->m_roleNames = QAbstractItemModel::roleNames();
+ QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
+ for (int r = Qt::UserRole; r < int(Role::NRoles); ++r)
+ d->m_roleNames.insert(r, QByteArray(rolesMetaEnum.valueToKey(r)).toLower());
+}
+
+/*!
+ Destroys the model.
+*/
+QPdfBookmarkModel::~QPdfBookmarkModel() = default;
+
+QPdfDocument* QPdfBookmarkModel::document() const
+{
+ return d->m_document;
+}
+
+/*!
+ \property QPdfBookmarkModel::document
+ \brief the PDF document in which bookmarks are to be found.
+*/
+void QPdfBookmarkModel::setDocument(QPdfDocument *document)
+{
+ if (d->m_document == document)
+ return;
+
+ if (d->m_document)
+ disconnect(d->m_document, SIGNAL(statusChanged(QPdfDocument::Status)), this, SLOT(_q_documentStatusChanged()));
+
+ d->m_document = document;
+ emit documentChanged(d->m_document);
+
+ if (d->m_document)
+ connect(d->m_document, SIGNAL(statusChanged(QPdfDocument::Status)), this, SLOT(_q_documentStatusChanged()));
+
+ d->rebuild();
+}
+
+/*!
+ \reimp
+*/
+int QPdfBookmarkModel::columnCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return 1;
+}
+
+/*!
+ \reimp
+*/
+QHash<int, QByteArray> QPdfBookmarkModel::roleNames() const
+{
+ return d->m_roleNames;
+}
+
+/*!
+ \reimp
+*/
+QVariant QPdfBookmarkModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ const BookmarkNode *node = static_cast<BookmarkNode*>(index.internalPointer());
+ switch (Role(role)) {
+ case Role::Title:
+ return node->title();
+ case Role::Level:
+ return node->level();
+ case Role::Page:
+ return node->pageNumber();
+ case Role::Location:
+ return node->location();
+ case Role::Zoom:
+ return node->zoom();
+ case Role::NRoles:
+ break;
+ }
+ if (role == Qt::DisplayRole)
+ return node->title();
+ return QVariant();
+}
+
+/*!
+ \reimp
+*/
+QModelIndex QPdfBookmarkModel::index(int row, int column, const QModelIndex &parent) const
+{
+ if (!hasIndex(row, column, parent))
+ return QModelIndex();
+
+ BookmarkNode *parentNode;
+
+ if (!parent.isValid())
+ parentNode = d->m_rootNode.data();
+ else
+ parentNode = static_cast<BookmarkNode*>(parent.internalPointer());
+
+ BookmarkNode *childNode = parentNode->child(row);
+ if (childNode)
+ return createIndex(row, column, childNode);
+ else
+ return QModelIndex();
+}
+
+/*!
+ \reimp
+*/
+QModelIndex QPdfBookmarkModel::parent(const QModelIndex &index) const
+{
+ if (!index.isValid())
+ return QModelIndex();
+
+ const BookmarkNode *childNode = static_cast<BookmarkNode*>(index.internalPointer());
+ BookmarkNode *parentNode = childNode->parentNode();
+
+ if (parentNode == d->m_rootNode.data())
+ return QModelIndex();
+
+ return createIndex(parentNode->row(), 0, parentNode);
+}
+
+/*!
+ \reimp
+*/
+int QPdfBookmarkModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.column() > 0)
+ return 0;
+
+ BookmarkNode *parentNode = nullptr;
+
+ if (!parent.isValid())
+ parentNode = d->m_rootNode.data();
+ else
+ parentNode = static_cast<BookmarkNode*>(parent.internalPointer());
+
+ return parentNode->childCount();
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qpdfbookmarkmodel.cpp"
diff --git a/src/pdf/qpdfbookmarkmodel.h b/src/pdf/qpdfbookmarkmodel.h
new file mode 100644
index 000000000..5a3c24f84
--- /dev/null
+++ b/src/pdf/qpdfbookmarkmodel.h
@@ -0,0 +1,60 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFBOOKMARKMODEL_H
+#define QPDFBOOKMARKMODEL_H
+
+#include <QtPdf/qtpdfglobal.h>
+#include <QtCore/qabstractitemmodel.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfDocument;
+struct QPdfBookmarkModelPrivate;
+
+class Q_PDF_EXPORT QPdfBookmarkModel : public QAbstractItemModel
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QPdfDocument* document READ document WRITE setDocument NOTIFY documentChanged)
+
+public:
+ enum class Role : int
+ {
+ Title = Qt::UserRole,
+ Level,
+ Page,
+ Location,
+ Zoom,
+ NRoles
+ };
+ Q_ENUM(Role)
+
+ QPdfBookmarkModel() : QPdfBookmarkModel(nullptr) {}
+ explicit QPdfBookmarkModel(QObject *parent);
+ ~QPdfBookmarkModel() override;
+
+ QPdfDocument* document() const;
+ void setDocument(QPdfDocument *document);
+
+ QVariant data(const QModelIndex &index, int role) const override;
+ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
+ QModelIndex parent(const QModelIndex &index) const override;
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QHash<int, QByteArray> roleNames() const override;
+
+Q_SIGNALS:
+ void documentChanged(QPdfDocument *document);
+
+private:
+ std::unique_ptr<QPdfBookmarkModelPrivate> d;
+
+ Q_PRIVATE_SLOT(d, void _q_documentStatusChanged())
+
+ friend struct QPdfBookmarkModelPrivate;
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp
new file mode 100644
index 000000000..17fdb29b9
--- /dev/null
+++ b/src/pdf/qpdfdocument.cpp
@@ -0,0 +1,1111 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfdocument.h"
+#include "qpdfdocument_p.h"
+
+#include "third_party/pdfium/public/fpdf_doc.h"
+#include "third_party/pdfium/public/fpdf_text.h"
+
+#include <QDateTime>
+#include <QDebug>
+#include <QElapsedTimer>
+#include <QFile>
+#include <QHash>
+#include <QLoggingCategory>
+#include <QMetaEnum>
+#include <QMutex>
+#include <QPixmap>
+#include <QVector2D>
+
+#include <QtCore/private/qtools_p.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_GLOBAL_STATIC(QRecursiveMutex, pdfMutex)
+static int libraryRefCount;
+static const double CharacterHitTolerance = 16.0;
+Q_LOGGING_CATEGORY(qLcDoc, "qt.pdf.document")
+
+QPdfMutexLocker::QPdfMutexLocker()
+ : std::unique_lock<QRecursiveMutex>(*pdfMutex())
+{
+}
+
+class Q_PDF_EXPORT QPdfPageModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ QPdfPageModel(QPdfDocument *doc) : QAbstractListModel(doc)
+ {
+ m_roleNames = QAbstractItemModel::roleNames();
+ QMetaEnum rolesMetaEnum = doc->metaObject()->enumerator(doc->metaObject()->indexOfEnumerator("PageModelRole"));
+ for (int r = Qt::UserRole; r < int(QPdfDocument::PageModelRole::NRoles); ++r) {
+ auto name = QByteArray(rolesMetaEnum.valueToKey(r));
+ name[0] = QtMiscUtils::toAsciiLower(name[0]);
+ m_roleNames.insert(r, name);
+ }
+ connect(doc, &QPdfDocument::statusChanged, this, [this](QPdfDocument::Status s) {
+ if (s == QPdfDocument::Status::Loading)
+ beginResetModel();
+ else if (s == QPdfDocument::Status::Ready)
+ endResetModel();
+ });
+ }
+
+ QVariant data(const QModelIndex &index, int role) const override
+ {
+ if (!index.isValid())
+ return QVariant();
+
+ switch (QPdfDocument::PageModelRole(role)) {
+ case QPdfDocument::PageModelRole::Label:
+ return document()->pageLabel(index.row());
+ case QPdfDocument::PageModelRole::PointSize:
+ return document()->pagePointSize(index.row());
+ case QPdfDocument::PageModelRole::NRoles:
+ break;
+ }
+
+ switch (role) {
+ case Qt::DecorationRole:
+ return pageThumbnail(index.row());
+ case Qt::DisplayRole:
+ return document()->pageLabel(index.row());
+ }
+
+ return QVariant();
+ }
+
+ int rowCount(const QModelIndex & = QModelIndex()) const override { return document()->pageCount(); }
+
+ QHash<int, QByteArray> roleNames() const override { return m_roleNames; }
+
+private:
+ QPdfDocument *document() const { return static_cast<QPdfDocument *>(parent()); }
+ QPixmap pageThumbnail(int page) const
+ {
+ auto it = m_thumbnails.constFind(page);
+ if (it == m_thumbnails.constEnd()) {
+ auto doc = document();
+ auto size = doc->pagePointSize(page);
+ size.scale(128, 128, Qt::KeepAspectRatio);
+ // TODO use QPdfPageRenderer for threading?
+ auto image = document()->render(page, size.toSize());
+ QPixmap ret = QPixmap::fromImage(image);
+ m_thumbnails.insert(page, ret);
+ return ret;
+ }
+ return it.value();
+ }
+
+ QHash<int, QByteArray> m_roleNames;
+ mutable QHash<int, QPixmap> m_thumbnails;
+};
+
+QPdfDocumentPrivate::QPdfDocumentPrivate()
+ : avail(nullptr)
+ , doc(nullptr)
+ , loadComplete(false)
+ , status(QPdfDocument::Status::Null)
+ , lastError(QPdfDocument::Error::None)
+ , pageCount(0)
+{
+ asyncBuffer.setData(QByteArray());
+ asyncBuffer.open(QIODevice::ReadWrite);
+
+ const QPdfMutexLocker lock;
+
+ if (libraryRefCount == 0) {
+ QElapsedTimer timer;
+ timer.start();
+ FPDF_InitLibrary();
+ qCDebug(qLcDoc) << "FPDF_InitLibrary took" << timer.elapsed() << "ms";
+ }
+ ++libraryRefCount;
+
+ // FPDF_FILEACCESS setup
+ m_Param = this;
+ m_GetBlock = fpdf_GetBlock;
+
+ // FX_FILEAVAIL setup
+ FX_FILEAVAIL::version = 1;
+ IsDataAvail = fpdf_IsDataAvail;
+
+ // FX_DOWNLOADHINTS setup
+ FX_DOWNLOADHINTS::version = 1;
+ AddSegment = fpdf_AddSegment;
+}
+
+QPdfDocumentPrivate::~QPdfDocumentPrivate()
+{
+ q->close();
+
+ const QPdfMutexLocker lock;
+
+ if (!--libraryRefCount) {
+ qCDebug(qLcDoc) << "FPDF_DestroyLibrary";
+ FPDF_DestroyLibrary();
+ }
+}
+
+void QPdfDocumentPrivate::clear()
+{
+ QPdfMutexLocker lock;
+
+ if (doc)
+ FPDF_CloseDocument(doc);
+ doc = nullptr;
+
+ if (avail)
+ FPDFAvail_Destroy(avail);
+ avail = nullptr;
+ lock.unlock();
+
+ if (pageCount != 0) {
+ pageCount = 0;
+ emit q->pageCountChanged(pageCount);
+ emit q->pageModelChanged();
+ }
+
+ loadComplete = false;
+
+ asyncBuffer.close();
+ asyncBuffer.setData(QByteArray());
+ asyncBuffer.open(QIODevice::ReadWrite);
+
+ if (sequentialSourceDevice)
+ sequentialSourceDevice->disconnect(q);
+}
+
+void QPdfDocumentPrivate::updateLastError()
+{
+ if (doc) {
+ lastError = QPdfDocument::Error::None;
+ return;
+ }
+
+ QPdfMutexLocker lock;
+ const unsigned long error = FPDF_GetLastError();
+ lock.unlock();
+
+ switch (error) {
+ case FPDF_ERR_SUCCESS: lastError = QPdfDocument::Error::None; break;
+ case FPDF_ERR_UNKNOWN: lastError = QPdfDocument::Error::Unknown; break;
+ case FPDF_ERR_FILE: lastError = QPdfDocument::Error::FileNotFound; break;
+ case FPDF_ERR_FORMAT: lastError = QPdfDocument::Error::InvalidFileFormat; break;
+ case FPDF_ERR_PASSWORD: lastError = QPdfDocument::Error::IncorrectPassword; break;
+ case FPDF_ERR_SECURITY: lastError = QPdfDocument::Error::UnsupportedSecurityScheme; break;
+ default:
+ Q_UNREACHABLE();
+ }
+ if (lastError != QPdfDocument::Error::None)
+ qCDebug(qLcDoc) << "FPDF error" << error << "->" << lastError;
+}
+
+void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnership)
+{
+ if (transferDeviceOwnership)
+ ownDevice.reset(newDevice);
+ else
+ ownDevice.reset();
+
+ if (newDevice->isSequential()) {
+ sequentialSourceDevice = newDevice;
+ device = &asyncBuffer;
+ QNetworkReply *reply = qobject_cast<QNetworkReply*>(sequentialSourceDevice);
+
+ if (!reply) {
+ setStatus(QPdfDocument::Status::Error);
+ qWarning() << "QPdfDocument: Loading from sequential devices only supported with QNetworkAccessManager.";
+ return;
+ }
+
+ if (reply->isFinished() && reply->error() != QNetworkReply::NoError) {
+ setStatus(QPdfDocument::Status::Error);
+ return;
+ }
+
+ QObject::connect(reply, &QNetworkReply::finished, q, [this, reply](){
+ if (reply->error() != QNetworkReply::NoError || reply->bytesAvailable() == 0) {
+ this->setStatus(QPdfDocument::Status::Error);
+ }
+ });
+
+ if (reply->header(QNetworkRequest::ContentLengthHeader).isValid())
+ _q_tryLoadingWithSizeFromContentHeader();
+ else
+ QObject::connect(reply, SIGNAL(metaDataChanged()), q, SLOT(_q_tryLoadingWithSizeFromContentHeader()));
+ } else {
+ device = newDevice;
+ initiateAsyncLoadWithTotalSizeKnown(device->size());
+ if (!avail) {
+ setStatus(QPdfDocument::Status::Error);
+ return;
+ }
+
+ if (!doc)
+ tryLoadDocument();
+
+ if (!doc) {
+ updateLastError();
+ setStatus(QPdfDocument::Status::Error);
+ return;
+ }
+
+ QPdfMutexLocker lock;
+ const int newPageCount = FPDF_GetPageCount(doc);
+ lock.unlock();
+ if (newPageCount != pageCount) {
+ pageCount = newPageCount;
+ emit q->pageCountChanged(pageCount);
+ emit q->pageModelChanged();
+ }
+
+ // 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::Status::Ready);
+ } else {
+ updateLastError();
+ setStatus(QPdfDocument::Status::Error);
+ }
+ }
+}
+
+void QPdfDocumentPrivate::_q_tryLoadingWithSizeFromContentHeader()
+{
+ if (avail)
+ return;
+
+ const QNetworkReply *networkReply = qobject_cast<QNetworkReply*>(sequentialSourceDevice);
+ if (!networkReply) {
+ setStatus(QPdfDocument::Status::Error);
+ return;
+ }
+
+ const QVariant contentLength = networkReply->header(QNetworkRequest::ContentLengthHeader);
+ if (!contentLength.isValid()) {
+ setStatus(QPdfDocument::Status::Error);
+ return;
+ }
+
+ QObject::connect(sequentialSourceDevice, SIGNAL(readyRead()), q, SLOT(_q_copyFromSequentialSourceDevice()));
+
+ initiateAsyncLoadWithTotalSizeKnown(contentLength.toULongLong());
+
+ if (sequentialSourceDevice->bytesAvailable())
+ _q_copyFromSequentialSourceDevice();
+}
+
+void QPdfDocumentPrivate::initiateAsyncLoadWithTotalSizeKnown(quint64 totalSize)
+{
+ // FPDF_FILEACCESS setup
+ m_FileLen = totalSize;
+
+ const QPdfMutexLocker lock;
+
+ avail = FPDFAvail_Create(this, this);
+}
+
+void QPdfDocumentPrivate::_q_copyFromSequentialSourceDevice()
+{
+ if (loadComplete)
+ return;
+
+ const QByteArray data = sequentialSourceDevice->read(sequentialSourceDevice->bytesAvailable());
+ if (data.isEmpty())
+ return;
+
+ asyncBuffer.seek(asyncBuffer.size());
+ asyncBuffer.write(data);
+
+ checkComplete();
+}
+
+void QPdfDocumentPrivate::tryLoadDocument()
+{
+ QPdfMutexLocker lock;
+ switch (FPDFAvail_IsDocAvail(avail, this)) {
+ case PDF_DATA_ERROR:
+ qCDebug(qLcDoc) << "error loading";
+ break;
+ case PDF_DATA_NOTAVAIL:
+ qCDebug(qLcDoc) << "data not yet available";
+ lastError = QPdfDocument::Error::DataNotYetAvailable;
+ break;
+ case PDF_DATA_AVAIL:
+ lastError = QPdfDocument::Error::None;
+ break;
+ }
+
+ Q_ASSERT(!doc);
+
+ doc = FPDFAvail_GetDocument(avail, password);
+ lock.unlock();
+
+ updateLastError();
+ if (lastError != QPdfDocument::Error::None)
+ setStatus(QPdfDocument::Status::Error);
+
+ if (lastError == QPdfDocument::Error::IncorrectPassword) {
+ FPDF_CloseDocument(doc);
+ doc = nullptr;
+
+ setStatus(QPdfDocument::Status::Error);
+ emit q->passwordRequired();
+ }
+}
+
+void QPdfDocumentPrivate::checkComplete()
+{
+ if (!avail || loadComplete)
+ return;
+
+ if (!doc)
+ tryLoadDocument();
+
+ if (!doc)
+ return;
+
+ loadComplete = true;
+
+ QPdfMutexLocker lock;
+
+ const int newPageCount = FPDF_GetPageCount(doc);
+ for (int i = 0; i < newPageCount; ++i) {
+ int result = PDF_DATA_NOTAVAIL;
+ while (result == PDF_DATA_NOTAVAIL) {
+ result = FPDFAvail_IsPageAvail(avail, i, this);
+ }
+
+ if (result == PDF_DATA_ERROR)
+ loadComplete = false;
+ }
+
+ lock.unlock();
+
+ if (loadComplete) {
+ if (newPageCount != pageCount) {
+ pageCount = newPageCount;
+ emit q->pageCountChanged(pageCount);
+ emit q->pageModelChanged();
+ }
+
+ setStatus(QPdfDocument::Status::Ready);
+ }
+}
+
+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)
+ return;
+
+ status = documentStatus;
+ emit q->statusChanged(status);
+}
+
+FPDF_BOOL QPdfDocumentPrivate::fpdf_IsDataAvail(_FX_FILEAVAIL *pThis, size_t offset, size_t size)
+{
+ QPdfDocumentPrivate *d = static_cast<QPdfDocumentPrivate*>(pThis);
+ return offset + size <= static_cast<quint64>(d->device->size());
+}
+
+int QPdfDocumentPrivate::fpdf_GetBlock(void *param, unsigned long position, unsigned char *pBuf, unsigned long size)
+{
+ QPdfDocumentPrivate *d = static_cast<QPdfDocumentPrivate*>(reinterpret_cast<FPDF_FILEACCESS*>(param));
+ d->device->seek(position);
+ return qMax(qint64(0), d->device->read(reinterpret_cast<char *>(pBuf), size));
+
+}
+
+void QPdfDocumentPrivate::fpdf_AddSegment(_FX_DOWNLOADHINTS *pThis, size_t offset, size_t size)
+{
+ Q_UNUSED(pThis);
+ Q_UNUSED(offset);
+ Q_UNUSED(size);
+}
+
+QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int count) const
+{
+ QList<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
+ return QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.constData()), len - 1);
+}
+
+QPointF QPdfDocumentPrivate::getCharPosition(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const
+{
+ double x, y;
+ const int count = FPDFText_CountChars(textPage);
+ if (FPDFText_GetCharOrigin(textPage, qMin(count - 1, charIndex), &x, &y))
+ return mapPageToView(pdfPage, x, y);
+ return {};
+}
+
+QRectF QPdfDocumentPrivate::getCharBox(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const
+{
+ double l, t, r, b;
+ if (FPDFText_GetCharBox(textPage, charIndex, &l, &r, &b, &t))
+ return mapPageToView(pdfPage, l, t, r, b);
+ return {};
+}
+
+/*! \internal
+ Convert the point \a x , \a y to the usual 1x (pixels = points)
+ 4th-quadrant "view" coordinate system relative to the top-left corner of
+ the rendered page. Some PDF files have internal transforms that make this
+ coordinate system different from "page coordinates", so we cannot just
+ subtract from page height to invert the y coordinates, in general.
+ */
+QPointF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double x, double y) const
+{
+ const auto pageHeight = FPDF_GetPageHeight(pdfPage);
+ const auto pageWidth = FPDF_GetPageWidth(pdfPage);
+ int rx, ry;
+ if (FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, x, y, &rx, &ry))
+ return QPointF(rx, ry);
+ return {};
+}
+
+/*! \internal
+ Convert the bounding box defined by \a left \a top \a right and \a bottom
+ to the usual 1x (pixels = points) 4th-quadrant "view" coordinate system
+ that we use for rendering things on top of the page image.
+ Some PDF files have internal transforms that make this coordinate
+ system different from "page coordinates", so we cannot just
+ subtract from page height to invert the y coordinates, in general.
+ */
+QRectF QPdfDocumentPrivate::mapPageToView(FPDF_PAGE pdfPage, double left, double top, double right, double bottom) const
+{
+ const auto pageHeight = FPDF_GetPageHeight(pdfPage);
+ const auto pageWidth = FPDF_GetPageWidth(pdfPage);
+ int xfmLeft, xfmTop, xfmRight, xfmBottom;
+ if ( FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, left, top, &xfmLeft, &xfmTop) &&
+ FPDF_PageToDevice(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, right, bottom, &xfmRight, &xfmBottom) )
+ return QRectF(xfmLeft, xfmTop, xfmRight - xfmLeft, xfmBottom - xfmTop);
+ return {};
+}
+
+/*! \internal
+ Convert the point \a x , \a y \a from the usual 1x (pixels = points)
+ 4th-quadrant "view" coordinate system relative to the top-left corner of
+ the rendered page, to "page coordinates" suited to the given \a pdfPage,
+ which may have arbitrary internal transforms.
+ */
+QPointF QPdfDocumentPrivate::mapViewToPage(FPDF_PAGE pdfPage, QPointF position) const
+{
+ const auto pageHeight = FPDF_GetPageHeight(pdfPage);
+ const auto pageWidth = FPDF_GetPageWidth(pdfPage);
+ double rx, ry;
+ if (FPDF_DeviceToPage(pdfPage, 0, 0, qRound(pageWidth), qRound(pageHeight), 0, position.x(), position.y(), &rx, &ry))
+ return QPointF(rx, ry);
+ return {};
+}
+
+QPdfDocumentPrivate::TextPosition QPdfDocumentPrivate::hitTest(int page, QPointF position)
+{
+ const QPdfMutexLocker lock;
+
+ TextPosition result;
+ FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ const QPointF pagePos = mapViewToPage(pdfPage, position);
+ int hitIndex = FPDFText_GetCharIndexAtPos(textPage, pagePos.x(), pagePos.y(),
+ CharacterHitTolerance, CharacterHitTolerance);
+ if (hitIndex >= 0) {
+ QPointF charPos = getCharPosition(pdfPage, textPage, hitIndex);
+ if (!charPos.isNull()) {
+ QRectF charBox = getCharBox(pdfPage, textPage, hitIndex);
+ // If the given position is past the end of the line, i.e. if the right edge of the found character's
+ // bounding box is closer to it than the left edge is, we say that we "hit" the next character index after
+ if (qAbs(charBox.right() - position.x()) < qAbs(charPos.x() - position.x())) {
+ charPos.setX(charBox.right());
+ ++hitIndex;
+ }
+ qCDebug(qLcDoc) << "on page" << page << "@" << position << "got char position" << charPos << "index" << hitIndex;
+ result = { charPos, charBox.height(), hitIndex };
+ }
+ }
+
+ FPDFText_ClosePage(textPage);
+ FPDF_ClosePage(pdfPage);
+
+ return result;
+}
+
+/*!
+ \class QPdfDocument
+ \since 5.10
+ \inmodule QtPdf
+
+ \brief The QPdfDocument class loads a PDF document and renders pages from it.
+*/
+
+/*!
+ Constructs a new document with parent object \a parent.
+*/
+QPdfDocument::QPdfDocument(QObject *parent)
+ : QObject(parent)
+ , d(new QPdfDocumentPrivate)
+{
+ d->q = this;
+}
+
+/*!
+ Destroys the document.
+*/
+QPdfDocument::~QPdfDocument()
+{
+}
+
+/*!
+ Loads the document contents from \a fileName.
+*/
+QPdfDocument::Error QPdfDocument::load(const QString &fileName)
+{
+ qCDebug(qLcDoc) << "loading" << fileName;
+
+ close();
+
+ d->setStatus(QPdfDocument::Status::Loading);
+
+ std::unique_ptr<QFile> f(new QFile(fileName));
+ if (!f->open(QIODevice::ReadOnly)) {
+ d->lastError = Error::FileNotFound;
+ d->setStatus(QPdfDocument::Status::Error);
+ } else {
+ d->load(f.release(), /*transfer ownership*/true);
+ }
+ return d->lastError;
+}
+
+/*! \internal
+ Returns the filename of the document that has been opened,
+ or an empty string if no document is open.
+*/
+QString QPdfDocument::fileName() const
+{
+ const QFile *f = qobject_cast<QFile *>(d->device.data());
+ if (f)
+ return f->fileName();
+ return QString();
+}
+
+/*!
+ \enum QPdfDocument::Status
+
+ This enum describes the current status of the document.
+
+ \value Null The initial status after the document has been created or after it has been closed.
+ \value Loading The status after load() has been called and before the document is fully loaded.
+ \value Ready The status when the document is fully loaded and its data can be accessed.
+ \value Unloading The status after close() has been called on an open document.
+ At this point the document is still valid and all its data can be accessed.
+ \value Error The status after Loading, if loading has failed.
+
+ \sa QPdfDocument::status()
+*/
+
+/*!
+ \property QPdfDocument::status
+
+ This property holds the current status of the document.
+*/
+QPdfDocument::Status QPdfDocument::status() const
+{
+ return d->status;
+}
+
+/*!
+ Loads the document contents from \a device.
+*/
+void QPdfDocument::load(QIODevice *device)
+{
+ close();
+
+ d->setStatus(QPdfDocument::Status::Loading);
+
+ d->load(device, /*transfer ownership*/false);
+}
+
+/*!
+ \property QPdfDocument::password
+
+ This property holds the document password.
+
+ If the document is protected by a password, the user must provide it, and
+ the application must set this property. Otherwise, it's not needed.
+*/
+void QPdfDocument::setPassword(const QString &password)
+{
+ const QByteArray newPassword = password.toUtf8();
+
+ if (d->password == newPassword)
+ return;
+
+ d->password = newPassword;
+ emit passwordChanged();
+}
+
+QString QPdfDocument::password() const
+{
+ return QString::fromUtf8(d->password);
+}
+
+/*!
+ \enum QPdfDocument::MetaDataField
+
+ This enum describes the available fields of meta data.
+
+ \value Title The document's title as QString.
+ \value Author The name of the person who created the document as QString.
+ \value Subject The subject of the document as QString.
+ \value Keywords Keywords associated with the document as QString.
+ \value Creator If the document was converted to PDF from another format,
+ the name of the conforming product that created the original document
+ from which it was converted as QString.
+ \value Producer If the document was converted to PDF from another format,
+ the name of the conforming product that converted it to PDF as QString.
+ \value CreationDate The date and time the document was created as QDateTime.
+ \value ModificationDate The date and time the document was most recently modified as QDateTime.
+
+ \sa QPdfDocument::metaData()
+*/
+
+/*!
+ Returns the meta data of the document for the given \a field.
+*/
+QVariant QPdfDocument::metaData(MetaDataField field) const
+{
+ if (!d->doc)
+ return QString();
+
+ static QMetaEnum fieldsMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("MetaDataField"));
+ QByteArray fieldName;
+ switch (field) {
+ case MetaDataField::ModificationDate:
+ fieldName = "ModDate";
+ break;
+ default:
+ fieldName = QByteArray(fieldsMetaEnum.valueToKey(int(field)));
+ break;
+ }
+
+ QPdfMutexLocker lock;
+ const unsigned long len = FPDF_GetMetaText(d->doc, fieldName.constData(), nullptr, 0);
+
+ QList<ushort> buf(len);
+ FPDF_GetMetaText(d->doc, fieldName.constData(), buf.data(), buf.size());
+ lock.unlock();
+
+ QString text = QString::fromUtf16(reinterpret_cast<const char16_t *>(buf.data()));
+
+ switch (field) {
+ case MetaDataField::Title: // fall through
+ case MetaDataField::Subject:
+ case MetaDataField::Author:
+ case MetaDataField::Keywords:
+ case MetaDataField::Producer:
+ case MetaDataField::Creator:
+ return text;
+ case MetaDataField::CreationDate: // fall through
+ case MetaDataField::ModificationDate:
+ // convert a "D:YYYYMMDDHHmmSSOHH'mm'" into "YYYY-MM-DDTHH:mm:ss+HH:mm"
+ if (text.startsWith(QLatin1String("D:")))
+ text = text.mid(2);
+ text.insert(4, QLatin1Char('-'));
+ text.insert(7, QLatin1Char('-'));
+ text.insert(10, QLatin1Char('T'));
+ text.insert(13, QLatin1Char(':'));
+ text.insert(16, QLatin1Char(':'));
+ text.replace(QLatin1Char('\''), QLatin1Char(':'));
+ if (text.endsWith(QLatin1Char(':')))
+ text.chop(1);
+
+ return QDateTime::fromString(text, Qt::ISODate);
+ }
+
+ return QVariant();
+}
+
+/*!
+ \enum QPdfDocument::Error
+
+ This enum describes the error while attempting the last operation on the document.
+
+ \value None No error occurred.
+ \value Unknown Unknown type of error.
+ \value DataNotYetAvailable The document is still loading, it's too early to attempt the operation.
+ \value FileNotFound The file given to load() was not found.
+ \value InvalidFileFormat The file given to load() is not a valid PDF file.
+ \value IncorrectPassword The password given to setPassword() is not correct for this file.
+ \value UnsupportedSecurityScheme QPdfDocument is not able to unlock this kind of PDF file.
+
+ \sa QPdfDocument::error()
+*/
+
+/*!
+ Returns the type of error if \l status is \c Error, or \c NoError if there
+ is no error.
+*/
+QPdfDocument::Error QPdfDocument::error() const
+{
+ return d->lastError;
+}
+
+/*!
+ Closes the document.
+*/
+void QPdfDocument::close()
+{
+ if (!d->doc)
+ return;
+
+ d->setStatus(Status::Unloading);
+
+ d->clear();
+
+ if (!d->password.isEmpty()) {
+ d->password.clear();
+ emit passwordChanged();
+ }
+
+ d->setStatus(Status::Null);
+}
+
+/*!
+ \property QPdfDocument::pageCount
+
+ This property holds the number of pages in the loaded document or \c 0 if
+ no document is loaded.
+*/
+int QPdfDocument::pageCount() const
+{
+ return d->pageCount;
+}
+
+/*!
+ Returns the size of page \a page in points (1/72 of an inch).
+*/
+QSizeF QPdfDocument::pagePointSize(int page) const
+{
+ QSizeF result;
+ if (!d->doc || !d->checkPageComplete(page))
+ return result;
+
+ const QPdfMutexLocker lock;
+
+ FPDF_GetPageSizeByIndex(d->doc, page, &result.rwidth(), &result.rheight());
+ return result;
+}
+
+/*!
+ \enum QPdfDocument::PageModelRole
+
+ Roles in pageModel().
+
+ \value Label The page number to be used for display purposes (QString).
+ \value PointSize The page size in points (1/72 of an inch) (QSizeF).
+ \omitvalue NRoles
+*/
+
+/*!
+ \property QPdfDocument::pageModel
+
+ This property holds an instance of QAbstractListModel to provide
+ page-specific metadata, containing one row for each page in the document.
+
+ \sa QPdfDocument::PageModelRole
+*/
+QAbstractListModel *QPdfDocument::pageModel()
+{
+ if (!d->pageModel)
+ d->pageModel = new QPdfPageModel(this);
+ return d->pageModel;
+}
+
+/*!
+ Returns the \a page number to be used for display purposes.
+
+ For example, a document may have multiple sections with different numbering.
+ Perhaps the preface uses roman numerals, the body starts on page 1, and the
+ appendix starts at A1. Whenever a PDF viewer shows a page number, to avoid
+ confusing the user it should be the same "number" as is printed on the
+ corner of the page, rather than the zero-based page index that we use in
+ APIs (assuming the document author has made the page labels match the
+ printed numbers).
+
+ If the document does not have custom page numbering, this function returns
+ \c {page + 1}.
+
+ \sa pageIndexForLabel()
+*/
+QString QPdfDocument::pageLabel(int page)
+{
+ const unsigned long len = FPDF_GetPageLabel(d->doc, page, nullptr, 0);
+ if (len == 0)
+ return QString::number(page + 1);
+ QList<char16_t> buf(len);
+ QPdfMutexLocker lock;
+ FPDF_GetPageLabel(d->doc, page, buf.data(), len);
+ lock.unlock();
+ return QString::fromUtf16(buf.constData());
+}
+
+/*!
+ Returns the index of the page that has the \a label, or \c -1 if not found.
+
+ \sa pageLabel()
+ \since 6.6
+*/
+int QPdfDocument::pageIndexForLabel(const QString &label)
+{
+ for (int i = 0; i < d->pageCount; ++i) {
+ if (pageLabel(i) == label)
+ return i;
+ }
+ return -1;
+}
+
+/*!
+ Renders the \a page into a QImage of size \a imageSize according to the
+ provided \a renderOptions.
+
+ Returns the rendered page or an empty image in case of an error.
+
+ Note: If the \a imageSize does not match the aspect ratio of the page in the
+ PDF document, the page is rendered scaled, so that it covers the
+ complete \a imageSize.
+*/
+QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions)
+{
+ if (!d->doc || !d->checkPageComplete(page))
+ return QImage();
+
+ const QPdfMutexLocker lock;
+
+ QElapsedTimer timer;
+ if (Q_UNLIKELY(qLcDoc().isDebugEnabled()))
+ timer.start();
+ FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
+ if (!pdfPage)
+ return QImage();
+
+ QImage result(imageSize, QImage::Format_ARGB32);
+ result.fill(Qt::transparent);
+ FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(result.width(), result.height(), FPDFBitmap_BGRA, result.bits(), result.bytesPerLine());
+
+ const QPdfDocumentRenderOptions::RenderFlags renderFlags = renderOptions.renderFlags();
+ int flags = 0;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Annotations)
+ flags |= FPDF_ANNOT;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::OptimizedForLcd)
+ flags |= FPDF_LCD_TEXT;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::Grayscale)
+ flags |= FPDF_GRAYSCALE;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ForceHalftone)
+ flags |= FPDF_RENDER_FORCEHALFTONE;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::TextAliased)
+ flags |= FPDF_RENDER_NO_SMOOTHTEXT;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::ImageAliased)
+ flags |= FPDF_RENDER_NO_SMOOTHIMAGE;
+ if (renderFlags & QPdfDocumentRenderOptions::RenderFlag::PathAliased)
+ flags |= FPDF_RENDER_NO_SMOOTHPATH;
+
+ 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 = pagePointSize(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 {
+ const auto rotation = QPdfDocumentPrivate::toFPDFRotation(renderOptions.rotation());
+ 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);
+ 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);
+ const QPointF pageStart = d->mapViewToPage(pdfPage, start);
+ const QPointF pageEnd = d->mapViewToPage(pdfPage, end);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ int startIndex = FPDFText_GetCharIndexAtPos(textPage, pageStart.x(), pageStart.y(),
+ CharacterHitTolerance, CharacterHitTolerance);
+ int endIndex = FPDFText_GetCharIndexAtPos(textPage, pageEnd.x(), pageEnd.y(),
+ CharacterHitTolerance, CharacterHitTolerance);
+
+ QPdfSelection result;
+
+ if (startIndex >= 0 && endIndex != startIndex) {
+ if (startIndex > endIndex)
+ qSwap(startIndex, endIndex);
+
+ // If the given end position is past the end of the line, i.e. if the right edge of the last character's
+ // bounding box is closer to it than the left edge is, then extend the char range by one
+ QRectF endCharBox = d->getCharBox(pdfPage, textPage, endIndex);
+ if (qAbs(endCharBox.right() - end.x()) < qAbs(endCharBox.x() - end.x()))
+ ++endIndex;
+
+ int count = endIndex - startIndex;
+ QString text = d->getText(textPage, startIndex, count);
+ QList<QPolygonF> bounds;
+ QRectF hull;
+ 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);
+ const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
+ }
+ qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text;
+ result = QPdfSelection(text, bounds, hull, startIndex, endIndex);
+ } else {
+ qCDebug(qLcDoc) << page << start << "->" << end << "nothing found";
+ }
+
+ FPDFText_ClosePage(textPage);
+ FPDF_ClosePage(pdfPage);
+
+ return result;
+}
+
+/*!
+ Returns information about the text on the given \a page that can be found
+ beginning at the given \a startIndex with at most \a maxLength characters.
+*/
+QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int maxLength)
+{
+
+ if (page < 0 || startIndex < 0 || maxLength < 0)
+ return {};
+ const QPdfMutexLocker lock;
+ FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ int pageCount = FPDFText_CountChars(textPage);
+ if (startIndex >= pageCount)
+ return QPdfSelection();
+ QList<QPolygonF> bounds;
+ QRectF hull;
+ int rectCount = 0;
+ QString text;
+ if (maxLength > 0) {
+ text = d->getText(textPage, startIndex, maxLength);
+ rectCount = FPDFText_CountRects(textPage, startIndex, text.size());
+ for (int i = 0; i < rectCount; ++i) {
+ double l, r, b, t;
+ FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
+ const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
+ }
+ }
+ if (bounds.isEmpty())
+ hull = QRectF(d->getCharPosition(pdfPage, textPage, startIndex), QSizeF());
+ qCDebug(qLcDoc) << "on page" << page << "at index" << startIndex << "maxLength" << maxLength
+ << "got" << text.size() << "chars," << rectCount << "rects within" << hull;
+
+ FPDFText_ClosePage(textPage);
+ FPDF_ClosePage(pdfPage);
+
+ return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.size());
+}
+
+/*!
+ Returns all the text and its bounds on the given \a page.
+*/
+QPdfSelection QPdfDocument::getAllText(int page)
+{
+ const QPdfMutexLocker lock;
+ FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ int count = FPDFText_CountChars(textPage);
+ if (count < 1)
+ return QPdfSelection();
+ QString text = d->getText(textPage, 0, count);
+ QList<QPolygonF> bounds;
+ QRectF hull;
+ int rectCount = FPDFText_CountRects(textPage, 0, count);
+ for (int i = 0; i < rectCount; ++i) {
+ double l, r, b, t;
+ FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
+ const QRectF rect = d->mapPageToView(pdfPage, l, t, r, b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
+ }
+ qCDebug(qLcDoc) << "on page" << page << "got" << count << "chars," << rectCount << "rects within" << hull;
+
+ FPDFText_ClosePage(textPage);
+ FPDF_ClosePage(pdfPage);
+
+ return QPdfSelection(text, bounds, hull, 0, count);
+}
+
+QT_END_NAMESPACE
+
+#include "qpdfdocument.moc"
+#include "moc_qpdfdocument.cpp"
diff --git a/src/pdf/qpdfdocument.h b/src/pdf/qpdfdocument.h
new file mode 100644
index 000000000..8355246ae
--- /dev/null
+++ b/src/pdf/qpdfdocument.h
@@ -0,0 +1,127 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFDOCUMENT_H
+#define QPDFDOCUMENT_H
+
+#include <QtPdf/qtpdfglobal.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/QAbstractListModel>
+#include <QtGui/qimage.h>
+#include <QtPdf/qpdfdocumentrenderoptions.h>
+#include <QtPdf/qpdfselection.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfDocumentPrivate;
+class QNetworkReply;
+
+class Q_PDF_EXPORT QPdfDocument : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged FINAL)
+ Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged FINAL)
+ Q_PROPERTY(Status status READ status NOTIFY statusChanged FINAL)
+ Q_PROPERTY(QAbstractListModel* pageModel READ pageModel NOTIFY pageModelChanged FINAL)
+
+public:
+ enum class Status {
+ Null,
+ Loading,
+ Ready,
+ Unloading,
+ Error
+ };
+ Q_ENUM(Status)
+
+ enum class Error {
+ None,
+ Unknown,
+ DataNotYetAvailable,
+ FileNotFound,
+ InvalidFileFormat,
+ IncorrectPassword,
+ UnsupportedSecurityScheme
+ };
+ Q_ENUM(Error)
+
+ enum class MetaDataField {
+ Title,
+ Subject,
+ Author,
+ Keywords,
+ Producer,
+ Creator,
+ CreationDate,
+ ModificationDate
+ };
+ Q_ENUM(MetaDataField)
+
+ enum class PageModelRole {
+ Label = Qt::UserRole,
+ PointSize,
+ NRoles
+ };
+ Q_ENUM(PageModelRole)
+
+ QPdfDocument() : QPdfDocument(nullptr) {}
+ explicit QPdfDocument(QObject *parent);
+ ~QPdfDocument() override;
+
+ Error load(const QString &fileName);
+
+ Status status() const;
+
+ void load(QIODevice *device);
+ void setPassword(const QString &password);
+ QString password() const;
+
+ QVariant metaData(MetaDataField field) const;
+
+ Error error() const;
+
+ void close();
+
+ int pageCount() const;
+
+ Q_INVOKABLE QSizeF pagePointSize(int page) const;
+
+ Q_INVOKABLE QString pageLabel(int page);
+ Q_INVOKABLE int pageIndexForLabel(const QString &label);
+
+ QAbstractListModel *pageModel();
+
+ QImage render(int page, QSize imageSize, QPdfDocumentRenderOptions options = QPdfDocumentRenderOptions());
+
+ Q_INVOKABLE QPdfSelection getSelection(int page, QPointF start, QPointF end);
+ Q_INVOKABLE QPdfSelection getSelectionAtIndex(int page, int startIndex, int maxLength);
+ Q_INVOKABLE QPdfSelection getAllText(int page);
+
+Q_SIGNALS:
+ void passwordChanged();
+ void passwordRequired();
+ void statusChanged(QPdfDocument::Status status);
+ void pageCountChanged(int pageCount);
+ void pageModelChanged();
+
+private:
+ friend struct QPdfBookmarkModelPrivate;
+ friend class QPdfFile;
+ friend class QPdfLinkModelPrivate;
+ friend class QPdfPageModel;
+ friend class QPdfSearchModel;
+ friend class QPdfSearchModelPrivate;
+ friend class QQuickPdfSelection;
+
+ QString fileName() const;
+
+ Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader())
+ Q_PRIVATE_SLOT(d, void _q_copyFromSequentialSourceDevice())
+ QScopedPointer<QPdfDocumentPrivate> d;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFDOCUMENT_H
diff --git a/src/pdf/qpdfdocument_p.h b/src/pdf/qpdfdocument_p.h
new file mode 100644
index 000000000..cdb76d16f
--- /dev/null
+++ b/src/pdf/qpdfdocument_p.h
@@ -0,0 +1,123 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFDOCUMENT_P_H
+#define QPDFDOCUMENT_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 "qpdfdocument.h"
+#include "qtpdfexports.h"
+
+#include "third_party/pdfium/public/fpdfview.h"
+#include "third_party/pdfium/public/fpdf_dataavail.h"
+
+#include <QtCore/qbuffer.h>
+#include <QtCore/qmutex.h>
+#include <QtCore/qpointer.h>
+#include <QtNetwork/qnetworkreply.h>
+
+#include <mutex>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfMutexLocker : public std::unique_lock<QRecursiveMutex>
+{
+public:
+ QPdfMutexLocker();
+};
+
+class QPdfPageModel;
+
+class Q_PDF_EXPORT QPdfDocumentPrivate: public FPDF_FILEACCESS, public FX_FILEAVAIL, public FX_DOWNLOADHINTS
+{
+public:
+ QPdfDocumentPrivate();
+ ~QPdfDocumentPrivate();
+
+ QPdfDocument *q;
+ QPdfPageModel *pageModel = nullptr;
+
+ FPDF_AVAIL avail;
+ FPDF_DOCUMENT doc;
+ bool loadComplete;
+
+ QPointer<QIODevice> device;
+ QScopedPointer<QIODevice> ownDevice;
+ QBuffer asyncBuffer;
+ QPointer<QIODevice> sequentialSourceDevice;
+ QByteArray password;
+
+ QPdfDocument::Status status;
+ QPdfDocument::Error lastError;
+ int pageCount;
+
+ void clear();
+
+ void load(QIODevice *device, bool ownDevice);
+ void loadAsync(QIODevice *device);
+
+ void _q_tryLoadingWithSizeFromContentHeader();
+ void initiateAsyncLoadWithTotalSizeKnown(quint64 totalSize);
+ 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);
+ static int fpdf_GetBlock(void* param, unsigned long position, unsigned char* pBuf, unsigned long size);
+ static void fpdf_AddSegment(struct _FX_DOWNLOADHINTS* pThis, size_t offset, size_t size);
+ void updateLastError();
+ QString getText(FPDF_TEXTPAGE textPage, int startIndex, int count) const;
+ QPointF getCharPosition(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const;
+ QRectF getCharBox(FPDF_PAGE pdfPage, FPDF_TEXTPAGE textPage, int charIndex) const;
+ QPointF mapPageToView(FPDF_PAGE pdfPage, double x, double y) const;
+ QRectF mapPageToView(FPDF_PAGE pdfPage, double left, double top, double right, double bottom) const;
+ QPointF mapViewToPage(FPDF_PAGE pdfPage, QPointF position) const;
+
+ // FPDF takes the rotation parameter as an int.
+ // This enum is mapping the int values defined in fpdfview.h:956.
+ // (not using enum class to ensure int convertability)
+ enum QFPDFRotation {
+ Normal = 0,
+ ClockWise90 = 1,
+ ClockWise180 = 2,
+ CounterClockWise90 = 3
+ };
+
+ static constexpr QFPDFRotation toFPDFRotation(QPdfDocumentRenderOptions::Rotation rotation)
+ {
+ switch (rotation) {
+ case QPdfDocumentRenderOptions::Rotation::None:
+ return QFPDFRotation::Normal;
+ case QPdfDocumentRenderOptions::Rotation::Clockwise90:
+ return QFPDFRotation::ClockWise90;
+ case QPdfDocumentRenderOptions::Rotation::Clockwise180:
+ return QFPDFRotation::ClockWise180;
+ case QPdfDocumentRenderOptions::Rotation::Clockwise270:
+ return QFPDFRotation::CounterClockWise90;
+ }
+ Q_UNREACHABLE();
+ }
+
+ struct TextPosition {
+ QPointF position;
+ qreal height = 0;
+ int charIndex = -1;
+ };
+ TextPosition hitTest(int page, QPointF position);
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFDOCUMENT_P_H
diff --git a/src/pdf/qpdfdocumentrenderoptions.h b/src/pdf/qpdfdocumentrenderoptions.h
new file mode 100644
index 000000000..af074d976
--- /dev/null
+++ b/src/pdf/qpdfdocumentrenderoptions.h
@@ -0,0 +1,81 @@
+// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFDOCUMENTRENDEROPTIONS_H
+#define QPDFDOCUMENTRENDEROPTIONS_H
+
+#include <QtPdf/qtpdfglobal.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qrect.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfDocumentRenderOptions
+{
+public:
+ enum class Rotation {
+ None,
+ Clockwise90,
+ Clockwise180,
+ Clockwise270
+ };
+
+ enum class RenderFlag {
+ None = 0x000,
+ Annotations = 0x001,
+ OptimizedForLcd = 0x002,
+ Grayscale = 0x004,
+ ForceHalftone = 0x008,
+ TextAliased = 0x010,
+ ImageAliased = 0x020,
+ PathAliased = 0x040
+ };
+ Q_DECLARE_FLAGS(RenderFlags, RenderFlag)
+
+ constexpr QPdfDocumentRenderOptions() noexcept : m_renderFlags(0), m_rotation(0), m_reserved(0) {}
+
+ constexpr Rotation rotation() const noexcept { return static_cast<Rotation>(m_rotation); }
+ constexpr void setRotation(Rotation r) noexcept { m_rotation = quint32(r); }
+
+ constexpr RenderFlags renderFlags() const noexcept { return static_cast<RenderFlags>(m_renderFlags); }
+ constexpr void setRenderFlags(RenderFlags r) noexcept { m_renderFlags = quint32(r.toInt()); }
+
+ constexpr QRect scaledClipRect() const noexcept { return m_clipRect; }
+ constexpr void setScaledClipRect(const QRect &r) noexcept { m_clipRect = r; }
+
+ constexpr QSize scaledSize() const noexcept { return m_scaledSize; }
+ constexpr void setScaledSize(const QSize &s) noexcept { m_scaledSize = s; }
+
+private:
+ friend constexpr inline bool operator==(const QPdfDocumentRenderOptions &lhs, const QPdfDocumentRenderOptions &rhs) noexcept;
+
+ QRect m_clipRect;
+ QSize m_scaledSize;
+
+ quint32 m_renderFlags : 8;
+ quint32 m_rotation : 3;
+ quint32 m_reserved : 21;
+ quint32 m_reserved2 = 0;
+};
+
+Q_DECLARE_TYPEINFO(QPdfDocumentRenderOptions, Q_PRIMITIVE_TYPE);
+Q_DECLARE_OPERATORS_FOR_FLAGS(QPdfDocumentRenderOptions::RenderFlags)
+
+constexpr inline bool operator==(const QPdfDocumentRenderOptions &lhs, const QPdfDocumentRenderOptions &rhs) noexcept
+{
+ return lhs.m_clipRect == rhs.m_clipRect && lhs.m_scaledSize == rhs.m_scaledSize &&
+ lhs.m_renderFlags == rhs.m_renderFlags && lhs.m_rotation == rhs.m_rotation &&
+ lhs.m_reserved == rhs.m_reserved && lhs.m_reserved2 == rhs.m_reserved2; // fix -Wunused-private-field
+}
+
+constexpr inline bool operator!=(const QPdfDocumentRenderOptions &lhs, const QPdfDocumentRenderOptions &rhs) noexcept
+{
+ return !operator==(lhs, rhs);
+}
+
+QT_END_NAMESPACE
+
+Q_DECLARE_METATYPE(QPdfDocumentRenderOptions)
+
+#endif // QPDFDOCUMENTRENDEROPTIONS_H
diff --git a/src/pdf/qpdfdocumentrenderoptions.qdoc b/src/pdf/qpdfdocumentrenderoptions.qdoc
new file mode 100644
index 000000000..ad8e7bfdb
--- /dev/null
+++ b/src/pdf/qpdfdocumentrenderoptions.qdoc
@@ -0,0 +1,135 @@
+// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfdocumentrenderoptions.h"
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \class QPdfDocumentRenderOptions
+ \since 5.10
+ \inmodule QtPdf
+
+ \brief The QPdfDocumentRenderOptions class holds the options to render a page from a PDF document.
+
+ \sa QPdfDocument
+*/
+
+/*!
+ \enum QPdfDocumentRenderOptions::Rotation
+
+ This enum describes the rotation of the page for rendering.
+
+ \value None Do not rotate (the default)
+ \value Clockwise90 Rotate 90 degrees clockwise
+ \value Clockwise180 Rotate 180 degrees
+ \value Clockwise270 Rotate 270 degrees clockwise
+
+ \sa QPdfDocument::render()
+*/
+/*!
+ \enum QPdfDocumentRenderOptions::RenderFlag
+
+ This enum is used to describe how a page should be rendered.
+
+ \value None The default value, representing no flags.
+ \value Annotations The page is rendered with annotations.
+ \value OptimizedForLcd The text of the page is rendered optimized for LCD display.
+ \value Grayscale The page is rendered grayscale.
+ \value ForceHalftone Always use halftones for rendering if the output image is stretched.
+ \value TextAliased Anti-aliasing is disabled for rendering text.
+ \value ImageAliased Anti-aliasing is disabled for rendering images.
+ \value PathAliased Anti-aliasing is disabled for rendering paths.
+
+ \sa QPdfDocument::render()
+*/
+
+/*!
+ \fn QPdfDocumentRenderOptions::QPdfDocumentRenderOptions()
+
+ Constructs a QPdfDocumentRenderOptions object.
+*/
+
+/*!
+ \fn QPdfDocumentRenderOptions::Rotation QPdfDocumentRenderOptions::rotation() const
+
+ Returns the rotation used for rendering a page from a PDF document.
+
+ \sa setRotation()
+*/
+
+/*!
+ \fn void QPdfDocumentRenderOptions::setRotation(QPdfDocumentRenderOptions::Rotation rotation)
+
+ Sets the \a rotation used for rendering a page from a PDF document.
+
+ \sa rotation()
+*/
+
+/*!
+ \fn QPdfDocumentRenderOptions::RenderFlags QPdfDocumentRenderOptions::renderFlags() const
+
+ Returns the special flags used for rendering a page from a PDF document.
+
+ \sa setRenderFlags()
+*/
+
+/*!
+ \fn void QPdfDocumentRenderOptions::setRenderFlags(QPdfDocumentRenderOptions::RenderFlags flags)
+
+ Sets the special \a flags used for rendering a page from a PDF document.
+
+ \sa renderFlags()
+*/
+
+/*!
+ \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(const QRect &r)
+
+ Sets the rectangle region (\a r) to be clipped from the page after having
+ been scaled to \l scaledSize().
+
+ \sa scaledClipRect()
+*/
+
+/*!
+ \fn QRect QPdfDocumentRenderOptions::scaledSize() const
+
+ Returns the size of the page to be rendered, in pixels.
+
+ \sa setScaledSize()
+*/
+
+/*!
+ \fn void QPdfDocumentRenderOptions::setScaledSize(const QSize &s)
+
+ Sets the size (\a s) of the page to be rendered, in pixels.
+
+ \sa scaledSize()
+*/
+
+/*!
+ \fn bool operator!=(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs)
+ \relates QPdfDocumentRenderOptions
+
+ Returns \c true if the options \a lhs and \a rhs are different, otherwise
+ returns \c false.
+*/
+
+/*!
+ \fn bool operator==(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs)
+ \relates QPdfDocumentRenderOptions
+
+ Returns \c true if the options \a lhs and \a rhs are equal,
+ otherwise returns \c false.
+*/
+
+QT_END_NAMESPACE
diff --git a/src/pdf/qpdffile.cpp b/src/pdf/qpdffile.cpp
new file mode 100644
index 000000000..a54f6a568
--- /dev/null
+++ b/src/pdf/qpdffile.cpp
@@ -0,0 +1,28 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdffile_p.h"
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \internal
+ \class QPdfFile
+ \inmodule QtPdf
+
+ QPdfFile is a means of passing a PDF file along with the associated
+ QPdfDocument together into QPdfIOHandler::load(QIODevice *device) so that
+ QPdfIOHandler does not need to construct its own redundant QPdfDocument
+ instance. If it succeeds in casting the QIODevice to a QPdfFile, it is
+ expected to use the QPdfDocument operations for all I/O, and thus the
+ normal QFile I/O functions are not needed for that use case.
+*/
+
+QPdfFile::QPdfFile(QPdfDocument *doc)
+ : QFile(doc->fileName()), m_document(doc)
+{
+}
+
+QT_END_NAMESPACE
+
+//#include "moc_qpdffile_p.cpp"
diff --git a/src/pdf/qpdffile_p.h b/src/pdf/qpdffile_p.h
new file mode 100644
index 000000000..f678cdcdc
--- /dev/null
+++ b/src/pdf/qpdffile_p.h
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFFILE_P_H
+#define QPDFFILE_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 "qpdfdocument.h"
+
+#include <QtCore/qfile.h>
+
+QT_BEGIN_NAMESPACE
+
+class Q_PDF_EXPORT QPdfFile : public QFile
+{
+ Q_OBJECT
+public:
+ QPdfFile(QPdfDocument *doc);
+ QPdfDocument *document() { return m_document; }
+
+private:
+ QPdfDocument *m_document;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFFILE_P_H
diff --git a/src/pdf/qpdflink.cpp b/src/pdf/qpdflink.cpp
new file mode 100644
index 000000000..0c2867086
--- /dev/null
+++ b/src/pdf/qpdflink.cpp
@@ -0,0 +1,189 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdflink.h"
+#include "qpdflink_p.h"
+#include "qpdflinkmodel_p.h"
+#include <QGuiApplication>
+#include <QDebug>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \class QPdfLink
+ \since 6.4
+ \inmodule QtPdf
+
+ \brief The QPdfLink class defines a link between a region on a page
+ (such as a hyperlink or a search result) and a destination
+ (page, location on the page, and zoom level at which to view it).
+*/
+
+/*!
+ Constructs an invalid Destination.
+
+ \sa valid
+*/
+QPdfLink::QPdfLink() :
+ QPdfLink(new QPdfLinkPrivate()) { }
+
+QPdfLink::QPdfLink(int page, QPointF location, qreal zoom)
+ : QPdfLink(new QPdfLinkPrivate(page, location, zoom))
+{
+}
+
+QPdfLink::QPdfLink(int page, QList<QRectF> rects,
+ QString contextBefore, QString contextAfter)
+ : QPdfLink(new QPdfLinkPrivate(page, std::move(rects),
+ std::move(contextBefore),
+ std::move(contextAfter)))
+{
+}
+
+QPdfLink::QPdfLink(QPdfLinkPrivate *d) : d(d) {}
+
+QPdfLink::~QPdfLink() = default;
+QPdfLink::QPdfLink(const QPdfLink &other) noexcept = default;
+QPdfLink::QPdfLink(QPdfLink &&other) noexcept = default;
+QPdfLink &QPdfLink::operator=(const QPdfLink &other) = default;
+
+/*!
+ \property QPdfLink::valid
+
+ This property holds whether the link is valid.
+*/
+bool QPdfLink::isValid() const
+{
+ return d->page >= 0;
+}
+
+/*!
+ \property QPdfLink::page
+
+ This property holds the page number.
+ If the link is a search result, it is the page number on which the result is found;
+ if the link is a hyperlink, it is the destination page number.
+*/
+int QPdfLink::page() const
+{
+ return d->page;
+}
+
+/*!
+ \property QPdfLink::location
+
+ This property holds the location on the \l page, in units of points.
+ If the link is a search result, it is the location where the result is found;
+ if the link is a hyperlink, it is the destination location.
+*/
+QPointF QPdfLink::location() const
+{
+ return d->location;
+}
+
+/*!
+ \property QPdfLink::zoom
+
+ This property holds the suggested magnification level, where 1.0 means default scale
+ (1 pixel = 1 point). If the link is a search result, this value is not used.
+*/
+qreal QPdfLink::zoom() const
+{
+ return d->zoom;
+}
+
+/*!
+ \property QPdfLink::url
+
+ This property holds the destination URL if the link is an external hyperlink;
+ otherwise, it's empty.
+*/
+QUrl QPdfLink::url() const
+{
+ return d->url;
+}
+
+/*!
+ \property QPdfLink::contextBefore
+
+ This property holds adjacent text found on the page before the search string.
+ If the link is a hyperlink, this string is empty.
+
+ \sa QPdfSearchModel::resultsOnPage(), QPdfSearchModel::resultAtIndex()
+*/
+QString QPdfLink::contextBefore() const
+{
+ return d->contextBefore;
+}
+
+/*!
+ \property QPdfLink::contextAfter
+
+ This property holds adjacent text found on the page after the search string.
+ If the link is a hyperlink, this string is empty.
+
+ \sa QPdfSearchModel::resultsOnPage(), QPdfSearchModel::resultAtIndex()
+*/
+QString QPdfLink::contextAfter() const
+{
+ return d->contextAfter;
+}
+
+/*!
+ \property QPdfLink::rectangles
+
+ This property holds the region (set of rectangles) occupied by the link or
+ search result on the page where it was found. If the text wraps around to
+ multiple lines on the page, there may be multiple rectangles:
+
+ \image wrapping-search-result.png
+
+ \sa QPdfSearchModel::resultsOnPage(), QPdfSearchModel::resultAtIndex()
+*/
+QList<QRectF> QPdfLink::rectangles() const
+{
+ return d->rects;
+}
+
+/*!
+ Returns a translated representation for display.
+
+ \sa copyToClipboard()
+*/
+QString QPdfLink::toString() const
+{
+ if (d->page <= 0)
+ return d->url.toString();
+ return QPdfLinkModel::tr("Page %1 location %2, %3 zoom %4")
+ .arg(d->page).arg(d->location.x(), 0, 'f', 1).arg(d->location.y(), 0, 'f', 1)
+ .arg(d->zoom, 0, 'f', 0);
+}
+
+/*!
+ Copies the toString() representation of the link to the
+ \l {QGuiApplication::clipboard()}{system clipboard} depending on the \a mode given.
+*/
+void QPdfLink::copyToClipboard(QClipboard::Mode mode) const
+{
+ QGuiApplication::clipboard()->setText(toString(), mode);
+}
+
+#ifndef QT_NO_DEBUG_STREAM
+QDebug operator<<(QDebug dbg, const QPdfLink &link)
+{
+ QDebugStateSaver saver(dbg);
+ dbg.nospace();
+ dbg << "QPdfLink(page=" << link.page()
+ << " location=" << link.location()
+ << " zoom=" << link.zoom()
+ << " contextBefore=" << link.contextBefore()
+ << " contextAfter=" << link.contextAfter()
+ << " rects=" << link.rectangles();
+ dbg << ')';
+ return dbg;
+}
+#endif
+
+QT_END_NAMESPACE
+
+#include "moc_qpdflink.cpp"
diff --git a/src/pdf/qpdflink.h b/src/pdf/qpdflink.h
new file mode 100644
index 000000000..63389afe6
--- /dev/null
+++ b/src/pdf/qpdflink.h
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFLINK_H
+#define QPDFLINK_H
+
+#include <QtPdf/qtpdfglobal.h>
+#include <QtCore/qlist.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qpoint.h>
+#include <QtCore/qrect.h>
+#include <QtCore/qshareddata.h>
+#include <QtGui/qclipboard.h>
+
+QT_BEGIN_NAMESPACE
+
+class QDebug;
+class QPdfLinkPrivate;
+
+class QPdfLink
+{
+ Q_GADGET_EXPORT(Q_PDF_EXPORT)
+ Q_PROPERTY(bool valid READ isValid)
+ Q_PROPERTY(int page READ page)
+ Q_PROPERTY(QPointF location READ location)
+ Q_PROPERTY(qreal zoom READ zoom)
+ Q_PROPERTY(QUrl url READ url)
+ Q_PROPERTY(QString contextBefore READ contextBefore)
+ Q_PROPERTY(QString contextAfter READ contextAfter)
+ Q_PROPERTY(QList<QRectF> rectangles READ rectangles)
+
+public:
+ Q_PDF_EXPORT QPdfLink();
+ Q_PDF_EXPORT ~QPdfLink();
+ Q_PDF_EXPORT QPdfLink &operator=(const QPdfLink &other);
+
+ Q_PDF_EXPORT QPdfLink(const QPdfLink &other) noexcept;
+ Q_PDF_EXPORT QPdfLink(QPdfLink &&other) noexcept;
+ QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(QPdfLink)
+
+ void swap(QPdfLink &other) noexcept { d.swap(other.d); }
+
+ Q_PDF_EXPORT bool isValid() const;
+ Q_PDF_EXPORT int page() const;
+ Q_PDF_EXPORT QPointF location() const;
+ Q_PDF_EXPORT qreal zoom() const;
+ Q_PDF_EXPORT QUrl url() const;
+ Q_PDF_EXPORT QString contextBefore() const;
+ Q_PDF_EXPORT QString contextAfter() const;
+ Q_PDF_EXPORT QList<QRectF> rectangles() const;
+ Q_PDF_EXPORT Q_INVOKABLE QString toString() const;
+ Q_PDF_EXPORT Q_INVOKABLE void copyToClipboard(QClipboard::Mode mode = QClipboard::Clipboard) const;
+
+private: // methods
+ QPdfLink(int page, QPointF location, qreal zoom);
+ QPdfLink(int page, QList<QRectF> rects, QString contextBefore, QString contextAfter);
+ QPdfLink(QPdfLinkPrivate *d);
+ friend class QPdfDocument;
+ friend class QPdfLinkModelPrivate;
+ friend class QPdfSearchModelPrivate;
+ friend class QPdfPageNavigator;
+ friend class QQuickPdfPageNavigator;
+
+private: // storage
+ QExplicitlySharedDataPointer<QPdfLinkPrivate> d;
+
+};
+Q_DECLARE_SHARED(QPdfLink)
+
+#ifndef QT_NO_DEBUG_STREAM
+Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfLink &);
+#endif
+
+QT_END_NAMESPACE
+
+Q_DECLARE_METATYPE(QPdfLink)
+
+#endif // QPDFLINK_H
diff --git a/src/pdf/qpdflink_p.h b/src/pdf/qpdflink_p.h
new file mode 100644
index 000000000..fa82f47c3
--- /dev/null
+++ b/src/pdf/qpdflink_p.h
@@ -0,0 +1,53 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFLINK_P_H
+#define QPDFLINK_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 "qpdflink.h"
+
+#include <QPointF>
+#include <QRectF>
+#include <QUrl>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfLinkPrivate : public QSharedData
+{
+public:
+ QPdfLinkPrivate() = default;
+ QPdfLinkPrivate(int page, QPointF location, qreal zoom)
+ : page(page),
+ location(location),
+ zoom(zoom) { }
+ QPdfLinkPrivate(int page, QList<QRectF> rects, QString contextBefore, QString contextAfter)
+ : page(page),
+ location(rects.first().topLeft()),
+ zoom(0),
+ contextBefore{std::move(contextBefore)},
+ contextAfter{std::move(contextAfter)},
+ rects{std::move(rects)} {}
+
+ int page = -1;
+ QPointF location;
+ qreal zoom = 1;
+ QString contextBefore;
+ QString contextAfter;
+ QUrl url;
+ QList<QRectF> rects;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFLINK_P_H
diff --git a/src/pdf/qpdflinkmodel.cpp b/src/pdf/qpdflinkmodel.cpp
new file mode 100644
index 000000000..0a8b1e812
--- /dev/null
+++ b/src/pdf/qpdflinkmodel.cpp
@@ -0,0 +1,338 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdflink_p.h"
+#include "qpdflinkmodel.h"
+#include "qpdflinkmodel_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")
+
+/*!
+ \class QPdfLinkModel
+ \since 6.6
+ \inmodule QtPdf
+ \inherits QAbstractListModel
+
+ \brief The QPdfLinkModel class holds the geometry and the destination for
+ each link that the specified \l page contains.
+
+ This is used in PDF viewers to implement the hyperlink mechanism.
+*/
+
+/*!
+ \enum QPdfLinkModel::Role
+
+ \value Link A QPdfLink object.
+ \value Rectangle 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 suggested zoom level on the destination page.
+ \omitvalue NRoles
+*/
+
+/*!
+ Constructs a new link model with parent object \a parent.
+*/
+QPdfLinkModel::QPdfLinkModel(QObject *parent)
+ : QAbstractListModel(parent),
+ d_ptr{std::make_unique<QPdfLinkModelPrivate>(this)}
+{
+ Q_D(QPdfLinkModel);
+ QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
+ for (int r = Qt::UserRole; r < int(Role::NRoles); ++r)
+ d->roleNames.insert(r, QByteArray(rolesMetaEnum.valueToKey(r)).toLower());
+}
+
+/*!
+ Destroys the model.
+*/
+QPdfLinkModel::~QPdfLinkModel() {}
+
+QHash<int, QByteArray> QPdfLinkModel::roleNames() const
+{
+ Q_D(const QPdfLinkModel);
+ return d->roleNames;
+}
+
+/*!
+ \reimp
+*/
+int QPdfLinkModel::rowCount(const QModelIndex &parent) const
+{
+ Q_D(const QPdfLinkModel);
+ Q_UNUSED(parent);
+ return d->links.size();
+}
+
+/*!
+ \reimp
+*/
+QVariant QPdfLinkModel::data(const QModelIndex &index, int role) const
+{
+ Q_D(const QPdfLinkModel);
+ const auto &link = d->links.at(index.row());
+ switch (Role(role)) {
+ case Role::Link:
+ return QVariant::fromValue(link);
+ case Role::Rectangle:
+ return link.rectangles().empty() ? QVariant() : link.rectangles().constFirst();
+ 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::NRoles:
+ break;
+ }
+ if (role == Qt::DisplayRole)
+ return link.toString();
+ return QVariant();
+}
+
+/*!
+ \property QPdfLinkModel::document
+ \brief The document to load links from.
+*/
+QPdfDocument *QPdfLinkModel::document() const
+{
+ Q_D(const QPdfLinkModel);
+ return d->document;
+}
+
+void QPdfLinkModel::setDocument(QPdfDocument *document)
+{
+ Q_D(QPdfLinkModel);
+ if (d->document == document)
+ return;
+ if (d->document)
+ 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();
+}
+
+/*!
+ \property QPdfLinkModel::page
+ \brief The page to load links from.
+*/
+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();
+}
+
+/*!
+ Returns a \l {QPdfLink::isValid()}{valid} link if found under the \a point
+ (given in units of points, 1/72 of an inch), or an invalid link if it is
+ not found. In other words, this function is useful for picking, to handle
+ mouse click or hover.
+*/
+QPdfLink QPdfLinkModel::linkAt(QPointF point) const
+{
+ Q_D(const QPdfLinkModel);
+ for (const auto &link : std::as_const(d->links)) {
+ for (const auto &rect : link.rectangles()) {
+ if (rect.contains(point))
+ return link;
+ }
+ }
+ return {};
+}
+
+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) {
+ qCWarning(qLcLink) << "failed to load page" << page;
+ return;
+ }
+ q->beginResetModel();
+ links.clear();
+
+ // Iterate the ordinary links
+ int linkStart = 0;
+ bool hasNext = true;
+ while (hasNext) {
+ FPDF_LINK linkAnnot;
+ hasNext = FPDFLink_Enumerate(pdfPage, &linkStart, &linkAnnot);
+ if (!hasNext)
+ break;
+ FS_RECTF rect;
+ bool ok = FPDFLink_GetAnnotRect(linkAnnot, &rect);
+ if (!ok) {
+ qCWarning(qLcLink) << "skipping link with invalid bounding box";
+ continue; // while enumerating links
+ }
+ // In case horizontal/vertical coordinates are flipped, swap them.
+ if (rect.right < rect.left)
+ std::swap(rect.right, rect.left);
+ if (rect.bottom > rect.top)
+ std::swap(rect.bottom, rect.top);
+
+ QPdfLink linkData;
+ // Use quad points if present; otherwise use the rect.
+ if (int quadPointsCount = FPDFLink_CountQuadPoints(linkAnnot) > 0) {
+ for (int i = 0; i < quadPointsCount; ++i) {
+ FS_QUADPOINTSF point;
+ if (FPDFLink_GetQuadPoints(linkAnnot, i, &point)) {
+ // Quadpoints are counter clockwise from bottom left (x1, y1)
+ QPolygonF poly;
+ poly << QPointF(point.x1, point.y1);
+ poly << QPointF(point.x2, point.y2);
+ poly << QPointF(point.x3, point.y3);
+ poly << QPointF(point.x4, point.y4);
+ QRectF bounds = poly.boundingRect();
+ bounds = document->d->mapPageToView(pdfPage, bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
+ qCDebug(qLcLink) << "quadpoints" << i << "of" << quadPointsCount << ":" << poly << "mapped bounds" << bounds;
+ linkData.d->rects << bounds;
+ // QPdfLink could store polygons rather than rects, to get the benefit of quadpoints;
+ // so far we didn't bother. It would be an API change, and we'd need to use Shapes in PdfLinkDelegate.qml
+ }
+ }
+ } else {
+ linkData.d->rects << document->d->mapPageToView(pdfPage, rect.left, rect.top, rect.right, rect.bottom);
+ }
+ FPDF_DEST dest = FPDFLink_GetDest(doc, linkAnnot);
+ FPDF_ACTION action = FPDFLink_GetAction(linkAnnot);
+ switch (FPDFAction_GetType(action)) {
+ case PDFACTION_UNSUPPORTED: // this happens with valid links in some PDFs
+ case PDFACTION_GOTO: {
+ linkData.d->page = FPDFDest_GetDestPageIndex(doc, dest);
+ if (linkData.d->page < 0) {
+ qCWarning(qLcLink) << "skipping link with invalid page number" << linkData.d->page;
+ continue; // while enumerating links
+ }
+ FPDF_BOOL hasX, hasY, hasZoom;
+ FS_FLOAT x, y, zoom;
+ ok = FPDFDest_GetLocationInPage(dest, &hasX, &hasY, &hasZoom, &x, &y, &zoom);
+ if (!ok) {
+ qCWarning(qLcLink) << "link with invalid location and/or zoom @" << linkData.d->rects;
+ break; // at least we got a page number, so the link will jump there
+ }
+ if (hasX && hasY)
+ linkData.d->location = document->d->mapPageToView(pdfPage, x, y);
+ if (hasZoom)
+ linkData.d->zoom = zoom;
+ break;
+ }
+ case PDFACTION_URI: {
+ unsigned long len = FPDFAction_GetURIPath(doc, action, nullptr, 0);
+ if (len < 1) {
+ qCWarning(qLcLink) << "skipping link with empty URI @" << linkData.d->rects;
+ continue; // while enumerating links
+ } else {
+ QByteArray buf(len, 0);
+ unsigned long got = FPDFAction_GetURIPath(doc, action, buf.data(), len);
+ Q_ASSERT(got == len);
+ linkData.d->url = QString::fromLatin1(buf.data(), got - 1);
+ }
+ break;
+ }
+ case PDFACTION_LAUNCH:
+ case PDFACTION_REMOTEGOTO: {
+ unsigned long len = FPDFAction_GetFilePath(action, nullptr, 0);
+ if (len < 1) {
+ qCWarning(qLcLink) << "skipping link with empty file path @" << linkData.d->rects;
+ continue; // while enumerating links
+ } else {
+ QByteArray buf(len, 0);
+ unsigned long got = FPDFAction_GetFilePath(action, buf.data(), len);
+ Q_ASSERT(got == len);
+ linkData.d->url = QUrl::fromLocalFile(QString::fromLatin1(buf.data(), got - 1)).toString();
+
+ // Unfortunately, according to comments in fpdf_doc.h, if it's PDFACTION_REMOTEGOTO,
+ // we can't get the page and location without first opening the linked document
+ // and then calling FPDFAction_GetDest() again.
+ }
+ break;
+ }
+ }
+ 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) {
+ QPdfLink linkData;
+ int len = FPDFLink_GetURL(webLinks, i, nullptr, 0);
+ if (len < 1) {
+ qCWarning(qLcLink) << "skipping link" << i << "with empty URL";
+ } else {
+ QList<unsigned short> buf(len);
+ int got = FPDFLink_GetURL(webLinks, i, buf.data(), len);
+ Q_ASSERT(got == len);
+ linkData.d->url = QString::fromUtf16(
+ reinterpret_cast<const char16_t *>(buf.data()), got - 1);
+ }
+ 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.d->rects << document->d->mapPageToView(pdfPage, left, top, right, bottom);
+ links << linkData;
+ }
+ }
+ }
+ FPDFLink_CloseWebLinks(webLinks);
+ }
+ FPDFText_ClosePage(textPage);
+ }
+
+ // All done
+ FPDF_ClosePage(pdfPage);
+ if (Q_UNLIKELY(qLcLink().isDebugEnabled())) {
+ for (const auto &l : links)
+ qCDebug(qLcLink) << l;
+ }
+ q->endResetModel();
+}
+
+void QPdfLinkModel::onStatusChanged(QPdfDocument::Status status)
+{
+ Q_D(QPdfLinkModel);
+ qCDebug(qLcLink) << "sees document statusChanged" << status;
+ if (status == QPdfDocument::Status::Ready)
+ d->update();
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qpdflinkmodel_p.cpp"
diff --git a/src/pdf/qpdflinkmodel.h b/src/pdf/qpdflinkmodel.h
new file mode 100644
index 000000000..be2ce890c
--- /dev/null
+++ b/src/pdf/qpdflinkmodel.h
@@ -0,0 +1,67 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFLINKMODEL_H
+#define QPDFLINKMODEL_H
+
+#include <QtPdf/qtpdfglobal.h>
+#include <QtPdf/qpdfdocument.h>
+#include <QtPdf/qpdflink.h>
+
+#include <QtCore/QAbstractListModel>
+
+#include <memory>
+
+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 {
+ Link = Qt::UserRole,
+ Rectangle,
+ Url,
+ Page,
+ Location,
+ Zoom,
+ NRoles
+ };
+ Q_ENUM(Role)
+ explicit QPdfLinkModel(QObject *parent = nullptr);
+ ~QPdfLinkModel() override;
+
+ 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;
+
+ QPdfLink linkAt(QPointF point) 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:
+ Q_DECLARE_PRIVATE(QPdfLinkModel)
+ const std::unique_ptr<QPdfLinkModelPrivate> d_ptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFLINKMODEL_H
diff --git a/src/pdf/qpdflinkmodel_p.h b/src/pdf/qpdflinkmodel_p.h
new file mode 100644
index 000000000..ba46a6e00
--- /dev/null
+++ b/src/pdf/qpdflinkmodel_p.h
@@ -0,0 +1,42 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#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 "qpdflinkmodel.h"
+#include <private/qabstractitemmodel_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfLinkModelPrivate
+{
+ QPdfLinkModel *q_ptr;
+ Q_DECLARE_PUBLIC(QPdfLinkModel)
+
+public:
+ explicit QPdfLinkModelPrivate(QPdfLinkModel *qq)
+ : q_ptr(qq) {}
+
+ void update();
+
+ QHash<int, QByteArray> roleNames;
+ QPdfDocument *document = nullptr;
+ QList<QPdfLink> links;
+ int page = 0;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFLINKMODEL_P_H
diff --git a/src/pdf/qpdfpagenavigator.cpp b/src/pdf/qpdfpagenavigator.cpp
new file mode 100644
index 000000000..e077e2184
--- /dev/null
+++ b/src/pdf/qpdfpagenavigator.cpp
@@ -0,0 +1,362 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfpagenavigator.h"
+#include "qpdfdocument.h"
+#include "qpdflink_p.h"
+
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qpointer.h>
+
+QT_BEGIN_NAMESPACE
+
+Q_LOGGING_CATEGORY(qLcNav, "qt.pdf.pagenavigator")
+
+struct QPdfPageNavigatorPrivate
+{
+ QPdfPageNavigator *q = nullptr;
+
+ QList<QExplicitlySharedDataPointer<QPdfLinkPrivate>> pageHistory;
+ int currentHistoryIndex = 0;
+ bool changing = false;
+};
+
+/*!
+ \class QPdfPageNavigator
+ \since 6.4
+ \inmodule QtPdf
+ \brief Navigation history within a PDF document.
+
+ The QPdfPageNavigator class remembers which destinations the user
+ has visited in a PDF document, and provides the ability to traverse
+ backward and forward. It is used to implement back and forward actions
+ similar to the back and forward buttons in a web browser.
+
+ \sa QPdfDocument
+*/
+
+/*!
+ Constructs a page navigation stack with parent object \a parent.
+*/
+QPdfPageNavigator::QPdfPageNavigator(QObject *parent)
+ : QObject(parent), d(new QPdfPageNavigatorPrivate)
+{
+ d->q = this;
+ clear();
+}
+
+/*!
+ Destroys the page navigation stack.
+*/
+QPdfPageNavigator::~QPdfPageNavigator()
+{
+}
+
+/*!
+ 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 QPdfPageNavigator::forward()
+{
+ if (d->currentHistoryIndex >= d->pageHistory.size() - 1)
+ return;
+ const bool backAvailableWas = backAvailable();
+ const bool forwardAvailableWas = forwardAvailable();
+ QPointF currentLocationWas = currentLocation();
+ qreal currentZoomWas = currentZoom();
+ ++d->currentHistoryIndex;
+ d->changing = true;
+ emit jumped(currentLink());
+ if (currentZoomWas != currentZoom())
+ emit currentZoomChanged(currentZoom());
+ emit currentPageChanged(currentPage());
+ if (currentLocationWas != currentLocation())
+ emit currentLocationChanged(currentLocation());
+ if (!backAvailableWas)
+ emit backAvailableChanged(backAvailable());
+ if (forwardAvailableWas != forwardAvailable())
+ emit forwardAvailableChanged(forwardAvailable());
+ d->changing = false;
+ qCDebug(qLcNav) << "forward: index" << d->currentHistoryIndex << "page" << currentPage()
+ << "@" << currentLocation() << "zoom" << currentZoom();
+}
+
+/*!
+ 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 QPdfPageNavigator::back()
+{
+ if (d->currentHistoryIndex <= 0)
+ return;
+ const bool backAvailableWas = backAvailable();
+ const bool forwardAvailableWas = forwardAvailable();
+ QPointF currentLocationWas = currentLocation();
+ qreal currentZoomWas = currentZoom();
+ --d->currentHistoryIndex;
+ d->changing = true;
+ emit jumped(currentLink());
+ if (currentZoomWas != currentZoom())
+ emit currentZoomChanged(currentZoom());
+ emit currentPageChanged(currentPage());
+ if (currentLocationWas != currentLocation())
+ emit currentLocationChanged(currentLocation());
+ if (backAvailableWas != backAvailable())
+ emit backAvailableChanged(backAvailable());
+ if (!forwardAvailableWas)
+ emit forwardAvailableChanged(forwardAvailable());
+ d->changing = false;
+ qCDebug(qLcNav) << "back: index" << d->currentHistoryIndex << "page" << currentPage()
+ << "@" << currentLocation() << "zoom" << currentZoom();
+}
+/*!
+ \property QPdfPageNavigator::currentPage
+
+ This property holds the current page that is being viewed.
+ The default is \c 0.
+*/
+int QPdfPageNavigator::currentPage() const
+{
+ if (d->currentHistoryIndex < 0 || d->currentHistoryIndex >= d->pageHistory.size())
+ return -1; // only until ctor or clear() runs
+ return d->pageHistory.at(d->currentHistoryIndex)->page;
+}
+
+/*!
+ \property QPdfPageNavigator::currentLocation
+
+ This property holds the current location on the page that is being viewed
+ (the location that was last given to jump() or update()). The default is
+ \c {0, 0}.
+*/
+QPointF QPdfPageNavigator::currentLocation() const
+{
+ if (d->currentHistoryIndex < 0 || d->currentHistoryIndex >= d->pageHistory.size())
+ return QPointF();
+ return d->pageHistory.at(d->currentHistoryIndex)->location;
+}
+
+/*!
+ \property QPdfPageNavigator::currentZoom
+
+ This property holds the magnification scale (1 logical pixel = 1 point)
+ on the page that is being viewed. The default is \c 1.
+*/
+qreal QPdfPageNavigator::currentZoom() const
+{
+ if (d->currentHistoryIndex < 0 || d->currentHistoryIndex >= d->pageHistory.size())
+ return 1;
+ return d->pageHistory.at(d->currentHistoryIndex)->zoom;
+}
+
+QPdfLink QPdfPageNavigator::currentLink() const
+{
+ if (d->currentHistoryIndex < 0 || d->currentHistoryIndex >= d->pageHistory.size())
+ return QPdfLink();
+ return QPdfLink(d->pageHistory.at(d->currentHistoryIndex).data());
+}
+
+/*!
+ Clear the history and restore \l currentPage, \l currentLocation and
+ \l currentZoom to their default values.
+*/
+void QPdfPageNavigator::clear()
+{
+ d->pageHistory.clear();
+ d->currentHistoryIndex = 0;
+ // Begin with an implicit jump to page 0, so that
+ // backAvailable() will become true after jump() is called one more time.
+ d->pageHistory.append(QExplicitlySharedDataPointer<QPdfLinkPrivate>(new QPdfLinkPrivate(0, {}, 1)));
+}
+
+/*!
+ Adds the given \a destination to the history of visited locations.
+
+ In this case, PDF views respond to the \l jumped signal by scrolling to
+ place \c destination.rectangles in the viewport, as opposed to placing
+ \c destination.location in the viewport. So it's appropriate to call this
+ method to jump to a search result from QPdfSearchModel (because the
+ rectangles cover the region of text found). To jump to a hyperlink
+ destination, call jump(page, location, zoom) instead, because in that
+ case the QPdfLink object's \c rectangles cover the hyperlink origin
+ location rather than the destination.
+*/
+void QPdfPageNavigator::jump(QPdfLink destination)
+{
+ const bool zoomChange = !qFuzzyCompare(destination.zoom(), currentZoom());
+ const bool pageChange = (destination.page() != currentPage());
+ const bool locationChange = (destination.location() != currentLocation());
+ const bool backAvailableWas = backAvailable();
+ const bool forwardAvailableWas = forwardAvailable();
+ if (!d->changing) {
+ if (d->currentHistoryIndex >= 0 && forwardAvailableWas)
+ d->pageHistory.remove(d->currentHistoryIndex + 1, d->pageHistory.size() - d->currentHistoryIndex - 1);
+ d->pageHistory.append(destination.d);
+ d->currentHistoryIndex = d->pageHistory.size() - 1;
+ }
+ if (zoomChange)
+ emit currentZoomChanged(currentZoom());
+ if (pageChange)
+ emit currentPageChanged(currentPage());
+ if (locationChange)
+ emit currentLocationChanged(currentLocation());
+ if (d->changing)
+ return;
+ if (backAvailableWas != backAvailable())
+ emit backAvailableChanged(backAvailable());
+ if (forwardAvailableWas != forwardAvailable())
+ emit forwardAvailableChanged(forwardAvailable());
+ emit jumped(currentLink());
+ qCDebug(qLcNav) << "push: index" << d->currentHistoryIndex << destination << "-> history" <<
+ [this]() {
+ QStringList ret;
+ for (auto d : d->pageHistory)
+ ret << QString::number(d->page);
+ return ret.join(QLatin1Char(','));
+ }();
+}
+
+/*!
+ Adds the given destination, consisting of \a page, \a location, and \a zoom,
+ to the history of visited locations.
+
+ The \a zoom argument represents magnification (where \c 1 is the default
+ scale, 1 logical pixel = 1 point). If \a zoom is not given or is \c 0,
+ currentZoom keeps its existing value, and currentZoomChanged is not emitted.
+
+ The \a location should be the same as QPdfLink::location() if the user is
+ following a link; and since that is specified as the upper-left corner of
+ the destination, it is best for consistency to always use the location
+ visible in the upper-left corner of the viewport, in points.
+
+ 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 QPdfPageNavigator::jump(int page, const QPointF &location, qreal zoom)
+{
+ if (page == currentPage() && location == currentLocation() && zoom == currentZoom())
+ return;
+ if (qFuzzyIsNull(zoom))
+ zoom = currentZoom();
+ const bool zoomChange = !qFuzzyCompare(zoom, currentZoom());
+ const bool pageChange = (page != currentPage());
+ const bool locationChange = (location != currentLocation());
+ const bool backAvailableWas = backAvailable();
+ const bool forwardAvailableWas = forwardAvailable();
+ if (!d->changing) {
+ if (d->currentHistoryIndex >= 0 && forwardAvailableWas)
+ d->pageHistory.remove(d->currentHistoryIndex + 1, d->pageHistory.size() - d->currentHistoryIndex - 1);
+ d->pageHistory.append(QExplicitlySharedDataPointer<QPdfLinkPrivate>(new QPdfLinkPrivate(page, location, zoom)));
+ d->currentHistoryIndex = d->pageHistory.size() - 1;
+ }
+ if (zoomChange)
+ emit currentZoomChanged(currentZoom());
+ if (pageChange)
+ emit currentPageChanged(currentPage());
+ if (locationChange)
+ emit currentLocationChanged(currentLocation());
+ if (d->changing)
+ return;
+ if (backAvailableWas != backAvailable())
+ emit backAvailableChanged(backAvailable());
+ if (forwardAvailableWas != forwardAvailable())
+ emit forwardAvailableChanged(forwardAvailable());
+ emit jumped(currentLink());
+ qCDebug(qLcNav) << "push: index" << d->currentHistoryIndex << "page" << page
+ << "@" << location << "zoom" << zoom << "-> history" <<
+ [this]() {
+ QStringList ret;
+ for (auto d : d->pageHistory)
+ ret << QString::number(d->page);
+ return ret.join(QLatin1Char(','));
+ }();
+}
+
+/*!
+ 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 QPdfPageNavigator::update(int page, const QPointF &location, qreal zoom)
+{
+ if (d->currentHistoryIndex < 0 || d->currentHistoryIndex >= d->pageHistory.size())
+ return;
+ int currentPageWas = currentPage();
+ QPointF currentLocationWas = currentLocation();
+ qreal currentZoomWas = currentZoom();
+ if (page == currentPageWas && location == currentLocationWas && zoom == currentZoomWas)
+ return;
+ d->pageHistory[d->currentHistoryIndex]->page = page;
+ d->pageHistory[d->currentHistoryIndex]->location = location;
+ d->pageHistory[d->currentHistoryIndex]->zoom = zoom;
+ if (currentZoomWas != zoom)
+ emit currentZoomChanged(currentZoom());
+ if (currentPageWas != page)
+ emit currentPageChanged(currentPage());
+ if (currentLocationWas != location)
+ emit currentLocationChanged(currentLocation());
+ qCDebug(qLcNav) << "update: index" << d->currentHistoryIndex << "page" << page
+ << "@" << location << "zoom" << zoom << "-> history" <<
+ [this]() {
+ QStringList ret;
+ for (auto d : d->pageHistory)
+ ret << QString::number(d->page);
+ return ret.join(QLatin1Char(','));
+ }();
+}
+
+/*!
+ \property QPdfPageNavigator::backAvailable
+ \readonly
+
+ Holds \c true if a \e back destination is available in the history:
+ that is, if push() or forward() has been called.
+*/
+bool QPdfPageNavigator::backAvailable() const
+{
+ return d->currentHistoryIndex > 0;
+}
+
+/*!
+ \property QPdfPageNavigator::forwardAvailable
+ \readonly
+
+ Holds \c true if a \e forward destination is available in the history:
+ that is, if back() has been previously called.
+*/
+bool QPdfPageNavigator::forwardAvailable() const
+{
+ return d->currentHistoryIndex < d->pageHistory.size() - 1;
+}
+
+/*!
+ \fn void QPdfPageNavigator::jumped(QPdfLink current)
+
+ This signal is emitted when an abrupt jump occurs, to the \a current
+ page index, location on the page, and zoom level; but \e not when simply
+ scrolling through the document one page at a time. That is, jump(),
+ forward() and back() emit this signal, but update() does not.
+
+ If \c {current.rectangles.length > 0}, they are rectangles that cover
+ a specific destination area: a search result that should be made
+ visible; otherwise, \c {current.location} is the destination location on
+ the \c page (a hyperlink destination, or during forward/back navigation).
+*/
+
+QT_END_NAMESPACE
+
+#include "moc_qpdfpagenavigator.cpp"
diff --git a/src/pdf/qpdfpagenavigator.h b/src/pdf/qpdfpagenavigator.h
new file mode 100644
index 000000000..cec89ef5a
--- /dev/null
+++ b/src/pdf/qpdfpagenavigator.h
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFPAGENAVIGATOR_H
+#define QPDFPAGENAVIGATOR_H
+
+#include <QtPdf/qtpdfglobal.h>
+#include <QtPdf/qpdflink.h>
+#include <QtCore/qobject.h>
+
+QT_BEGIN_NAMESPACE
+
+struct QPdfPageNavigatorPrivate;
+
+class Q_PDF_EXPORT QPdfPageNavigator : 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:
+ QPdfPageNavigator() : QPdfPageNavigator(nullptr) {}
+ explicit QPdfPageNavigator(QObject *parent);
+ ~QPdfPageNavigator() override;
+
+ int currentPage() const;
+ QPointF currentLocation() const;
+ qreal currentZoom() const;
+
+ bool backAvailable() const;
+ bool forwardAvailable() const;
+
+public Q_SLOTS:
+ void clear();
+ void jump(QPdfLink destination);
+ void jump(int page, const QPointF &location, qreal zoom = 0);
+ void update(int page, const QPointF &location, qreal zoom);
+ void forward();
+ void back();
+
+Q_SIGNALS:
+ void currentPageChanged(int page);
+ void currentLocationChanged(QPointF location);
+ void currentZoomChanged(qreal zoom);
+ void backAvailableChanged(bool available);
+ void forwardAvailableChanged(bool available);
+ void jumped(QPdfLink current);
+
+protected:
+ QPdfLink currentLink() const;
+
+private:
+ QScopedPointer<QPdfPageNavigatorPrivate> d;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFPAGENAVIGATOR_H
diff --git a/src/pdf/qpdfpagerenderer.cpp b/src/pdf/qpdfpagerenderer.cpp
new file mode 100644
index 000000000..771fc67ef
--- /dev/null
+++ b/src/pdf/qpdfpagerenderer.cpp
@@ -0,0 +1,310 @@
+// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfpagerenderer.h"
+
+#include <private/qobject_p.h>
+#include <QMutex>
+#include <QPointer>
+#include <QThread>
+
+QT_BEGIN_NAMESPACE
+
+class RenderWorker : public QObject
+{
+ Q_OBJECT
+
+public:
+ RenderWorker();
+ ~RenderWorker();
+
+ void setDocument(QPdfDocument *document);
+
+public Q_SLOTS:
+ void requestPage(quint64 requestId, int page, QSize imageSize,
+ QPdfDocumentRenderOptions options);
+
+Q_SIGNALS:
+ void pageRendered(int page, QSize imageSize, const QImage &image,
+ QPdfDocumentRenderOptions options, quint64 requestId);
+
+private:
+ QPointer<QPdfDocument> m_document;
+ QMutex m_mutex;
+};
+
+class QPdfPageRendererPrivate
+{
+public:
+ QPdfPageRendererPrivate();
+ ~QPdfPageRendererPrivate();
+
+ void handleNextRequest();
+ void requestFinished(int page, QSize imageSize, const QImage &image,
+ QPdfDocumentRenderOptions options, quint64 requestId);
+
+ QPdfPageRenderer::RenderMode m_renderMode = QPdfPageRenderer::RenderMode::SingleThreaded;
+ QPointer<QPdfDocument> m_document;
+
+ struct PageRequest
+ {
+ quint64 id;
+ int pageNumber;
+ QSize imageSize;
+ QPdfDocumentRenderOptions options;
+ };
+
+ QList<PageRequest> m_requests;
+ QList<PageRequest> m_pendingRequests;
+ quint64 m_requestIdCounter = 1;
+
+ QThread *m_renderThread = nullptr;
+ QScopedPointer<RenderWorker> m_renderWorker;
+};
+
+Q_DECLARE_TYPEINFO(QPdfPageRendererPrivate::PageRequest, Q_PRIMITIVE_TYPE);
+
+
+RenderWorker::RenderWorker()
+ : m_document(nullptr)
+{
+}
+
+RenderWorker::~RenderWorker()
+{
+}
+
+void RenderWorker::setDocument(QPdfDocument *document)
+{
+ const QMutexLocker locker(&m_mutex);
+
+ if (m_document == document)
+ return;
+
+ m_document = document;
+}
+
+void RenderWorker::requestPage(quint64 requestId, int pageNumber, QSize imageSize,
+ QPdfDocumentRenderOptions options)
+{
+ const QMutexLocker locker(&m_mutex);
+
+ if (!m_document || m_document->status() != QPdfDocument::Status::Ready)
+ return;
+
+ const QImage image = m_document->render(pageNumber, imageSize, options);
+
+ emit pageRendered(pageNumber, imageSize, image, options, requestId);
+}
+
+QPdfPageRendererPrivate::QPdfPageRendererPrivate() : m_renderWorker(new RenderWorker) { }
+
+QPdfPageRendererPrivate::~QPdfPageRendererPrivate()
+{
+ if (m_renderThread) {
+ m_renderThread->quit();
+ m_renderThread->wait();
+ }
+}
+
+void QPdfPageRendererPrivate::handleNextRequest()
+{
+ if (m_requests.isEmpty())
+ return;
+
+ const PageRequest request = m_requests.takeFirst();
+ m_pendingRequests.append(request);
+
+ QMetaObject::invokeMethod(m_renderWorker.data(), "requestPage", Qt::QueuedConnection,
+ Q_ARG(quint64, request.id), Q_ARG(int, request.pageNumber),
+ Q_ARG(QSize, request.imageSize), Q_ARG(QPdfDocumentRenderOptions,
+ request.options));
+}
+
+void QPdfPageRendererPrivate::requestFinished(int page, QSize imageSize, const QImage &image, QPdfDocumentRenderOptions options, quint64 requestId)
+{
+ Q_UNUSED(image);
+ Q_UNUSED(requestId);
+ const auto it = std::find_if(m_pendingRequests.begin(), m_pendingRequests.end(),
+ [page, imageSize, options](const PageRequest &request){ return request.pageNumber == page && request.imageSize == imageSize && request.options == options; });
+
+ if (it != m_pendingRequests.end())
+ m_pendingRequests.erase(it);
+}
+
+/*!
+ \class QPdfPageRenderer
+ \since 5.11
+ \inmodule QtPdf
+
+ \brief The QPdfPageRenderer class encapsulates the rendering of pages of a PDF document.
+
+ The QPdfPageRenderer contains a queue that collects all render requests that are invoked through
+ requestPage(). Depending on the configured RenderMode the QPdfPageRenderer processes this queue
+ in the main UI thread on next event loop invocation (\c RenderMode::SingleThreaded) or in a separate worker thread
+ (\c RenderMode::MultiThreaded) and emits the result through the pageRendered() signal for each request once
+ the rendering is done.
+
+ \sa QPdfDocument
+*/
+
+
+/*!
+ Constructs a page renderer object with parent object \a parent.
+*/
+QPdfPageRenderer::QPdfPageRenderer(QObject *parent)
+ : QObject(parent), d_ptr(new QPdfPageRendererPrivate)
+{
+ qRegisterMetaType<QPdfDocumentRenderOptions>();
+
+ connect(d_ptr->m_renderWorker.data(), &RenderWorker::pageRendered, this,
+ [this](int page, QSize imageSize, const QImage &image,
+ QPdfDocumentRenderOptions options, quint64 requestId) {
+ d_ptr->requestFinished(page, imageSize, image, options, requestId);
+ emit pageRendered(page, imageSize, image, options, requestId);
+ d_ptr->handleNextRequest();
+ });
+}
+
+/*!
+ Destroys the page renderer object.
+*/
+QPdfPageRenderer::~QPdfPageRenderer()
+{
+}
+
+/*!
+ \enum QPdfPageRenderer::RenderMode
+
+ This enum describes how the pages are rendered.
+
+ \value MultiThreaded All pages are rendered in a separate worker thread.
+ \value SingleThreaded All pages are rendered in the main UI thread (default).
+
+ \sa renderMode(), setRenderMode()
+*/
+
+/*!
+ \property QPdfPageRenderer::renderMode
+ \brief The mode the renderer uses to render the pages.
+
+ By default, this property is \c RenderMode::SingleThreaded.
+
+ \sa setRenderMode(), RenderMode
+*/
+
+/*!
+ Returns the mode of how the pages are rendered.
+
+ \sa RenderMode
+*/
+QPdfPageRenderer::RenderMode QPdfPageRenderer::renderMode() const
+{
+ return d_ptr->m_renderMode;
+}
+
+/*!
+ Sets the mode of how the pages are rendered to \a mode.
+
+ \sa RenderMode
+*/
+void QPdfPageRenderer::setRenderMode(RenderMode mode)
+{
+ if (d_ptr->m_renderMode == mode)
+ return;
+
+ d_ptr->m_renderMode = mode;
+ emit renderModeChanged(d_ptr->m_renderMode);
+
+ if (d_ptr->m_renderMode == RenderMode::MultiThreaded) {
+ d_ptr->m_renderThread = new QThread;
+ d_ptr->m_renderWorker->moveToThread(d_ptr->m_renderThread);
+ d_ptr->m_renderThread->start();
+ } else {
+ d_ptr->m_renderThread->quit();
+ d_ptr->m_renderThread->wait();
+ delete d_ptr->m_renderThread;
+ d_ptr->m_renderThread = nullptr;
+
+ // pulling the object from another thread should be fine, once that thread is deleted
+ d_ptr->m_renderWorker->moveToThread(this->thread());
+ }
+}
+
+/*!
+ \property QPdfPageRenderer::document
+ \brief The document instance this object renders the pages from.
+
+ By default, this property is \c nullptr.
+
+ \sa document(), setDocument(), QPdfDocument
+*/
+
+/*!
+ Returns the document this objects renders the pages from, or a \c nullptr
+ if none has been set before.
+
+ \sa QPdfDocument
+*/
+QPdfDocument* QPdfPageRenderer::document() const
+{
+ return d_ptr->m_document;
+}
+
+/*!
+ Sets the \a document this object renders the pages from.
+
+ \sa QPdfDocument
+*/
+void QPdfPageRenderer::setDocument(QPdfDocument *document)
+{
+ if (d_ptr->m_document == document)
+ return;
+
+ d_ptr->m_document = document;
+ emit documentChanged(d_ptr->m_document);
+
+ d_ptr->m_renderWorker->setDocument(d_ptr->m_document);
+}
+
+/*!
+ Requests the renderer to render the page \a pageNumber into a QImage of size \a imageSize
+ according to the provided \a options.
+
+ Once the rendering is done the pageRendered() signal is emitted with the result as parameters.
+
+ The return value is an ID that uniquely identifies the render request. If a request with the
+ same parameters is still in the queue, the ID of that queued request is returned.
+*/
+quint64 QPdfPageRenderer::requestPage(int pageNumber, QSize imageSize,
+ QPdfDocumentRenderOptions options)
+{
+ if (!d_ptr->m_document || d_ptr->m_document->status() != QPdfDocument::Status::Ready)
+ return 0;
+
+ for (const auto &request : std::as_const(d_ptr->m_pendingRequests)) {
+ if (request.pageNumber == pageNumber
+ && request.imageSize == imageSize
+ && request.options == options)
+ return request.id;
+ }
+
+ const auto id = d_ptr->m_requestIdCounter++;
+
+ QPdfPageRendererPrivate::PageRequest request;
+ request.id = id;
+ request.pageNumber = pageNumber;
+ request.imageSize = imageSize;
+ request.options = options;
+
+ d_ptr->m_requests.append(request);
+
+ d_ptr->handleNextRequest();
+
+ return id;
+}
+
+QT_END_NAMESPACE
+
+#include "qpdfpagerenderer.moc"
+#include "moc_qpdfpagerenderer.cpp"
diff --git a/src/pdf/qpdfpagerenderer.h b/src/pdf/qpdfpagerenderer.h
new file mode 100644
index 000000000..cb9be06fe
--- /dev/null
+++ b/src/pdf/qpdfpagerenderer.h
@@ -0,0 +1,60 @@
+// Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Tobias König <tobias.koenig@kdab.com>
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFPAGERENDERER_H
+#define QPDFPAGERENDERER_H
+
+#include <QtPdf/qtpdfglobal.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/qsize.h>
+#include <QtPdf/qpdfdocument.h>
+#include <QtPdf/qpdfdocumentrenderoptions.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfPageRendererPrivate;
+
+class Q_PDF_EXPORT QPdfPageRenderer : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QPdfDocument* document READ document WRITE setDocument NOTIFY documentChanged)
+ Q_PROPERTY(RenderMode renderMode READ renderMode WRITE setRenderMode NOTIFY renderModeChanged)
+
+public:
+ enum class RenderMode
+ {
+ MultiThreaded,
+ SingleThreaded
+ };
+ Q_ENUM(RenderMode)
+
+ QPdfPageRenderer() : QPdfPageRenderer(nullptr) {}
+ explicit QPdfPageRenderer(QObject *parent);
+ ~QPdfPageRenderer() override;
+
+ RenderMode renderMode() const;
+ void setRenderMode(RenderMode mode);
+
+ QPdfDocument* document() const;
+ void setDocument(QPdfDocument *document);
+
+ quint64 requestPage(int pageNumber, QSize imageSize,
+ QPdfDocumentRenderOptions options = QPdfDocumentRenderOptions());
+
+Q_SIGNALS:
+ void documentChanged(QPdfDocument *document);
+ void renderModeChanged(QPdfPageRenderer::RenderMode renderMode);
+
+ void pageRendered(int pageNumber, QSize imageSize, const QImage &image,
+ QPdfDocumentRenderOptions options, quint64 requestId);
+
+private:
+ QScopedPointer<QPdfPageRendererPrivate> d_ptr;
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp
new file mode 100644
index 000000000..a81ae77dc
--- /dev/null
+++ b/src/pdf/qpdfsearchmodel.cpp
@@ -0,0 +1,373 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qpdfdocument_p.h"
+#include "qpdflink.h"
+#include "qpdfsearchmodel.h"
+#include "qpdfsearchmodel_p.h"
+
+#include "third_party/pdfium/public/fpdf_text.h"
+#include "third_party/pdfium/public/fpdfview.h"
+
+#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 = 64;
+
+/*!
+ \class QPdfSearchModel
+ \since 5.15
+ \inmodule QtPdf
+ \inherits QAbstractListModel
+
+ \brief The QPdfSearchModel class searches for a string in a PDF document
+ and holds the results.
+
+ This is used in the \l {Model/View Programming} paradigm to display
+ a list of search results, to highlight them on the rendered PDF pages,
+ and to iterate through them using the "search forward" / "search backward"
+ buttons and shortcuts that would be found in a typical document-viewing UI:
+
+ \image search-results.png
+*/
+
+/*!
+ \enum QPdfSearchModel::Role
+
+ \value Page The page number where the search result is found (int).
+ \value IndexOnPage The index of the search result on the page (int).
+ \value Location The position of the search result on the page (QPointF).
+ \value ContextBefore The adjacent text on the page, before the search string (QString).
+ \value ContextAfter The adjacent text on the page, after the search string (QString).
+ \omitvalue NRoles
+
+ \sa QPdfLink
+*/
+
+/*!
+ Constructs a new search model with parent object \a parent.
+*/
+QPdfSearchModel::QPdfSearchModel(QObject *parent)
+ : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent)
+{
+ QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
+ for (int r = Qt::UserRole; r < int(Role::NRoles); ++r) {
+ QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r));
+ if (roleName.isEmpty())
+ continue;
+ roleName[0] = QChar::toLower(roleName[0]);
+ m_roleNames.insert(r, roleName);
+ }
+ connect(this, &QAbstractListModel::dataChanged, this, &QPdfSearchModel::countChanged);
+ connect(this, &QAbstractListModel::modelReset, this, &QPdfSearchModel::countChanged);
+ connect(this, &QAbstractListModel::rowsRemoved, this, &QPdfSearchModel::countChanged);
+ connect(this, &QAbstractListModel::rowsInserted, this, &QPdfSearchModel::countChanged);
+}
+
+/*!
+ Destroys the model.
+*/
+QPdfSearchModel::~QPdfSearchModel() {}
+
+/*!
+ \reimp
+*/
+QHash<int, QByteArray> QPdfSearchModel::roleNames() const
+{
+ return m_roleNames;
+}
+
+/*!
+ \reimp
+
+ The number of rows in the model is equal to the number of search results found.
+*/
+int QPdfSearchModel::rowCount(const QModelIndex &parent) const
+{
+ Q_D(const QPdfSearchModel);
+ Q_UNUSED(parent);
+ return d->rowCountSoFar;
+}
+
+/*!
+ \reimp
+*/
+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::ContextBefore:
+ return d->searchResults[pi.page][pi.index].contextBefore();
+ case Role::ContextAfter:
+ return d->searchResults[pi.page][pi.index].contextAfter();
+ case Role::NRoles:
+ break;
+ }
+ if (role == Qt::DisplayRole) {
+ const QString ret = d->searchResults[pi.page][pi.index].contextBefore() +
+ QLatin1String("<b>") + d->searchString + QLatin1String("</b>") +
+ d->searchResults[pi.page][pi.index].contextAfter();
+ return ret;
+ }
+ return QVariant();
+}
+
+/*!
+ \since 6.8
+ \property QPdfSearchModel::count
+ \brief the number of search results found
+*/
+int QPdfSearchModel::count() const
+{
+ return rowCount(QModelIndex());
+}
+
+void QPdfSearchModel::updatePage(int page)
+{
+ Q_D(QPdfSearchModel);
+ d->doSearch(page);
+}
+
+/*!
+ \property QPdfSearchModel::searchString
+ \brief the string to search for
+*/
+QString QPdfSearchModel::searchString() const
+{
+ Q_D(const QPdfSearchModel);
+ return d->searchString;
+}
+
+void QPdfSearchModel::setSearchString(const QString &searchString)
+{
+ Q_D(QPdfSearchModel);
+ if (d->searchString == searchString)
+ return;
+
+ d->searchString = searchString;
+ beginResetModel();
+ d->clearResults();
+ emit searchStringChanged();
+ endResetModel();
+}
+
+/*!
+ Returns the list of all results found on the given \a page.
+*/
+QList<QPdfLink> QPdfSearchModel::resultsOnPage(int page) const
+{
+ Q_D(const QPdfSearchModel);
+ const_cast<QPdfSearchModelPrivate *>(d)->doSearch(page);
+ if (d->searchResults.size() <= page)
+ return {};
+ return d->searchResults[page];
+}
+
+/*!
+ Returns a result found by \a index in the \l document, regardless of the
+ page on which it was found. \a index must be less than \l rowCount.
+*/
+QPdfLink QPdfSearchModel::resultAtIndex(int index) const
+{
+ Q_D(const QPdfSearchModel);
+ const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index);
+ if (pi.page < 0 || index < 0)
+ return {};
+ return d->searchResults[pi.page][pi.index];
+}
+
+/*!
+ \property QPdfSearchModel::document
+ \brief the document to search
+*/
+QPdfDocument *QPdfSearchModel::document() const
+{
+ Q_D(const QPdfSearchModel);
+ return d->document;
+}
+
+void QPdfSearchModel::setDocument(QPdfDocument *document)
+{
+ Q_D(QPdfSearchModel);
+ if (d->document == document)
+ return;
+
+ disconnect(d->documentConnection);
+ d->documentConnection = connect(document, &QPdfDocument::pageCountChanged, this,
+ [this]() { d_func()->clearResults(); });
+
+ d->document = document;
+ d->clearResults();
+ emit documentChanged();
+}
+
+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->searchResults.size() << "pages";
+ killTimer(d->updateTimerId);
+ d->updateTimerId = -1;
+ }
+ d->doSearch(d->nextPageToUpdate++);
+}
+
+QPdfSearchModelPrivate::QPdfSearchModelPrivate() : QAbstractItemModelPrivate()
+{
+}
+
+void QPdfSearchModelPrivate::clearResults()
+{
+ Q_Q(QPdfSearchModel);
+ rowCountSoFar = 0;
+ searchResults.clear();
+ pagesSearched.clear();
+ if (document) {
+ searchResults.resize(document->pageCount());
+ pagesSearched.resize(document->pageCount());
+ }
+ nextPageToUpdate = 0;
+ updateTimerId = q->startTimer(UpdateTimerInterval);
+}
+
+bool QPdfSearchModelPrivate::doSearch(int page)
+{
+ if (page < 0 || page >= pagesSearched.size() || searchString.isEmpty())
+ return false;
+ if (pagesSearched[page])
+ return true;
+ Q_Q(QPdfSearchModel);
+
+ const QPdfMutexLocker lock;
+ QElapsedTimer timer;
+ timer.start();
+ FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page);
+ if (!pdfPage) {
+ qWarning() << "failed to load page" << page;
+ return false;
+ }
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ if (!textPage) {
+ qWarning() << "failed to load text of page" << page;
+ FPDF_ClosePage(pdfPage);
+ return false;
+ }
+ FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0);
+ QList<QPdfLink> newSearchResults;
+ constexpr double CharacterHitTolerance = 6.0;
+ while (FPDFText_FindNext(sh)) {
+ int idx = FPDFText_GetSchResultIndex(sh);
+ int count = FPDFText_GetSchCount(sh);
+ int rectCount = FPDFText_CountRects(textPage, idx, count);
+ QList<QRectF> rects;
+ int startIndex = -1;
+ int endIndex = -1;
+ for (int r = 0; r < rectCount; ++r) {
+ // get bounding box of search result in page coordinates
+ double left, top, right, bottom;
+ FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom);
+ // deal with any internal PDF transforms and
+ // convert to the 1x (pixels = points) 4th-quadrant coordinate system
+ rects << document->d->mapPageToView(pdfPage, left, top, right, 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
+ << "from page rect" << left << top << right << bottom;
+ }
+ QString contextBefore, contextAfter;
+ if (startIndex >= 0 || endIndex >= 0) {
+ startIndex = qMax(0, startIndex - ContextChars);
+ endIndex += ContextChars;
+ int count = endIndex - startIndex + 1;
+ if (count > 0) {
+ QList<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
+ QString context = QString::fromUtf16(
+ reinterpret_cast<const char16_t *>(buf.constData()), len - 1);
+ context = context.replace(QLatin1Char('\n'), QStringLiteral("\u23CE"));
+ context = context.remove(QLatin1Char('\r'));
+ // try to find the search string near the middle of the context if possible
+ int si = context.indexOf(searchString, ContextChars - 5, Qt::CaseInsensitive);
+ if (si < 0)
+ si = context.indexOf(searchString, Qt::CaseInsensitive);
+ if (si < 0)
+ qWarning() << "search string" << searchString << "not found in context" << context;
+ contextBefore = context.mid(0, si);
+ contextAfter = context.mid(si + searchString.size());
+ }
+ }
+ if (!rects.isEmpty())
+ newSearchResults << QPdfLink(page, rects, contextBefore, contextAfter);
+ }
+ FPDFText_FindClose(sh);
+ FPDFText_ClosePage(textPage);
+ FPDF_ClosePage(pdfPage);
+ qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find"
+ << newSearchResults.size() << "results on page" << page;
+
+ pagesSearched[page] = true;
+ searchResults[page] = newSearchResults;
+ if (newSearchResults.size() > 0) {
+ int rowsBefore = rowsBeforePage(page);
+ qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.size();
+ rowCountSoFar += newSearchResults.size();
+ q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.size() - 1);
+ q->endInsertRows();
+ }
+ return true;
+}
+
+QPdfSearchModelPrivate::PageAndIndex QPdfSearchModelPrivate::pageAndIndexForResult(int resultIndex)
+{
+ if (pagesSearched.isEmpty())
+ return {-1, -1};
+ 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].size();
+ if (totalSoFar > resultIndex)
+ return {page, resultIndex - previousTotalSoFar};
+ previousTotalSoFar = totalSoFar;
+ }
+ return {-1, -1};
+}
+
+int QPdfSearchModelPrivate::rowsBeforePage(int page)
+{
+ int ret = 0;
+ for (int i = 0; i < page; ++i)
+ ret += searchResults[i].size();
+ return ret;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qpdfsearchmodel.cpp"
diff --git a/src/pdf/qpdfsearchmodel.h b/src/pdf/qpdfsearchmodel.h
new file mode 100644
index 000000000..04f8b9140
--- /dev/null
+++ b/src/pdf/qpdfsearchmodel.h
@@ -0,0 +1,70 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFSEARCHMODEL_H
+#define QPDFSEARCHMODEL_H
+
+#include <QtPdf/qtpdfglobal.h>
+
+#include <QtCore/qabstractitemmodel.h>
+#include <QtPdf/qpdfdocument.h>
+#include <QtPdf/qpdflink.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfSearchModelPrivate;
+
+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)
+ Q_PROPERTY(int count READ count NOTIFY countChanged REVISION(6, 8) FINAL)
+
+public:
+ enum class Role : int {
+ Page = Qt::UserRole,
+ IndexOnPage,
+ Location,
+ ContextBefore,
+ ContextAfter,
+ NRoles
+ };
+ Q_ENUM(Role)
+ QPdfSearchModel() : QPdfSearchModel(nullptr) {}
+ explicit QPdfSearchModel(QObject *parent);
+ ~QPdfSearchModel() override;
+
+ QList<QPdfLink> resultsOnPage(int page) const;
+ QPdfLink 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;
+
+ int count() const;
+
+public Q_SLOTS:
+ void setSearchString(const QString &searchString);
+ void setDocument(QPdfDocument *document);
+
+Q_SIGNALS:
+ void documentChanged();
+ void searchStringChanged();
+ Q_REVISION(6, 8) void countChanged();
+
+protected:
+ void updatePage(int page);
+ void timerEvent(QTimerEvent *event) override;
+
+private:
+ QHash<int, QByteArray> m_roleNames;
+ Q_DECLARE_PRIVATE(QPdfSearchModel)
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFSEARCHMODEL_H
diff --git a/src/pdf/qpdfsearchmodel_p.h b/src/pdf/qpdfsearchmodel_p.h
new file mode 100644
index 000000000..5ffa08f5d
--- /dev/null
+++ b/src/pdf/qpdfsearchmodel_p.h
@@ -0,0 +1,54 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#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 <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;
+ QList<bool> pagesSearched;
+ QList<QList<QPdfLink>> searchResults;
+ int rowCountSoFar = 0;
+ int updateTimerId = -1;
+ int nextPageToUpdate = 0;
+
+ QMetaObject::Connection documentConnection;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFSEARCHMODEL_P_H
diff --git a/src/pdf/qpdfselection.cpp b/src/pdf/qpdfselection.cpp
new file mode 100644
index 000000000..df30eb353
--- /dev/null
+++ b/src/pdf/qpdfselection.cpp
@@ -0,0 +1,132 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#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, QList<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex)
+ : d(new QPdfSelectionPrivate(text, bounds, boundingRect, startIndex, endIndex))
+{
+}
+
+QPdfSelection::QPdfSelection(QPdfSelectionPrivate *d)
+ : d(d)
+{
+}
+
+QPdfSelection::~QPdfSelection() = default;
+QPdfSelection::QPdfSelection(const QPdfSelection &other) = default;
+QPdfSelection::QPdfSelection(QPdfSelection &&other) noexcept = default;
+QPdfSelection &QPdfSelection::operator=(const QPdfSelection &other) = default;
+
+/*!
+ \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.
+*/
+QList<QPolygonF> QPdfSelection::bounds() const
+{
+ return d->bounds;
+}
+
+/*!
+ \property QPdfSelection::text
+
+ This property holds the selected text.
+*/
+QString QPdfSelection::text() const
+{
+ return d->text;
+}
+
+/*!
+ \property QPdfSelection::boundingRectangle
+
+ This property holds the overall bounding rectangle (convex hull) around \l bounds.
+*/
+QRectF QPdfSelection::boundingRectangle() const
+{
+ return d->boundingRect;
+}
+
+/*!
+ \property QPdfSelection::startIndex
+
+ This property holds the index at the beginning of \l text within the full text on the page.
+*/
+int QPdfSelection::startIndex() const
+{
+ return d->startIndex;
+}
+
+/*!
+ \property QPdfSelection::endIndex
+
+ This property holds the index at the end of \l text within the full text on the page.
+*/
+int QPdfSelection::endIndex() const
+{
+ return d->endIndex;
+}
+
+#if QT_CONFIG(clipboard)
+/*!
+ Copies \l text to the \l {QGuiApplication::clipboard()}{system clipboard}
+ depending on the \a mode selected.
+*/
+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/qpdfselection.h b/src/pdf/qpdfselection.h
new file mode 100644
index 000000000..8fcfaf2d8
--- /dev/null
+++ b/src/pdf/qpdfselection.h
@@ -0,0 +1,62 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFSELECTION_H
+#define QPDFSELECTION_H
+
+#include <QtPdf/qtpdfglobal.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/qshareddata.h>
+#include <QtGui/qclipboard.h>
+#include <QtGui/qpolygon.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfSelectionPrivate;
+
+class QPdfSelection
+{
+ Q_GADGET_EXPORT(Q_PDF_EXPORT)
+ Q_PROPERTY(bool valid READ isValid)
+ Q_PROPERTY(QList<QPolygonF> bounds READ bounds)
+ Q_PROPERTY(QRectF boundingRectangle READ boundingRectangle)
+ Q_PROPERTY(QString text READ text)
+ Q_PROPERTY(int startIndex READ startIndex)
+ Q_PROPERTY(int endIndex READ endIndex)
+
+public:
+ Q_PDF_EXPORT ~QPdfSelection();
+ Q_PDF_EXPORT QPdfSelection(const QPdfSelection &other);
+ Q_PDF_EXPORT QPdfSelection &operator=(const QPdfSelection &other);
+
+ Q_PDF_EXPORT QPdfSelection(QPdfSelection &&other) noexcept;
+ QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP(QPdfSelection)
+
+ void swap(QPdfSelection &other) noexcept { d.swap(other.d); }
+
+ Q_PDF_EXPORT bool isValid() const;
+ Q_PDF_EXPORT QList<QPolygonF> bounds() const;
+ Q_PDF_EXPORT QString text() const;
+ Q_PDF_EXPORT QRectF boundingRectangle() const;
+ Q_PDF_EXPORT int startIndex() const;
+ Q_PDF_EXPORT int endIndex() const;
+#if QT_CONFIG(clipboard)
+ Q_PDF_EXPORT void copyToClipboard(QClipboard::Mode mode = QClipboard::Clipboard) const;
+#endif
+
+private:
+ QPdfSelection();
+ QPdfSelection(const QString &text, QList<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex);
+ QPdfSelection(QPdfSelectionPrivate *d);
+ friend class QPdfDocument;
+ friend class QQuickPdfSelection;
+
+private:
+ QExplicitlySharedDataPointer<QPdfSelectionPrivate> d;
+};
+Q_DECLARE_SHARED(QPdfSelection)
+
+QT_END_NAMESPACE
+
+#endif // QPDFSELECTION_H
diff --git a/src/pdf/qpdfselection_p.h b/src/pdf/qpdfselection_p.h
new file mode 100644
index 000000000..541480746
--- /dev/null
+++ b/src/pdf/qpdfselection_p.h
@@ -0,0 +1,45 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QPDFSELECTION_P_H
+#define QPDFSELECTION_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 "qpdfselection.h"
+
+#include <QList>
+#include <QPolygonF>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfSelectionPrivate : public QSharedData
+{
+public:
+ QPdfSelectionPrivate() = default;
+ QPdfSelectionPrivate(const QString &text, QList<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex)
+ : text(text),
+ bounds(bounds),
+ boundingRect(boundingRect),
+ startIndex(startIndex),
+ endIndex(endIndex) { }
+
+ QString text;
+ QList<QPolygonF> bounds;
+ QRectF boundingRect;
+ int startIndex;
+ int endIndex;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFSELECTION_P_H
diff --git a/src/pdf/qtpdfglobal.h b/src/pdf/qtpdfglobal.h
new file mode 100644
index 000000000..2d0900029
--- /dev/null
+++ b/src/pdf/qtpdfglobal.h
@@ -0,0 +1,11 @@
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QTPDFGLOBAL_H
+#define QTPDFGLOBAL_H
+
+#include <QtCore/qglobal.h>
+#include <QtPdf/qtpdfexports.h>
+
+#endif // QTPDFGLOBAL_H
+