pdf_thumbnail_viewer.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /* Copyright 2012 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. /** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
  16. /** @typedef {import("./event_utils").EventBus} EventBus */
  17. /** @typedef {import("./interfaces").IL10n} IL10n */
  18. /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
  19. // eslint-disable-next-line max-len
  20. /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
  21. import {
  22. getVisibleElements,
  23. isValidRotation,
  24. RenderingStates,
  25. scrollIntoView,
  26. watchScroll,
  27. } from "./ui_utils.js";
  28. import { PDFThumbnailView, TempImageFactory } from "./pdf_thumbnail_view.js";
  29. const THUMBNAIL_SCROLL_MARGIN = -19;
  30. const THUMBNAIL_SELECTED_CLASS = "selected";
  31. /**
  32. * @typedef {Object} PDFThumbnailViewerOptions
  33. * @property {HTMLDivElement} container - The container for the thumbnail
  34. * elements.
  35. * @property {EventBus} eventBus - The application event bus.
  36. * @property {IPDFLinkService} linkService - The navigation/linking service.
  37. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
  38. * @property {IL10n} l10n - Localization service.
  39. * @property {Object} [pageColors] - Overwrites background and foreground colors
  40. * with user defined ones in order to improve readability in high contrast
  41. * mode.
  42. */
  43. /**
  44. * Viewer control to display thumbnails for pages in a PDF document.
  45. */
  46. class PDFThumbnailViewer {
  47. /**
  48. * @param {PDFThumbnailViewerOptions} options
  49. */
  50. constructor({
  51. container,
  52. eventBus,
  53. linkService,
  54. renderingQueue,
  55. l10n,
  56. pageColors,
  57. }) {
  58. this.container = container;
  59. this.linkService = linkService;
  60. this.renderingQueue = renderingQueue;
  61. this.l10n = l10n;
  62. this.pageColors = pageColors || null;
  63. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
  64. if (
  65. this.pageColors &&
  66. !(
  67. CSS.supports("color", this.pageColors.background) &&
  68. CSS.supports("color", this.pageColors.foreground)
  69. )
  70. ) {
  71. if (this.pageColors.background || this.pageColors.foreground) {
  72. console.warn(
  73. "PDFThumbnailViewer: Ignoring `pageColors`-option, since the browser doesn't support the values used."
  74. );
  75. }
  76. this.pageColors = null;
  77. }
  78. }
  79. this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this));
  80. this._resetView();
  81. }
  82. /**
  83. * @private
  84. */
  85. _scrollUpdated() {
  86. this.renderingQueue.renderHighestPriority();
  87. }
  88. getThumbnail(index) {
  89. return this._thumbnails[index];
  90. }
  91. /**
  92. * @private
  93. */
  94. _getVisibleThumbs() {
  95. return getVisibleElements({
  96. scrollEl: this.container,
  97. views: this._thumbnails,
  98. });
  99. }
  100. scrollThumbnailIntoView(pageNumber) {
  101. if (!this.pdfDocument) {
  102. return;
  103. }
  104. const thumbnailView = this._thumbnails[pageNumber - 1];
  105. if (!thumbnailView) {
  106. console.error('scrollThumbnailIntoView: Invalid "pageNumber" parameter.');
  107. return;
  108. }
  109. if (pageNumber !== this._currentPageNumber) {
  110. const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1];
  111. // Remove the highlight from the previous thumbnail...
  112. prevThumbnailView.div.classList.remove(THUMBNAIL_SELECTED_CLASS);
  113. // ... and add the highlight to the new thumbnail.
  114. thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
  115. }
  116. const { first, last, views } = this._getVisibleThumbs();
  117. // If the thumbnail isn't currently visible, scroll it into view.
  118. if (views.length > 0) {
  119. let shouldScroll = false;
  120. if (pageNumber <= first.id || pageNumber >= last.id) {
  121. shouldScroll = true;
  122. } else {
  123. for (const { id, percent } of views) {
  124. if (id !== pageNumber) {
  125. continue;
  126. }
  127. shouldScroll = percent < 100;
  128. break;
  129. }
  130. }
  131. if (shouldScroll) {
  132. scrollIntoView(thumbnailView.div, { top: THUMBNAIL_SCROLL_MARGIN });
  133. }
  134. }
  135. this._currentPageNumber = pageNumber;
  136. }
  137. get pagesRotation() {
  138. return this._pagesRotation;
  139. }
  140. set pagesRotation(rotation) {
  141. if (!isValidRotation(rotation)) {
  142. throw new Error("Invalid thumbnails rotation angle.");
  143. }
  144. if (!this.pdfDocument) {
  145. return;
  146. }
  147. if (this._pagesRotation === rotation) {
  148. return; // The rotation didn't change.
  149. }
  150. this._pagesRotation = rotation;
  151. const updateArgs = { rotation };
  152. for (const thumbnail of this._thumbnails) {
  153. thumbnail.update(updateArgs);
  154. }
  155. }
  156. cleanup() {
  157. for (const thumbnail of this._thumbnails) {
  158. if (thumbnail.renderingState !== RenderingStates.FINISHED) {
  159. thumbnail.reset();
  160. }
  161. }
  162. TempImageFactory.destroyCanvas();
  163. }
  164. /**
  165. * @private
  166. */
  167. _resetView() {
  168. this._thumbnails = [];
  169. this._currentPageNumber = 1;
  170. this._pageLabels = null;
  171. this._pagesRotation = 0;
  172. // Remove the thumbnails from the DOM.
  173. this.container.textContent = "";
  174. }
  175. /**
  176. * @param {PDFDocumentProxy} pdfDocument
  177. */
  178. setDocument(pdfDocument) {
  179. if (this.pdfDocument) {
  180. this._cancelRendering();
  181. this._resetView();
  182. }
  183. this.pdfDocument = pdfDocument;
  184. if (!pdfDocument) {
  185. return;
  186. }
  187. const firstPagePromise = pdfDocument.getPage(1);
  188. const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
  189. firstPagePromise
  190. .then(firstPdfPage => {
  191. const pagesCount = pdfDocument.numPages;
  192. const viewport = firstPdfPage.getViewport({ scale: 1 });
  193. for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
  194. const thumbnail = new PDFThumbnailView({
  195. container: this.container,
  196. id: pageNum,
  197. defaultViewport: viewport.clone(),
  198. optionalContentConfigPromise,
  199. linkService: this.linkService,
  200. renderingQueue: this.renderingQueue,
  201. l10n: this.l10n,
  202. pageColors: this.pageColors,
  203. });
  204. this._thumbnails.push(thumbnail);
  205. }
  206. // Set the first `pdfPage` immediately, since it's already loaded,
  207. // rather than having to repeat the `PDFDocumentProxy.getPage` call in
  208. // the `this.#ensurePdfPageLoaded` method before rendering can start.
  209. this._thumbnails[0]?.setPdfPage(firstPdfPage);
  210. // Ensure that the current thumbnail is always highlighted on load.
  211. const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
  212. thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
  213. })
  214. .catch(reason => {
  215. console.error("Unable to initialize thumbnail viewer", reason);
  216. });
  217. }
  218. /**
  219. * @private
  220. */
  221. _cancelRendering() {
  222. for (const thumbnail of this._thumbnails) {
  223. thumbnail.cancelRendering();
  224. }
  225. }
  226. /**
  227. * @param {Array|null} labels
  228. */
  229. setPageLabels(labels) {
  230. if (!this.pdfDocument) {
  231. return;
  232. }
  233. if (!labels) {
  234. this._pageLabels = null;
  235. } else if (
  236. !(Array.isArray(labels) && this.pdfDocument.numPages === labels.length)
  237. ) {
  238. this._pageLabels = null;
  239. console.error("PDFThumbnailViewer_setPageLabels: Invalid page labels.");
  240. } else {
  241. this._pageLabels = labels;
  242. }
  243. // Update all the `PDFThumbnailView` instances.
  244. for (let i = 0, ii = this._thumbnails.length; i < ii; i++) {
  245. this._thumbnails[i].setPageLabel(this._pageLabels?.[i] ?? null);
  246. }
  247. }
  248. /**
  249. * @param {PDFThumbnailView} thumbView
  250. * @returns {Promise<PDFPageProxy | null>}
  251. */
  252. async #ensurePdfPageLoaded(thumbView) {
  253. if (thumbView.pdfPage) {
  254. return thumbView.pdfPage;
  255. }
  256. try {
  257. const pdfPage = await this.pdfDocument.getPage(thumbView.id);
  258. if (!thumbView.pdfPage) {
  259. thumbView.setPdfPage(pdfPage);
  260. }
  261. return pdfPage;
  262. } catch (reason) {
  263. console.error("Unable to get page for thumb view", reason);
  264. return null; // Page error -- there is nothing that can be done.
  265. }
  266. }
  267. #getScrollAhead(visible) {
  268. if (visible.first?.id === 1) {
  269. return true;
  270. } else if (visible.last?.id === this._thumbnails.length) {
  271. return false;
  272. }
  273. return this.scroll.down;
  274. }
  275. forceRendering() {
  276. const visibleThumbs = this._getVisibleThumbs();
  277. const scrollAhead = this.#getScrollAhead(visibleThumbs);
  278. const thumbView = this.renderingQueue.getHighestPriority(
  279. visibleThumbs,
  280. this._thumbnails,
  281. scrollAhead
  282. );
  283. if (thumbView) {
  284. this.#ensurePdfPageLoaded(thumbView).then(() => {
  285. this.renderingQueue.renderView(thumbView);
  286. });
  287. return true;
  288. }
  289. return false;
  290. }
  291. }
  292. export { PDFThumbnailViewer };