diff options
Diffstat (limited to 'src/pdf')
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 Binary files differnew file mode 100644 index 000000000..2f0bb62a2 --- /dev/null +++ b/src/pdf/doc/images/multipageviewer.png diff --git a/src/pdf/doc/images/pdfviewer.png b/src/pdf/doc/images/pdfviewer.png Binary files differnew file mode 100644 index 000000000..ac8a31ac0 --- /dev/null +++ b/src/pdf/doc/images/pdfviewer.png diff --git a/src/pdf/doc/images/search-results.png b/src/pdf/doc/images/search-results.png Binary files differnew file mode 100644 index 000000000..91ee53b83 --- /dev/null +++ b/src/pdf/doc/images/search-results.png diff --git a/src/pdf/doc/images/singlepageviewer.webp b/src/pdf/doc/images/singlepageviewer.webp Binary files differnew file mode 100644 index 000000000..e429cb818 --- /dev/null +++ b/src/pdf/doc/images/singlepageviewer.webp diff --git a/src/pdf/doc/images/wrapping-search-result.png b/src/pdf/doc/images/wrapping-search-result.png Binary files differnew file mode 100644 index 000000000..108ec0444 --- /dev/null +++ b/src/pdf/doc/images/wrapping-search-result.png 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 + |