You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

pdfjs-viewer.js 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /*
  2. Copyright 2020 Carlos de Alfonso (https://github.com/dealfonso)
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. (function(exports, $) {
  14. 'use strict';
  15. let defaults = {
  16. // Threshold to consider that a page is visible
  17. visibleThreshold: 0.5,
  18. // Number of extra pages to load (appart from the visible)
  19. extraPagesToLoad: 3,
  20. // The class used for each page (the div that wraps the content of the page)
  21. pageClass: "pdfpage",
  22. // The class used for the content of each page (the div that contains the page)
  23. contentClass: "content-wrapper",
  24. // Function called when a document has been loaded and its structure has been created
  25. onDocumentReady: () => {},
  26. // Function called when a new page is created (it is binded to the object, and receives a jQuery object as parameter)
  27. onNewPage: (page, i) => {},
  28. // Function called when a page is rendered
  29. onPageRender: (page, i) => {},
  30. // Function called to obtain a page that shows an error when the document could not be loaded (returns a jQuery object)
  31. errorPage: () => {
  32. $(`<div class="placeholder"></div>`).addClass(this.settings.pageClass).append($(`<p class="m-auto"></p>`).text("could not load document"))
  33. },
  34. // Posible zoom values to iterate over using "in" and "out"
  35. zoomValues: [ 0.25, 0.5, 0.75, 1, 1.25, 1.50, 2, 4, 8 ],
  36. // Function called when the zoom level changes (it receives the zoom level)
  37. onZoomChange: (zoomlevel) => {},
  38. // Function called whenever the active page is changed (the active page is the one that is shown in the viewer)
  39. onActivePageChanged: (page, i) => {},
  40. // Percentage of the container that will be filled with the page
  41. zoomFillArea: 0.95,
  42. // Function called to get the content of an empty page
  43. emptyContent: () => $('<div class="loader"></div>'),
  44. // The scale to which the pages are rendered (1.5 is the default value for the PDFjs viewer); a higher value will render the pages with a higher resolution
  45. // but it will consume more memory and CPU. A lower value will render the pages with a lower resolution, but they will be uglier.
  46. renderingScale: 1.5,
  47. }
  48. // Class used to help in zoom management; probably it can be moved to the main class, but it is used to group methods
  49. class Zoomer {
  50. /**
  51. * Construct the helper class
  52. * @param {PDFjsViewer} viewer - the viewer object
  53. * @param {*} options - the options object
  54. */
  55. constructor(viewer, options = {}) {
  56. let defaults = {
  57. // The possible zoom values to iterate through using "in" and "out"
  58. zoomValues: [ 0.25, 0.5, 0.75, 1, 1.25, 1.50, 2, 4, 8 ],
  59. // The area to fill the container with the zoomed pages
  60. fillArea: 0.9,
  61. }
  62. // The current zooom value
  63. this.current = 1;
  64. // The viewer instance whose pages may be zoomed
  65. this.viewer = viewer;
  66. // The settings
  67. this.settings = $.extend(defaults, options);
  68. // Need having the zoom values in order
  69. this.settings.zoomValues = this.settings.zoomValues.sort();
  70. }
  71. /** Translates a zoom value into a float value; possible values:
  72. * - a float value
  73. * - a string with a keyword (e.g. "width", "height", "fit", "in", "out")
  74. * @param {number} zoom - the zoom value to be translated
  75. * @return {number} The zoom value
  76. */
  77. get(zoom = null) {
  78. // If no zoom is specified, return the current one
  79. if (zoom === null) {
  80. return this.current;
  81. }
  82. // If it is a number, return it
  83. if (parseFloat(zoom) == zoom) {
  84. return zoom;
  85. }
  86. let $activepage = this.viewer.getActivePage();
  87. let zoomValues = [];
  88. // If it is a keyword, return the corresponding value
  89. switch(zoom) {
  90. case "in":
  91. zoom = this.current;
  92. zoomValues = this.settings.zoomValues.filter((x) => x > zoom);
  93. if (zoomValues.length > 0) {
  94. zoom = Math.min(...zoomValues);
  95. }
  96. break;
  97. case "out":
  98. zoom = this.current;
  99. zoomValues = this.settings.zoomValues.filter((x) => x < zoom);
  100. if (zoomValues.length > 0) {
  101. zoom = Math.max(...zoomValues);
  102. }
  103. break;
  104. case "fit":
  105. zoom = Math.min(this.get("width"), this.get("height"));
  106. break;
  107. case "width":
  108. zoom = this.settings.fillArea * this.viewer.$container.width() / $activepage.data("width");
  109. break;
  110. case "height":
  111. zoom = this.settings.fillArea * this.viewer.$container.height() / $activepage.data("height");
  112. break;
  113. default:
  114. zoom = this.current;
  115. break;
  116. }
  117. return zoom;
  118. }
  119. /**
  120. * Sets the zoom value to each page (changes both the page and the content div); relies on the data-values for the page
  121. * @param {number} zoom - the zoom value to be set
  122. */
  123. zoomPages(zoom) {
  124. zoom = this.get(zoom);
  125. this.viewer.getPages().forEach(function(page) {
  126. let $page = page.$div;
  127. let c_width = $page.data("width");
  128. let c_height = $page.data("height");
  129. $page.width(c_width * zoom).height(c_height * zoom);
  130. $page.data('zoom', zoom);
  131. $page.find(`.${this.viewer.settings.contentClass}`).width(c_width * zoom).height(c_height * zoom);
  132. }.bind(this));
  133. this.current = zoom;
  134. }
  135. }
  136. class PDFjsViewer {
  137. /**
  138. * Constructs the object, and initializes actions:
  139. * - add the scroll handler to the container
  140. * - set the first adjusting action when the page is loaded
  141. * - creates the zoom helper
  142. * @param {jQuery} $container the jQuery value that will hold the pages
  143. * @param {dictionary} options options for the viewer
  144. */
  145. constructor($container, options = {}) {
  146. this.settings = $.extend(Object.assign({}, defaults), options);
  147. // Create the zoomer helper
  148. this._zoom = new Zoomer(this, {
  149. zoomValues: this.settings.zoomValues,
  150. fillArea: this.settings.zoomFillArea,
  151. });
  152. // Store the container
  153. this.$container = $container;
  154. // Add a reference to this object to the container
  155. $container.get(0)._pdfjsViewer = this;
  156. // Add the event listeners
  157. this._setScrollListener();
  158. // Initialize some variables
  159. this.pages = [];
  160. this.pdf = null;
  161. // Whether the document is ready or not
  162. this._documentReady = false;
  163. }
  164. /**
  165. * Sets the current zoom level and applies it to all the pages
  166. * @param {number} zoom the desired zoom level, which will be a value (1 equals to 100%), or the keywords 'in', 'out', 'width', 'height' or 'fit'
  167. */
  168. setZoom(zoom) {
  169. let container = this.$container.get(0);
  170. // Get the previous zoom and scroll position
  171. let prevzoom = this._zoom.current;
  172. let prevScroll = {
  173. top: container.scrollTop,
  174. left: container.scrollLeft
  175. };
  176. // Now zoom the pages
  177. this._zoom.zoomPages(zoom);
  178. // Update the scroll position (to match the previous one), according to the new relationship of zoom
  179. container.scrollLeft = prevScroll.left * this._zoom.current / prevzoom;
  180. container.scrollTop = prevScroll.top * this._zoom.current / prevzoom;
  181. // Force to redraw the visible pages to upgrade the resolution
  182. this._visiblePages(true);
  183. // Call the callback (if provided)
  184. if (this._documentReady) {
  185. if (typeof this.settings.onZoomChange === "function")
  186. this.settings.onZoomChange.call(this, this._zoom.current);
  187. this.$container.get(0).dispatchEvent(new CustomEvent("zoomchange", { detail: { zoom: this._zoom.current } }));
  188. }
  189. return this._zoom.current;
  190. }
  191. /**
  192. * Obtain the current zoom level
  193. * @returns {number} the current zoom level
  194. */
  195. getZoom() {
  196. return this._zoom.current;
  197. }
  198. /**
  199. * Function that removes the content of a page and replaces it with the empty content (i.e. a content generated by function emptyContent)
  200. * such content will not be visible except for the time that the
  201. * @param {jQuery} $page the page to be emptied
  202. */
  203. _cleanPage($page) {
  204. let $emptyContent = this.settings.emptyContent();
  205. $page.find(`.${this.settings.contentClass}`).empty().append($emptyContent)
  206. }
  207. /**
  208. * Function that replaces the content with the empty class in a page with a new content
  209. * @param {*} $page the page to be modified
  210. * @param {*} $content the new content that will be set in the page
  211. */
  212. _setPageContent($page, $content) {
  213. $page.find(`.${this.settings.contentClass}`).empty().append($content)
  214. }
  215. /**
  216. * Recalculates which pages are now visible and forces redrawing them (moreover it cleans those not visible)
  217. */
  218. refreshAll() {
  219. this._visiblePages(true);
  220. }
  221. /** Function that creates a scroll handler to update the active page and to load more pages as the scroll position changes */
  222. _setScrollListener() {
  223. // Create a scroll handler that prevents reentrance if called multiple times and the loading of pages is not finished
  224. let scrollLock = false;
  225. let scrollPos = { top:0 , left:0 };
  226. this.__scrollHandler = function(e) {
  227. // Avoid re-entrance for the same event while loading pages
  228. if (scrollLock === true) {
  229. return;
  230. }
  231. scrollLock = true;
  232. let container = this.$container.get(0);
  233. if ((Math.abs(container.scrollTop - scrollPos.top) > (container.clientHeight * 0.2 * this._zoom.current)) ||
  234. (Math.abs(container.scrollLeft - scrollPos.left) > (container.clientWidth * 0.2 * this._zoom.current))) {
  235. scrollPos = {
  236. top: container.scrollTop,
  237. left: container.scrollLeft
  238. }
  239. this._visiblePages();
  240. }
  241. scrollLock = false;
  242. }.bind(this);
  243. // Set the scroll handler
  244. this.$container.off('scroll');
  245. this.$container.on('scroll', this.__scrollHandler);
  246. }
  247. /**
  248. * Function that creates the pageinfo structure for one page, along with the skeleton to host the page (i.e. <div class="page"><div class="content-wrapper"></div></div>)
  249. * If the page is a pageinfo, the new pageinfo structure will not rely on the size (it will copy it, but it won't be marked as loaded). If it is a page, the size will
  250. * be calculated from the viewport and it will be marked as loaded.
  251. * This is done in this way, because when creating the pages in the first time, they will be created assuming that they are of the same size than the first one. If they
  252. * are not, the size will be adjusted later, when the pages are loaded.
  253. *
  254. * @param {*} page - the pageinfo (or the page) from which to create the pageinfo structure
  255. * @param {*} i - the number of the page to be created
  256. * @returns pageinfo - the pageinfo structure for the page
  257. */
  258. _createSkeleton(page, i) {
  259. let pageinfo = {
  260. $div: null,
  261. width: 0,
  262. height: 0,
  263. loaded: false,
  264. };
  265. // If it is a page, the size will be obtained from the viewport; otherwise, it will be copied from the provided pageinfo
  266. if (page.getViewport !== undefined) {
  267. let viewport = page.getViewport({rotation:this._rotation,scale:1});
  268. pageinfo.width = viewport.width;
  269. pageinfo.height = viewport.height;
  270. pageinfo.loaded = true;
  271. } else {
  272. pageinfo.width = page.width;
  273. pageinfo.height = page.height;
  274. }
  275. console.assert(((pageinfo.width > 0) && (pageinfo.height > 0)), "Page width and height must be greater than 0");
  276. // Now create the skeleton for the divs
  277. pageinfo.$div = $(`<div id="page-${i}">`)
  278. .attr('data-page', i)
  279. .data('width', pageinfo.width)
  280. .data('height', pageinfo.height)
  281. .data('zoom', this._zoom.current)
  282. .addClass(this.settings.pageClass)
  283. .width(pageinfo.width * this._zoom.current)
  284. .height(pageinfo.height * this._zoom.current);
  285. let $content = $(`<div class="${this.settings.contentClass}">`)
  286. .width(pageinfo.width)
  287. .height(pageinfo.height);
  288. pageinfo.$div.append($content);
  289. // Clean the page (i.e. put the empty content, etc.)
  290. this._cleanPage(pageinfo.$div);
  291. return pageinfo;
  292. }
  293. /**
  294. * This function places the page.$div in the container, according to its page number (i.e. it searches for the previous page and puts this page after)
  295. * * in principle, this method sould not be needed because all the pages are put in order; but this is created just in case it is needed in further versions
  296. * @param {*} pageinfo - the pageinfo structure for the page (needs a valid $div)
  297. * @param {*} i - the number of the page
  298. */
  299. _placeSkeleton(pageinfo, i) {
  300. let prevpage = i - 1;
  301. let $prevpage = null;
  302. while ((prevpage>0) && (($prevpage = this.$container.find(`.${this.settings.pageClass}[data-page="${prevpage}"]`)).length === 0)) {
  303. prevpage--;
  304. }
  305. if (prevpage === 0) {
  306. this.$container.append(pageinfo.$div);
  307. }
  308. else {
  309. $prevpage.after(pageinfo.$div);
  310. }
  311. }
  312. /**
  313. * Creates the initial skeletons for all the pages, and places them into the container
  314. * @param {page/pageinfo} pageinfo - the initial pageinfo (or page) structure
  315. */
  316. _createSkeletons(pageinfo) {
  317. for (let i = 1; i <= this.pageCount; i++) {
  318. if (this.pages[i] === undefined) {
  319. // Create the pageinfo structure, store it and place it in the appropriate place (the next page will be created similar to the previous one)
  320. pageinfo = this._createSkeleton(pageinfo, i);
  321. this.pages[i] = pageinfo;
  322. this._placeSkeleton(pageinfo, i);
  323. // Call the callback function (if provided)
  324. if (this._documentReady) {
  325. if (typeof this.settings.onNewPage === "function") {
  326. this.settings.onNewPage.call(this, pageinfo.$div, i);
  327. }
  328. this.$container.get(0).dispatchEvent(new CustomEvent("newpage", { detail: { pageNumber: i, page: pageinfo.$div.get(0) } }));
  329. }
  330. }
  331. }
  332. }
  333. /**
  334. * Function to set the active page, and calling the callback (if provided)
  335. * @param {*} i - the number of the page to set active
  336. */
  337. _setActivePage(i) {
  338. if (this._activePage !== i) {
  339. this._activePage = i;
  340. let activePage = this.getActivePage();
  341. if (this._documentReady) {
  342. if (typeof this.settings.onActivePageChanged === "function")
  343. this.settings.onActivePageChanged.call(this, activePage, i);
  344. this.$container.get(0).dispatchEvent(new CustomEvent("activepagechanged", { detail: { activePageNumber: i, activePage: activePage==null?null:activePage.get(0) } }));
  345. }
  346. }
  347. }
  348. /**
  349. * Obtains the area of a div that falls in the viewer
  350. * @param {*} $page - div whose area is to be calculated
  351. * @returns the visible area
  352. */
  353. _areaOfPageVisible($page) {
  354. if ($page === undefined) {
  355. return 0;
  356. }
  357. let c_offset = this.$container.offset();
  358. let c_width = this.$container.width();
  359. let c_height = this.$container.height();
  360. let position = $page.offset();
  361. position.top -= c_offset.top;
  362. position.left -= c_offset.left;
  363. position.bottom = position.top + $page.outerHeight();
  364. position.right = position.left + $page.outerWidth();
  365. let page_y0 = Math.min(Math.max(position.top, 0), c_height);
  366. let page_y1 = Math.min(Math.max($page.outerHeight() + position.top, 0), c_height);
  367. let page_x0 = Math.min(Math.max(position.left, 0), c_width);
  368. let page_x1 = Math.min(Math.max($page.outerWidth() + position.left, 0), c_width);
  369. let vis_x = page_x1 - page_x0;
  370. let vis_y = page_y1 - page_y0;
  371. return (vis_x * vis_y);
  372. }
  373. /**
  374. * Function that returns true if the page is considered to be visible (the amount of visible area is greater than the threshold)
  375. * @param {*} i - the number of page to check
  376. * @returns true if the page is visible
  377. */
  378. isPageVisible(i) {
  379. if ((this.pdf === null) || (i === undefined) || (i === null) || (i < 1) || (i > this.pdf.numPages)) {
  380. return false;
  381. }
  382. let $page = i;
  383. if (typeof i === "number") {
  384. if (this.pages[i] === undefined)
  385. return false;
  386. $page = this.pages[i].$div;
  387. }
  388. return this._areaOfPageVisible($page) > ($page.outerWidth() * $page.outerHeight() * this.settings.visibleThreshold);
  389. }
  390. /**
  391. * Function that calculates which pages are visible in the viewer, draws them (if not already drawn), and clears those not visible
  392. * @param {*} forceRedraw - if true, the visible pages will be redrawn regardless of whether they are already drawn (useful for zoom changes)
  393. */
  394. _visiblePages(forceRedraw = false) {
  395. // Will grab the page with the greater visible area to set it as active
  396. let max_area = 0;
  397. let i_page = null;
  398. // If there are no visible pages, return
  399. if (this.pages.length === 0) {
  400. this._visibles = [];
  401. this._setActivePage(0);
  402. return;
  403. }
  404. // Calculate the visible area for each page and consider it visible if the visible area is greater than 0
  405. let $visibles = this.pages.filter(function(pageinfo) {
  406. let areaVisible = this._areaOfPageVisible(pageinfo.$div);
  407. if (areaVisible > max_area) {
  408. max_area = areaVisible;
  409. i_page = pageinfo.$div.data('page');
  410. }
  411. return areaVisible > 0;
  412. }.bind(this)).map((x) => x.$div);
  413. // Set the active page
  414. this._setActivePage(i_page);
  415. // Now get the visible pages
  416. let visibles = $visibles.map((x) => $(x).data('page'));
  417. if (visibles.length > 0) {
  418. // Now will add some extra pages (before and after) the visible ones, to have them prepared in case of scroll
  419. let minVisible = Math.min(...visibles);
  420. let maxVisible = Math.max(...visibles);
  421. for (let i = Math.max(1, minVisible - this.settings.extraPagesToLoad) ; i < minVisible ; i++) {
  422. if (!visibles.includes(i))
  423. visibles.push(i)
  424. }
  425. for (let i = maxVisible + 1; i <= Math.min(maxVisible + this.settings.extraPagesToLoad, this.pdf.numPages); i++) {
  426. if (!visibles.includes(i))
  427. visibles.push(i)
  428. }
  429. }
  430. // Now will draw the visible pages, but if not forcing, will only draw those that were not visible before
  431. let nowVisibles = visibles;
  432. if (! forceRedraw) {
  433. nowVisibles = visibles.filter(function (x) {
  434. return !this._visibles.includes(x)
  435. }.bind(this));
  436. }
  437. // Get the pages that were visible before, that are not visible now, and clear them
  438. this._visibles.filter(function (x) {
  439. return !visibles.includes(x)
  440. }).forEach(function (i) {
  441. this._cleanPage(this.pages[i].$div);
  442. }.bind(this))
  443. // Store the new visible pages
  444. this._visibles = visibles;
  445. // And now we'll queue the pages to load
  446. this.loadPages(...nowVisibles);
  447. }
  448. /**
  449. * Function queue a set of pages to be loaded; if not loading, the function starts the loading worker
  450. * @param {...pageinfo} pages - the pages to load
  451. */
  452. loadPages(...pages) {
  453. this._pagesLoading.push(...pages);
  454. if (this._loading) {
  455. return;
  456. }
  457. this._loadingTask();
  458. }
  459. /**
  460. * Function that gets the pages pending to load and renders them sequentially (to avoid multiple rendering promises)
  461. */
  462. _loadingTask() {
  463. this._loading = true;
  464. if (this._pagesLoading.length > 0) {
  465. let pagei = this._pagesLoading.shift();
  466. this.pdf.getPage(pagei).then(function(page) {
  467. // Render the page and update the information about the page with the loaded values
  468. this._renderPage(page, pagei);
  469. }.bind(this)).then(function(pageinfo) {
  470. // Once loaded, we are not loading anymore
  471. if (this._pagesLoading.length > 0) {
  472. this._loadingTask();
  473. }
  474. }.bind(this));
  475. }
  476. // Free the loading state
  477. this._loading = false;
  478. }
  479. /**
  480. * Function that sets the scroll position of the container to the specified page
  481. * @param {*} i - the number of the page to set the scroll position
  482. */
  483. scrollToPage(i) {
  484. if ((this.pages.length === 0) || (this.pages[i] === undefined)) {
  485. return;
  486. }
  487. let $page = this.pages[i].$div;
  488. if ($page.length === 0) {
  489. console.warn(`Page ${i} not found`);
  490. return;
  491. }
  492. let position = $page.position();
  493. let containerPosition = this.$container.position();
  494. if (position !== undefined) {
  495. this.$container.get(0).scrollTop = this.$container.get(0).scrollTop + position.top - containerPosition.top;
  496. this.$container.get(0).scrollLeft = this.$container.get(0).scrollLeft + position.left - containerPosition.left;
  497. }
  498. this._setActivePage(i);
  499. }
  500. /**
  501. * Function that renders the page in a canvas, and sets the canvas into the $div
  502. * @param {*} page - the page to be rendered
  503. * @param {*} i - the number of the page to be rendered
  504. * @returns a promise to render the page (the result of the promise will be the pageinfo)
  505. */
  506. _renderPage(page, i) {
  507. // Get the pageinfo structure
  508. let pageinfo = this.pages[i];
  509. let scale = this.settings.renderingScale;
  510. // Calculate the pixel ratio of the device (we'll use a minimum of 1)
  511. let pixel_ratio = window.devicePixelRatio || 1;
  512. // Update the information that we know about the page to the actually loaded page
  513. let viewport = page.getViewport({rotation: this._rotation, scale: this._zoom.current * scale});
  514. pageinfo.width = (viewport.width / this._zoom.current) / scale;
  515. pageinfo.height = (viewport.height / this._zoom.current) / scale;
  516. pageinfo.$div.data("width", pageinfo.width);
  517. pageinfo.$div.data("height", pageinfo.height);
  518. pageinfo.$div.width(pageinfo.width * this._zoom.current);
  519. pageinfo.$div.height(pageinfo.height * this._zoom.current);
  520. pageinfo.loaded = true;
  521. // Create the canvas and prepare the rendering context
  522. let $canvas = $('<canvas></canvas>');
  523. let canvas = $canvas.get(0);
  524. let context = canvas.getContext('2d');
  525. canvas.height = viewport.height * pixel_ratio;
  526. canvas.width = viewport.width * pixel_ratio;
  527. canvas.getContext("2d")//.scale(pixel_ratio, pixel_ratio);
  528. var transform = pixel_ratio !== 1
  529. ? [pixel_ratio, 0, 0, pixel_ratio, 0, 0]
  530. : null;
  531. var renderContext = {
  532. canvasContext: context,
  533. viewport: viewport,
  534. transform: transform,
  535. };
  536. // Render the page and put the resulting rendered canvas into the page $div
  537. return page.render(renderContext).promise.then(function() {
  538. this._setPageContent(pageinfo.$div, $canvas);
  539. // Call the callback (if provided)
  540. if (this._documentReady) {
  541. if (typeof this.settings.onPageRender === "function") {
  542. this.settings.onPageRender.call(this, pageinfo.$div, i);
  543. }
  544. this.$container.get(0).dispatchEvent(new CustomEvent("pagerender", { detail: { pageNumber: i, page: pageinfo.$div.get(0) } }));
  545. }
  546. return pageinfo;
  547. }.bind(this));
  548. }
  549. /** Gets the div object corresponding to the active page */
  550. getActivePage() {
  551. if ((this._activePage === null) || (this.pdf === null)) {
  552. return null;
  553. }
  554. if ((this._activePage < 1) || (this._activePage > this.pdf.numPages)) {
  555. return null;
  556. }
  557. return this.pages[this._activePage].$div;
  558. }
  559. /** Gets all the pages of the document (the pageinfo structures) */
  560. getPages() {
  561. return this.pages;
  562. }
  563. /** Gets the number of pages of the document */
  564. getPageCount() {
  565. if (this.pdf === null) {
  566. return 0;
  567. }
  568. return this.pdf.numPages;
  569. }
  570. /** Scrolls to the next page (if any) */
  571. next() {
  572. if (this._activePage < this.pdf.numPages) {
  573. this.scrollToPage(this._activePage + 1);
  574. }
  575. }
  576. /** Scrolls to the previous page (if any) */
  577. prev() {
  578. if (this._activePage > 1) {
  579. this.scrollToPage(this._activePage - 1);
  580. }
  581. }
  582. first() {
  583. if (this._activePage !== 1) {
  584. this.scrollToPage(1);
  585. }
  586. }
  587. last() {
  588. if (this.pdf === null)
  589. return;
  590. if (this._activePage !== this.pdf.numPages) {
  591. this.scrollToPage(this.pdf.numPages);
  592. }
  593. }
  594. /**
  595. * Rotates the pages of the document
  596. * @param {*} deg - degrees to rotate the pages
  597. * @param {*} accumulate - whether the rotation is accumulated or not
  598. */
  599. rotate(deg, accumulate = false) {
  600. if (accumulate) {
  601. deg = deg + this._rotation;
  602. }
  603. this._rotation = deg;
  604. let container = this.$container.get(0);
  605. let prevScroll = {
  606. top: container.scrollTop,
  607. left: container.scrollLeft,
  608. height: container.scrollHeight,
  609. width: container.scrollWidth
  610. };
  611. return this.forceViewerInitialization().then(function() {
  612. let newScroll = {
  613. top: container.scrollTop,
  614. left: container.scrollLeft,
  615. height: container.scrollHeight,
  616. width: container.scrollWidth
  617. };
  618. container.scrollTop = prevScroll.top * (newScroll.height / prevScroll.height);
  619. container.scrollLeft = prevScroll.left * (newScroll.width / prevScroll.width);
  620. }.bind(this));
  621. }
  622. /**
  623. * This functions forces the creation of the whole content of the viewer (i.e. new divs, structures, etc.). It is usefull for full refresh of the viewer (e.g. when changes
  624. * the rotation of the pages)
  625. * @returns a promise that is resolved when the viewer is fully initialized
  626. */
  627. forceViewerInitialization() {
  628. // Store the pdf file
  629. // Now prepare a placeholder for the pages
  630. this.pages = [];
  631. // Remove all the pages
  632. this.$container.find(`.${this.settings.pageClass}`).remove();
  633. this._pagesLoading = [];
  634. this._loading = false;
  635. this._visibles = [];
  636. this._activePage = null;
  637. return this.pdf.getPage(1).then(function(page) {
  638. this._createSkeletons(page);
  639. this._visiblePages();
  640. this._setActivePage(1);
  641. }.bind(this));
  642. }
  643. /**
  644. * Loads the document and creates the pages
  645. * @param {string} document - the url of the document to load
  646. */
  647. async loadDocument(document) {
  648. // The document is not ready while loading
  649. this._documentReady = false;
  650. // Now prepare a placeholder for the pages
  651. this.pages = [];
  652. // Remove all the pages
  653. this.$container.find(`.${this.settings.pageClass}`).remove();
  654. // Let's free the pdf file (if there was one before), and rely on the garbage collector to free the memory
  655. this.pdf = null;
  656. // Load the task and return the promise to load the document
  657. let loadingTask = pdfjsLib.getDocument(document);
  658. return loadingTask.promise.then(function(pdf) {
  659. // Store the pdf file and get the
  660. this.pdf = pdf;
  661. this.pageCount = pdf.numPages;
  662. this._rotation = 0;
  663. return this.forceViewerInitialization();
  664. }.bind(this)).then(function() {
  665. if (typeof this.settings.onDocumentReady === "function") {
  666. this.settings.onDocumentReady.call(this);
  667. }
  668. this.$container.get(0).dispatchEvent(new CustomEvent("documentready", { detail: { document: this.pdf } }));
  669. // This is a trick to force active page changed event triggering after the document is ready
  670. this._setActivePage(0)
  671. this._documentReady = true;
  672. this._setActivePage(1)
  673. }.bind(this));
  674. }
  675. }
  676. function recoverAttributes(target, attributeDefaults) {
  677. const camelcaseToSnakecase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
  678. let $target = $(target);
  679. let result = {};
  680. if ($target.length > 0) {
  681. $target = $($target[0]);
  682. for (let originalAttributeName in attributeDefaults) {
  683. let attributeName = camelcaseToSnakecase(originalAttributeName)
  684. let attributeValue = $target.attr(attributeName);
  685. if (attributeValue != null) {
  686. switch (typeof(attributeDefaults[originalAttributeName])) {
  687. case 'float':
  688. try {
  689. attributeValue = parseFloat(attributeValue);
  690. } catch (_) {
  691. }
  692. break;
  693. case 'number':
  694. try {
  695. attributeValue = parseInt(attributeValue);
  696. } catch (_) {
  697. }
  698. break;
  699. case 'function':
  700. let functionString = attributeValue;
  701. attributeValue = function() { eval(functionString); }.bind(target[0]); break;
  702. default:
  703. break;
  704. }
  705. result[originalAttributeName] = attributeValue;
  706. }
  707. };
  708. }
  709. return result;
  710. }
  711. function init(element) {
  712. let options = recoverAttributes(element, Object.assign({
  713. pdfDocument: "", initialZoom: ""
  714. }, defaults));
  715. if (options["pdfDocument"] != null) {
  716. let pdfViewer = new PDFjsViewer($(element), options);
  717. pdfViewer.loadDocument(options["pdfDocument"]).then(function() {
  718. if (options["initialZoom"] != null) {
  719. pdfViewer.setZoom(options["initialZoom"]);
  720. }
  721. })
  722. element.get(0).pdfViewer = pdfViewer;
  723. }
  724. }
  725. $(function() {
  726. $('.pdfjs-viewer').each(function() {
  727. let $viewer = $(this);
  728. init($viewer);
  729. })
  730. });
  731. exports.PDFjsViewer = PDFjsViewer;
  732. })(window, jQuery)