PdfScrollablePageView.qml 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright (C) 2022 The Qt Company Ltd.
  2. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. pragma ComponentBehavior: Bound
  4. import QtQuick
  5. import QtQuick.Controls
  6. import QtQuick.Pdf
  7. import QtQuick.Shapes
  8. /*!
  9. \qmltype PdfScrollablePageView
  10. \inqmlmodule QtQuick.Pdf
  11. \brief A complete PDF viewer component to show one page a time, with scrolling.
  12. PdfScrollablePageView provides a PDF viewer component that shows one page
  13. at a time, with scrollbars to move around the page. It also supports
  14. selecting text and copying it to the clipboard, zooming in and out,
  15. clicking an internal link to jump to another section in the document,
  16. rotating the view, and searching for text. The pdfviewer example
  17. demonstrates how to use these features in an application.
  18. The implementation is a QML assembly of smaller building blocks that are
  19. available separately. In case you want to make changes in your own version
  20. of this component, you can copy the QML, which is installed into the
  21. \c QtQuick/Pdf/qml module directory, and modify it as needed.
  22. \sa PdfPageView, PdfMultiPageView, PdfStyle
  23. */
  24. Flickable {
  25. /*!
  26. \qmlproperty PdfDocument PdfScrollablePageView::document
  27. A PdfDocument object with a valid \c source URL is required:
  28. \snippet multipageview.qml 0
  29. */
  30. required property PdfDocument document
  31. /*!
  32. \qmlproperty int PdfScrollablePageView::status
  33. This property holds the \l {QtQuick::Image::status}{rendering status} of
  34. the \l {currentPage}{current page}.
  35. */
  36. property alias status: image.status
  37. /*!
  38. \qmlproperty PdfDocument PdfScrollablePageView::selectedText
  39. The selected text.
  40. */
  41. property alias selectedText: selection.text
  42. /*!
  43. \qmlmethod void PdfScrollablePageView::selectAll()
  44. Selects all the text on the \l {currentPage}{current page}, and makes it
  45. available as the system \l {QClipboard::Selection}{selection} on systems
  46. that support that feature.
  47. \sa copySelectionToClipboard()
  48. */
  49. function selectAll() {
  50. selection.selectAll()
  51. }
  52. /*!
  53. \qmlmethod void PdfScrollablePageView::copySelectionToClipboard()
  54. Copies the selected text (if any) to the
  55. \l {QClipboard::Clipboard}{system clipboard}.
  56. \sa selectAll()
  57. */
  58. function copySelectionToClipboard() {
  59. selection.copyToClipboard()
  60. }
  61. // --------------------------------
  62. // page navigation
  63. /*!
  64. \qmlproperty int PdfScrollablePageView::currentPage
  65. \readonly
  66. This property holds the zero-based page number of the page visible in the
  67. scrollable view. If there is no current page, it holds -1.
  68. This property is read-only, and is typically used in a binding (or
  69. \c onCurrentPageChanged script) to update the part of the user interface
  70. that shows the current page number, such as a \l SpinBox.
  71. \sa PdfPageNavigator::currentPage
  72. */
  73. property alias currentPage: pageNavigator.currentPage
  74. /*!
  75. \qmlproperty bool PdfScrollablePageView::backEnabled
  76. \readonly
  77. This property indicates if it is possible to go back in the navigation
  78. history to a previous-viewed page.
  79. \sa PdfPageNavigator::backAvailable, back()
  80. */
  81. property alias backEnabled: pageNavigator.backAvailable
  82. /*!
  83. \qmlproperty bool PdfScrollablePageView::forwardEnabled
  84. \readonly
  85. This property indicates if it is possible to go to next location in the
  86. navigation history.
  87. \sa PdfPageNavigator::forwardAvailable, forward()
  88. */
  89. property alias forwardEnabled: pageNavigator.forwardAvailable
  90. /*!
  91. \qmlmethod void PdfScrollablePageView::back()
  92. Scrolls the view back to the previous page that the user visited most
  93. recently; or does nothing if there is no previous location on the
  94. navigation stack.
  95. \sa PdfPageNavigator::back(), currentPage, backEnabled
  96. */
  97. function back() { pageNavigator.back() }
  98. /*!
  99. \qmlmethod void PdfScrollablePageView::forward()
  100. Scrolls the view to the page that the user was viewing when the back()
  101. method was called; or does nothing if there is no "next" location on the
  102. navigation stack.
  103. \sa PdfPageNavigator::forward(), currentPage
  104. */
  105. function forward() { pageNavigator.forward() }
  106. /*!
  107. \qmlmethod void PdfScrollablePageView::goToPage(int page)
  108. Changes the view to the \a page, if possible.
  109. \sa PdfPageNavigator::jump(), currentPage
  110. */
  111. function goToPage(page) {
  112. if (page === pageNavigator.currentPage)
  113. return
  114. goToLocation(page, Qt.point(0, 0), 0)
  115. }
  116. /*!
  117. \qmlmethod void PdfScrollablePageView::goToLocation(int page, point location, real zoom)
  118. Scrolls the view to the \a location on the \a page, if possible,
  119. and sets the \a zoom level.
  120. \sa PdfPageNavigator::jump(), currentPage
  121. */
  122. function goToLocation(page, location, zoom) {
  123. if (zoom > 0)
  124. root.renderScale = zoom
  125. pageNavigator.jump(page, location, zoom)
  126. }
  127. // --------------------------------
  128. // page scaling
  129. /*!
  130. \qmlproperty real PdfScrollablePageView::renderScale
  131. This property holds the ratio of pixels to points. The default is \c 1,
  132. meaning one point (1/72 of an inch) equals 1 logical pixel.
  133. */
  134. property real renderScale: 1
  135. /*!
  136. \qmlproperty real PdfScrollablePageView::pageRotation
  137. This property holds the clockwise rotation of the pages.
  138. The default value is \c 0 degrees (that is, no rotation relative to the
  139. orientation of the pages as stored in the PDF file).
  140. */
  141. property real pageRotation: 0
  142. /*!
  143. \qmlproperty size PdfScrollablePageView::sourceSize
  144. This property holds the scaled width and height of the full-frame image.
  145. \sa {QtQuick::Image::sourceSize}{Image.sourceSize}
  146. */
  147. property alias sourceSize: image.sourceSize
  148. /*!
  149. \qmlmethod void PdfScrollablePageView::resetScale()
  150. Sets \l renderScale back to its default value of \c 1.
  151. */
  152. function resetScale() {
  153. paper.scale = 1
  154. root.renderScale = 1
  155. }
  156. /*!
  157. \qmlmethod void PdfScrollablePageView::scaleToWidth(real width, real height)
  158. Sets \l renderScale such that the width of the first page will fit into a
  159. viewport with the given \a width and \a height. If the page is not rotated,
  160. it will be scaled so that its width fits \a width. If it is rotated +/- 90
  161. degrees, it will be scaled so that its width fits \a height.
  162. */
  163. function scaleToWidth(width, height) {
  164. const pagePointSize = document.pagePointSize(pageNavigator.currentPage)
  165. root.renderScale = root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width)
  166. console.log(lcSPV, "scaling", pagePointSize, "to fit", root.width, "rotated?", paper.rot90, "scale", root.renderScale)
  167. root.contentX = 0
  168. root.contentY = 0
  169. }
  170. /*!
  171. \qmlmethod void PdfScrollablePageView::scaleToPage(real width, real height)
  172. Sets \l renderScale such that the whole first page will fit into a viewport
  173. with the given \a width and \a height. The resulting \l renderScale depends
  174. on \l pageRotation: the page will fit into the viewport at a larger size if
  175. it is first rotated to have a matching aspect ratio.
  176. */
  177. function scaleToPage(width, height) {
  178. const pagePointSize = document.pagePointSize(pageNavigator.currentPage)
  179. root.renderScale = Math.min(
  180. root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width),
  181. root.height / (paper.rot90 ? pagePointSize.width : pagePointSize.height) )
  182. root.contentX = 0
  183. root.contentY = 0
  184. }
  185. // --------------------------------
  186. // text search
  187. /*!
  188. \qmlproperty PdfSearchModel PdfScrollablePageView::searchModel
  189. This property holds a PdfSearchModel containing the list of search results
  190. for a given \l searchString.
  191. \sa PdfSearchModel
  192. */
  193. property alias searchModel: searchModel
  194. /*!
  195. \qmlproperty string PdfScrollablePageView::searchString
  196. This property holds the search string that the user may choose to search
  197. for. It is typically used in a binding to the \c text property of a
  198. TextField.
  199. \sa searchModel
  200. */
  201. property alias searchString: searchModel.searchString
  202. /*!
  203. \qmlmethod void PdfScrollablePageView::searchBack()
  204. Decrements the
  205. \l{PdfSearchModel::currentResult}{searchModel's current result}
  206. so that the view will jump to the previous search result.
  207. */
  208. function searchBack() { --searchModel.currentResult }
  209. /*!
  210. \qmlmethod void PdfScrollablePageView::searchForward()
  211. Increments the
  212. \l{PdfSearchModel::currentResult}{searchModel's current result}
  213. so that the view will jump to the next search result.
  214. */
  215. function searchForward() { ++searchModel.currentResult }
  216. // --------------------------------
  217. // implementation
  218. id: root
  219. PdfStyle { id: style }
  220. contentWidth: paper.width
  221. contentHeight: paper.height
  222. ScrollBar.vertical: ScrollBar {
  223. onActiveChanged:
  224. if (!active ) {
  225. const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale,
  226. (root.contentY + root.height / 2) / root.renderScale)
  227. pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale)
  228. }
  229. }
  230. ScrollBar.horizontal: ScrollBar {
  231. onActiveChanged:
  232. if (!active ) {
  233. const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale,
  234. (root.contentY + root.height / 2) / root.renderScale)
  235. pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale)
  236. }
  237. }
  238. onRenderScaleChanged: {
  239. paper.scale = 1
  240. const currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale,
  241. (root.contentY + root.height / 2) / root.renderScale)
  242. pageNavigator.update(pageNavigator.currentPage, currentLocation, root.renderScale)
  243. }
  244. PdfSearchModel {
  245. id: searchModel
  246. document: root.document === undefined ? null : root.document
  247. onCurrentResultChanged: pageNavigator.jump(currentResultLink)
  248. }
  249. PdfPageNavigator {
  250. id: pageNavigator
  251. onJumped: function(current) {
  252. root.renderScale = current.zoom
  253. const dx = Math.max(0, current.location.x * root.renderScale - root.width / 2) - root.contentX
  254. const dy = Math.max(0, current.location.y * root.renderScale - root.height / 2) - root.contentY
  255. // don't jump if location is in the viewport already, i.e. if the "error" between desired and actual contentX/Y is small
  256. if (Math.abs(dx) > root.width / 3)
  257. root.contentX += dx
  258. if (Math.abs(dy) > root.height / 3)
  259. root.contentY += dy
  260. console.log(lcSPV, "going to zoom", current.zoom, "loc", current.location,
  261. "on page", current.page, "ended up @", root.contentX + ", " + root.contentY)
  262. }
  263. onCurrentPageChanged: searchModel.currentPage = currentPage
  264. property url documentSource: root.document.source
  265. onDocumentSourceChanged: {
  266. pageNavigator.clear()
  267. root.resetScale()
  268. root.contentX = 0
  269. root.contentY = 0
  270. }
  271. }
  272. LoggingCategory {
  273. id: lcSPV
  274. name: "qt.pdf.singlepageview"
  275. }
  276. Rectangle {
  277. id: paper
  278. width: rot90 ? image.height : image.width
  279. height: rot90 ? image.width : image.height
  280. property real rotationModulus: Math.abs(root.pageRotation % 180)
  281. property bool rot90: rotationModulus > 45 && rotationModulus < 135
  282. property real minScale: 0.1
  283. property real maxScale: 10
  284. PdfPageImage {
  285. id: image
  286. document: root.document
  287. currentFrame: pageNavigator.currentPage
  288. asynchronous: true
  289. fillMode: Image.PreserveAspectFit
  290. rotation: root.pageRotation
  291. anchors.centerIn: parent
  292. property real pageScale: image.paintedWidth / document.pagePointSize(pageNavigator.currentPage).width
  293. width: document.pagePointSize(pageNavigator.currentPage).width * root.renderScale
  294. height: document.pagePointSize(pageNavigator.currentPage).height * root.renderScale
  295. sourceSize.width: width * Screen.devicePixelRatio
  296. sourceSize.height: 0
  297. Shape {
  298. anchors.fill: parent
  299. visible: image.status === Image.Ready
  300. ShapePath {
  301. strokeWidth: -1
  302. fillColor: style.pageSearchResultsColor
  303. scale: Qt.size(image.pageScale, image.pageScale)
  304. PathMultiline {
  305. paths: searchModel.currentPageBoundingPolygons
  306. }
  307. }
  308. ShapePath {
  309. strokeWidth: style.currentSearchResultStrokeWidth
  310. strokeColor: style.currentSearchResultStrokeColor
  311. fillColor: "transparent"
  312. scale: Qt.size(image.pageScale, image.pageScale)
  313. PathMultiline {
  314. paths: searchModel.currentResultBoundingPolygons
  315. }
  316. }
  317. ShapePath {
  318. fillColor: style.selectionColor
  319. scale: Qt.size(image.pageScale, image.pageScale)
  320. PathMultiline {
  321. paths: selection.geometry
  322. }
  323. }
  324. }
  325. Repeater {
  326. model: PdfLinkModel {
  327. id: linkModel
  328. document: root.document
  329. page: pageNavigator.currentPage
  330. }
  331. delegate: PdfLinkDelegate {
  332. x: rectangle.x * image.pageScale
  333. y: rectangle.y * image.pageScale
  334. width: rectangle.width * image.pageScale
  335. height: rectangle.height * image.pageScale
  336. visible: image.status === Image.Ready
  337. onTapped:
  338. (link) => {
  339. if (link.page >= 0)
  340. pageNavigator.jump(link.page, link.location, link.zoom)
  341. else
  342. Qt.openUrlExternally(url)
  343. }
  344. }
  345. }
  346. DragHandler {
  347. id: textSelectionDrag
  348. acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
  349. target: null
  350. }
  351. TapHandler {
  352. id: mouseClickHandler
  353. acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
  354. }
  355. TapHandler {
  356. id: touchTapHandler
  357. acceptedDevices: PointerDevice.TouchScreen
  358. onTapped: {
  359. selection.clear()
  360. selection.focus = true
  361. }
  362. }
  363. }
  364. PdfSelection {
  365. id: selection
  366. anchors.fill: parent
  367. document: root.document
  368. page: pageNavigator.currentPage
  369. renderScale: image.pageScale == 0 ? 1.0 : image.pageScale
  370. from: textSelectionDrag.centroid.pressPosition
  371. to: textSelectionDrag.centroid.position
  372. hold: !textSelectionDrag.active && !mouseClickHandler.pressed
  373. focus: true
  374. }
  375. PinchHandler {
  376. id: pinch
  377. minimumScale: paper.minScale / root.renderScale
  378. maximumScale: Math.max(1, paper.maxScale / root.renderScale)
  379. minimumRotation: 0
  380. maximumRotation: 0
  381. onActiveChanged:
  382. if (!active) {
  383. const centroidInPoints = Qt.point(pinch.centroid.position.x / root.renderScale,
  384. pinch.centroid.position.y / root.renderScale)
  385. const centroidInFlickable = root.mapFromItem(paper, pinch.centroid.position.x, pinch.centroid.position.y)
  386. const newSourceWidth = image.sourceSize.width * paper.scale
  387. const ratio = newSourceWidth / image.sourceSize.width
  388. console.log(lcSPV, "pinch ended with centroid", pinch.centroid.position, centroidInPoints, "wrt flickable", centroidInFlickable,
  389. "page at", paper.x.toFixed(2), paper.y.toFixed(2),
  390. "contentX/Y were", root.contentX.toFixed(2), root.contentY.toFixed(2))
  391. if (ratio > 1.1 || ratio < 0.9) {
  392. const centroidOnPage = Qt.point(centroidInPoints.x * root.renderScale * ratio, centroidInPoints.y * root.renderScale * ratio)
  393. paper.scale = 1
  394. paper.x = 0
  395. paper.y = 0
  396. root.contentX = centroidOnPage.x - centroidInFlickable.x
  397. root.contentY = centroidOnPage.y - centroidInFlickable.y
  398. root.renderScale *= ratio // onRenderScaleChanged calls pageNavigator.update() so we don't need to here
  399. console.log(lcSPV, "contentX/Y adjusted to", root.contentX.toFixed(2), root.contentY.toFixed(2))
  400. } else {
  401. paper.x = 0
  402. paper.y = 0
  403. }
  404. }
  405. grabPermissions: PointerHandler.CanTakeOverFromAnything
  406. }
  407. }
  408. }