pdfRenderer.js 19 KB

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