overleaf/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
Brian Gough 7555382e33 remove the render timeout
pdfjs uses requestAnimationFrame to schedule the rendering, and this
does not fire if the tab is inactive. That causes an unwanted error if
switching tabs for more than the one minute timeout.
2016-07-12 11:51:30 +01:00

325 lines
9.6 KiB
CoffeeScript

define [
"base"
], (App) ->
# App = angular.module 'PDFRenderer', ['pdfAnnotations', 'pdfTextLayer']
App.factory 'PDFRenderer', ['$q', '$timeout', 'pdfAnnotations', 'pdfTextLayer', 'pdfSpinner', ($q, $timeout, pdfAnnotations, pdfTextLayer, pdfSpinner) ->
class PDFRenderer
JOB_QUEUE_INTERVAL: 25
PAGE_LOAD_TIMEOUT: 60*1000
INDICATOR_DELAY1: 100 # time to delay before showing the indicator
INDICATOR_DELAY2: 250 # time until the indicator starts animating
constructor: (@url, @options) ->
# PDFJS.disableFontFace = true # avoids repaints, uses worker more
if @options.disableAutoFetch
PDFJS.disableAutoFetch = true # prevent loading whole file
# PDFJS.disableStream
# PDFJS.disableRange
@scale = @options.scale || 1
@pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536}
@pdfjs.onProgress = @options.progressCallback
@document = $q.when(@pdfjs)
@navigateFn = @options.navigateFn
@spinner = new pdfSpinner
@resetState()
@document.then (pdfDocument) =>
pdfDocument.getDownloadInfo().then () =>
@options.loadedCallback()
@errorCallback = @options.errorCallback
@pageSizeChangeCallback = @options.pageSizeChangeCallback
@pdfjs.promise.catch (exception) =>
# error getting document
@errorCallback(exception)
resetState: () ->
@renderQueue = []
clearTimeout @queueTimer if @queueTimer?
# clear any existing timers, render tasks
for timer in @spinTimer or []
clearTimeout timer
for page in @pageState or []
page?.loadTask?.cancel()
page?.renderTask?.cancel()
# initialise/reset the state
@pageState = []
@spinTimer = [] # timers for starting the spinners (to avoid jitter)
@spinTimerDone = [] # array of pages where the spinner has activated
@jobs = 0
getNumPages: () ->
@document.then (pdfDocument) ->
pdfDocument.numPages
getPage: (pageNum) ->
@document.then (pdfDocument) ->
pdfDocument.getPage(pageNum)
getPdfViewport: (pageNum, scale) ->
scale ?= @scale
@document.then (pdfDocument) =>
pdfDocument.getPage(pageNum).then (page) ->
viewport = page.getViewport scale
, (error) =>
@errorCallback?(error)
getDestinations: () ->
@document.then (pdfDocument) ->
pdfDocument.getDestinations()
getDestination: (dest) ->
@document.then (pdfDocument) ->
pdfDocument.getDestination(dest)
, (error) =>
@errorCallback?(error)
getPageIndex: (ref) ->
@document.then (pdfDocument) =>
pdfDocument.getPageIndex(ref).then (idx) ->
idx
, (error) =>
@errorCallback?(error)
getScale: () ->
@scale
setScale: (@scale) ->
@resetState()
triggerRenderQueue: (interval = @JOB_QUEUE_INTERVAL) ->
if @queueTimer?
clearTimeout @queueTimer
@queueTimer = setTimeout () =>
@queueTimer = null
@processRenderQueue()
, interval
removeCompletedJob: (pagenum) ->
@jobs = @jobs - 1
@triggerRenderQueue(0)
renderPages: (pages) ->
return if @shuttingDown
@renderQueue = for page in pages
{
'element': page.elementChildren
'pagenum': page.pageNum
}
@triggerRenderQueue()
renderPage: (page) ->
return if @shuttingDown
current = {
'element': page.elementChildren
'pagenum': page.pageNum
}
@renderQueue.push current
@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
@queuedPages = []
@queuedPages[page.pagenum] = true for page in @renderQueue
# clear any unfinished spinner timers on pages that aren't in the queue any more
for pagenum of @spinTimer when not @queuedPages[pagenum]
clearTimeout @spinTimer[pagenum]
delete @spinTimer[pagenum]
# add indicators for any new pages in the current queue
for page in @renderQueue when not @spinTimer[page.pagenum] and not @spinTimerDone[page.pagenum]
@startIndicator page
startIndicator: (page) ->
[canvas, pagenum] = @getPageDetails page
canvas.addClass('pdfng-loading')
@spinTimer[pagenum] = setTimeout () =>
for queuedPage in @renderQueue
if pagenum == queuedPage.pagenum
@spinner.add(canvas, {static:true})
@spinTimerDone[pagenum] = true
break
delete @spinTimer[pagenum]
, @INDICATOR_DELAY1
updateIndicator: (page) ->
[canvas, pagenum] = @getPageDetails page
# did the spinner insert itself already?
if @spinTimerDone[pagenum]
@spinTimer[pagenum] = setTimeout () =>
@spinner.start(canvas)
delete @spinTimer[pagenum]
, @INDICATOR_DELAY2
else
# stop the existing spin timer
clearTimeout @spinTimer[pagenum]
# start a new one which will also start spinning
@spinTimer[pagenum] = setTimeout () =>
@spinner.add(canvas, {static:true})
@spinTimerDone[pagenum] = true
@spinTimer[pagenum] = setTimeout () =>
@spinner.start(canvas)
delete @spinTimer[pagenum]
, @INDICATOR_DELAY2
, @INDICATOR_DELAY1
clearIndicator: (page) ->
[canvas, pagenum] = @getPageDetails page
@spinner.stop(canvas)
clearTimeout @spinTimer[pagenum]
delete @spinTimer[pagenum]
@spinTimerDone[pagenum] = true
# handle the queue of pages to be rendered
processRenderQueue: () ->
return if @shuttingDown
# mark all pages in the queue as loading
@startIndicators()
# bail out if there is already a render job running
return if @jobs > 0
# take the first page in the queue
page = @renderQueue.shift()
# check if it is in action already
while page? and @pageState[page.pagenum]?
page = @renderQueue.shift()
return unless page?
[element, pagenum] = [page.element, page.pagenum]
@jobs = @jobs + 1
# update the spinner to make it spinning (signifies loading has begun)
@updateIndicator page
timedOut = false
timer = $timeout () => # page load timed out
return if loadTask.cancelled # return from cancelled page load
Raven?.captureMessage?('pdfng page load timed out after ' + @PAGE_LOAD_TIMEOUT + 'ms (1% sample)') if Math.random() < 0.01
timedOut = true
@clearIndicator page
# @jobs = @jobs - 1
# @triggerRenderQueue(0)
@errorCallback?('timeout')
, @PAGE_LOAD_TIMEOUT
loadTask = @getPage(pagenum)
loadTask.cancel = () ->
@cancelled = true
@pageState[pagenum] = pageState = { loadTask: loadTask }
loadTask.then (pageObject) =>
# page load success
$timeout.cancel(timer)
return if loadTask.cancelled # return from cancelled page load
pageState.renderTask = @doRender element, pagenum, pageObject
pageState.renderTask.then () =>
# render task success
@clearIndicator page
pageState.complete = true
delete pageState.renderTask
@removeCompletedJob pagenum
, () =>
# render task failed
# could display an error icon
pageState.complete = false
delete pageState.renderTask
@removeCompletedJob pagenum
.catch (error) ->
# page load error
$timeout.cancel(timer)
@clearIndicator page
doRender: (element, pagenum, page) ->
self = this
scale = @scale
if (not scale?)
# scale is undefined, returning
return
canvas = $('<canvas class="pdf-canvas pdfng-rendering"></canvas>')
viewport = page.getViewport (scale)
devicePixelRatio = window.devicePixelRatio || 1
ctx = canvas[0].getContext '2d'
backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1
pixelRatio = devicePixelRatio / backingStoreRatio
scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
newWidth = Math.floor(viewport.width)
newHeight = Math.floor(viewport.height)
canvas[0].height = scaledHeight
canvas[0].width = scaledWidth
canvas.height(newHeight + 'px')
canvas.width(newWidth + 'px')
oldHeight = element.canvas.height()
oldWidth = element.canvas.width()
if newHeight != oldHeight or newWidth != oldWidth
element.canvas.height(newHeight + 'px')
element.canvas.width(newWidth + 'px')
element.container.height(newHeight + 'px')
element.container.width(newWidth + 'px')
@pageSizeChangeCallback?(pagenum, newHeight - oldHeight)
textLayer = new pdfTextLayer({
textLayerDiv: element.text[0]
viewport: viewport
})
annotationsLayer = new pdfAnnotations({
annotations: element.annotations[0]
viewport: viewport
navigateFn: @navigateFn
})
result = page.render {
canvasContext: ctx
viewport: viewport
transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
}
result.then () ->
# page render success
element.canvas.replaceWith(canvas)
canvas.removeClass('pdfng-rendering')
page.getTextContent().then (textContent) ->
textLayer.setTextContent textContent
, (error) ->
self.errorCallback?(error)
page.getAnnotations().then (annotations) ->
annotationsLayer.setAnnotations annotations
, (error) ->
self.errorCallback?(error)
.catch (error) ->
# page render failed
if error is 'cancelled'
return # do nothing when cancelled
else
self.errorCallback?(error)
return result
destroy: () ->
@shuttingDown = true
@resetState()
@pdfjs.then (document) ->
document.cleanup()
document.destroy()
]