pdfRenderer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /* eslint-disable
  2. node/handle-callback-err,
  3. max-len,
  4. new-cap,
  5. no-return-assign,
  6. no-unused-vars,
  7. */
  8. // TODO: This file was created by bulk-decaffeinate.
  9. // Fix any style issues and re-enable lint.
  10. /*
  11. * decaffeinate suggestions:
  12. * DS101: Remove unnecessary use of Array.from
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS103: Rewrite code to no longer use __guard__
  15. * DS205: Consider reworking code to avoid use of IIFEs
  16. * DS206: Consider reworking classes to avoid initClass
  17. * DS207: Consider shorter variations of null checks
  18. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  19. */
  20. import App from '../../../base'
  21. import PDFJS from './pdfJsLoader'
  22. import { captureMessage } from '../../../infrastructure/error-reporter'
  23. export default 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. captureMessage(
  314. `pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
  315. )
  316. timedOut = true
  317. this.clearIndicator(page)
  318. // @jobs = @jobs - 1
  319. // @triggerRenderQueue(0)
  320. return typeof this.errorCallback === 'function'
  321. ? this.errorCallback('timeout')
  322. : undefined
  323. }, this.PAGE_LOAD_TIMEOUT)
  324. var loadTask = this.getPage(pagenum)
  325. loadTask.cancel = function() {
  326. return (this.cancelled = true)
  327. }
  328. this.pageState[pagenum] = pageState = { loadTask }
  329. return loadTask
  330. .then(pageObject => {
  331. // page load success
  332. $timeout.cancel(timer)
  333. if (loadTask.cancelled) {
  334. return
  335. } // return from cancelled page load
  336. pageState.renderTask = this.doRender(element, pagenum, pageObject)
  337. return pageState.renderTask.promise.then(
  338. () => {
  339. // render task success
  340. this.clearIndicator(page)
  341. pageState.complete = true
  342. delete pageState.renderTask
  343. return this.removeCompletedJob(pagenum)
  344. },
  345. () => {
  346. // render task failed
  347. // could display an error icon
  348. pageState.complete = false
  349. delete pageState.renderTask
  350. return this.removeCompletedJob(pagenum)
  351. }
  352. )
  353. })
  354. .catch(error => {
  355. // page load error
  356. $timeout.cancel(timer)
  357. return this.clearIndicator(page)
  358. })
  359. }
  360. doRender(element, pagenum, page) {
  361. const self = this
  362. const { scale } = this
  363. if (scale == null) {
  364. // scale is undefined, returning
  365. return
  366. }
  367. const canvas = $('<canvas class="pdf-canvas pdfng-rendering"></canvas>')
  368. // In Windows+IE we must have the canvas in the DOM during
  369. // rendering to see the fonts defined in the DOM. If we try to
  370. // render 'offscreen' then all the text will be sans-serif.
  371. // Previously we rendered offscreen and added in the canvas
  372. // when rendering was complete.
  373. element.canvas.replaceWith(canvas)
  374. const viewport = page.getViewport({ scale: scale })
  375. const devicePixelRatio = window.devicePixelRatio || 1
  376. const ctx = canvas[0].getContext('2d')
  377. const backingStoreRatio =
  378. ctx.webkitBackingStorePixelRatio ||
  379. ctx.mozBackingStorePixelRatio ||
  380. ctx.msBackingStorePixelRatio ||
  381. ctx.oBackingStorePixelRatio ||
  382. ctx.backingStorePixelRatio ||
  383. 1
  384. const pixelRatio = devicePixelRatio / backingStoreRatio
  385. const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
  386. const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
  387. const newWidth = Math.floor(viewport.width)
  388. const newHeight = Math.floor(viewport.height)
  389. canvas[0].height = scaledHeight
  390. canvas[0].width = scaledWidth
  391. canvas.height(newHeight + 'px')
  392. canvas.width(newWidth + 'px')
  393. const oldHeight = element.canvas.height()
  394. const oldWidth = element.canvas.width()
  395. if (newHeight !== oldHeight || newWidth !== oldWidth) {
  396. element.canvas.height(newHeight + 'px')
  397. element.canvas.width(newWidth + 'px')
  398. element.container.height(newHeight + 'px')
  399. element.container.width(newWidth + 'px')
  400. if (typeof this.pageSizeChangeCallback === 'function') {
  401. this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
  402. }
  403. }
  404. const textLayer = new pdfTextLayer({
  405. textLayerDiv: element.text[0],
  406. viewport,
  407. renderer: PDFJS.renderTextLayer
  408. })
  409. const annotationsLayer = new pdfAnnotations({
  410. annotations: element.annotations[0],
  411. viewport,
  412. navigateFn: this.navigateFn
  413. })
  414. const result = page.render({
  415. canvasContext: ctx,
  416. viewport,
  417. transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
  418. })
  419. const textLayerTimeout = this.TEXTLAYER_TIMEOUT
  420. result.promise
  421. .then(function() {
  422. // page render success
  423. canvas.removeClass('pdfng-rendering')
  424. page.getTextContent({ normalizeWhitespace: true }).then(
  425. function(textContent) {
  426. textLayer.setTextContent(textContent)
  427. return textLayer.render(textLayerTimeout)
  428. },
  429. error =>
  430. typeof self.errorCallback === 'function'
  431. ? self.errorCallback(error)
  432. : undefined
  433. )
  434. return page.getAnnotations().then(
  435. annotations => annotationsLayer.setAnnotations(annotations),
  436. error =>
  437. typeof self.errorCallback === 'function'
  438. ? self.errorCallback(error)
  439. : undefined
  440. )
  441. })
  442. .catch(function(error) {
  443. // page render failed
  444. if (error.name === 'RenderingCancelledException') {
  445. // do nothing when cancelled
  446. } else {
  447. return typeof self.errorCallback === 'function'
  448. ? self.errorCallback(error)
  449. : undefined
  450. }
  451. })
  452. return result
  453. }
  454. destroy() {
  455. this.shuttingDown = true
  456. this.resetState()
  457. return this.pdfjs.promise.then(function(document) {
  458. document.cleanup()
  459. return document.destroy()
  460. })
  461. }
  462. }
  463. PDFRenderer.initClass()
  464. return PDFRenderer
  465. })())
  466. })
  467. function __guard__(value, transform) {
  468. return typeof value !== 'undefined' && value !== null
  469. ? transform(value)
  470. : undefined
  471. }
  472. function __guardMethod__(obj, methodName, transform) {
  473. if (
  474. typeof obj !== 'undefined' &&
  475. obj !== null &&
  476. typeof obj[methodName] === 'function'
  477. ) {
  478. return transform(obj, methodName)
  479. } else {
  480. return undefined
  481. }
  482. }