mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
86099725ad
[frontend] fix broken PDF preview caused by missing error handling GitOrigin-RevId: a7bba8d1b351a53fb20eb670d2ae4c6d215d0d85
558 lines
19 KiB
JavaScript
558 lines
19 KiB
JavaScript
/* eslint-disable
|
|
node/handle-callback-err,
|
|
max-len,
|
|
new-cap,
|
|
no-return-assign,
|
|
no-unused-vars,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
* DS206: Consider reworking classes to avoid initClass
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import App from '../../../base'
|
|
import PDFJS from './pdfJsLoader'
|
|
import { captureMessage } from '../../../infrastructure/error-reporter'
|
|
|
|
export default App.factory(
|
|
'PDFRenderer',
|
|
function ($timeout, pdfAnnotations, pdfTextLayer, pdfSpinner) {
|
|
let PDFRenderer
|
|
return (PDFRenderer = (function () {
|
|
PDFRenderer = class PDFRenderer {
|
|
static initClass() {
|
|
this.prototype.JOB_QUEUE_INTERVAL = 25
|
|
this.prototype.PAGE_LOAD_TIMEOUT = 60 * 1000
|
|
this.prototype.INDICATOR_DELAY1 = 100 // time to delay before showing the indicator
|
|
this.prototype.INDICATOR_DELAY2 = 250 // time until the indicator starts animating
|
|
this.prototype.TEXTLAYER_TIMEOUT = 100
|
|
}
|
|
|
|
constructor(url, options) {
|
|
// set up external character mappings - needed for Japanese etc
|
|
this.url = url
|
|
this.options = options
|
|
|
|
this.scale = this.options.scale || 1
|
|
|
|
let disableFontFace
|
|
if (
|
|
__guard__(
|
|
window.location != null ? window.location.search : undefined,
|
|
x => x.indexOf('disable-font-face=true')
|
|
) >= 0
|
|
) {
|
|
disableFontFace = true
|
|
} else {
|
|
disableFontFace = false
|
|
}
|
|
this.pdfjs = PDFJS.getDocument({
|
|
url: this.url,
|
|
cMapUrl: window.pdfCMapsPath,
|
|
cMapPacked: true,
|
|
disableFontFace,
|
|
// Enable fetching with Range headers to restrict individual
|
|
// requests to 128kb.
|
|
// To do this correctly we must:
|
|
// a) disable auto-fetching of the whole file upfront
|
|
// b) disable streaming (which in this context means streaming of
|
|
// the response into memory). This isn't supported when using
|
|
// Range headers, but shouldn't be a problem since we are already
|
|
// limiting individual response size through chunked range
|
|
// requests
|
|
rangeChunkSize: 128 * 1024,
|
|
disableAutoFetch: !!this.options.disableAutoFetch,
|
|
disableStream: !!this.options.disableAutoFetch,
|
|
})
|
|
this.errorCallback = this.options.errorCallback
|
|
this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
|
|
this.pdfjs.onProgress = this.options.progressCallback
|
|
this.document = this.pdfjs
|
|
this.navigateFn = this.options.navigateFn
|
|
this.spinner = new pdfSpinner()
|
|
this.resetState()
|
|
this.document.promise
|
|
.then(pdfDocument => {
|
|
return pdfDocument.getDownloadInfo().then(() => {
|
|
return this.options.loadedCallback()
|
|
})
|
|
})
|
|
.catch(exception => {
|
|
// error getting document
|
|
return this.errorCallback(exception)
|
|
})
|
|
}
|
|
|
|
resetState() {
|
|
this.renderQueue = []
|
|
if (this.queueTimer != null) {
|
|
clearTimeout(this.queueTimer)
|
|
}
|
|
// clear any existing timers, render tasks
|
|
for (const timer of Array.from(this.spinTimer || [])) {
|
|
clearTimeout(timer)
|
|
}
|
|
for (const page of Array.from(this.pageState || [])) {
|
|
__guard__(page != null ? page.loadTask : undefined, x => x.cancel())
|
|
__guard__(page != null ? page.renderTask : undefined, x1 =>
|
|
x1.cancel()
|
|
)
|
|
}
|
|
// initialise/reset the state
|
|
this.pageState = []
|
|
this.spinTimer = [] // timers for starting the spinners (to avoid jitter)
|
|
this.spinTimerDone = [] // array of pages where the spinner has activated
|
|
return (this.jobs = 0)
|
|
}
|
|
|
|
getNumPages() {
|
|
return this.document.promise.then(pdfDocument => pdfDocument.numPages)
|
|
}
|
|
|
|
getPage(pageNum) {
|
|
return this.document.promise.then(pdfDocument =>
|
|
pdfDocument.getPage(pageNum)
|
|
)
|
|
}
|
|
|
|
getPdfViewport(pageNum, scale) {
|
|
if (scale == null) {
|
|
;({ scale } = this)
|
|
}
|
|
return this.document.promise.then(pdfDocument => {
|
|
return pdfDocument.getPage(pageNum).then(
|
|
function (page) {
|
|
let viewport
|
|
return (viewport = page.getViewport({ scale: scale }))
|
|
},
|
|
error => {
|
|
return typeof this.errorCallback === 'function'
|
|
? this.errorCallback(error)
|
|
: undefined
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
getDestinations() {
|
|
return this.document.promise.then(pdfDocument =>
|
|
pdfDocument.getDestinations()
|
|
)
|
|
}
|
|
|
|
getDestination(dest) {
|
|
return this.document.promise.then(
|
|
pdfDocument => pdfDocument.getDestination(dest),
|
|
error => {
|
|
return typeof this.errorCallback === 'function'
|
|
? this.errorCallback(error)
|
|
: undefined
|
|
}
|
|
)
|
|
}
|
|
|
|
getPageIndex(ref) {
|
|
return this.document.promise.then(pdfDocument => {
|
|
return pdfDocument.getPageIndex(ref).then(
|
|
idx => idx,
|
|
error => {
|
|
return typeof this.errorCallback === 'function'
|
|
? this.errorCallback(error)
|
|
: undefined
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
getScale() {
|
|
return this.scale
|
|
}
|
|
|
|
setScale(scale) {
|
|
this.scale = scale
|
|
return this.resetState()
|
|
}
|
|
|
|
triggerRenderQueue(interval) {
|
|
if (interval == null) {
|
|
interval = this.JOB_QUEUE_INTERVAL
|
|
}
|
|
if (this.queueTimer != null) {
|
|
clearTimeout(this.queueTimer)
|
|
}
|
|
return (this.queueTimer = setTimeout(() => {
|
|
this.queueTimer = null
|
|
return this.processRenderQueue()
|
|
}, interval))
|
|
}
|
|
|
|
removeCompletedJob(pagenum) {
|
|
this.jobs = this.jobs - 1
|
|
return this.triggerRenderQueue(0)
|
|
}
|
|
|
|
renderPages(pages) {
|
|
if (this.shuttingDown) {
|
|
return
|
|
}
|
|
this.renderQueue = Array.from(pages).map(page => ({
|
|
element: page.elementChildren,
|
|
pagenum: page.pageNum,
|
|
}))
|
|
return this.triggerRenderQueue()
|
|
}
|
|
|
|
renderPage(page) {
|
|
if (this.shuttingDown) {
|
|
return
|
|
}
|
|
const current = {
|
|
element: page.elementChildren,
|
|
pagenum: page.pageNum,
|
|
}
|
|
this.renderQueue.push(current)
|
|
return this.processRenderQueue()
|
|
}
|
|
|
|
getPageDetails(page) {
|
|
return [page.element.canvas, page.pagenum]
|
|
}
|
|
|
|
// handle the loading indicators for each page
|
|
|
|
startIndicators() {
|
|
// make an array of the pages in the queue
|
|
this.queuedPages = []
|
|
for (var page of Array.from(this.renderQueue)) {
|
|
this.queuedPages[page.pagenum] = true
|
|
}
|
|
// clear any unfinished spinner timers on pages that aren't in the queue any more
|
|
for (const pagenum in this.spinTimer) {
|
|
if (!this.queuedPages[pagenum]) {
|
|
clearTimeout(this.spinTimer[pagenum])
|
|
delete this.spinTimer[pagenum]
|
|
}
|
|
}
|
|
// add indicators for any new pages in the current queue
|
|
return (() => {
|
|
const result = []
|
|
for (page of Array.from(this.renderQueue)) {
|
|
if (
|
|
!this.spinTimer[page.pagenum] &&
|
|
!this.spinTimerDone[page.pagenum]
|
|
) {
|
|
result.push(this.startIndicator(page))
|
|
}
|
|
}
|
|
return result
|
|
})()
|
|
}
|
|
|
|
startIndicator(page) {
|
|
const [canvas, pagenum] = Array.from(this.getPageDetails(page))
|
|
canvas.addClass('pdfng-loading')
|
|
return (this.spinTimer[pagenum] = setTimeout(() => {
|
|
for (const queuedPage of Array.from(this.renderQueue)) {
|
|
if (pagenum === queuedPage.pagenum) {
|
|
this.spinner.add(canvas, { static: true })
|
|
this.spinTimerDone[pagenum] = true
|
|
break
|
|
}
|
|
}
|
|
return delete this.spinTimer[pagenum]
|
|
}, this.INDICATOR_DELAY1))
|
|
}
|
|
|
|
updateIndicator(page) {
|
|
const [canvas, pagenum] = Array.from(this.getPageDetails(page))
|
|
// did the spinner insert itself already?
|
|
if (this.spinTimerDone[pagenum]) {
|
|
return (this.spinTimer[pagenum] = setTimeout(() => {
|
|
this.spinner.start(canvas)
|
|
return delete this.spinTimer[pagenum]
|
|
}, this.INDICATOR_DELAY2))
|
|
} else {
|
|
// stop the existing spin timer
|
|
clearTimeout(this.spinTimer[pagenum])
|
|
// start a new one which will also start spinning
|
|
return (this.spinTimer[pagenum] = setTimeout(() => {
|
|
this.spinner.add(canvas, { static: true })
|
|
this.spinTimerDone[pagenum] = true
|
|
return (this.spinTimer[pagenum] = setTimeout(() => {
|
|
this.spinner.start(canvas)
|
|
return delete this.spinTimer[pagenum]
|
|
}, this.INDICATOR_DELAY2))
|
|
}, this.INDICATOR_DELAY1))
|
|
}
|
|
}
|
|
|
|
clearIndicator(page) {
|
|
const [canvas, pagenum] = Array.from(this.getPageDetails(page))
|
|
this.spinner.stop(canvas)
|
|
clearTimeout(this.spinTimer[pagenum])
|
|
delete this.spinTimer[pagenum]
|
|
return (this.spinTimerDone[pagenum] = true)
|
|
}
|
|
|
|
// handle the queue of pages to be rendered
|
|
|
|
processRenderQueue() {
|
|
let pageState
|
|
if (this.shuttingDown) {
|
|
return
|
|
}
|
|
// mark all pages in the queue as loading
|
|
this.startIndicators()
|
|
// bail out if there is already a render job running
|
|
if (this.jobs > 0) {
|
|
return
|
|
}
|
|
// take the first page in the queue
|
|
let page = this.renderQueue.shift()
|
|
// check if it is in action already
|
|
while (page != null && this.pageState[page.pagenum] != null) {
|
|
page = this.renderQueue.shift()
|
|
}
|
|
if (page == null) {
|
|
return
|
|
}
|
|
const [element, pagenum] = Array.from([page.element, page.pagenum])
|
|
this.jobs = this.jobs + 1
|
|
|
|
// update the spinner to make it spinning (signifies loading has begun)
|
|
this.updateIndicator(page)
|
|
|
|
let timedOut = false
|
|
const timer = $timeout(() => {
|
|
// page load timed out
|
|
if (loadTask.cancelled) {
|
|
return
|
|
} // return from cancelled page load
|
|
captureMessage(
|
|
`pdfng page load timed out after ${this.PAGE_LOAD_TIMEOUT}ms`
|
|
)
|
|
|
|
timedOut = true
|
|
this.clearIndicator(page)
|
|
// @jobs = @jobs - 1
|
|
// @triggerRenderQueue(0)
|
|
return typeof this.errorCallback === 'function'
|
|
? this.errorCallback('timeout')
|
|
: undefined
|
|
}, this.PAGE_LOAD_TIMEOUT)
|
|
|
|
var loadTask = this.getPage(pagenum)
|
|
|
|
loadTask.cancel = function () {
|
|
return (this.cancelled = true)
|
|
}
|
|
|
|
this.pageState[pagenum] = pageState = { loadTask }
|
|
|
|
return loadTask
|
|
.then(pageObject => {
|
|
// page load success
|
|
$timeout.cancel(timer)
|
|
if (loadTask.cancelled) {
|
|
return
|
|
} // return from cancelled page load
|
|
const timePDFFetched = performance.now()
|
|
|
|
const visible = !document.hidden
|
|
if (!visible) {
|
|
// Flush the fetch time early, omit the render time.
|
|
if (typeof this.options.firstRenderDone === 'function') {
|
|
this.options.firstRenderDone({ timePDFFetched })
|
|
// Do not submit the actual rendering time.
|
|
this.options.firstRenderDone = null
|
|
}
|
|
}
|
|
|
|
pageState.renderTask = this.doRender(element, pagenum, pageObject)
|
|
return pageState.renderTask.promise.then(
|
|
() => {
|
|
if (typeof this.options.firstRenderDone === 'function') {
|
|
const timePDFRendered = performance.now()
|
|
this.options.firstRenderDone({
|
|
timePDFFetched,
|
|
timePDFRendered,
|
|
})
|
|
// The rendering pipeline is processed repeatedly, skip the next ones.
|
|
this.options.firstRenderDone = null
|
|
}
|
|
|
|
// render task success
|
|
this.clearIndicator(page)
|
|
pageState.complete = true
|
|
delete pageState.renderTask
|
|
return this.removeCompletedJob(pagenum)
|
|
},
|
|
() => {
|
|
// render task failed
|
|
// could display an error icon
|
|
pageState.complete = false
|
|
delete pageState.renderTask
|
|
return this.removeCompletedJob(pagenum)
|
|
}
|
|
)
|
|
})
|
|
.catch(error => {
|
|
// page load error
|
|
$timeout.cancel(timer)
|
|
return this.clearIndicator(page)
|
|
})
|
|
}
|
|
|
|
doRender(element, pagenum, page) {
|
|
const self = this
|
|
const { scale } = this
|
|
|
|
if (scale == null) {
|
|
// scale is undefined, returning
|
|
return
|
|
}
|
|
|
|
const canvas = $(
|
|
'<canvas class="pdf-canvas pdfng-rendering"></canvas>'
|
|
)
|
|
// In Windows+IE we must have the canvas in the DOM during
|
|
// rendering to see the fonts defined in the DOM. If we try to
|
|
// render 'offscreen' then all the text will be sans-serif.
|
|
// Previously we rendered offscreen and added in the canvas
|
|
// when rendering was complete.
|
|
element.canvas.replaceWith(canvas)
|
|
|
|
const viewport = page.getViewport({ scale: scale })
|
|
|
|
const devicePixelRatio = window.devicePixelRatio || 1
|
|
|
|
const ctx = canvas[0].getContext('2d')
|
|
const backingStoreRatio =
|
|
ctx.webkitBackingStorePixelRatio ||
|
|
ctx.mozBackingStorePixelRatio ||
|
|
ctx.msBackingStorePixelRatio ||
|
|
ctx.oBackingStorePixelRatio ||
|
|
ctx.backingStorePixelRatio ||
|
|
1
|
|
const pixelRatio = devicePixelRatio / backingStoreRatio
|
|
|
|
const scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
|
|
const scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
|
|
|
|
const newWidth = Math.floor(viewport.width)
|
|
const newHeight = Math.floor(viewport.height)
|
|
|
|
canvas[0].height = scaledHeight
|
|
canvas[0].width = scaledWidth
|
|
|
|
canvas.height(newHeight + 'px')
|
|
canvas.width(newWidth + 'px')
|
|
|
|
const oldHeight = element.canvas.height()
|
|
const oldWidth = element.canvas.width()
|
|
if (newHeight !== oldHeight || newWidth !== oldWidth) {
|
|
element.canvas.height(newHeight + 'px')
|
|
element.canvas.width(newWidth + 'px')
|
|
element.container.height(newHeight + 'px')
|
|
element.container.width(newWidth + 'px')
|
|
if (typeof this.pageSizeChangeCallback === 'function') {
|
|
this.pageSizeChangeCallback(pagenum, newHeight - oldHeight)
|
|
}
|
|
}
|
|
|
|
const textLayer = new pdfTextLayer({
|
|
textLayerDiv: element.text[0],
|
|
viewport,
|
|
renderer: PDFJS.renderTextLayer,
|
|
})
|
|
|
|
const annotationsLayer = new pdfAnnotations({
|
|
annotations: element.annotations[0],
|
|
viewport,
|
|
navigateFn: this.navigateFn,
|
|
})
|
|
|
|
const result = page.render({
|
|
canvasContext: ctx,
|
|
viewport,
|
|
transform: [pixelRatio, 0, 0, pixelRatio, 0, 0],
|
|
})
|
|
|
|
const textLayerTimeout = this.TEXTLAYER_TIMEOUT
|
|
|
|
result.promise
|
|
.then(function () {
|
|
// page render success
|
|
canvas.removeClass('pdfng-rendering')
|
|
page.getTextContent({ normalizeWhitespace: true }).then(
|
|
function (textContent) {
|
|
textLayer.setTextContent(textContent)
|
|
return textLayer.render(textLayerTimeout)
|
|
},
|
|
error =>
|
|
typeof self.errorCallback === 'function'
|
|
? self.errorCallback(error)
|
|
: undefined
|
|
)
|
|
return page.getAnnotations().then(
|
|
annotations => annotationsLayer.setAnnotations(annotations),
|
|
error =>
|
|
typeof self.errorCallback === 'function'
|
|
? self.errorCallback(error)
|
|
: undefined
|
|
)
|
|
})
|
|
.catch(function (error) {
|
|
// page render failed
|
|
if (error.name === 'RenderingCancelledException') {
|
|
// do nothing when cancelled
|
|
} else {
|
|
return typeof self.errorCallback === 'function'
|
|
? self.errorCallback(error)
|
|
: undefined
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
destroy() {
|
|
this.shuttingDown = true
|
|
this.resetState()
|
|
return this.pdfjs.promise
|
|
.then(function (document) {
|
|
document.cleanup()
|
|
return document.destroy()
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
}
|
|
PDFRenderer.initClass()
|
|
return PDFRenderer
|
|
})())
|
|
}
|
|
)
|
|
|
|
function __guard__(value, transform) {
|
|
return typeof value !== 'undefined' && value !== null
|
|
? transform(value)
|
|
: undefined
|
|
}
|
|
function __guardMethod__(obj, methodName, transform) {
|
|
if (
|
|
typeof obj !== 'undefined' &&
|
|
obj !== null &&
|
|
typeof obj[methodName] === 'function'
|
|
) {
|
|
return transform(obj, methodName)
|
|
} else {
|
|
return undefined
|
|
}
|
|
}
|