Merge pull request #123 from sharelatex/pdfng

new pdf viewer using angularjs
This commit is contained in:
Brian Gough
2014-12-10 13:56:31 +00:00
23 changed files with 47767 additions and 27 deletions
+2 -2
View File
@@ -46,10 +46,10 @@ TpdsWorker.js
BackgroundJobsWorker.js
public/js/history/versiondetail.js
!public/js/libs/*
!public/js/libs/
public/js/*
!public/js/ace/*
!public/js/libs/*
!public/js/libs/
public/js/editor.js
public/js/home.js
public/js/forms.js
+1 -1
View File
@@ -98,7 +98,7 @@ module.exports = (grunt) ->
exclude: ["libs"]
}, {
name: "ide",
exclude: ["libs"]
exclude: ["libs", "libs/pdf"]
}, {
name: "libs"
},{
@@ -78,7 +78,12 @@ module.exports = CompileController =
url = "#{compilerUrl}#{url}"
logger.log url: url, "proxying to CLSI"
oneMinute = 60 * 1000
proxy = request(url: url, method: req.method, timeout: oneMinute)
# pass through If-* and Range headers for byte serving pdfs
# do not send any others, potential proxying loop if Host: is passed!
newHeaders = {}
for h, v of req.headers
newHeaders[h] = req.headers[h] if h.match /^(If-|Range)/i
proxy = request(url: url, method: req.method, timeout: oneMinute, headers: newHeaders)
proxy.pipe(res)
proxy.on "error", (error) ->
logger.warn err: error, url: url, "CLSI proxy error"
@@ -18,14 +18,18 @@ jsPath =
logger.log "Generating file fingerprints..."
for path in [
"#{jsPath}libs/require.js",
"#{jsPath}ide.js",
"#{jsPath}main.js",
"#{jsPath}libs/require.js",
"#{jsPath}ide.js",
"#{jsPath}main.js",
"#{jsPath}libs.js",
"#{jsPath}ace/ace.js",
"#{jsPath}libs/pdf.js",
"#{jsPath}libs/pdf.worker.js",
"/stylesheets/style.css"
"#{jsPath}libs/pdf.js",
"#{jsPath}libs/pdf.worker.js",
"#{jsPath}libs/compatibility.js",
"#{jsPath}libs/pdfjs-1.0.712/pdf.js",
"#{jsPath}libs/pdfjs-1.0.712/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.0.712/compatibility.js",
"/stylesheets/style.css"
]
filePath = Path.join __dirname, "../../../", "public#{path}"
exists = fs.existsSync filePath
+3 -1
View File
@@ -35,7 +35,9 @@ UserSchema = new Schema
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
}
featureSwitches : {}
featureSwitches : {
pdfng: { type: Boolean }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
refered_user_count: { type:Number, default: 0 }
+12 -2
View File
@@ -112,11 +112,21 @@ block content
- locals.suppressDefaultJs = true
- var fingerprintedPath = fingerprint(jsPath+'libs/pdf.worker.js')
- var pdfJsWorkerPath = jsPath+'libs/pdf.worker.js?fingerprint='+fingerprintedPath
- // user.featureSwitches = {} // override
- var usePdfNG = user.featureSwitches && user.featureSwitches.pdfng;
- var pdfPath = 'libs/pdf.worker.js';
- if (usePdfNG) { pdfPath = 'libs/pdfjs-1.0.712/pdf.worker.js'; }
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
script(type='text/javascript').
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
// when using pdfng we need a different pdfjs version
if usePdfNG
script(type='text/javascript').
window.requirejs.paths['libs/pdf'] = 'libs/pdfjs-1.0.712/pdf'
window.requirejs.paths['libs/compatibility'] = 'libs/pdfjs-1.0.712/compatibility'
script(
data-main=jsPath+"ide.js",
baseurl=jsPath,
+25 -10
View File
@@ -58,16 +58,31 @@ div.full-size.pdf(ng-controller="PdfController")
i.split-screen
.pdf-viewer(ng-show="pdf.url && pdf.view == 'pdf' && !pdf.failure && !pdf.timeout && !pdf.error")
div(
pdfjs
ng-if="settings.pdfViewer == 'pdfjs'"
pdf-src="pdf.url"
key="{{ project_id }}"
resize-on="layout:main:resize,layout:pdf:resize"
highlights="pdf.highlights"
position="pdf.position"
dbl-click-callback="syncToCode"
)
- // user.featureSwitches = {} // override
- var usePdfNG = user.featureSwitches && user.featureSwitches.pdfng;
if !usePdfNG
div(
pdfjs
ng-if="settings.pdfViewer == 'pdfjs'"
pdf-src="pdf.url"
key="{{ project_id }}"
resize-on="layout:main:resize,layout:pdf:resize"
highlights="pdf.highlights"
position="pdf.position"
dbl-click-callback="syncToCode"
)
else
div(
pdfng
ng-if="settings.pdfViewer == 'pdfjs'"
pdf-src="pdf.url"
key="{{ project_id }}"
resize-on="layout:main:resize,layout:pdf:resize"
highlights="pdf.highlights"
position="pdf.position"
dbl-click-callback="syncToCode"
)
iframe(
ng-src="{{ pdf.url }}"
ng-if="settings.pdfViewer == 'native'"
+1 -1
View File
@@ -13,4 +13,4 @@ define [
"ipCookie"
])
return App
return App
+1 -1
View File
@@ -106,4 +106,4 @@ define [
else
$scope.darkTheme = false
angular.bootstrap(document.body, ["SharelatexApp"])
angular.bootstrap(document.body, ["SharelatexApp"])
@@ -2,6 +2,7 @@ define [
"ide/pdf/controllers/PdfController"
"ide/pdf/controllers/PdfViewToggleController"
"ide/pdf/directives/pdfJs"
"ide/pdfng/directives/pdfJs" # alternative incremental viewer
], () ->
class PdfManager
constructor: (@ide, @$scope) ->
@@ -0,0 +1,45 @@
define [
"base"
], (App) ->
# App = angular.module 'pdfAnnotations', []
App.factory 'pdfAnnotations', [ () ->
class pdfAnnotations
@EXTERNAL_LINK_TARGET = "_blank";
constructor: (options) ->
@annotationsLayerDiv = options.annotations;
@viewport = options.viewport
@navigateFn = options.navigateFn
setAnnotations: (annotations) ->
for annotation in annotations
switch annotation.subtype
when 'Link' then @addLink(annotation);
when 'Text' then continue
addLink: (link) ->
element = @buildLinkElementFromRect(link.rect);
@setLinkTarget(element, link);
@annotationsLayerDiv.appendChild(element);
buildLinkElementFromRect: (rect) ->
rect = @viewport.convertToViewportRectangle(rect);
rect = PDFJS.Util.normalizeRect(rect);
element = document.createElement("a");
element.style.left = Math.floor(rect[0]) + 'px';
element.style.top = Math.floor(rect[1]) + 'px';
element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
element
setLinkTarget: (element, link) ->
if link.url
element.href = link.url;
element.target = @EXTERNAL_LINK_TARGET;
else if (link.dest)
element.href = "#" + link.dest;
element.onclick = (e) =>
@navigateFn link
return false
]
@@ -0,0 +1,29 @@
define [
"base"
], (App) ->
#app = angular.module 'pdfHighlights', []
App.factory 'pdfHighlights', [ () ->
class pdfHighlights
constructor: (options) ->
@highlightsLayerDiv = options.highlights[0]
@highlightElements = []
addHighlight: (viewport, left, top, width, height) ->
rect = viewport.convertToViewportRectangle([left, top, left + width, top + height])
rect = PDFJS.Util.normalizeRect(rect)
element = document.createElement("div")
element.style.left = Math.floor(rect[0]) + 'px'
element.style.top = Math.floor(rect[1]) + 'px'
element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'
element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'
@highlightElements.push(element)
@highlightsLayerDiv.appendChild(element)
element
clearHighlights: () ->
for h in @highlightElements
h.remove()
@highlightElements = []
]
@@ -0,0 +1,208 @@
define [
"base"
"ide/pdfng/directives/pdfViewer"
"text!libs/pdfListView/TextLayer.css"
"text!libs/pdfListView/AnnotationsLayer.css"
"text!libs/pdfListView/HighlightsLayer.css"
], (
App
pdfViewer
textLayerCss
annotationsLayerCss
highlightsLayerCss
) ->
if PDFJS?
PDFJS.workerSrc = window.pdfJsWorkerPath
style = $("<style/>")
style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
$("body").append(style)
App.directive "pdfng", ["$timeout", ($timeout) ->
return {
scope: {
"pdfSrc": "="
"highlights": "="
"position": "="
"dblClickCallback": "="
}
link: (scope, element, attrs) ->
# pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
# textLayerBuilder: TextLayerBuilder
# annotationsLayerBuilder: AnnotationsLayerBuilder
# highlightsLayerBuilder: HighlightsLayerBuilder
# ondblclick: (e) -> onDoubleClick(e)
# # logLevel: PDFListView.Logger.DEBUG
# pdfListView.listView.pageWidthOffset = 20
# pdfListView.listView.pageHeightOffset = 20
scope.loading = false
scope.pleaseJumpTo = null
scope.scale = null
initializedPosition = false
initializePosition = () ->
return if initializedPosition
initializedPosition = true
if (scale = $.localStorage("pdf.scale"))?
scope.scale = { scaleMode: scale.scaleMode, scale: +scale.scale}
else
scope.scale = { scaleMode: 'scale_mode_fit_width' }
if (position = $.localStorage("pdf.position.#{attrs.key}"))
scope.position = { page: +position.page, offset: { "top": +position.offset.top, "left": +position.offset.left } }
#scope.position = pdfListView.getPdfPosition(true)
scope.$on "$destroy", () =>
$.localStorage "pdf.scale", scope.scale
$.localStorage "pdf.position.#{attrs.key}", scope.position
$(window).unload () =>
$.localStorage "pdf.scale", scope.scale
$.localStorage "pdf.position.#{attrs.key}", scope.position
flashControls = () ->
scope.flashControls = true
$timeout () ->
scope.flashControls = false
, 1000
scope.$on 'pdfDoubleClick', (event, e) ->
scope.dblClickCallback?(page: e.page - 1, offset: { top: e.y, left: e.x })
scope.$on 'flash-controls', () ->
flashControls()
scope.$watch "pdfSrc", (url) ->
if url
scope.loading = true
# console.log 'pdfSrc =', url
initializePosition()
flashControls()
scope.$broadcast 'layout-ready'
# pdfListView
# .loadPdf(url, onProgress)
# .then () ->
# scope.$apply () ->
# scope.loading = false
# delete scope.progress
# initializePosition()
# flashControls()
scope.$on "loaded", () ->
# console.log "LOADED"
scope.loading = false
#scope.$watch "highlights", (areas) ->
# console.log 'got HIGHLIGHTS in pdfJS', areas
# return if !areas?
# highlights = for area in areas or []
# {
# page: area.page
# highlight:
# left: area.h
# top: area.v
# height: area.height
# width: area.width
# }
# if highlights.length > 0
# first = highlights[0]
# position = {
# page: first.page
# offset:
# left: first.highlight.left
# top: first.highlight.top - 80
# }
# console.log 'position is', position, 'in highlights'
# scope.pleaseJumpTo = position
# pdfListView.clearHighlights()
# pdfListView.setHighlights(highlights, true)
# setTimeout () =>
# pdfListView.clearHighlights()
# , 1000
scope.fitToHeight = () ->
scale = angular.copy (scope.scale)
scale.scaleMode = 'scale_mode_fit_height'
scope.scale = scale
scope.fitToWidth = () ->
scale = angular.copy (scope.scale)
scale.scaleMode = 'scale_mode_fit_width'
scope.scale = scale
scope.zoomIn = () ->
scale = angular.copy (scope.scale)
scale.scaleMode = 'scale_mode_value'
scale.scale = scale.scale * 1.2
scope.scale = scale
scope.zoomOut = () ->
scale = angular.copy (scope.scale)
scale.scaleMode = 'scale_mode_value'
scale.scale = scale.scale / 1.2
scope.scale = scale
if attrs.resizeOn?
for event in attrs.resizeOn.split(",")
scope.$on event, (e) ->
#console.log 'got a resize event', event, e
#
scope.$on 'progress', (event, progress) ->
scope.$apply () ->
#console.log 'progress', progress.loaded, progress.total, progress
scope.progress = Math.floor(progress.loaded/progress.total*100)
template: """
<div data-pdf-viewer class="pdfjs-viewer" pdf-src='pdfSrc' position='position' scale='scale' highlights='highlights' please-jump-to='pleaseJumpTo'></div>
<div class="pdfjs-controls" ng-class="{'flash': flashControls }">
<div class="btn-group">
<a href
class="btn btn-info btn-lg"
ng-click="fitToWidth()"
tooltip="Fit to Width"
tooltip-append-to-body="true"
tooltip-placement="bottom"
>
<i class="fa fa-fw fa-arrows-h"></i>
</a>
<a href
class="btn btn-info btn-lg"
ng-click="fitToHeight()"
tooltip="Fit to Height"
tooltip-append-to-body="true"
tooltip-placement="bottom"
>
<i class="fa fa-fw fa-arrows-v"></i>
</a>
<a href
class="btn btn-info btn-lg"
ng-click="zoomIn()"
tooltip="Zoom In"
tooltip-append-to-body="true"
tooltip-placement="bottom"
>
<i class="fa fa-fw fa-search-plus"></i>
</a>
<a href
class="btn btn-info btn-lg"
ng-click="zoomOut()"
tooltip="Zoom Out"
tooltip-append-to-body="true"
tooltip-placement="bottom"
>
<i class="fa fa-fw fa-search-minus"></i>
</a>
</div>
</div>
<div class="progress-thin" ng-show="loading">
<div class="progress-bar" ng-style="{ 'width': progress + '%' }"></div>
</div>
"""
}
]
@@ -0,0 +1,129 @@
define [
"base"
], (App) ->
# App = angular.module 'pdfPage', ['pdfHighlights']
App.directive 'pdfPage', ['$timeout', 'pdfHighlights', ($timeout, pdfHighlights) ->
{
require: '^pdfViewer',
template: '''
<div class="plv-page-view page-view">
<div class="pdf-canvas pdfng-empty"></div>
<div class="plv-text-layer text-layer"></div>
<div class="plv-annotations-layer annotations-layer"></div>
<div class="plv-highlights-layer highlights-layer"></div>
</div>
'''
link: (scope, element, attrs, ctrl) ->
canvasElement = $(element).find('.pdf-canvas')
textElement = $(element).find('.text-layer')
annotationsElement = $(element).find('.annotations-layer')
highlightsElement = $(element).find('.highlights-layer')
updatePageSize = (size) ->
h = Math.floor(size[0])
w = Math.floor(size[1])
element.height(h)
element.width(w)
canvasElement.height(h)
canvasElement.width(w)
scope.page.sized = true
isVisible = (containerSize) ->
elemTop = element.offset().top - containerSize[2]
elemBottom = elemTop + element.innerHeight()
visible = (elemTop < containerSize[1] and elemBottom > 0)
scope.page.visible = visible
scope.page.elemTop = elemTop
scope.page.elemBottom = elemBottom
return visible
renderPage = () ->
scope.document.renderPage {
canvas: canvasElement,
text: textElement
annotations: annotationsElement
highlights: highlightsElement
}, scope.page.pageNum
pausePage = () ->
scope.document.pause {
canvas: canvasElement,
text: textElement
}, scope.page.pageNum
# keep track of our page element, so we can access it in the
# parent with scope.pages[i].element
scope.page.element = element
if (!scope.page.sized && scope.defaultPageSize)
updatePageSize scope.defaultPageSize
if scope.page.current
# console.log 'we must scroll to this page', scope.page.pageNum, 'at position', scope.page.position
renderPage()
# this is the current page, we want to scroll it into view
ctrl.setPdfPosition(scope.page, scope.page.position)
scope.$watch 'defaultPageSize', (defaultPageSize) ->
return unless defaultPageSize?
updatePageSize defaultPageSize
watchHandle = scope.$watch 'containerSize', (containerSize, oldVal) ->
return unless containerSize?
return unless scope.page.sized
oldVisible = scope.page.visible
newVisible = isVisible containerSize
scope.page.visible = newVisible
if newVisible && !oldVisible
renderPage()
# TODO deregister this listener after the page is rendered
#watchHandle()
else if !newVisible && oldVisible
pausePage()
element.on 'dblclick', (e) ->
offset = $(element).find('.pdf-canvas').offset()
dx = e.pageX - offset.left
dy = e.pageY - offset.top
scope.document.getPdfViewport(scope.page.pageNum).then (viewport) ->
pdfPoint = viewport.convertToPdfPoint(dx, dy);
event = {
page: scope.page.pageNum
x: pdfPoint[0],
y: viewport.viewBox[3] - pdfPoint[1]
}
scope.$emit 'pdfDoubleClick', event
highlightsLayer = new pdfHighlights({
highlights: highlightsElement
})
scope.$watch 'highlights', (highlights, oldVal) ->
return unless highlights?
return unless highlights.length > 0
if scope.timeoutHandler
$timeout.cancel(scope.timeoutHandler)
highlightsLayer.clearHighlights()
scope.timeoutHandler
# console.log 'got highlight watch in pdfPage', scope.page
pageHighlights = (h for h in highlights when h.page == scope.page.pageNum)
return unless pageHighlights.length
scope.document.getPdfViewport(scope.page.pageNum).then (viewport) ->
for hl in pageHighlights
# console.log 'adding highlight', h, viewport
top = viewport.viewBox[3] - hl.v
highlightsLayer.addHighlight viewport, hl.h, top, hl.width, hl.height
scope.timeoutHandler = $timeout () ->
highlightsLayer.clearHighlights()
scope.timeoutHandler = null
, 1000
scope.$on "$destroy", () ->
if scope.timeoutHandler?
$timeout.cancel(scope.timeoutHandler)
highlightsLayer.clearHighlights()
}
]
@@ -0,0 +1,210 @@
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
constructor: (@url, @options) ->
PDFJS.disableFontFace = true # avoids repaints, uses worker more
# PDFJS.disableAutoFetch = true # enable this to prevent loading whole file
# PDFJS.disableStream
# PDFJS.disableRange
@scale = @options.scale || 1
@pdfjs = PDFJS.getDocument @url, null, null, @options.progressCallback
@document = $q.when(@pdfjs)
@navigateFn = @options.navigateFn
@spinner = new pdfSpinner
@resetState()
resetState: () ->
@page = []
@complete = []
@timeout = []
@pageLoad = []
@renderTask = []
@renderQueue = []
@jobs = 0
getNumPages: () ->
@document.then (pdfDocument) ->
pdfDocument.numPages
getPage: (pageNum) ->
# with promise caching
return @page[pageNum] if @page[pageNum]?
@page[pageNum] = @document.then (pdfDocument) ->
pdfDocument.getPage(pageNum)
getPdfViewport: (pageNum, scale) ->
scale ?= @scale
@document.then (pdfDocument) ->
pdfDocument.getPage(pageNum).then (page) ->
viewport = page.getViewport scale
getDestinations: () ->
@document.then (pdfDocument) ->
pdfDocument.getDestinations()
# Not available in pdf.js-1.0.712, in later versions there is a direct
# call for this - we should use it as soon as it is available in a
# stable version
getDestination: (dest) ->
@destinations = @document.then (pdfDocument) ->
pdfDocument.getDestinations()
return @destinations.then (all) ->
all[dest]
@document.then (pdfDocument) ->
pdfDocument.getDestination(dest)
getPageIndex: (ref) ->
@document.then (pdfDocument) ->
pdfDocument.getPageIndex(ref).then (idx) ->
idx
getScale: () ->
@scale
setScale: (@scale) ->
@resetState()
pause: (element, pagenum) ->
return if @complete[pagenum]
return if @shuttingDown
@renderQueue = @renderQueue.filter (q) ->
q.pagenum != pagenum
@spinner.stop(element.canvas)
triggerRenderQueue: (interval = @JOB_QUEUE_INTERVAL) ->
$timeout () =>
@processRenderQueue()
, interval
removeCompletedJob: (taskRef, pagenum) ->
# may need to clean up deferred object here
delete taskRef[pagenum]
@jobs = @jobs - 1
@triggerRenderQueue(0)
renderPage: (element, pagenum) ->
return if @shuttingDown
current = {
'element': element
'pagenum': pagenum
}
@renderQueue.push(current)
@triggerRenderQueue()
processRenderQueue: () ->
return if @jobs > 0
current = @renderQueue.shift()
return unless current?
[element, pagenum] = [current.element, current.pagenum]
# if task is underway or complete, go to the next entry in the
# render queue
# console.log 'processing renderq', pagenum, @renderTask[pagenum], @complete[pagenum]
if @pageLoad[pagenum] or @renderTask[pagenum] or @complete[pagenum]
@processRenderQueue()
return
@jobs = @jobs + 1
element.canvas.addClass('pdfng-loading')
@spinner.add(element.canvas)
completeRef = @complete
renderTaskRef = @renderTask
@pageLoad[pagenum] = @getPage(pagenum)
@pageLoad[pagenum].then (pageObject) =>
@renderTask[pagenum] = @doRender element, pagenum, pageObject
@renderTask[pagenum].then () =>
# complete
completeRef[pagenum] = true
@removeCompletedJob renderTaskRef, pagenum
, () =>
# rejected
@removeCompletedJob renderTaskRef, pagenum
doRender: (element, pagenum, page) ->
self = this
scale = @scale
if (not scale?)
# console.log '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')
element.canvas.height(newHeight)
element.canvas.width(newWidth)
if pixelRatio != 1
ctx.scale(pixelRatio, pixelRatio)
textLayer = new pdfTextLayer({
textLayerDiv: element.text[0]
viewport: viewport
})
annotationsLayer = new pdfAnnotations({
annotations: element.annotations[0]
viewport: viewport
navigateFn: @navigateFn
})
element.canvas.replaceWith(canvas)
result = page.render {
canvasContext: ctx
viewport: viewport
}
result.then () ->
canvas.removeClass('pdfng-rendering')
page.getTextContent().then (textContent) ->
textLayer.setTextContent textContent
page.getAnnotations().then (annotations) ->
annotationsLayer.setAnnotations annotations
return result
destroy: () ->
# console.log 'in pdf renderer destroy', @renderQueue
@shuttingDown = true
@renderQueue = []
for task in @renderTask
task.cancel() if task?
@pdfjs.then (document) ->
document.cleanup()
document.destroy()
]
@@ -0,0 +1,25 @@
define [
"base"
], (App) ->
App.factory 'pdfSpinner', [() ->
class pdfSpinner
constructor: () ->
# handler for spinners
add: (element) ->
h = element.height()
w = element.width()
size = 64
spinner = $('<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner fa-spin" style="color: #999"></i></div>')
spinner.css({'font-size' : size + 'px'})
element.append(spinner)
stop: (element) ->
element.find('.fa-spin').removeClass('fa-spin')
remove: (element) ->
element.find('.fa-spin').remove()
]
@@ -0,0 +1,217 @@
define [
"base"
], (App) ->
# App = angular.module 'pdfTextLayer', []
App.factory 'pdfTextLayer', [ () ->
# TRANSLATED FROM pdf.js-1.0.712
# pdf.js-1.0.712/web/ui_utils.js
# pdf.js-1.0.712/web/text_layer_builder.js
# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
# Copyright 2012 Mozilla Foundation
# *
# * Licensed under the Apache License, Version 2.0 (the "License");
# * you may not use this file except in compliance with the License.
# * You may obtain a copy of the License at
# *
# * http://www.apache.org/licenses/LICENSE-2.0
# *
# * Unless required by applicable law or agreed to in writing, software
# * distributed under the License is distributed on an "AS IS" BASIS,
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# * See the License for the specific language governing permissions and
# * limitations under the License.
#
# globals CustomStyle, scrollIntoView, PDFJS
# ms
# optimised CSS custom property getter/setter
CustomStyle = (CustomStyleClosure = ->
# As noted on: http://www.zachstronaut.com/posts/2009/02/17/
# animate-css-transforms-firefox-webkit.html
# in some versions of IE9 it is critical that ms appear in this list
# before Moz
CustomStyle = ->
prefixes = [
'ms'
'Moz'
'Webkit'
'O'
]
_cache = {}
CustomStyle.getProp = get = (propName, element) ->
# check cache only when no element is given
return _cache[propName] if arguments.length is 1 and typeof _cache[propName] is 'string'
element = element or document.documentElement
style = element.style
prefixed = undefined
uPropName = undefined
# test standard property first
return (_cache[propName] = propName) if typeof style[propName] is 'string'
# capitalize
uPropName = propName.charAt(0).toUpperCase() + propName.slice(1)
# test vendor specific properties
i = 0
l = prefixes.length
while i < l
prefixed = prefixes[i] + uPropName
return (_cache[propName] = prefixed) if typeof style[prefixed] is 'string'
i++
#if all fails then set to undefined
_cache[propName] = 'undefined'
CustomStyle.setProp = set = (propName, element, str) ->
prop = @getProp(propName)
element.style[prop] = str if prop isnt 'undefined'
return
CustomStyle
)()
#################################
isAllWhitespace = (str) ->
not NonWhitespaceRegexp.test(str)
'use strict'
FIND_SCROLL_OFFSET_TOP = -50
FIND_SCROLL_OFFSET_LEFT = -400
MAX_TEXT_DIVS_TO_RENDER = 100000
RENDER_DELAY = 200
NonWhitespaceRegexp = /\S/
###*
TextLayerBuilder provides text-selection functionality for the PDF.
It does this by creating overlay divs over the PDF text. These divs
contain text that matches the PDF text they are overlaying. This object
also provides a way to highlight text that is being searched for.
###
class pdfTextLayer
constructor: (options) ->
@textLayerDiv = options.textLayerDiv
@layoutDone = false
@divContentDone = false
@pageIdx = options.pageIndex
@matches = []
@lastScrollSource = options.lastScrollSource or null
@viewport = options.viewport
@isViewerInPresentationMode = options.isViewerInPresentationMode
@textDivs = []
@findController = options.findController or null
renderLayer: () ->
textLayerFrag = document.createDocumentFragment()
textDivs = @textDivs
textDivsLength = textDivs.length
canvas = document.createElement('canvas')
ctx = canvas.getContext('2d')
# No point in rendering many divs as it would make the browser
# unusable even after the divs are rendered.
return if textDivsLength > MAX_TEXT_DIVS_TO_RENDER
lastFontSize = undefined
lastFontFamily = undefined
i = 0
while i < textDivsLength
textDiv = textDivs[i]
if textDiv.dataset.isWhitespace
i++
continue
fontSize = textDiv.style.fontSize
fontFamily = textDiv.style.fontFamily
# Only build font string and set to context if different from last.
if fontSize isnt lastFontSize or fontFamily isnt lastFontFamily
ctx.font = fontSize + ' ' + fontFamily
lastFontSize = fontSize
lastFontFamily = fontFamily
width = ctx.measureText(textDiv.textContent).width
if width > 0
textLayerFrag.appendChild textDiv
# Dataset values come of type string.
textScale = textDiv.dataset.canvasWidth / width
rotation = textDiv.dataset.angle
transform = 'scale(' + textScale + ', 1)'
transform = 'rotate(' + rotation + 'deg) ' + transform if rotation
CustomStyle.setProp 'transform', textDiv, transform
CustomStyle.setProp 'transformOrigin', textDiv, '0% 0%'
i++
@textLayerDiv.appendChild textLayerFrag
return
appendText: (geom, styles) ->
style = styles[geom.fontName]
textDiv = document.createElement('div')
@textDivs.push textDiv
if isAllWhitespace(geom.str)
textDiv.dataset.isWhitespace = true
return
tx = PDFJS.Util.transform(@viewport.transform, geom.transform)
angle = Math.atan2(tx[1], tx[0])
angle += Math.PI / 2 if style.vertical
fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]))
fontAscent = fontHeight
if style.ascent
fontAscent = style.ascent * fontAscent
else fontAscent = (1 + style.descent) * fontAscent if style.descent
left = undefined
top = undefined
if angle is 0
left = tx[4]
top = tx[5] - fontAscent
else
left = tx[4] + (fontAscent * Math.sin(angle))
top = tx[5] - (fontAscent * Math.cos(angle))
textDiv.style.left = left + 'px'
textDiv.style.top = top + 'px'
textDiv.style.fontSize = fontHeight + 'px'
textDiv.style.fontFamily = style.fontFamily
textDiv.textContent = geom.str
textDiv.ondblclick = (e) ->
if (window.getSelection)
window.getSelection().removeAllRanges();
else if (document.selection)
document.selection.empty();
# |fontName| is only used by the Font Inspector. This test will succeed
# when e.g. the Font Inspector is off but the Stepper is on, but it's
# not worth the effort to do a more accurate test.
textDiv.dataset.fontName = geom.fontName if PDFJS.pdfBug
# Storing into dataset will convert number into string.
textDiv.dataset.angle = angle * (180 / Math.PI) if angle isnt 0
if style.vertical
textDiv.dataset.canvasWidth = geom.height * @viewport.scale
else
textDiv.dataset.canvasWidth = geom.width * @viewport.scale
return
setTextContent: (textContent) ->
@textContent = textContent
textItems = textContent.items
i = 0
len = textItems.length
while i < len
@appendText textItems[i], textContent.styles
i++
@divContentDone = true
@renderLayer()
return
]
@@ -0,0 +1,424 @@
define [
"base"
"ide/pdfng/directives/pdfTextLayer"
"ide/pdfng/directives/pdfAnnotations"
"ide/pdfng/directives/pdfHighlights"
"ide/pdfng/directives/pdfRenderer"
"ide/pdfng/directives/pdfPage"
"ide/pdfng/directives/pdfSpinner"
"libs/pdf" # needs pdfjs-1.0.712, override the path in require.js to get it
], (
App
pdfTextLayer
pdfAnnotations
pdfHighlights
pdfRenderer
pdfPage
pdfSpinner
pdf
) ->
# App = angular.module 'pdfViewerApp', ['pdfPage', 'PDFRenderer', 'pdfHighlights']
App.controller 'pdfViewerController', ['$scope', '$q', '$timeout', 'PDFRenderer', '$element', 'pdfHighlights', 'pdfSpinner', ($scope, $q, $timeout, PDFRenderer, $element, pdfHighlights, pdfSpinner) ->
@load = () ->
# $scope.pages = []
$scope.document.destroy() if $scope.document?
$scope.document = new PDFRenderer($scope.pdfSrc, {
scale: 1,
navigateFn: (ref) ->
# this function captures clicks on the annotation links
$scope.navigateTo = ref
$scope.$apply()
progressCallback: (progress) ->
$scope.$emit 'progress', progress
})
# we will have all the main information needed to start display
# after the following promise is resolved
$scope.loaded = $q.all({
numPages: $scope.document.getNumPages()
# get size of first page as default @ scale 1
pdfViewport: $scope.document.getPdfViewport 1, 1
}).then (result) ->
$scope.pdfViewport = result.pdfViewport
$scope.pdfPageSize = [
result.pdfViewport.height,
result.pdfViewport.width
]
# console.log 'resolved q.all, page size is', result
$scope.numPages = result.numPages
$scope.$emit "loaded"
@setScale = (scale, containerHeight, containerWidth) ->
$scope.loaded.then () ->
scale = {} if not scale?
if containerHeight == 0 or containerWidth == 0
numScale = 1
else if scale.scaleMode == 'scale_mode_fit_width'
# TODO make this dynamic
numScale = (containerWidth - 40) / ($scope.pdfPageSize[1])
else if scale.scaleMode == 'scale_mode_fit_height'
# TODO magic numbers for jquery ui layout
numScale = (containerHeight - 20) / ($scope.pdfPageSize[0])
else if scale.scaleMode == 'scale_mode_value'
numScale = scale.scale
else if scale.scaleMode == 'scale_mode_auto'
# TODO
else
scale.scaleMode = 'scale_mode_fit_width'
numScale = (containerWidth - 40) / ($scope.pdfPageSize[1])
# TODO
$scope.scale.scale = numScale
$scope.document.setScale(numScale)
$scope.defaultPageSize = [
numScale * $scope.pdfPageSize[0],
numScale * $scope.pdfPageSize[1]
]
# console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize
@redraw = (position) ->
# console.log 'in redraw'
# console.log 'reseting pages array for', $scope.numPages
$scope.pages = ({
pageNum: i + 1
} for i in [0 .. $scope.numPages-1])
if position? && position.page?
# console.log 'position is', position.page, position.offset
# console.log 'setting current page', position.page
pagenum = position.page
$scope.pages[pagenum].current = true
$scope.pages[pagenum].position = position
@zoomIn = () ->
# console.log 'zoom in'
newScale = $scope.scale.scale * 1.2
$scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale }
@zoomOut = () ->
# console.log 'zoom out'
newScale = $scope.scale.scale / 1.2
$scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale }
@fitWidth = () ->
# console.log 'fit width'
$scope.forceScale = { scaleMode: 'scale_mode_fit_width' }
@fitHeight = () ->
# console.log 'fit height'
$scope.forceScale = { scaleMode: 'scale_mode_fit_height' }
@checkPosition = () ->
# console.log 'check position'
$scope.forceCheck = ($scope.forceCheck || 0) + 1
@showRandomHighlights = () ->
# console.log 'show highlights'
$scope.highlights = [
{
page: 3
h: 100
v: 100
height: 30
width: 200
}
]
# we work with (pagenumber, % of height down page from top)
# pdfListView works with (pagenumber, vertical position up page from
# bottom measured in pts)
@getPdfPosition = () ->
# console.log 'in getPdfPosition'
topPageIdx = 0
topPage = $scope.pages[0]
# find first visible page
visible = $scope.pages.some (page, i) ->
[topPageIdx, topPage] = [i, page] if page.visible
if visible
# console.log 'found it', topPageIdx
else
# console.log 'CANNOT FIND TOP PAGE'
return
# console.log 'top page is', topPage.pageNum, topPage.elemTop, topPage.elemBottom, topPage
top = topPage.elemTop
bottom = topPage.elemBottom
viewportTop = 0
viewportHeight = $element.height()
topVisible = (top >= viewportTop && top < viewportTop + viewportHeight)
someContentVisible = (top < viewportTop && bottom > viewportTop)
# console.log 'in PdfListView', top, topVisible, someContentVisible, viewportTop
if topVisible
canvasOffset = 0
else if someContentVisible
canvasOffset = viewportTop - top
else
canvasOffset = null
# console.log 'pdfListview position = ', canvasOffset
# instead of using promise, check if size is known and revert to
# default otherwise
# console.log 'looking up viewport', topPage.viewport, $scope.pdfViewport
if topPage.viewport
viewport = topPage.viewport
pdfOffset = viewport.convertToPdfPoint(0, canvasOffset);
else
# console.log 'WARNING: had to default to global page size'
viewport = $scope.pdfViewport
scaledOffset = canvasOffset / $scope.scale.scale
pdfOffset = viewport.convertToPdfPoint(0, scaledOffset);
# console.log 'converted to offset = ', pdfOffset
newPosition = {
"page": topPageIdx,
"offset" : { "top" : pdfOffset[1], "left": 0 }
}
return newPosition
@computeOffset = (page, position) ->
# console.log 'computing offset for', page, position
element = page.element
#console.log 'element =', $(element), 'parent =', $(element).parent()
t1 = $(element).offset()?.top
t2 = $(element).parent().offset()?.top
if not (t1? and t2?)
return $q((resolve, reject) -> reject('elements destroyed'))
pageTop = $(element).offset().top - $(element).parent().offset().top
# console.log('top of page scroll is', pageTop, 'vs', page.elemTop)
# console.log('inner height is', $(element).innerHeight())
currentScroll = $(element).parent().scrollTop()
offset = position.offset
# convert offset to pixels
return $scope.document.getPdfViewport(page.pageNum).then (viewport) ->
page.viewport = viewport
pageOffset = viewport.convertToViewportPoint(offset.left, offset.top)
# console.log 'addition offset =', pageOffset
# console.log 'total', pageTop + pageOffset[1]
Math.round(pageTop + pageOffset[1] + currentScroll) ## 10 is margin
@setPdfPosition = (page, position) ->
# console.log 'required pdf Position is', position
@computeOffset(page, position).then (offset) ->
$scope.pleaseScrollTo = offset
$scope.position = position
return this
]
App.directive 'pdfViewer', ['$q', '$timeout', 'pdfSpinner', ($q, $timeout, pdfSpinner) ->
{
controller: 'pdfViewerController'
controllerAs: 'ctrl'
scope: {
"pdfSrc": "="
"highlights": "="
"position": "="
"scale": "="
"pleaseJumpTo": "="
}
template: """
<div data-pdf-page class='pdf-page-container page-container' ng-repeat='page in pages'></div>
"""
link: (scope, element, attrs, ctrl) ->
# console.log 'in pdfViewer element is', element
# console.log 'attrs', attrs
spinner = new pdfSpinner
layoutReady = $q.defer()
layoutReady.notify 'waiting for layout'
layoutReady.promise.then () ->
# console.log 'layoutReady was resolved'
# TODO can we combine this with scope.parentSize, need to finalize boxes
updateContainer = () ->
scope.containerSize = [
element.innerWidth()
element.innerHeight()
element.offset().top
]
doRescale = (scale) ->
# console.log 'doRescale', scale
origposition = angular.copy scope.position
# console.log 'origposition', origposition
layoutReady.promise.then () ->
[h, w] = [element.innerHeight(), element.width()]
# console.log 'in promise', h, w
ctrl.setScale(scale, h, w).then () ->
spinner.remove(element)
ctrl.redraw(origposition)
checkElementReady = () ->
# if element is zero-sized keep checking until it is ready
if element.height() == 0 or element.width() == 0
$timeout () ->
checkElementReady()
, 250
else
scope.$broadcast 'layout-ready' if !scope.parentSize?
checkElementReady()
scope.$on 'layout-ready', () ->
# console.log 'GOT LAYOUT READY EVENT'
# console.log 'calling refresh'
updateContainer()
spinner.add(element)
layoutReady.resolve 'layout is ready'
scope.parentSize = [
element.innerHeight(),
element.innerWidth()
]
scope.$emit 'flash-controls'
#scope.$apply()
scope.$on 'layout:main:resize', () ->
# console.log 'GOT LAYOUT-MAIN-RESIZE EVENT'
scope.parentSize = [
element.innerHeight(),
element.innerWidth()
]
scope.$apply()
scope.$on 'layout:pdf:resize', () ->
# console.log 'GOT LAYOUT-PDF-RESIZE EVENT'
scope.parentSize = [
element.innerHeight(),
element.innerWidth()
]
#scope.$apply()
element.on 'scroll', () ->
#console.log 'scroll event', element.scrollTop(), 'adjusting?', scope.adjustingScroll
if scope.adjustingScroll
updateContainer()
scope.$apply()
scope.adjustingScroll = false
return
scope.scrolled = true
if scope.scrollHandlerTimeout
$timeout.cancel(scope.scrollHandlerTimeout)
scope.scrollHandlerTimeout = $timeout scrollHandler, 100
scrollHandler = () ->
scope.scrollHandlerTimeout = null
updateContainer()
scope.$apply()
newPosition = ctrl.getPdfPosition()
if newPosition?
scope.position = newPosition
scope.$apply()
scope.$watch 'pdfSrc', (newVal, oldVal) ->
# console.log 'loading pdf', newVal, oldVal
return unless newVal?
ctrl.load()
# trigger a redraw
scope.scale = angular.copy (scope.scale)
scope.$watch 'scale', (newVal, oldVal) ->
# no need to set scale when initialising, done in pdfSrc
return if newVal == oldVal
# console.log 'XXX calling Setscale in scale watch'
doRescale newVal
scope.$watch 'forceScale', (newVal, oldVal) ->
# console.log 'got change in numscale watcher', newVal, oldVal
return unless newVal?
doRescale newVal
# scope.$watch 'position', (newVal, oldVal) ->
# console.log 'got change in position watcher', newVal, oldVal
scope.$watch 'forceCheck', (newVal, oldVal) ->
# console.log 'forceCheck', newVal, oldVal
return unless newVal?
scope.adjustingScroll = true # temporarily disable scroll
doRescale scope.scale
scope.$watch('parentSize', (newVal, oldVal) ->
# console.log 'XXX in parentSize watch', newVal, oldVal
# if newVal == oldVal
# console.log 'returning because old and new are the same'
# return
return unless oldVal?
# console.log 'XXX calling setScale in parentSize watcher'
doRescale scope.scale
, true)
# scope.$watch 'elementWidth', (newVal, oldVal) ->
# console.log '*** watch INTERVAL element width is', newVal, oldVal
scope.$watch 'pleaseScrollTo', (newVal, oldVal) ->
# console.log 'got request to ScrollTo', newVal, 'oldVal', oldVal
return unless newVal?
scope.adjustingScroll = true # temporarily disable scroll
# handler while we reposition
$(element).scrollTop(newVal)
scope.pleaseScrollTo = undefined
scope.$watch 'pleaseJumpTo', (newPosition, oldPosition) ->
# console.log 'in pleaseJumpTo', newPosition, oldPosition
return unless newPosition?
ctrl.setPdfPosition scope.pages[newPosition.page-1], newPosition
scope.$watch 'navigateTo', (newVal, oldVal) ->
return unless newVal?
# console.log 'got request to navigate to', newVal, 'oldVal', oldVal
scope.navigateTo = undefined
# console.log 'navigate to', newVal
# console.log 'look up page num'
scope.document.getDestination(newVal.dest).then (r) ->
# console.log 'need to go to', r
# console.log 'page ref is', r[0]
scope.document.getPageIndex(r[0]).then (pidx) ->
# console.log 'page num is', pidx
page = scope.pages[pidx]
scope.document.getPdfViewport(page.pageNum).then (viewport) ->
#console.log 'got viewport', viewport
coords = viewport.convertToViewportPoint r[2], r[3]
#console.log 'viewport position', coords
#console.log 'r is', r, 'r[1]', r[1], 'r[1].name', r[1].name
if r[1].name == 'XYZ'
#console.log 'XYZ:', r[2], r[3]
newPosition = {page: pidx, offset: {top: r[3], left: r[2]}}
ctrl.setPdfPosition scope.pages[pidx], newPosition # XXX?
scope.$watch "highlights", (areas) ->
# console.log 'got HIGHLIGHTS in pdfViewer', areas
return if !areas?
#console.log 'areas are', areas
highlights = for area in areas or []
{
page: area.page - 1
highlight:
left: area.h
top: area.v
height: area.height
width: area.width
}
#console.log 'highlights', highlights
return if !highlights.length
first = highlights[0]
pageNum = scope.pages[first.page].pageNum
scope.document.getPdfViewport(pageNum).then (viewport) ->
position = {
page: first.page
offset:
left: first.highlight.left
top: viewport.viewBox[3] - first.highlight.top + first.highlight.height + 72
}
ctrl.setPdfPosition(scope.pages[first.page], position)
scope.$watch '$destroy', () ->
#console.log 'handle pdfng directive destroy'
}
]
@@ -0,0 +1,563 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals VBArray, PDFJS */
'use strict';
// Initializing PDFJS global object here, it case if we need to change/disable
// some PDF.js features, e.g. range requests
if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
// Checking if the typed arrays are supported
// Support: iOS<6.0 (subarray), IE<10, Android<4.0
(function checkTypedArrayCompatibility() {
if (typeof Uint8Array !== 'undefined') {
// Support: iOS<6.0
if (typeof Uint8Array.prototype.subarray === 'undefined') {
Uint8Array.prototype.subarray = function subarray(start, end) {
return new Uint8Array(this.slice(start, end));
};
Float32Array.prototype.subarray = function subarray(start, end) {
return new Float32Array(this.slice(start, end));
};
}
// Support: Android<4.1
if (typeof Float64Array === 'undefined') {
window.Float64Array = Float32Array;
}
return;
}
function subarray(start, end) {
return new TypedArray(this.slice(start, end));
}
function setArrayOffset(array, offset) {
if (arguments.length < 2) {
offset = 0;
}
for (var i = 0, n = array.length; i < n; ++i, ++offset) {
this[offset] = array[i] & 0xFF;
}
}
function TypedArray(arg1) {
var result, i, n;
if (typeof arg1 === 'number') {
result = [];
for (i = 0; i < arg1; ++i) {
result[i] = 0;
}
} else if ('slice' in arg1) {
result = arg1.slice(0);
} else {
result = [];
for (i = 0, n = arg1.length; i < n; ++i) {
result[i] = arg1[i];
}
}
result.subarray = subarray;
result.buffer = result;
result.byteLength = result.length;
result.set = setArrayOffset;
if (typeof arg1 === 'object' && arg1.buffer) {
result.buffer = arg1.buffer;
}
return result;
}
window.Uint8Array = TypedArray;
window.Int8Array = TypedArray;
// we don't need support for set, byteLength for 32-bit array
// so we can use the TypedArray as well
window.Uint32Array = TypedArray;
window.Int32Array = TypedArray;
window.Uint16Array = TypedArray;
window.Float32Array = TypedArray;
window.Float64Array = TypedArray;
})();
// URL = URL || webkitURL
// Support: Safari<7, Android 4.2+
(function normalizeURLObject() {
if (!window.URL) {
window.URL = window.webkitURL;
}
})();
// Object.defineProperty()?
// Support: Android<4.0, Safari<5.1
(function checkObjectDefinePropertyCompatibility() {
if (typeof Object.defineProperty !== 'undefined') {
var definePropertyPossible = true;
try {
// some browsers (e.g. safari) cannot use defineProperty() on DOM objects
// and thus the native version is not sufficient
Object.defineProperty(new Image(), 'id', { value: 'test' });
// ... another test for android gb browser for non-DOM objects
var Test = function Test() {};
Test.prototype = { get id() { } };
Object.defineProperty(new Test(), 'id',
{ value: '', configurable: true, enumerable: true, writable: false });
} catch (e) {
definePropertyPossible = false;
}
if (definePropertyPossible) {
return;
}
}
Object.defineProperty = function objectDefineProperty(obj, name, def) {
delete obj[name];
if ('get' in def) {
obj.__defineGetter__(name, def['get']);
}
if ('set' in def) {
obj.__defineSetter__(name, def['set']);
}
if ('value' in def) {
obj.__defineSetter__(name, function objectDefinePropertySetter(value) {
this.__defineGetter__(name, function objectDefinePropertyGetter() {
return value;
});
return value;
});
obj[name] = def.value;
}
};
})();
// No XMLHttpRequest#response?
// Support: IE<11, Android <4.0
(function checkXMLHttpRequestResponseCompatibility() {
var xhrPrototype = XMLHttpRequest.prototype;
var xhr = new XMLHttpRequest();
if (!('overrideMimeType' in xhr)) {
// IE10 might have response, but not overrideMimeType
// Support: IE10
Object.defineProperty(xhrPrototype, 'overrideMimeType', {
value: function xmlHttpRequestOverrideMimeType(mimeType) {}
});
}
if ('responseType' in xhr) {
return;
}
// The worker will be using XHR, so we can save time and disable worker.
PDFJS.disableWorker = true;
// Support: IE9
if (typeof VBArray !== 'undefined') {
Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() {
if (this.responseType === 'arraybuffer') {
return new Uint8Array(new VBArray(this.responseBody).toArray());
} else {
return this.responseText;
}
}
});
return;
}
// other browsers
function responseTypeSetter() {
// will be only called to set "arraybuffer"
this.overrideMimeType('text/plain; charset=x-user-defined');
}
if (typeof xhr.overrideMimeType === 'function') {
Object.defineProperty(xhrPrototype, 'responseType',
{ set: responseTypeSetter });
}
function responseGetter() {
var text = this.responseText;
var i, n = text.length;
var result = new Uint8Array(n);
for (i = 0; i < n; ++i) {
result[i] = text.charCodeAt(i) & 0xFF;
}
return result.buffer;
}
Object.defineProperty(xhrPrototype, 'response', { get: responseGetter });
})();
// window.btoa (base64 encode function) ?
// Support: IE<10
(function checkWindowBtoaCompatibility() {
if ('btoa' in window) {
return;
}
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.btoa = function windowBtoa(chars) {
var buffer = '';
var i, n;
for (i = 0, n = chars.length; i < n; i += 3) {
var b1 = chars.charCodeAt(i) & 0xFF;
var b2 = chars.charCodeAt(i + 1) & 0xFF;
var b3 = chars.charCodeAt(i + 2) & 0xFF;
var d1 = b1 >> 2, d2 = ((b1 & 3) << 4) | (b2 >> 4);
var d3 = i + 1 < n ? ((b2 & 0xF) << 2) | (b3 >> 6) : 64;
var d4 = i + 2 < n ? (b3 & 0x3F) : 64;
buffer += (digits.charAt(d1) + digits.charAt(d2) +
digits.charAt(d3) + digits.charAt(d4));
}
return buffer;
};
})();
// window.atob (base64 encode function)?
// Support: IE<10
(function checkWindowAtobCompatibility() {
if ('atob' in window) {
return;
}
// https://github.com/davidchambers/Base64.js
var digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.atob = function (input) {
input = input.replace(/=+$/, '');
if (input.length % 4 === 1) {
throw new Error('bad atob input');
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = input.charAt(idx++);
// character found in table?
// initialize bit storage and add its ascii value
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = digits.indexOf(buffer);
}
return output;
};
})();
// Function.prototype.bind?
// Support: Android<4.0, iOS<6.0
(function checkFunctionPrototypeBindCompatibility() {
if (typeof Function.prototype.bind !== 'undefined') {
return;
}
Function.prototype.bind = function functionPrototypeBind(obj) {
var fn = this, headArgs = Array.prototype.slice.call(arguments, 1);
var bound = function functionPrototypeBindBound() {
var args = headArgs.concat(Array.prototype.slice.call(arguments));
return fn.apply(obj, args);
};
return bound;
};
})();
// HTMLElement dataset property
// Support: IE<11, Safari<5.1, Android<4.0
(function checkDatasetProperty() {
var div = document.createElement('div');
if ('dataset' in div) {
return; // dataset property exists
}
Object.defineProperty(HTMLElement.prototype, 'dataset', {
get: function() {
if (this._dataset) {
return this._dataset;
}
var dataset = {};
for (var j = 0, jj = this.attributes.length; j < jj; j++) {
var attribute = this.attributes[j];
if (attribute.name.substring(0, 5) !== 'data-') {
continue;
}
var key = attribute.name.substring(5).replace(/\-([a-z])/g,
function(all, ch) {
return ch.toUpperCase();
});
dataset[key] = attribute.value;
}
Object.defineProperty(this, '_dataset', {
value: dataset,
writable: false,
enumerable: false
});
return dataset;
},
enumerable: true
});
})();
// HTMLElement classList property
// Support: IE<10, Android<4.0, iOS<5.0
(function checkClassListProperty() {
var div = document.createElement('div');
if ('classList' in div) {
return; // classList property exists
}
function changeList(element, itemName, add, remove) {
var s = element.className || '';
var list = s.split(/\s+/g);
if (list[0] === '') {
list.shift();
}
var index = list.indexOf(itemName);
if (index < 0 && add) {
list.push(itemName);
}
if (index >= 0 && remove) {
list.splice(index, 1);
}
element.className = list.join(' ');
return (index >= 0);
}
var classListPrototype = {
add: function(name) {
changeList(this.element, name, true, false);
},
contains: function(name) {
return changeList(this.element, name, false, false);
},
remove: function(name) {
changeList(this.element, name, false, true);
},
toggle: function(name) {
changeList(this.element, name, true, true);
}
};
Object.defineProperty(HTMLElement.prototype, 'classList', {
get: function() {
if (this._classList) {
return this._classList;
}
var classList = Object.create(classListPrototype, {
element: {
value: this,
writable: false,
enumerable: true
}
});
Object.defineProperty(this, '_classList', {
value: classList,
writable: false,
enumerable: false
});
return classList;
},
enumerable: true
});
})();
// Check console compatibility
// In older IE versions the console object is not available
// unless console is open.
// Support: IE<10
(function checkConsoleCompatibility() {
if (!('console' in window)) {
window.console = {
log: function() {},
error: function() {},
warn: function() {}
};
} else if (!('bind' in console.log)) {
// native functions in IE9 might not have bind
console.log = (function(fn) {
return function(msg) { return fn(msg); };
})(console.log);
console.error = (function(fn) {
return function(msg) { return fn(msg); };
})(console.error);
console.warn = (function(fn) {
return function(msg) { return fn(msg); };
})(console.warn);
}
})();
// Check onclick compatibility in Opera
// Support: Opera<15
(function checkOnClickCompatibility() {
// workaround for reported Opera bug DSK-354448:
// onclick fires on disabled buttons with opaque content
function ignoreIfTargetDisabled(event) {
if (isDisabled(event.target)) {
event.stopPropagation();
}
}
function isDisabled(node) {
return node.disabled || (node.parentNode && isDisabled(node.parentNode));
}
if (navigator.userAgent.indexOf('Opera') !== -1) {
// use browser detection since we cannot feature-check this bug
document.addEventListener('click', ignoreIfTargetDisabled, true);
}
})();
// Checks if possible to use URL.createObjectURL()
// Support: IE
(function checkOnBlobSupport() {
// sometimes IE loosing the data created with createObjectURL(), see #3977
if (navigator.userAgent.indexOf('Trident') >= 0) {
PDFJS.disableCreateObjectURL = true;
}
})();
// Checks if navigator.language is supported
(function checkNavigatorLanguage() {
if ('language' in navigator &&
/^[a-z]+(-[A-Z]+)?$/.test(navigator.language)) {
return;
}
function formatLocale(locale) {
var split = locale.split(/[-_]/);
split[0] = split[0].toLowerCase();
if (split.length > 1) {
split[1] = split[1].toUpperCase();
}
return split.join('-');
}
var language = navigator.language || navigator.userLanguage || 'en-US';
PDFJS.locale = formatLocale(language);
})();
(function checkRangeRequests() {
// Safari has issues with cached range requests see:
// https://github.com/mozilla/pdf.js/issues/3260
// Last tested with version 6.0.4.
// Support: Safari 6.0+
var isSafari = Object.prototype.toString.call(
window.HTMLElement).indexOf('Constructor') > 0;
// Older versions of Android (pre 3.0) has issues with range requests, see:
// https://github.com/mozilla/pdf.js/issues/3381.
// Make sure that we only match webkit-based Android browsers,
// since Firefox/Fennec works as expected.
// Support: Android<3.0
var regex = /Android\s[0-2][^\d]/;
var isOldAndroid = regex.test(navigator.userAgent);
if (isSafari || isOldAndroid) {
PDFJS.disableRange = true;
}
})();
// Check if the browser supports manipulation of the history.
// Support: IE<10, Android<4.2
(function checkHistoryManipulation() {
// Android 2.x has so buggy pushState support that it was removed in
// Android 3.0 and restored as late as in Android 4.2.
// Support: Android 2.x
if (!history.pushState || navigator.userAgent.indexOf('Android 2.') >= 0) {
PDFJS.disableHistory = true;
}
})();
// Support: IE<11, Chrome<21, Android<4.4, Safari<6
(function checkSetPresenceInImageData() {
// IE < 11 will use window.CanvasPixelArray which lacks set function.
if (window.CanvasPixelArray) {
if (typeof window.CanvasPixelArray.prototype.set !== 'function') {
window.CanvasPixelArray.prototype.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
}
} else {
// Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
// Because we cannot feature detect it, we rely on user agent parsing.
var polyfill = false, versionMatch;
if (navigator.userAgent.indexOf('Chrom') >= 0) {
versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
// Chrome < 21 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
} else if (navigator.userAgent.indexOf('Android') >= 0) {
// Android < 4.4 lacks the set function.
// Android >= 4.4 will contain Chrome in the user agent,
// thus pass the Chrome check above and not reach this block.
polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
} else if (navigator.userAgent.indexOf('Safari') >= 0) {
versionMatch = navigator.userAgent.
match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
// Safari < 6 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
}
if (polyfill) {
var contextPrototype = window.CanvasRenderingContext2D.prototype;
contextPrototype._createImageData = contextPrototype.createImageData;
contextPrototype.createImageData = function(w, h) {
var imageData = this._createImageData(w, h);
imageData.data.set = function(arr) {
for (var i = 0, ii = this.length; i < ii; i++) {
this[i] = arr[i];
}
};
return imageData;
};
}
}
})();
// Support: IE<10, Android<4.0, iOS
(function checkRequestAnimationFrame() {
function fakeRequestAnimationFrame(callback) {
window.setTimeout(callback, 20);
}
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
if (isIOS) {
// requestAnimationFrame on iOS is broken, replacing with fake one.
window.requestAnimationFrame = fakeRequestAnimationFrame;
return;
}
if ('requestAnimationFrame' in window) {
return;
}
window.requestAnimationFrame =
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
fakeRequestAnimationFrame;
})();
(function checkCanvasSizeLimitation() {
var isIOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent);
var isAndroid = /Android/g.test(navigator.userAgent);
if (isIOS || isAndroid) {
// 5MP
PDFJS.maxCanvasPixels = 5242880;
}
})();
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -23,10 +23,16 @@
.full-size;
background-color: @gray-lighter;
overflow: scroll;
canvas {
canvas, div.pdf-canvas {
background: white;
box-shadow: black 0px 0px 10px;
}
div.pdf-canvas.pdfng-empty {
background-color: white;
}
div.pdf-canvas.pdfng-loading {
background-color: white;
}
.page-container {
margin: 10px auto;
padding: 0 10px;
@@ -137,7 +137,12 @@ describe "CompileController", ->
statusCode: 204
headers: { "mock": "header" }
@req.method = "mock-method"
@req.headers = {
'Mock': 'Headers',
'Range': 'should be passed - Range'
'If-Range': 'should be passed - If-Range'
'If-Modified-Since': 'should be passed - If-Modified-Since'
}
describe "user with standard priority", ->
@@ -152,6 +157,11 @@ describe "CompileController", ->
method: @req.method
url: "#{@settings.apis.clsi.url}#{@url}",
timeout: 60 * 1000
headers: {
'Range': 'should be passed - Range'
'If-Range': 'should be passed - If-Range'
'If-Modified-Since': 'should be passed - If-Modified-Since'
}
)
.should.equal true
@@ -175,6 +185,11 @@ describe "CompileController", ->
method: @req.method
url: "#{@settings.apis.clsi_priority.url}#{@url}",
timeout: 60 * 1000
headers: {
'Range': 'should be passed - Range'
'If-Range': 'should be passed - If-Range'
'If-Modified-Since': 'should be passed - If-Modified-Since'
}
)
.should.equal true