pdfRenderer.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /* eslint-disable
  2. handle-callback-err,
  3. max-len,
  4. new-cap,
  5. no-return-assign,
  6. no-undef,
  7. no-unused-vars,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS101: Remove unnecessary use of Array.from
  14. * DS102: Remove unnecessary code created because of implicit returns
  15. * DS103: Rewrite code to no longer use __guard__
  16. * DS205: Consider reworking code to avoid use of IIFEs
  17. * DS206: Consider reworking classes to avoid initClass
  18. * DS207: Consider shorter variations of null checks
  19. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  20. */
  21. define(['base', './pdfJsLoader'], (App, PDFJS) =>
  22. // App = angular.module 'PDFRenderer', ['pdfAnnotations', 'pdfTextLayer']
  23. App.factory('PDFRenderer', function(
  24. $timeout,
  25. pdfAnnotations,
  26. pdfTextLayer,
  27. pdfSpinner
  28. ) {
  29. let PDFRenderer
  30. return (PDFRenderer = (function() {
  31. PDFRenderer = class PDFRenderer {
  32. static initClass() {
  33. this.prototype.JOB_QUEUE_INTERVAL = 25
  34. this.prototype.PAGE_LOAD_TIMEOUT = 60 * 1000
  35. this.prototype.INDICATOR_DELAY1 = 100 // time to delay before showing the indicator
  36. this.prototype.INDICATOR_DELAY2 = 250 // time until the indicator starts animating
  37. this.prototype.TEXTLAYER_TIMEOUT = 100
  38. }
  39. constructor(url, options) {
  40. // set up external character mappings - needed for Japanese etc
  41. this.url = url
  42. this.options = options
  43. this.scale = this.options.scale || 1
  44. let disableFontFace
  45. if (
  46. __guard__(
  47. window.location != null ? window.location.search : undefined,
  48. x => x.indexOf('disable-font-face=true')
  49. ) >= 0
  50. ) {
  51. disableFontFace = true
  52. } else {
  53. disableFontFace = false
  54. }
  55. this.pdfjs = PDFJS.getDocument({
  56. url: this.url,
  57. cMapUrl: window.pdfCMapsPath,
  58. cMapPacked: true,
  59. disableFontFace,
  60. // Enable fetching with Range headers to restrict individual
  61. // requests to 128kb.
  62. // To do this correctly we must:
  63. // a) disable auto-fetching of the whole file upfront
  64. // b) disable streaming (which in this context means streaming of
  65. // the response into memory). This isn't supported when using
  66. // Range headers, but shouldn't be a problem since we are already
  67. // limiting individual response size through chunked range
  68. // requests
  69. rangeChunkSize: 128 * 1024,
  70. disableAutoFetch: !!this.options.disableAutoFetch,
  71. disableStream: !!this.options.disableAutoFetch
  72. })
  73. this.pdfjs.onProgress = this.options.progressCallback
  74. this.document = this.pdfjs
  75. this.navigateFn = this.options.navigateFn
  76. this.spinner = new pdfSpinner()
  77. this.resetState()
  78. this.document.promise.then(pdfDocument => {
  79. return pdfDocument.getDownloadInfo().then(() => {
  80. return this.options.loadedCallback()
  81. })
  82. })
  83. this.errorCallback = this.options.errorCallback
  84. this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
  85. this.pdfjs.promise.catch(exception => {
  86. // error getting document
  87. return this.errorCallback(exception)
  88. })
  89. }
  90. resetState() {
  91. this.renderQueue = []
  92. if (this.queueTimer != null) {
  93. clearTimeout(this.queueTimer)
  94. }
  95. // clear any existing timers, render tasks
  96. for (let timer of Array.from(this.spinTimer || [])) {
  97. clearTimeout(timer)
  98. }
  99. for (let page of Array.from(this.pageState || [])) {
  100. __guard__(page != null ? page.loadTask : undefined, x => x.cancel())
  101. __guard__(page != null ? page.renderTask : undefined, x1 =>
  102. x1.cancel()
  103. )
  104. }
  105. // initialise/reset the state
  106. this.pageState = []
  107. this.spinTimer = [] // timers for starting the spinners (to avoid jitter)
  108. this.spinTimerDone = [] // array of pages where the spinner has activated
  109. return (this.jobs = 0)
  110. }
  111. getNumPages() {
  112. return this.document.promise.then(pdfDocument => pdfDocument.numPages)
  113. }
  114. getPage(pageNum) {
  115. return this.document.promise.then(pdfDocument =>
  116. pdfDocument.getPage(pageNum)
  117. )
  118. }
  119. getPdfViewport(pageNum, scale) {
  120. if (scale == null) {
  121. ;({ scale } = this)
  122. }
  123. return this.document.promise.then(pdfDocument => {
  124. return pdfDocument.getPage(pageNum).then(
  125. function(page) {
  126. let viewport
  127. return (viewport = page.getViewport({ scale: scale }))
  128. },
  129. error => {
  130. return typeof this.errorCallback === 'function'
  131. ? this.errorCallback(error)
  132. : undefined
  133. }
  134. )
  135. })
  136. }
  137. getDestinations() {
  138. return this.document.promise.then(pdfDocument =>
  139. pdfDocument.getDestinations()
  140. )
  141. }
  142. getDestination(dest) {
  143. return this.document.promise.then(
  144. pdfDocument => pdfDocument.getDestination(dest),
  145. error => {
  146. return typeof this.errorCallback === 'function'
  147. ? this.errorCallback(error)
  148. : undefined
  149. }
  150. )
  151. }
  152. getPageIndex(ref) {
  153. return this.document.promise.then(pdfDocument => {
  154. return pdfDocument.getPageIndex(ref).then(
  155. idx => idx,
  156. error => {
  157. return typeof this.errorCallback === 'function'
  158. ? this.errorCallback(error)
  159. : undefined
  160. }
  161. )
  162. })
  163. }
  164. getScale() {
  165. return this.scale
  166. }
  167. setScale(scale) {
  168. this.scale = scale
  169. return this.resetState()
  170. }
  171. triggerRenderQueue(interval) {
  172. if (interval == null) {
  173. interval = this.JOB_QUEUE_INTERVAL
  174. }
  175. if (this.queueTimer != null) {
  176. clearTimeout(this.queueTimer)
  177. }
  178. return (this.queueTimer = setTimeout(() => {
  179. this.queueTimer = null
  180. return this.processRenderQueue()
  181. }, interval))
  182. }
  183. removeCompletedJob(pagenum) {
  184. this.jobs = this.jobs - 1
  185. return this.triggerRenderQueue(0)
  186. }
  187. renderPages(pages) {
  188. if (this.shuttingDown) {
  189. return
  190. }
  191. this.renderQueue = Array.from(pages).map(page => ({
  192. element: page.elementChildren,
  193. pagenum: page.pageNum
  194. }))
  195. return this.triggerRenderQueue()
  196. }
  197. renderPage(page) {
  198. if (this.shuttingDown) {
  199. return
  200. }
  201. const current = {
  202. element: page.elementChildren,
  203. pagenum: page.pageNum
  204. }
  205. this.renderQueue.push(current)
  206. return this.processRenderQueue()
  207. }
  208. getPageDetails(page) {
  209. return [page.element.canvas, page.pagenum]
  210. }
  211. // handle the loading indicators for each page
  212. startIndicators() {
  213. // make an array of the pages in the queue
  214. this.queuedPages = []
  215. for (var page of Array.from(this.renderQueue)) {
  216. this.queuedPages[page.pagenum] = true
  217. }
  218. // clear any unfinished spinner timers on pages that aren't in the queue any more
  219. for (let pagenum in this.spinTimer) {
  220. if (!this.queuedPages[pagenum]) {
  221. clearTimeout(this.spinTimer[pagenum])
  222. delete this.spinTimer[pagenum]
  223. }
  224. }
  225. // add indicators for any new pages in the current queue
  226. return (() => {
  227. const result = []
  228. for (page of Array.from(this.renderQueue)) {
  229. if (
  230. !this.spinTimer[page.pagenum] &&
  231. !this.spinTimerDone[page.pagenum]
  232. ) {
  233. result.push(this.startIndicator(page))
  234. }
  235. }
  236. return result
  237. })()
  238. }
  239. startIndicator(page) {
  240. const [canvas, pagenum] = Array.from(this.getPageDetails(page))
  241. canvas.addClass('pdfng-loading')
  242. return (this.spinTimer[pagenum] = setTimeout(() => {
  243. for (let queuedPage of Array.from(this.renderQueue)) {
  244. if (pagenum === queuedPage.pagenum) {
  245. this.spinner.add(canvas, { static: true })
  246. this.spinTimerDone[pagenum] = true
  247. break
  248. }
  249. }
  250. return delete this.spinTimer[pagenum]
  251. }, this.INDICATOR_DELAY1))
  252. }
  253. updateIndicator(page) {
  254. const [canvas, pagenum] = Array.from(this.getPageDetails(page))
  255. // did the spinner insert itself already?
  256. if (this.spinTimerDone[pagenum]) {
  257. return (this.spinTimer[pagenum] = setTimeout(() => {
  258. this.spinner.start(canvas)
  259. return delete this.spinTimer[pagenum]
  260. }, this.INDICATOR_DELAY2))
  261. } else {
  262. // stop the existing spin timer
  263. clearTimeout(this.spinTimer[pagenum])
  264. // start a new one which will also start spinning
  265. return (this.spinTimer[pagenum] = setTimeout(() => {
  266. this.spinner.add(canvas, { static: true })
  267. this.spinTimerDone[pagenum] = true
  268. return (this.spinTimer[pagenum] = setTimeout(() => {
  269. this.spinner.start(canvas)
  270. return delete this.spinTimer[pagenum]
  271. }, this.INDICATOR_DELAY2))
  272. }, this.INDICATOR_DELAY1))
  273. }
  274. }
  275. clearIndicator(page) {
  276. const [canvas, pagenum] = Array.from(this.getPageDetails(page))
  277. this.spinner.stop(canvas)
  278. clearTimeout(this.spinTimer[pagenum])
  279. delete this.spinTimer[pagenum]
  280. return (this.spinTimerDone[pagenum] = true)
  281. }
  282. // handle the queue of pages to be rendered
  283. processRenderQueue() {
  284. let pageState
  285. if (this.shuttingDown) {
  286. return
  287. }
  288. // mark all pages in the queue as loading
  289. this.startIndicators()
  290. // bail out if there is already a render job running
  291. if (this.jobs > 0) {
  292. return
  293. }
  294. // take the first page in the queue
  295. let page = this.renderQueue.shift()
  296. // check if it is in action already
  297. while (page != null && this.pageState[page.pagenum] != null) {
  298. page = this.renderQueue.shift()
  299. }
  300. if (page == null) {
  301. return
  302. }
  303. const [element, pagenum] = Array.from([page.element, page.pagenum])
  304. this.jobs = this.jobs + 1
  305. // update the spinner to make it spinning (signifies loading has begun)
  306. this.updateIndicator(page)
  307. let timedOut = false
  308. const timer = $timeout(() => {
  309. // page load timed out
  310. if (loadTask.cancelled) {
  311. return
  312. } // return from cancelled page load
  313. __guardMethod__(window.Raven, 'captureMessage', o =>
  314. o.captureMessage(
  315. `pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
  316. )
  317. )
  318. timedOut = true
  319. this.clearIndicator(page)
  320. // @jobs = @jobs - 1
  321. // @triggerRenderQueue(0)
  322. return typeof this.errorCallback === 'function'
  323. ? this.errorCallback('timeout')
  324. : undefined
  325. }, this.PAGE_LOAD_TIMEOUT)
  326. var loadTask = this.getPage(pagenum)
  327. loadTask.cancel = function() {
  328. return (this.cancelled = true)
  329. }
  330. this.pageState[pagenum] = pageState = { loadTask }
  331. return loadTask
  332. .then(pageObject => {
  333. // page load success
  334. $timeout.cancel(timer)
  335. if (loadTask.cancelled) {
  336. return
  337. } // return from cancelled page load
  338. pageState.renderTask = this.doRender(element, pagenum, pageObject)
  339. return pageState.renderTask.promise.then(
  340. () => {
  341. // render task success
  342. this.clearIndicator(page)
  343. pageState.complete = true
  344. delete pageState.renderTask
  345. return this.removeCompletedJob(pagenum)
  346. },
  347. () => {
  348. // render task failed
  349. // could display an error icon
  350. pageState.complete = false
  351. delete pageState.renderTask
  352. return this.removeCompletedJob(pagenum)
  353. }
  354. )
  355. })
  356. .catch(error => {
  357. // page load error
  358. $timeout.cancel(timer)
  359. return this.clearIndicator(page)
  360. })
  361. }
  362. doRender(element, pagenum, page) {
  363. const self = this
  364. const { scale } = this
  365. if (scale == null) {
  366. // scale is undefined, returning
  367. return
  368. }
  369. const canvas = $(
  370. '<canvas class="pdf-canvas pdfng-rendering"></canvas>'
  371. )
  372. // In Windows+IE we must have the canvas in the DOM during
  373. // rendering to see the fonts defined in the DOM. If we try to
  374. // render 'offscreen' then all the text will be sans-serif.
  375. // Previously we rendered offscreen and added in the canvas
  376. // when rendering was complete.
  377. element.canvas.replaceWith(canvas)
  378. const viewport = page.getViewport({ scale: scale })
  379. const devicePixelRatio = window.devicePixelRatio || 1
  380. const ctx = canvas[0].getContext('2d')
  381. const backingStoreRatio =
  382. ctx.webkitBackingStorePixelRatio ||
  383. ctx.mozBackingStorePixelRatio ||
  384. ctx.msBackingStorePixelRatio ||
  385. ctx.oBackingStorePixelRatio ||
  386. ctx.backingStorePixelRatio ||
  387. 1
  388. const pixelRatio = devicePixelRatio / backingStoreRatio
  389. const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
  390. const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
  391. const newWidth = Math.floor(viewport.width)
  392. const newHeight = Math.floor(viewport.height)
  393. canvas[0].height = scaledHeight
  394. canvas[0].width = scaledWidth
  395. canvas.height(newHeight + 'px')
  396. canvas.width(newWidth + 'px')
  397. const oldHeight = element.canvas.height()
  398. const oldWidth = element.canvas.width()
  399. if (newHeight !== oldHeight || newWidth !== oldWidth) {
  400. element.canvas.height(newHeight + 'px')
  401. element.canvas.width(newWidth + 'px')
  402. element.container.height(newHeight + 'px')
  403. element.container.width(newWidth + 'px')
  404. if (typeof this.pageSizeChangeCallback === 'function') {
  405. this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
  406. }
  407. }
  408. const textLayer = new pdfTextLayer({
  409. textLayerDiv: element.text[0],
  410. viewport,
  411. renderer: PDFJS.renderTextLayer
  412. })
  413. const annotationsLayer = new pdfAnnotations({
  414. annotations: element.annotations[0],
  415. viewport,
  416. navigateFn: this.navigateFn
  417. })
  418. const result = page.render({
  419. canvasContext: ctx,
  420. viewport,
  421. transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
  422. })
  423. const textLayerTimeout = this.TEXTLAYER_TIMEOUT
  424. result.promise
  425. .then(function() {
  426. // page render success
  427. canvas.removeClass('pdfng-rendering')
  428. page.getTextContent({ normalizeWhitespace: true }).then(
  429. function(textContent) {
  430. textLayer.setTextContent(textContent)
  431. return textLayer.render(textLayerTimeout)
  432. },
  433. error =>
  434. typeof self.errorCallback === 'function'
  435. ? self.errorCallback(error)
  436. : undefined
  437. )
  438. return page
  439. .getAnnotations()
  440. .then(
  441. annotations => annotationsLayer.setAnnotations(annotations),
  442. error =>
  443. typeof self.errorCallback === 'function'
  444. ? self.errorCallback(error)
  445. : undefined
  446. )
  447. })
  448. .catch(function(error) {
  449. // page render failed
  450. if (error.name === 'RenderingCancelledException') {
  451. // do nothing when cancelled
  452. } else {
  453. return typeof self.errorCallback === 'function'
  454. ? self.errorCallback(error)
  455. : undefined
  456. }
  457. })
  458. return result
  459. }
  460. destroy() {
  461. this.shuttingDown = true
  462. this.resetState()
  463. return this.pdfjs.promise.then(function(document) {
  464. document.cleanup()
  465. return document.destroy()
  466. })
  467. }
  468. }
  469. PDFRenderer.initClass()
  470. return PDFRenderer
  471. })())
  472. }))
  473. function __guard__(value, transform) {
  474. return typeof value !== 'undefined' && value !== null
  475. ? transform(value)
  476. : undefined
  477. }
  478. function __guardMethod__(obj, methodName, transform) {
  479. if (
  480. typeof obj !== 'undefined' &&
  481. obj !== null &&
  482. typeof obj[methodName] === 'function'
  483. ) {
  484. return transform(obj, methodName)
  485. } else {
  486. return undefined
  487. }
  488. }