mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
7b997f3946
* updated pdf.js to 2.5.207, and added worker-loader as a devDependency * updated pdf.js loaded to load ES5 build instead of the default one * replaced imports with named imports due to changes on pdf.js worker loading There are some hash downgrades in the lockfile. Running the commands through the appropriate methods yields the same result consistency GitOrigin-RevId: 37be3901abf1044d93d83cb684e4e32721550d5a
535 lines
17 KiB
JavaScript
535 lines
17 KiB
JavaScript
/* eslint-disable
|
|
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 { getDocument, renderTextLayer } from './pdfJsLoader'
|
|
|
|
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 = 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.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()
|
|
})
|
|
})
|
|
this.errorCallback = this.options.errorCallback
|
|
this.pageSizeChangeCallback = this.options.pageSizeChangeCallback
|
|
this.pdfjs.promise.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 (let timer of Array.from(this.spinTimer || [])) {
|
|
clearTimeout(timer)
|
|
}
|
|
for (let 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 (let 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 (let 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
|
|
__guardMethod__(window.Raven, 'captureMessage', o =>
|
|
o.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
|
|
pageState.renderTask = this.doRender(element, pagenum, pageObject)
|
|
return pageState.renderTask.promise.then(
|
|
() => {
|
|
// 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: 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()
|
|
})
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|