overleaf/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee
2017-08-25 15:48:13 +01:00

343 lines
10 KiB
CoffeeScript

define [
"base"
"pdfjs-dist/build/pdf"
], (App, PDFJS) ->
# 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
TEXTLAYER_TIMEOUT: 100
constructor: (@url, @options) ->
# set up external character mappings - needed for Japanese etc
window.PDFJS.cMapUrl = window.pdfCMapsPath # injected in editor.pug
window.PDFJS.cMapPacked = true
if window.location?.search?.indexOf("disable-font-face=true") >= 0
window.PDFJS.disableFontFace = true
else
window.PDFJS.disableFontFace = false
if @options.disableAutoFetch
window.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')
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>')
# 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)
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
renderer: PDFJS.renderTextLayer
})
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]
}
textLayerTimeout = @TEXTLAYER_TIMEOUT
result.then () ->
# page render success
canvas.removeClass('pdfng-rendering')
page.getTextContent({normalizeWhitespace: true}).then (textContent) ->
textLayer.setTextContent textContent
textLayer.render(textLayerTimeout)
, (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()
]