From 5e37a103349006c1b29a2f8585a416baab7ee187 Mon Sep 17 00:00:00 2001 From: Thomas Mueller Date: Tue, 12 Jun 2012 00:49:40 +0200 Subject: [PATCH] PDF.JS updated, compatibility.js added for browser support, should fix: oc-395 --- apps/files_pdfviewer/appinfo/app.php | 3 +- apps/files_pdfviewer/appinfo/info.xml | 2 +- apps/files_pdfviewer/css/viewer.css | 1 - apps/files_pdfviewer/js/pdfjs/build/pdf.js | 2830 +++++++++++++---- .../files_pdfviewer/js/pdfjs/compatibility.js | 340 ++ apps/files_pdfviewer/js/pdfjs/update.sh | 3 + apps/files_pdfviewer/js/pdfjs/viewer.js | 1936 +++++++++++ apps/files_pdfviewer/js/viewer.js | 19 +- 8 files changed, 4470 insertions(+), 664 deletions(-) create mode 100644 apps/files_pdfviewer/js/pdfjs/compatibility.js create mode 100644 apps/files_pdfviewer/js/pdfjs/viewer.js diff --git a/apps/files_pdfviewer/appinfo/app.php b/apps/files_pdfviewer/appinfo/app.php index 06b1567067..f700d004dc 100644 --- a/apps/files_pdfviewer/appinfo/app.php +++ b/apps/files_pdfviewer/appinfo/app.php @@ -3,5 +3,6 @@ OCP\Util::addscript( 'files_pdfviewer', 'viewer'); OCP\Util::addStyle( 'files_pdfviewer', 'viewer'); OCP\Util::addscript( 'files_pdfviewer', 'pdfjs/build/pdf'); -OCP\Util::addscript( 'files_pdfviewer', 'pdfview'); +OCP\Util::addscript( 'files_pdfviewer', 'pdfjs/compatibility'); +OCP\Util::addscript( 'files_pdfviewer', 'pdfjs/viewer'); ?> diff --git a/apps/files_pdfviewer/appinfo/info.xml b/apps/files_pdfviewer/appinfo/info.xml index e3813be100..074962f57f 100644 --- a/apps/files_pdfviewer/appinfo/info.xml +++ b/apps/files_pdfviewer/appinfo/info.xml @@ -4,7 +4,7 @@ PDF Viewer Inline PDF viewer (pdfjs-based) GPL - Joan Creus + Joan Creus, Thomas Müller 4 true diff --git a/apps/files_pdfviewer/css/viewer.css b/apps/files_pdfviewer/css/viewer.css index b735dbfedf..f9ce929d8b 100644 --- a/apps/files_pdfviewer/css/viewer.css +++ b/apps/files_pdfviewer/css/viewer.css @@ -2,7 +2,6 @@ /* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ #viewer { - background-color: #929292; font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; /*margin: 0px;*/ padding: 0px; diff --git a/apps/files_pdfviewer/js/pdfjs/build/pdf.js b/apps/files_pdfviewer/js/pdfjs/build/pdf.js index a19a9b75fe..b1fc9d9747 100644 --- a/apps/files_pdfviewer/js/pdfjs/build/pdf.js +++ b/apps/files_pdfviewer/js/pdfjs/build/pdf.js @@ -7,10 +7,9 @@ var PDFJS = {}; // Use strict in our context only - users might not want it 'use strict'; - PDFJS.build = 'd823592'; + PDFJS.build = '2aae4fd'; // Files are inserted below - see Makefile - /* PDFJSSCRIPT_INCLUDE_ALL */ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -20,7 +19,7 @@ var globalScope = (typeof window === 'undefined') ? this : window; var isWorker = (typeof window == 'undefined'); -var ERRORS = 0, WARNINGS = 1, TODOS = 5; +var ERRORS = 0, WARNINGS = 1, INFOS = 5; var verbosity = WARNINGS; // The global PDFJS object exposes the API @@ -44,7 +43,19 @@ function getPdf(arg, callback) { params = { url: arg }; var xhr = new XMLHttpRequest(); + xhr.open('GET', params.url); + + var headers = params.headers; + if (headers) { + for (var property in headers) { + if (typeof headers[property] === 'undefined') + continue; + + xhr.setRequestHeader(property, params.headers[property]); + } + } + xhr.mozResponseType = xhr.responseType = 'arraybuffer'; var protocol = params.url.indexOf(':') < 0 ? window.location.protocol : params.url.substring(0, params.url.indexOf(':') + 1); @@ -76,8 +87,6 @@ var Page = (function PageClosure() { function Page(xref, pageNumber, pageDict, ref) { this.pageNumber = pageNumber; this.pageDict = pageDict; - this.stats = new StatTimer(); - this.stats.enabled = !!globalScope.PDFJS.enableStats; this.xref = xref; this.ref = ref; @@ -113,18 +122,10 @@ var Page = (function PageClosure() { return shadow(this, 'mediaBox', obj); }, get view() { - var cropBox = this.inheritPageProp('CropBox'); - var view = { - x: 0, - y: 0, - width: this.width, - height: this.height - }; - if (!isArray(cropBox) || cropBox.length !== 4) - return shadow(this, 'view', view); - var mediaBox = this.mediaBox; - var offsetX = mediaBox[0], offsetY = mediaBox[1]; + var cropBox = this.inheritPageProp('CropBox'); + if (!isArray(cropBox) || cropBox.length !== 4) + return shadow(this, 'view', mediaBox); // From the spec, 6th ed., p.963: // "The crop, bleed, trim, and art boxes should not ordinarily @@ -132,42 +133,13 @@ var Page = (function PageClosure() { // effectively reduced to their intersection with the media box." cropBox = Util.intersect(cropBox, mediaBox); if (!cropBox) - return shadow(this, 'view', view); + return shadow(this, 'view', mediaBox); - var tl = this.rotatePoint(cropBox[0] - offsetX, cropBox[1] - offsetY); - var br = this.rotatePoint(cropBox[2] - offsetX, cropBox[3] - offsetY); - view.x = Math.min(tl.x, br.x); - view.y = Math.min(tl.y, br.y); - view.width = Math.abs(tl.x - br.x); - view.height = Math.abs(tl.y - br.y); - - return shadow(this, 'view', view); + return shadow(this, 'view', cropBox); }, get annotations() { return shadow(this, 'annotations', this.inheritPageProp('Annots')); }, - get width() { - var mediaBox = this.mediaBox; - var rotate = this.rotate; - var width; - if (rotate == 0 || rotate == 180) { - width = (mediaBox[2] - mediaBox[0]); - } else { - width = (mediaBox[3] - mediaBox[1]); - } - return shadow(this, 'width', width); - }, - get height() { - var mediaBox = this.mediaBox; - var rotate = this.rotate; - var height; - if (rotate == 0 || rotate == 180) { - height = (mediaBox[3] - mediaBox[1]); - } else { - height = (mediaBox[2] - mediaBox[0]); - } - return shadow(this, 'height', height); - }, get rotate() { var rotate = this.inheritPageProp('Rotate') || 0; // Normalize rotation so it's a multiple of 90 and between 0 and 270 @@ -183,43 +155,20 @@ var Page = (function PageClosure() { return shadow(this, 'rotate', rotate); }, - startRenderingFromOperatorList: - function Page_startRenderingFromOperatorList(operatorList, fonts) { - var self = this; - this.operatorList = operatorList; - - var displayContinuation = function pageDisplayContinuation() { - // Always defer call to display() to work around bug in - // Firefox error reporting from XHR callbacks. - setTimeout(function pageSetTimeout() { - self.displayReadyPromise.resolve(); - }); - }; - - this.ensureFonts(fonts, - function pageStartRenderingFromOperatorListEnsureFonts() { - displayContinuation(); - } - ); - }, - getOperatorList: function Page_getOperatorList(handler, dependency) { - if (this.operatorList) { - // content was compiled - return this.operatorList; - } - - this.stats.time('Build IR Queue'); - var xref = this.xref; var content = this.content; var resources = this.resources; if (isArray(content)) { // fetching items + var streams = []; var i, n = content.length; + var streams = []; for (i = 0; i < n; ++i) - content[i] = xref.fetchIfRef(content[i]); - content = new StreamsSequenceStream(content); + streams.push(xref.fetchIfRef(content[i])); + content = new StreamsSequenceStream(streams); + } else if (isStream(content)) { + content.reset(); } else if (!content) { // replacing non-existent page content with empty one content = new Stream(new Uint8Array(0)); @@ -228,9 +177,31 @@ var Page = (function PageClosure() { var pe = this.pe = new PartialEvaluator( xref, handler, 'p' + this.pageNumber + '_'); - this.operatorList = pe.getOperatorList(content, resources, dependency); - this.stats.timeEnd('Build IR Queue'); - return this.operatorList; + return pe.getOperatorList(content, resources, dependency); + }, + extractTextContent: function Page_extractTextContent() { + var handler = { + on: function nullHandlerOn() {}, + send: function nullHandlerSend() {} + }; + + var xref = this.xref; + var content = xref.fetchIfRef(this.content); + var resources = xref.fetchIfRef(this.resources); + if (isArray(content)) { + // fetching items + var i, n = content.length; + var streams = []; + for (i = 0; i < n; ++i) + streams.push(xref.fetchIfRef(content[i])); + content = new StreamsSequenceStream(streams); + } else if (isStream(content)) { + content.reset(); + } + + var pe = new PartialEvaluator( + xref, handler, 'p' + this.pageNumber + '_'); + return pe.getTextContent(content, resources); }, ensureFonts: function Page_ensureFonts(fonts, callback) { @@ -250,60 +221,6 @@ var Page = (function PageClosure() { }.bind(this) ); }, - - display: function Page_display(gfx, callback) { - var stats = this.stats; - stats.time('Rendering'); - var xref = this.xref; - var resources = this.resources; - var mediaBox = this.mediaBox; - assertWellFormed(isDict(resources), 'invalid page resources'); - - gfx.xref = xref; - gfx.res = resources; - gfx.beginDrawing({ x: mediaBox[0], y: mediaBox[1], - width: this.width, - height: this.height, - rotate: this.rotate }); - - var startIdx = 0; - var length = this.operatorList.fnArray.length; - var operatorList = this.operatorList; - var stepper = null; - if (PDFJS.pdfBug && StepperManager.enabled) { - stepper = StepperManager.create(this.pageNumber); - stepper.init(operatorList); - stepper.nextBreakPoint = stepper.getNextBreakPoint(); - } - - var self = this; - function next() { - startIdx = - gfx.executeOperatorList(operatorList, startIdx, next, stepper); - if (startIdx == length) { - gfx.endDrawing(); - stats.timeEnd('Rendering'); - stats.timeEnd('Overall'); - if (callback) callback(); - } - } - next(); - }, - rotatePoint: function Page_rotatePoint(x, y, reverse) { - var rotate = reverse ? (360 - this.rotate) : this.rotate; - switch (rotate) { - case 180: - return {x: this.width - x, y: y}; - case 90: - return {x: this.width - y, y: this.height - x}; - case 270: - return {x: y, y: x}; - case 360: - case 0: - default: - return {x: x, y: this.height - y}; - } - }, getLinks: function Page_getLinks() { var links = []; var annotations = pageGetAnnotations(); @@ -337,6 +254,7 @@ var Page = (function PageClosure() { case 'http': case 'https': case 'ftp': + case 'mailto': return true; default: return false; @@ -355,15 +273,10 @@ var Page = (function PageClosure() { if (!isName(subtype)) continue; var rect = annotation.get('Rect'); - var topLeftCorner = this.rotatePoint(rect[0], rect[1]); - var bottomRightCorner = this.rotatePoint(rect[2], rect[3]); var item = {}; item.type = subtype.name; - item.x = Math.min(topLeftCorner.x, bottomRightCorner.x); - item.y = Math.min(topLeftCorner.y, bottomRightCorner.y); - item.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); - item.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); + item.rect = rect; switch (subtype.name) { case 'Link': var a = annotation.get('A'); @@ -437,7 +350,8 @@ var Page = (function PageClosure() { var title = annotation.get('T'); item.content = stringToPDFString(content || ''); item.title = stringToPDFString(title || ''); - item.name = annotation.get('Name').name; + item.name = !annotation.has('Name') ? 'Note' : + annotation.get('Name').name; break; default: TODO('unimplemented annotation type: ' + subtype.name); @@ -446,37 +360,6 @@ var Page = (function PageClosure() { items.push(item); } return items; - }, - startRendering: function Page_startRendering(ctx, callback, textLayer) { - var stats = this.stats; - stats.time('Overall'); - // If there is no displayReadyPromise yet, then the operatorList was never - // requested before. Make the request and create the promise. - if (!this.displayReadyPromise) { - this.pdf.startRendering(this); - this.displayReadyPromise = new Promise(); - } - - // Once the operatorList and fonts are loaded, do the actual rendering. - this.displayReadyPromise.then( - function pageDisplayReadyPromise() { - var gfx = new CanvasGraphics(ctx, this.objs, textLayer); - try { - this.display(gfx, callback); - } catch (e) { - if (callback) - callback(e); - else - error(e); - } - }.bind(this), - function pageDisplayReadPromiseError(reason) { - if (callback) - callback(reason); - else - error(reason); - } - ); } }; @@ -484,26 +367,26 @@ var Page = (function PageClosure() { })(); /** - * The `PDFDocModel` holds all the data of the PDF file. Compared to the + * The `PDFDocument` holds all the data of the PDF file. Compared to the * `PDFDoc`, this one doesn't have any job management code. - * Right now there exists one PDFDocModel on the main thread + one object + * Right now there exists one PDFDocument on the main thread + one object * for each worker. If there is no worker support enabled, there are two - * `PDFDocModel` objects on the main thread created. + * `PDFDocument` objects on the main thread created. */ -var PDFDocModel = (function PDFDocModelClosure() { - function PDFDocModel(arg, callback) { +var PDFDocument = (function PDFDocumentClosure() { + function PDFDocument(arg, password) { if (isStream(arg)) - init.call(this, arg); + init.call(this, arg, password); else if (isArrayBuffer(arg)) - init.call(this, new Stream(arg)); + init.call(this, new Stream(arg), password); else - error('PDFDocModel: Unknown argument type'); + error('PDFDocument: Unknown argument type'); } - function init(stream) { + function init(stream, password) { assertWellFormed(stream.length > 0, 'stream must have data'); this.stream = stream; - this.setup(); + this.setup(password); this.acroForm = this.catalog.catDict.get('AcroForm'); } @@ -523,7 +406,7 @@ var PDFDocModel = (function PDFDocModelClosure() { return true; /* found */ } - PDFDocModel.prototype = { + PDFDocument.prototype = { get linearization() { var length = this.stream.length; var linearization = false; @@ -584,7 +467,7 @@ var PDFDocModel = (function PDFDocModelClosure() { }, // Find the header, remove leading garbage and setup the stream // starting from the header. - checkHeader: function PDFDocModel_checkHeader() { + checkHeader: function PDFDocument_checkHeader() { var stream = this.stream; stream.reset(); if (find(stream, '%PDF-', 1024)) { @@ -594,11 +477,12 @@ var PDFDocModel = (function PDFDocModelClosure() { } // May not be a PDF file, continue anyway. }, - setup: function PDFDocModel_setup(ownerPassword, userPassword) { + setup: function PDFDocument_setup(password) { this.checkHeader(); var xref = new XRef(this.stream, this.startXRef, - this.mainXRefEntriesOffset); + this.mainXRefEntriesOffset, + password); this.xref = xref; this.catalog = new Catalog(xref); }, @@ -608,7 +492,7 @@ var PDFDocModel = (function PDFDocModelClosure() { // shadow the prototype getter return shadow(this, 'numPages', num); }, - getDocumentInfo: function PDFDocModel_getDocumentInfo() { + getDocumentInfo: function PDFDocument_getDocumentInfo() { var info; if (this.xref.trailer.has('Info')) { var infoDict = this.xref.trailer.get('Info'); @@ -622,7 +506,7 @@ var PDFDocModel = (function PDFDocModelClosure() { return shadow(this, 'getDocumentInfo', info); }, - getFingerprint: function PDFDocModel_getFingerprint() { + getFingerprint: function PDFDocument_getFingerprint() { var xref = this.xref, fileID; if (xref.trailer.has('ID')) { fileID = ''; @@ -643,259 +527,22 @@ var PDFDocModel = (function PDFDocModelClosure() { return shadow(this, 'getFingerprint', fileID); }, - getPage: function PDFDocModel_getPage(n) { + getPage: function PDFDocument_getPage(n) { return this.catalog.getPage(n); } }; - return PDFDocModel; + return PDFDocument; })(); -var PDFDoc = (function PDFDocClosure() { - function PDFDoc(arg, callback) { - var stream = null; - var data = null; - - if (isStream(arg)) { - stream = arg; - data = arg.bytes; - } else if (isArrayBuffer(arg)) { - stream = new Stream(arg); - data = arg; - } else { - error('PDFDoc: Unknown argument type'); - } - - this.data = data; - this.stream = stream; - this.pdfModel = new PDFDocModel(stream); - this.fingerprint = this.pdfModel.getFingerprint(); - this.info = this.pdfModel.getDocumentInfo(); - this.catalog = this.pdfModel.catalog; - this.objs = new PDFObjects(); - - this.pageCache = []; - this.fontsLoading = {}; - this.workerReadyPromise = new Promise('workerReady'); - - // If worker support isn't disabled explicit and the browser has worker - // support, create a new web worker and test if it/the browser fullfills - // all requirements to run parts of pdf.js in a web worker. - // Right now, the requirement is, that an Uint8Array is still an Uint8Array - // as it arrives on the worker. Chrome added this with version 15. - if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { - var workerSrc = PDFJS.workerSrc; - if (typeof workerSrc === 'undefined') { - error('No PDFJS.workerSrc specified'); - } - - try { - var worker; - if (PDFJS.isFirefoxExtension) { - // The firefox extension can't load the worker from the resource:// - // url so we have to inline the script and then use the blob loader. - var bb = new MozBlobBuilder(); - bb.append(document.querySelector('#PDFJS_SCRIPT_TAG').textContent); - var blobUrl = window.URL.createObjectURL(bb.getBlob()); - worker = new Worker(blobUrl); - } else { - // Some versions of FF can't create a worker on localhost, see: - // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - worker = new Worker(workerSrc); - } - - var messageHandler = new MessageHandler('main', worker); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - // Some versions of Opera throw a DATA_CLONE_ERR on - // serializing the typed array. - messageHandler.send('test', testObj); - return; - } catch (e) { - warn('The worker has been disabled.'); - } - } - // Either workers are disabled, not supported or have thrown an exception. - // Thus, we fallback to a faked worker. - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); - } - - PDFDoc.prototype = { - setupFakeWorker: function PDFDoc_setupFakeWorker() { - // If we don't use a worker, just post/sendMessage to the main thread. - var fakeWorker = { - postMessage: function PDFDoc_postMessage(obj) { - fakeWorker.onmessage({data: obj}); - }, - terminate: function PDFDoc_terminate() {} - }; - - var messageHandler = new MessageHandler('main', fakeWorker); - this.setupMessageHandler(messageHandler); - - // If the main thread is our worker, setup the handling for the messages - // the main thread sends to it self. - WorkerMessageHandler.setup(messageHandler); - }, - - - setupMessageHandler: function PDFDoc_setupMessageHandler(messageHandler) { - this.messageHandler = messageHandler; - - messageHandler.on('page', function pdfDocPage(data) { - var pageNum = data.pageNum; - var page = this.pageCache[pageNum]; - var depFonts = data.depFonts; - - page.stats.timeEnd('Page Request'); - page.startRenderingFromOperatorList(data.operatorList, depFonts); - }, this); - - messageHandler.on('obj', function pdfDocObj(data) { - var id = data[0]; - var type = data[1]; - - switch (type) { - case 'JpegStream': - var imageData = data[2]; - loadJpegStream(id, imageData, this.objs); - break; - case 'Image': - var imageData = data[2]; - this.objs.resolve(id, imageData); - break; - case 'Font': - var name = data[2]; - var file = data[3]; - var properties = data[4]; - - if (file) { - // Rewrap the ArrayBuffer in a stream. - var fontFileDict = new Dict(); - file = new Stream(file, 0, file.length, fontFileDict); - } - - // At this point, only the font object is created but the font is - // not yet attached to the DOM. This is done in `FontLoader.bind`. - var font = new Font(name, file, properties); - this.objs.resolve(id, font); - break; - default: - error('Got unkown object type ' + type); - } - }, this); - - messageHandler.on('page_error', function pdfDocError(data) { - var page = this.pageCache[data.pageNum]; - if (page.displayReadyPromise) - page.displayReadyPromise.reject(data.error); - else - error(data.error); - }, this); - - messageHandler.on('jpeg_decode', function(data, promise) { - var imageData = data[0]; - var components = data[1]; - if (components != 3 && components != 1) - error('Only 3 component or 1 component can be returned'); - - var img = new Image(); - img.onload = (function messageHandler_onloadClosure() { - var width = img.width; - var height = img.height; - var size = width * height; - var rgbaLength = size * 4; - var buf = new Uint8Array(size * components); - var tmpCanvas = createScratchCanvas(width, height); - var tmpCtx = tmpCanvas.getContext('2d'); - tmpCtx.drawImage(img, 0, 0); - var data = tmpCtx.getImageData(0, 0, width, height).data; - - if (components == 3) { - for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { - buf[j] = data[i]; - buf[j + 1] = data[i + 1]; - buf[j + 2] = data[i + 2]; - } - } else if (components == 1) { - for (var i = 0, j = 0; i < rgbaLength; i += 4, j++) { - buf[j] = data[i]; - } - } - promise.resolve({ data: buf, width: width, height: height}); - }).bind(this); - var src = 'data:image/jpeg;base64,' + window.btoa(imageData); - img.src = src; - }); - - setTimeout(function pdfDocFontReadySetTimeout() { - messageHandler.send('doc', this.data); - this.workerReadyPromise.resolve(true); - }.bind(this)); - }, - - get numPages() { - return this.pdfModel.numPages; - }, - - startRendering: function PDFDoc_startRendering(page) { - // The worker might not be ready to receive the page request yet. - this.workerReadyPromise.then(function pdfDocStartRenderingThen() { - page.stats.time('Page Request'); - this.messageHandler.send('page_request', page.pageNumber + 1); - }.bind(this)); - }, - - getPage: function PDFDoc_getPage(n) { - if (this.pageCache[n]) - return this.pageCache[n]; - - var page = this.pdfModel.getPage(n); - // Add a reference to the objects such that Page can forward the reference - // to the CanvasGraphics and so on. - page.objs = this.objs; - page.pdf = this; - return (this.pageCache[n] = page); - }, - - destroy: function PDFDoc_destroy() { - if (this.worker) - this.worker.terminate(); - - if (this.fontWorker) - this.fontWorker.terminate(); - - for (var n in this.pageCache) - delete this.pageCache[n]; - - delete this.data; - delete this.stream; - delete this.pdf; - delete this.catalog; - } - }; - - return PDFDoc; -})(); - -globalScope.PDFJS.PDFDoc = PDFDoc; /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 'use strict'; +// Use only for debugging purposes. This should not be used in any code that is +// in mozilla master. function log(msg) { if (console && console.log) console.log(msg); @@ -903,9 +550,36 @@ function log(msg) { print(msg); } +// A notice for devs that will not trigger the fallback UI. These are good +// for things that are helpful to devs, such as warning that Workers were +// disabled, which is important to devs but not end users. +function info(msg) { + if (verbosity >= INFOS) { + log('Info: ' + msg); + PDFJS.LogManager.notify('info', msg); + } +} + +// Non-fatal warnings that should trigger the fallback UI. function warn(msg) { - if (verbosity >= WARNINGS) + if (verbosity >= WARNINGS) { log('Warning: ' + msg); + PDFJS.LogManager.notify('warn', msg); + } +} + +// Fatal errors that should trigger the fallback UI and halt execution by +// throwing an exception. +function error(msg) { + log('Error: ' + msg); + log(backtrace()); + PDFJS.LogManager.notify('error', msg); + throw new Error(msg); +} + +// Missing features that should trigger the fallback UI. +function TODO(what) { + warn('TODO: ' + what); } function backtrace() { @@ -916,21 +590,6 @@ function backtrace() { } } -function error(msg) { - log('Error: ' + msg); - log(backtrace()); - throw new Error(msg); -} - -function TODO(what) { - if (verbosity >= TODOS) - log('TODO: ' + what); -} - -function malformed(msg) { - error('Malformed PDF: ' + msg); -} - function assert(cond, msg) { if (!cond) error(msg); @@ -940,9 +599,25 @@ function assert(cond, msg) { // behavior is undefined. function assertWellFormed(cond, msg) { if (!cond) - malformed(msg); + error(msg); } +var LogManager = PDFJS.LogManager = (function LogManagerClosure() { + var loggers = []; + return { + addLogger: function logManager_addLogger(logger) { + loggers.push(logger); + }, + notify: function(type, message) { + for (var i = 0, ii = loggers.length; i < ii; i++) { + var logger = loggers[i]; + if (logger[type]) + logger[type](message); + } + } + }; +})(); + function shadow(obj, prop, value) { Object.defineProperty(obj, prop, { value: value, enumerable: true, @@ -951,6 +626,19 @@ function shadow(obj, prop, value) { return value; } +var PasswordException = (function PasswordExceptionClosure() { + function PasswordException(msg, code) { + this.name = 'PasswordException'; + this.message = msg; + this.code = code; + } + + PasswordException.prototype = new Error(); + PasswordException.constructor = PasswordException; + + return PasswordException; +})(); + function bytesToString(bytes) { var str = ''; var length = bytes.length; @@ -969,7 +657,7 @@ function stringToBytes(str) { var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; -var Util = (function UtilClosure() { +var Util = PDFJS.Util = (function UtilClosure() { function Util() {} Util.makeCssRgb = function Util_makeCssRgb(r, g, b) { @@ -990,6 +678,19 @@ var Util = (function UtilClosure() { return [xt, yt]; }; + Util.applyInverseTransform = function Util_applyInverseTransform(p, m) { + var d = m[0] * m[3] - m[1] * m[2]; + var xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d; + var yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d; + return [xt, yt]; + }; + + Util.inverseTransform = function Util_inverseTransform(m) { + var d = m[0] * m[3] - m[1] * m[2]; + return [m[3] / d, -m[1] / d, -m[2] / d, m[0] / d, + (m[2] * m[5] - m[4] * m[3]) / d, (m[4] * m[1] - m[5] * m[0]) / d]; + }; + // Apply a generic 3d matrix M on a 3-vector v: // | a b c | | X | // | d e f | x | Y | @@ -1058,7 +759,7 @@ var Util = (function UtilClosure() { } return result; - } + }; Util.sign = function Util_sign(num) { return num < 0 ? -1 : 1; @@ -1067,6 +768,80 @@ var Util = (function UtilClosure() { return Util; })(); +var PageViewport = PDFJS.PageViewport = (function PageViewportClosure() { + function PageViewport(viewBox, scale, rotate, offsetX, offsetY) { + // creating transform to convert pdf coordinate system to the normal + // canvas like coordinates taking in account scale and rotation + var centerX = (viewBox[2] + viewBox[0]) / 2; + var centerY = (viewBox[3] + viewBox[1]) / 2; + var rotateA, rotateB, rotateC, rotateD; + switch (rotate) { + case -180: + case 180: + rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; + break; + case -270: + case 90: + rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; + break; + case -90: + case 270: + rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; + break; + case 360: + case 0: + default: + rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; + break; + } + var offsetCanvasX, offsetCanvasY; + var width, height; + if (rotateA == 0) { + offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; + offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; + width = Math.abs(viewBox[3] - viewBox[1]) * scale; + height = Math.abs(viewBox[2] - viewBox[0]) * scale; + } else { + offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; + offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; + width = Math.abs(viewBox[2] - viewBox[0]) * scale; + height = Math.abs(viewBox[3] - viewBox[1]) * scale; + } + // creating transform for the following operations: + // translate(-centerX, -centerY), rotate and flip vertically, + // scale, and translate(offsetCanvasX, offsetCanvasY) + this.transform = [ + rotateA * scale, + rotateB * scale, + rotateC * scale, + rotateD * scale, + offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, + offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY + ]; + + this.offsetX = offsetX; + this.offsetY = offsetY; + this.width = width; + this.height = height; + this.fontScale = scale; + } + PageViewport.prototype = { + convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + }, + convertToViewportRectangle: + function PageViewport_convertToViewportRectangle(rect) { + var tl = Util.applyTransform([rect[0], rect[1]], this.transform); + var br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + }, + convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + } + }; + return PageViewport; +})(); + var PDFStringTranslateTable = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, @@ -1095,6 +870,10 @@ function stringToPDFString(str) { return str2; } +function stringToUTF8String(str) { + return decodeURIComponent(escape(str)); +} + function isBool(v) { return typeof v == 'boolean'; } @@ -1168,7 +947,7 @@ function isPDFFunction(v) { * can be set. If any of these happens twice or the data is required before * it was set, an exception is throw. */ -var Promise = (function PromiseClosure() { +var Promise = PDFJS.Promise = (function PromiseClosure() { var EMPTY_PROMISE = {}; /** @@ -1190,6 +969,7 @@ var Promise = (function PromiseClosure() { } this.callbacks = []; this.errbacks = []; + this.progressbacks = []; }; /** * Builds a promise that is resolved when all the passed in promises are @@ -1205,7 +985,7 @@ var Promise = (function PromiseClosure() { deferred.resolve(results); return deferred; } - for (var i = 0; i < unresolved; ++i) { + for (var i = 0, ii = promises.length; i < ii; ++i) { var promise = promises[i]; promise.then((function(i) { return function(value) { @@ -1261,7 +1041,7 @@ var Promise = (function PromiseClosure() { } this.isResolved = true; - this.data = data || null; + this.data = (typeof data !== 'undefined') ? data : null; var callbacks = this.callbacks; for (var i = 0, ii = callbacks.length; i < ii; i++) { @@ -1269,7 +1049,14 @@ var Promise = (function PromiseClosure() { } }, - reject: function Promise_reject(reason) { + progress: function Promise_progress(data) { + var callbacks = this.progressbacks; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + callbacks[i].call(null, data); + } + }, + + reject: function Promise_reject(reason, exception) { if (this.isRejected) { error('A Promise can be rejected only once ' + this.name); } @@ -1282,11 +1069,11 @@ var Promise = (function PromiseClosure() { var errbacks = this.errbacks; for (var i = 0, ii = errbacks.length; i < ii; i++) { - errbacks[i].call(null, reason); + errbacks[i].call(null, reason, exception); } }, - then: function Promise_then(callback, errback) { + then: function Promise_then(callback, errback, progressback) { if (!callback) { error('Requiring callback' + this.name); } @@ -1303,6 +1090,9 @@ var Promise = (function PromiseClosure() { if (errback) this.errbacks.push(errback); } + + if (progressback) + this.progressbacks.push(progressback); } }; @@ -1361,6 +1151,659 @@ var StatTimer = (function StatTimerClosure() { }; return StatTimer; })(); + +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +/** + * This is the main entry point for loading a PDF and interacting with it. + * NOTE: If a URL is used to fetch the PDF data a standard XMLHttpRequest(XHR) + * is used, which means it must follow the same origin rules that any XHR does + * e.g. No cross domain requests without CORS. + * + * @param {string|TypedAray|object} source Can be an url to where a PDF is + * located, a typed array (Uint8Array) already populated with data or + * and parameter object with the following possible fields: + * - url - The URL of the PDF. + * - data - A typed array with PDF data. + * - httpHeaders - Basic authentication headers. + * - password - For decrypting password-protected PDFs. + * + * @return {Promise} A promise that is resolved with {PDFDocumentProxy} object. + */ +PDFJS.getDocument = function getDocument(source) { + var url, data, headers, password, parameters = {}; + if (typeof source === 'string') { + url = source; + } else if (isArrayBuffer(source)) { + data = source; + } else if (typeof source === 'object') { + url = source.url; + data = source.data; + headers = source.httpHeaders; + password = source.password; + parameters.password = password || null; + + if (!url && !data) + error('Invalid parameter array, need either .data or .url'); + } else { + error('Invalid parameter in getDocument, need either Uint8Array, ' + + 'string or a parameter object'); + } + + var promise = new PDFJS.Promise(); + var transport = new WorkerTransport(promise); + if (data) { + // assuming the data is array, instantiating directly from it + transport.sendData(data, parameters); + } else if (url) { + // fetch url + PDFJS.getPdf( + { + url: url, + progress: function getPDFProgress(evt) { + if (evt.lengthComputable) + promise.progress({ + loaded: evt.loaded, + total: evt.total + }); + }, + error: function getPDFError(e) { + promise.reject('Unexpected server response of ' + + e.target.status + '.'); + }, + headers: headers + }, + function getPDFLoad(data) { + transport.sendData(data, parameters); + }); + } + + return promise; +}; + +/** + * Proxy to a PDFDocument in the worker thread. Also, contains commonly used + * properties that can be read synchronously. + */ +var PDFDocumentProxy = (function PDFDocumentProxyClosure() { + function PDFDocumentProxy(pdfInfo, transport) { + this.pdfInfo = pdfInfo; + this.transport = transport; + } + PDFDocumentProxy.prototype = { + /** + * @return {number} Total number of pages the PDF contains. + */ + get numPages() { + return this.pdfInfo.numPages; + }, + /** + * @return {string} A unique ID to identify a PDF. Not guaranteed to be + * unique. + */ + get fingerprint() { + return this.pdfInfo.fingerprint; + }, + /** + * @param {number} The page number to get. The first page is 1. + * @return {Promise} A promise that is resolved with a {PDFPageProxy} + * object. + */ + getPage: function PDFDocumentProxy_getPage(number) { + return this.transport.getPage(number); + }, + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named destinations to reference numbers. + */ + getDestinations: function PDFDocumentProxy_getDestinations() { + var promise = new PDFJS.Promise(); + var destinations = this.pdfInfo.destinations; + promise.resolve(destinations); + return promise; + }, + /** + * @return {Promise} A promise that is resolved with an {array} that is a + * tree outline (if it has one) of the PDF. The tree is in the format of: + * [ + * { + * title: string, + * bold: boolean, + * italic: boolean, + * color: rgb array, + * dest: dest obj, + * items: array of more items like this + * }, + * ... + * ]. + */ + getOutline: function PDFDocumentProxy_getOutline() { + var promise = new PDFJS.Promise(); + var outline = this.pdfInfo.outline; + promise.resolve(outline); + return promise; + }, + /** + * @return {Promise} A promise that is resolved with an {object} that has + * info and metadata properties. Info is an {object} filled with anything + * available in the information dictionary and similarly metadata is a + * {Metadata} object with information from the metadata section of the PDF. + */ + getMetadata: function PDFDocumentProxy_getMetadata() { + var promise = new PDFJS.Promise(); + var info = this.pdfInfo.info; + var metadata = this.pdfInfo.metadata; + promise.resolve({ + info: info, + metadata: metadata ? new PDFJS.Metadata(metadata) : null + }); + return promise; + }, + isEncrypted: function PDFDocumentProxy_isEncrypted() { + var promise = new PDFJS.Promise(); + promise.resolve(this.pdfInfo.encrypted); + return promise; + }, + /** + * @return {Promise} A promise that is resolved with a TypedArray that has + * the raw data from the PDF. + */ + getData: function PDFDocumentProxy_getData() { + var promise = new PDFJS.Promise(); + this.transport.getData(promise); + return promise; + }, + destroy: function PDFDocumentProxy_destroy() { + this.transport.destroy(); + } + }; + return PDFDocumentProxy; +})(); + +var PDFPageProxy = (function PDFPageProxyClosure() { + function PDFPageProxy(pageInfo, transport) { + this.pageInfo = pageInfo; + this.transport = transport; + this.stats = new StatTimer(); + this.stats.enabled = !!globalScope.PDFJS.enableStats; + this.objs = transport.objs; + this.renderInProgress = false; + } + PDFPageProxy.prototype = { + /** + * @return {number} Page number of the page. First page is 1. + */ + get pageNumber() { + return this.pageInfo.pageIndex + 1; + }, + /** + * @return {number} The number of degrees the page is rotated clockwise. + */ + get rotate() { + return this.pageInfo.rotate; + }, + /** + * @return {object} The reference that points to this page. It has 'num' and + * 'gen' properties. + */ + get ref() { + return this.pageInfo.ref; + }, + /** + * @return {array} An array of the visible portion of the PDF page in the + * user space units - [x1, y1, x2, y2]. + */ + get view() { + return this.pageInfo.view; + }, + /** + * @param {number} scale The desired scale of the viewport. + * @param {number} rotate Degrees to rotate the viewport. If omitted this + * defaults to the page rotation. + * @return {PageViewport} Contains 'width' and 'height' properties along + * with transforms required for rendering. + */ + getViewport: function PDFPageProxy_getViewport(scale, rotate) { + if (arguments.length < 2) + rotate = this.rotate; + return new PDFJS.PageViewport(this.view, scale, rotate, 0, 0); + }, + /** + * @return {Promise} A promise that is resolved with an {array} of the + * annotation objects. + */ + getAnnotations: function PDFPageProxy_getAnnotations() { + if (this.annotationsPromise) + return this.annotationsPromise; + + var promise = new PDFJS.Promise(); + this.annotationsPromise = promise; + this.transport.getAnnotations(this.pageInfo.pageIndex); + return promise; + }, + /** + * Begins the process of rendering a page to the desired context. + * @param {object} params A parameter object that supports: + * { + * canvasContext(required): A 2D context of a DOM Canvas object., + * textLayer(optional): An object that has beginLayout, endLayout, and + * appendText functions. + * }. + * @return {Promise} A promise that is resolved when the page finishes + * rendering. + */ + render: function PDFPageProxy_render(params) { + this.renderInProgress = true; + + var promise = new Promise(); + var stats = this.stats; + stats.time('Overall'); + // If there is no displayReadyPromise yet, then the operatorList was never + // requested before. Make the request and create the promise. + if (!this.displayReadyPromise) { + this.displayReadyPromise = new Promise(); + this.destroyed = false; + + this.stats.time('Page Request'); + this.transport.messageHandler.send('RenderPageRequest', { + pageIndex: this.pageNumber - 1 + }); + } + + var self = this; + function complete(error) { + self.renderInProgress = false; + if (self.destroyed) { + delete self.operatorList; + delete self.displayReadyPromise; + } + + if (error) + promise.reject(error); + else + promise.resolve(); + }; + + // Once the operatorList and fonts are loaded, do the actual rendering. + this.displayReadyPromise.then( + function pageDisplayReadyPromise() { + if (self.destroyed) { + complete(); + return; + } + + var gfx = new CanvasGraphics(params.canvasContext, + this.objs, params.textLayer); + try { + this.display(gfx, params.viewport, complete); + } catch (e) { + complete(e); + } + }.bind(this), + function pageDisplayReadPromiseError(reason) { + complete(reason); + } + ); + + return promise; + }, + /** + * For internal use only. + */ + startRenderingFromOperatorList: + function PDFPageProxy_startRenderingFromOperatorList(operatorList, + fonts) { + var self = this; + this.operatorList = operatorList; + + var displayContinuation = function pageDisplayContinuation() { + // Always defer call to display() to work around bug in + // Firefox error reporting from XHR callbacks. + setTimeout(function pageSetTimeout() { + self.displayReadyPromise.resolve(); + }); + }; + + this.ensureFonts(fonts, + function pageStartRenderingFromOperatorListEnsureFonts() { + displayContinuation(); + } + ); + }, + /** + * For internal use only. + */ + ensureFonts: function PDFPageProxy_ensureFonts(fonts, callback) { + this.stats.time('Font Loading'); + // Convert the font names to the corresponding font obj. + for (var i = 0, ii = fonts.length; i < ii; i++) { + fonts[i] = this.objs.objs[fonts[i]].data; + } + + // Load all the fonts + FontLoader.bind( + fonts, + function pageEnsureFontsFontObjs(fontObjs) { + this.stats.timeEnd('Font Loading'); + + callback.call(this); + }.bind(this) + ); + }, + /** + * For internal use only. + */ + display: function PDFPageProxy_display(gfx, viewport, callback) { + var stats = this.stats; + stats.time('Rendering'); + + gfx.beginDrawing(viewport); + + var startIdx = 0; + var length = this.operatorList.fnArray.length; + var operatorList = this.operatorList; + var stepper = null; + if (PDFJS.pdfBug && StepperManager.enabled) { + stepper = StepperManager.create(this.pageNumber - 1); + stepper.init(operatorList); + stepper.nextBreakPoint = stepper.getNextBreakPoint(); + } + + var self = this; + function next() { + startIdx = + gfx.executeOperatorList(operatorList, startIdx, next, stepper); + if (startIdx == length) { + gfx.endDrawing(); + stats.timeEnd('Rendering'); + stats.timeEnd('Overall'); + if (callback) callback(); + } + } + next(); + }, + /** + * @return {Promise} That is resolved with the a {string} that is the text + * content from the page. + */ + getTextContent: function PDFPageProxy_getTextContent() { + var promise = new PDFJS.Promise(); + this.transport.messageHandler.send('GetTextContent', { + pageIndex: this.pageNumber - 1 + }, + function textContentCallback(textContent) { + promise.resolve(textContent); + } + ); + return promise; + }, + /** + * Stub for future feature. + */ + getOperationList: function PDFPageProxy_getOperationList() { + var promise = new PDFJS.Promise(); + var operationList = { // not implemented + dependencyFontsID: null, + operatorList: null + }; + promise.resolve(operationList); + return promise; + }, + /** + * Destroys resources allocated by the page. + */ + destroy: function PDFPageProxy_destroy() { + this.destroyed = true; + + if (!this.renderInProgress) { + delete this.operatorList; + delete this.displayReadyPromise; + } + } + }; + return PDFPageProxy; +})(); +/** + * For internal use only. + */ +var WorkerTransport = (function WorkerTransportClosure() { + function WorkerTransport(promise) { + this.workerReadyPromise = promise; + this.objs = new PDFObjects(); + + this.pageCache = []; + this.pagePromises = []; + this.fontsLoading = {}; + + // If worker support isn't disabled explicit and the browser has worker + // support, create a new web worker and test if it/the browser fullfills + // all requirements to run parts of pdf.js in a web worker. + // Right now, the requirement is, that an Uint8Array is still an Uint8Array + // as it arrives on the worker. Chrome added this with version 15. + if (!globalScope.PDFJS.disableWorker && typeof Worker !== 'undefined') { + var workerSrc = PDFJS.workerSrc; + if (typeof workerSrc === 'undefined') { + error('No PDFJS.workerSrc specified'); + } + + try { + var worker; + if (PDFJS.isFirefoxExtension) { + // The firefox extension can't load the worker from the resource:// + // url so we have to inline the script and then use the blob loader. + var bb = new MozBlobBuilder(); + bb.append(document.querySelector('#PDFJS_SCRIPT_TAG').textContent); + var blobUrl = window.URL.createObjectURL(bb.getBlob()); + worker = new Worker(blobUrl); + } else { + // Some versions of FF can't create a worker on localhost, see: + // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 + worker = new Worker(workerSrc); + } + + var messageHandler = new MessageHandler('main', worker); + this.messageHandler = messageHandler; + + messageHandler.on('test', function transportTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); + return; + } catch (e) { + info('The worker has been disabled.'); + } + } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + WorkerTransport.prototype = { + destroy: function WorkerTransport_destroy() { + if (this.worker) + this.worker.terminate(); + + this.pageCache = []; + this.pagePromises = []; + }, + setupFakeWorker: function WorkerTransport_setupFakeWorker() { + // If we don't use a worker, just post/sendMessage to the main thread. + var fakeWorker = { + postMessage: function WorkerTransport_postMessage(obj) { + fakeWorker.onmessage({data: obj}); + }, + terminate: function WorkerTransport_terminate() {} + }; + + var messageHandler = new MessageHandler('main', fakeWorker); + this.setupMessageHandler(messageHandler); + + // If the main thread is our worker, setup the handling for the messages + // the main thread sends to it self. + WorkerMessageHandler.setup(messageHandler); + }, + + setupMessageHandler: + function WorkerTransport_setupMessageHandler(messageHandler) { + this.messageHandler = messageHandler; + + messageHandler.on('GetDoc', function transportDoc(data) { + var pdfInfo = data.pdfInfo; + var pdfDocument = new PDFDocumentProxy(pdfInfo, this); + this.pdfDocument = pdfDocument; + this.workerReadyPromise.resolve(pdfDocument); + }, this); + + messageHandler.on('NeedPassword', function transportPassword(data) { + this.workerReadyPromise.reject(data.exception.message, data.exception); + }, this); + + messageHandler.on('IncorrectPassword', function transportBadPass(data) { + this.workerReadyPromise.reject(data.exception.message, data.exception); + }, this); + + messageHandler.on('GetPage', function transportPage(data) { + var pageInfo = data.pageInfo; + var page = new PDFPageProxy(pageInfo, this); + this.pageCache[pageInfo.pageIndex] = page; + var promise = this.pagePromises[pageInfo.pageIndex]; + promise.resolve(page); + }, this); + + messageHandler.on('GetAnnotations', function transportAnnotations(data) { + var annotations = data.annotations; + var promise = this.pageCache[data.pageIndex].annotationsPromise; + promise.resolve(annotations); + }, this); + + messageHandler.on('RenderPage', function transportRender(data) { + var page = this.pageCache[data.pageIndex]; + var depFonts = data.depFonts; + + page.stats.timeEnd('Page Request'); + page.startRenderingFromOperatorList(data.operatorList, depFonts); + }, this); + + messageHandler.on('obj', function transportObj(data) { + var id = data[0]; + var type = data[1]; + if (this.objs.hasData(id)) + return; + + switch (type) { + case 'JpegStream': + var imageData = data[2]; + loadJpegStream(id, imageData, this.objs); + break; + case 'Image': + var imageData = data[2]; + this.objs.resolve(id, imageData); + break; + case 'Font': + var name = data[2]; + var file = data[3]; + var properties = data[4]; + + if (file) { + // Rewrap the ArrayBuffer in a stream. + var fontFileDict = new Dict(); + file = new Stream(file, 0, file.length, fontFileDict); + } + + // At this point, only the font object is created but the font is + // not yet attached to the DOM. This is done in `FontLoader.bind`. + var font = new Font(name, file, properties); + this.objs.resolve(id, font); + break; + default: + error('Got unkown object type ' + type); + } + }, this); + + messageHandler.on('PageError', function transportError(data) { + var page = this.pageCache[data.pageNum - 1]; + if (page.displayReadyPromise) + page.displayReadyPromise.reject(data.error); + else + error(data.error); + }, this); + + messageHandler.on('JpegDecode', function(data, promise) { + var imageData = data[0]; + var components = data[1]; + if (components != 3 && components != 1) + error('Only 3 component or 1 component can be returned'); + + var img = new Image(); + img.onload = (function messageHandler_onloadClosure() { + var width = img.width; + var height = img.height; + var size = width * height; + var rgbaLength = size * 4; + var buf = new Uint8Array(size * components); + var tmpCanvas = createScratchCanvas(width, height); + var tmpCtx = tmpCanvas.getContext('2d'); + tmpCtx.drawImage(img, 0, 0); + var data = tmpCtx.getImageData(0, 0, width, height).data; + + if (components == 3) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j += 3) { + buf[j] = data[i]; + buf[j + 1] = data[i + 1]; + buf[j + 2] = data[i + 2]; + } + } else if (components == 1) { + for (var i = 0, j = 0; i < rgbaLength; i += 4, j++) { + buf[j] = data[i]; + } + } + promise.resolve({ data: buf, width: width, height: height}); + }).bind(this); + var src = 'data:image/jpeg;base64,' + window.btoa(imageData); + img.src = src; + }); + }, + + sendData: function WorkerTransport_sendData(data, params) { + this.messageHandler.send('GetDocRequest', {data: data, params: params}); + }, + + getData: function WorkerTransport_sendData(promise) { + this.messageHandler.send('GetData', null, function(data) { + promise.resolve(data); + }); + }, + + getPage: function WorkerTransport_getPage(pageNumber, promise) { + var pageIndex = pageNumber - 1; + if (pageIndex in this.pagePromises) + return this.pagePromises[pageIndex]; + var promise = new PDFJS.Promise('Page ' + pageNumber); + this.pagePromises[pageIndex] = promise; + this.messageHandler.send('GetPageRequest', { pageIndex: pageIndex }); + return promise; + }, + + getAnnotations: function WorkerTransport_getAnnotations(pageIndex) { + this.messageHandler.send('GetAnnotationsRequest', + { pageIndex: pageIndex }); + } + }; + return WorkerTransport; + +})(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -1604,27 +2047,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { 'shadingFill': true }, - beginDrawing: function CanvasGraphics_beginDrawing(mediaBox) { - var cw = this.ctx.canvas.width, ch = this.ctx.canvas.height; + beginDrawing: function CanvasGraphics_beginDrawing(viewport) { + var transform = viewport.transform; this.ctx.save(); - switch (mediaBox.rotate) { - case 0: - this.ctx.transform(1, 0, 0, -1, 0, ch); - break; - case 90: - this.ctx.transform(0, 1, 1, 0, 0, 0); - break; - case 180: - this.ctx.transform(-1, 0, 0, 1, cw, 0); - break; - case 270: - this.ctx.transform(0, -1, -1, 0, cw, ch); - break; - } - // Scale so that canvas units are the same as PDF user space units - this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); - // Move the media left-top corner to the (0,0) canvas position - this.ctx.translate(-mediaBox.x, -mediaBox.y); + this.ctx.transform.apply(this.ctx, transform); if (this.textLayer) this.textLayer.beginLayout(); @@ -1723,10 +2149,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.ctx.webkitLineDashOffset = dashPhase; }, setRenderingIntent: function CanvasGraphics_setRenderingIntent(intent) { - TODO('set rendering intent: ' + intent); + // Maybe if we one day fully support color spaces this will be important + // for now we can ignore. + // TODO set rendering intent? }, setFlatness: function CanvasGraphics_setFlatness(flatness) { - TODO('set flatness: ' + flatness); + // There's no way to control this with canvas, but we can safely ignore. + // TODO set flatness? }, setGState: function CanvasGraphics_setGState(states) { for (var i = 0, ii = states.length; i < ii; i++) { @@ -2221,7 +2650,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { text.length += shownText.length; } } else { - malformed('TJ array element ' + e + ' is not string or num'); + error('TJ array element ' + e + ' is not string or num'); } } @@ -2474,6 +2903,40 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } } } + function rescaleImage(pixels, widthScale, heightScale) { + var scaledWidth = Math.ceil(width / widthScale); + var scaledHeight = Math.ceil(height / heightScale); + + var itemsSum = new Uint32Array(scaledWidth * scaledHeight * 4); + var itemsCount = new Uint32Array(scaledWidth * scaledHeight); + for (var i = 0, position = 0; i < height; i++) { + var lineOffset = (0 | (i / heightScale)) * scaledWidth; + for (var j = 0; j < width; j++) { + var countOffset = lineOffset + (0 | (j / widthScale)); + var sumOffset = countOffset << 2; + itemsSum[sumOffset] += pixels[position]; + itemsSum[sumOffset + 1] += pixels[position + 1]; + itemsSum[sumOffset + 2] += pixels[position + 2]; + itemsSum[sumOffset + 3] += pixels[position + 3]; + itemsCount[countOffset]++; + position += 4; + } + } + var tmpCanvas = createScratchCanvas(scaledWidth, scaledHeight); + var tmpCtx = tmpCanvas.getContext('2d'); + var imgData = tmpCtx.getImageData(0, 0, scaledWidth, scaledHeight); + pixels = imgData.data; + for (var i = 0, j = 0, ii = scaledWidth * scaledHeight; i < ii; i++) { + var count = itemsCount[i]; + pixels[j] = itemsSum[j] / count; + pixels[j + 1] = itemsSum[j + 1] / count; + pixels[j + 2] = itemsSum[j + 2] / count; + pixels[j + 3] = itemsSum[j + 3] / count; + j += 4; + } + tmpCtx.putImageData(imgData, 0, 0); + return tmpCanvas; + } this.save(); @@ -2496,8 +2959,19 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { applyStencilMask(pixels, inverseDecode); - tmpCtx.putImageData(imgData, 0, 0); - ctx.drawImage(tmpCanvas, 0, -h); + var currentTransform = ctx.mozCurrentTransformInverse; + var widthScale = Math.max(Math.abs(currentTransform[0]), 1); + var heightScale = Math.max(Math.abs(currentTransform[3]), 1); + if (widthScale >= 2 || heightScale >= 2) { + // canvas does not resize well large images to small -- using simple + // algorithm to perform pre-scaling + tmpCanvas = rescaleImage(imgData.data, widthScale, heightScale); + ctx.scale(widthScale, heightScale); + ctx.drawImage(tmpCanvas, 0, -h / heightScale); + } else { + tmpCtx.putImageData(imgData, 0, 0); + ctx.drawImage(tmpCanvas, 0, -h); + } this.restore(); }, @@ -2624,6 +3098,7 @@ if (!isWorker) { }; } } + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -2663,51 +3138,55 @@ var Dict = (function DictClosure() { // xref is optional function Dict(xref) { // Map should only be used internally, use functions below to access. - this.map = Object.create(null); - this.xref = xref; - } + var map = Object.create(null); + + this.assignXref = function Dict_assignXref(newXref) { + xref = newXref; + }; - Dict.prototype = { // automatically dereferences Ref objects - get: function Dict_get(key1, key2, key3) { + this.get = function Dict_get(key1, key2, key3) { var value; - var xref = this.xref; - if (typeof (value = this.map[key1]) != 'undefined' || key1 in this.map || + if (typeof (value = map[key1]) != 'undefined' || key1 in map || typeof key2 == 'undefined') { - return xref ? this.xref.fetchIfRef(value) : value; + return xref ? xref.fetchIfRef(value) : value; } - if (typeof (value = this.map[key2]) != 'undefined' || key2 in this.map || + if (typeof (value = map[key2]) != 'undefined' || key2 in map || typeof key3 == 'undefined') { - return xref ? this.xref.fetchIfRef(value) : value; + return xref ? xref.fetchIfRef(value) : value; } - value = this.map[key3] || null; - return xref ? this.xref.fetchIfRef(value) : value; - }, + value = map[key3] || null; + return xref ? xref.fetchIfRef(value) : value; + }; + // no dereferencing - getRaw: function Dict_getRaw(key) { - return this.map[key]; - }, + this.getRaw = function Dict_getRaw(key) { + return map[key]; + }; + // creates new map and dereferences all Refs - getAll: function Dict_getAll() { + this.getAll = function Dict_getAll() { var all = {}; - for (var key in this.map) - all[key] = this.get(key); + for (var key in map) { + var obj = this.get(key); + all[key] = obj instanceof Dict ? obj.getAll() : obj; + } return all; - }, + }; - set: function Dict_set(key, value) { - this.map[key] = value; - }, + this.set = function Dict_set(key, value) { + map[key] = value; + }; - has: function Dict_has(key) { - return key in this.map; - }, + this.has = function Dict_has(key) { + return key in map; + }; - forEach: function Dict_forEach(callback) { - for (var key in this.map) { + this.forEach = function Dict_forEach(callback) { + for (var key in map) { callback(key, this.get(key)); } - } + }; }; return Dict; @@ -2754,7 +3233,14 @@ var Catalog = (function CatalogClosure() { Catalog.prototype = { get metadata() { - var stream = this.catDict.get('Metadata'); + var streamRef = this.catDict.getRaw('Metadata'); + if (!isRef(streamRef)) + return shadow(this, 'metadata', null); + + var encryptMetadata = !this.xref.encrypt ? false : + this.xref.encrypt.encryptMetadata; + + var stream = this.xref.fetch(streamRef, !encryptMetadata); var metadata; if (stream && isDict(stream.dict)) { var type = stream.dict.get('Type'); @@ -2762,7 +3248,16 @@ var Catalog = (function CatalogClosure() { if (isName(type) && isName(subtype) && type.name === 'Metadata' && subtype.name === 'XML') { - metadata = stringToPDFString(bytesToString(stream.getBytes())); + // XXX: This should examine the charset the XML document defines, + // however since there are currently no real means to decode + // arbitrary charsets, let's just hope that the author of the PDF + // was reasonable enough to stick with the XML default charset, + // which is UTF-8. + try { + metadata = stringToUTF8String(bytesToString(stream.getBytes())); + } catch (e) { + info('Skipping invalid metadata.'); + } } } @@ -2920,12 +3415,12 @@ var Catalog = (function CatalogClosure() { })(); var XRef = (function XRefClosure() { - function XRef(stream, startXRef, mainXRefEntriesOffset) { + function XRef(stream, startXRef, mainXRefEntriesOffset, password) { this.stream = stream; this.entries = []; this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); - trailerDict.xref = this; + trailerDict.assignXref(this); this.trailer = trailerDict; // prepare the XRef cache this.cache = []; @@ -2933,8 +3428,7 @@ var XRef = (function XRefClosure() { var encrypt = trailerDict.get('Encrypt'); if (encrypt) { var fileId = trailerDict.get('ID'); - this.encrypt = new CipherTransformFactory(encrypt, - fileId[0] /*, password */); + this.encrypt = new CipherTransformFactory(encrypt, fileId[0], password); } // get the root dictionary (catalog) object @@ -2986,9 +3480,8 @@ var XRef = (function XRefClosure() { } } - // Sanity check: as per spec, first object must have these properties - if (this.entries[0] && - !(this.entries[0].gen === 65535 && this.entries[0].free)) + // Sanity check: as per spec, first object must be free + if (this.entries[0] && !this.entries[0].free) error('Invalid XRef table: unexpected first object'); // Sanity check @@ -3147,7 +3640,7 @@ var XRef = (function XRefClosure() { } // reading XRef streams for (var i = 0, ii = xrefStms.length; i < ii; ++i) { - this.readXRef(xrefStms[i]); + this.readXRef(xrefStms[i], true); } // finding main trailer var dict; @@ -3170,7 +3663,7 @@ var XRef = (function XRefClosure() { // nothing helps error('Invalid PDF structure'); }, - readXRef: function XRef_readXRef(startXRef) { + readXRef: function XRef_readXRef(startXRef, recoveryMode) { var stream = this.stream; stream.pos = startXRef; @@ -3203,16 +3696,18 @@ var XRef = (function XRefClosure() { error('Invalid XRef stream'); } dict = this.readXRefStream(obj); + if (!dict) + error('Failed to read XRef stream'); } // Recursively get previous dictionary, if any obj = dict.get('Prev'); if (isInt(obj)) - this.readXRef(obj); + this.readXRef(obj, recoveryMode); else if (isRef(obj)) { // The spec says Prev must not be a reference, i.e. "/Prev NNN" // This is a fallback for non-compliant PDFs, i.e. "/Prev NNN 0 R" - this.readXRef(obj.num); + this.readXRef(obj.num, recoveryMode); } return dict; @@ -3220,6 +3715,9 @@ var XRef = (function XRefClosure() { log('(while reading XRef): ' + e); } + if (recoveryMode) + return; + warn('Indexing all PDF objects'); return this.indexObjects(); }, @@ -3432,6 +3930,7 @@ var PDFObjects = (function PDFObjectsClosure() { return PDFObjects; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -4270,7 +4769,10 @@ var PostScriptLexer = (function PostScriptLexerClosure() { // operator var str = ch.toLowerCase(); while (true) { - ch = stream.lookChar().toLowerCase(); + ch = stream.lookChar(); + if (ch === null) + break; + ch = ch.toLowerCase(); if (ch >= 'a' && ch <= 'z') str += ch; else @@ -4306,6 +4808,7 @@ var PostScriptLexer = (function PostScriptLexerClosure() { return PostScriptLexer; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -4411,6 +4914,7 @@ var ExpertSubsetCharset = [ 'periodinferior', 'commainferior' ]; + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -11344,6 +11848,7 @@ var CIDToUnicodeMaps = { {f: 7, c: 19887}] }; + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -11797,12 +12302,12 @@ var LabCS = (function LabCSClosure() { error('Invalid WhitePoint components, no fallback available'); if (this.XB < 0 || this.YB < 0 || this.ZB < 0) { - warn('Invalid BlackPoint, falling back to default'); + info('Invalid BlackPoint, falling back to default'); this.XB = this.YB = this.ZB = 0; } if (this.amin > this.amax || this.bmin > this.bmax) { - warn('Invalid Range, falling back to defaults'); + info('Invalid Range, falling back to defaults'); this.amin = -100; this.amax = 100; this.bmin = -100; @@ -11876,6 +12381,7 @@ var LabCS = (function LabCSClosure() { }; return LabCS; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -12297,13 +12803,14 @@ var CipherTransform = (function CipherTransformClosure() { })(); var CipherTransformFactory = (function CipherTransformFactoryClosure() { + var defaultPasswordBytes = new Uint8Array([ + 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, + 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, + 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, + 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]); + function prepareKeyData(fileId, password, ownerPassword, userPassword, flags, revision, keyLength, encryptMetadata) { - var defaultPasswordBytes = new Uint8Array([ - 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, - 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, - 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, - 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]); var hashData = new Uint8Array(100), i = 0, j, n; if (password) { n = Math.min(32, password.length); @@ -12340,9 +12847,8 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { var cipher, checkData; if (revision >= 3) { - // padded password in hashData, we can use this array for user - // password check - i = 32; + for (i = 0; i < 32; ++i) + hashData[i] = defaultPasswordBytes[i]; for (j = 0, n = fileId.length; j < n; ++j) hashData[i++] = fileId[j]; cipher = new ARCFourCipher(encryptionKey); @@ -12355,16 +12861,53 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { cipher = new ARCFourCipher(derivedKey); checkData = cipher.encryptBlock(checkData); } + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] != checkData[j]) + return null; + } } else { cipher = new ARCFourCipher(encryptionKey); - checkData = cipher.encryptBlock(hashData.subarray(0, 32)); - } - for (j = 0, n = checkData.length; j < n; ++j) { - if (userPassword[j] != checkData[j]) - error('incorrect password'); + checkData = cipher.encryptBlock(defaultPasswordBytes); + for (j = 0, n = checkData.length; j < n; ++j) { + if (userPassword[j] != checkData[j]) + return null; + } } return encryptionKey; } + function decodeUserPassword(password, ownerPassword, revision, keyLength) { + var hashData = new Uint8Array(32), i = 0, j, n; + n = Math.min(32, password.length); + for (; i < n; ++i) + hashData[i] = password[i]; + j = 0; + while (i < 32) { + hashData[i++] = defaultPasswordBytes[j++]; + } + var hash = calculateMD5(hashData, 0, i); + var keyLengthInBytes = keyLength >> 3; + if (revision >= 3) { + for (j = 0; j < 50; ++j) { + hash = calculateMD5(hash, 0, hash.length); + } + } + + var cipher, userPassword; + if (revision >= 3) { + userPassword = ownerPassword; + var derivedKey = new Uint8Array(keyLengthInBytes), k; + for (j = 19; j >= 0; j--) { + for (k = 0; k < keyLengthInBytes; ++k) + derivedKey[k] = hash[k] ^ j; + cipher = new ARCFourCipher(derivedKey); + userPassword = cipher.encryptBlock(userPassword); + } + } else { + cipher = new ARCFourCipher(hash.subarray(0, keyLengthInBytes)); + userPassword = cipher.encryptBlock(ownerPassword); + } + return userPassword; + } var identityName = new Name('Identity'); @@ -12387,17 +12930,34 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { var userPassword = stringToBytes(dict.get('U')); var flags = dict.get('P'); var revision = dict.get('R'); - var encryptMetadata = + var encryptMetadata = algorithm == 4 && // meaningful when V is 4 dict.get('EncryptMetadata') !== false; // makes true as default value + this.encryptMetadata = encryptMetadata; + var fileIdBytes = stringToBytes(fileId); var passwordBytes; if (password) passwordBytes = stringToBytes(password); - this.encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, - ownerPassword, userPassword, - flags, revision, - keyLength, encryptMetadata); + var encryptionKey = prepareKeyData(fileIdBytes, passwordBytes, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + if (!encryptionKey && !password) { + throw new PasswordException('No password given', 'needpassword'); + } else if (!encryptionKey && password) { + // Attempting use the password as an owner password + var decodedPassword = decodeUserPassword(passwordBytes, ownerPassword, + revision, keyLength); + encryptionKey = prepareKeyData(fileIdBytes, decodedPassword, + ownerPassword, userPassword, flags, + revision, keyLength, encryptMetadata); + } + + if (!encryptionKey) + throw new PasswordException('Incorrect Password', 'incorrectpassword'); + + this.encryptionKey = encryptionKey; + if (algorithm == 4) { this.cf = dict.get('CF'); this.stmf = dict.get('StmF') || identityName; @@ -12472,6 +13032,7 @@ var CipherTransformFactory = (function CipherTransformFactoryClosure() { return CipherTransformFactory; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -12582,19 +13143,20 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // Compatibility BX: 'beginCompat', - EX: 'endCompat' - }; + EX: 'endCompat', - function splitCombinedOperations(operations) { - // Two operations can be combined together, trying to find which two - // operations were concatenated. - for (var i = operations.length - 1; i > 0; i--) { - var op1 = operations.substring(0, i), op2 = operations.substring(i); - if (op1 in OP_MAP && op2 in OP_MAP) - return [op1, op2]; // operations found - } - return null; - } + // (reserved partial commands for the lexer) + BM: null, + BD: null, + 'true': null, + fa: null, + fal: null, + fals: null, + 'false': null, + nu: null, + nul: null, + 'null': null + }; PartialEvaluator.prototype = { getOperatorList: function PartialEvaluator_getOperatorList(stream, @@ -12627,13 +13189,15 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { font = xref.fetchIfRef(font) || fontRes.get(fontName); assertWellFormed(isDict(font)); - if (!font.translated) { + + ++self.objIdCounter; + if (!font.loadedName) { font.translated = self.translateFont(font, xref, resources, dependency); if (font.translated) { // keep track of each font we translated so the caller can // load them asynchronously before calling display on a page - loadedName = 'font_' + uniquePrefix + (++self.objIdCounter); + loadedName = 'font_' + uniquePrefix + self.objIdCounter; font.translated.properties.loadedName = loadedName; font.loadedName = loadedName; @@ -12738,36 +13302,19 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { resources = resources || new Dict(); var xobjs = resources.get('XObject') || new Dict(); var patterns = resources.get('Pattern') || new Dict(); - var parser = new Parser(new Lexer(stream), false, xref); + var parser = new Parser(new Lexer(stream, OP_MAP), false, xref); var res = resources; - var hasNextObj = false, nextObj; var args = [], obj; var TILING_PATTERN = 1, SHADING_PATTERN = 2; while (true) { - if (hasNextObj) { - obj = nextObj; - hasNextObj = false; - } else { - obj = parser.getObj(); - if (isEOF(obj)) - break; - } + obj = parser.getObj(); + if (isEOF(obj)) + break; if (isCmd(obj)) { var cmd = obj.cmd; var fn = OP_MAP[cmd]; - if (!fn) { - // invalid content command, trying to recover - var cmds = splitCombinedOperations(cmd); - if (cmds) { - cmd = cmds[0]; - fn = OP_MAP[cmd]; - // feeding other command on the next interation - hasNextObj = true; - nextObj = Cmd.get(cmds[1]); - } - } assertWellFormed(fn, 'Unknown command "' + cmd + '"'); // TODO figure out how to type-check vararg functions @@ -12907,6 +13454,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { value[1] ]); break; + case 'BM': + // We support the default so don't trigger the TODO. + if (!isName(value) || value.name != 'Normal') + TODO('graphic state operator ' + key); + break; + case 'SMask': + // We support the default so don't trigger the TODO. + if (!isName(value) || value.name != 'None') + TODO('graphic state operator ' + key); + break; + // Only generate info log messages for the following since + // they are unlikey to have a big impact on the rendering. case 'OP': case 'op': case 'OPM': @@ -12919,14 +13478,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { case 'HT': case 'SM': case 'SA': - case 'BM': - case 'SMask': case 'AIS': case 'TK': - TODO('graphic state operator ' + key); + // TODO implement these operators. + info('graphic state operator ' + key); break; default: - warn('Unknown graphic state operator ' + key); + info('Unknown graphic state operator ' + key); break; } } @@ -12940,13 +13498,88 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { args = []; } else if (obj != null) { assertWellFormed(args.length <= 33, 'Too many arguments'); - args.push(obj); + args.push(obj instanceof Dict ? obj.getAll() : obj); } } return queue; }, + getTextContent: function partialEvaluatorGetIRQueue(stream, resources) { + + var self = this; + var xref = this.xref; + + function handleSetFont(fontName, fontRef) { + var fontRes = resources.get('Font'); + + // TODO: TOASK: Is it possible to get here? If so, what does + // args[0].name should be like??? + assert(fontRes, 'fontRes not available'); + + fontRes = xref.fetchIfRef(fontRes); + fontRef = fontRef || fontRes.get(fontName); + var font = xref.fetchIfRef(fontRef), tra; + assertWellFormed(isDict(font)); + if (!font.translated) { + font.translated = self.translateFont(font, xref, resources); + } + return font; + } + + resources = xref.fetchIfRef(resources) || new Dict(); + + var parser = new Parser(new Lexer(stream), false); + var res = resources; + var args = [], obj; + + var text = ''; + var chunk = ''; + var font = null; + while (!isEOF(obj = parser.getObj())) { + if (isCmd(obj)) { + var cmd = obj.cmd; + switch (cmd) { + case 'Tf': + font = handleSetFont(args[0].name); + break; + case 'TJ': + var items = args[0]; + for (var j = 0, jj = items.length; j < jj; j++) { + if (typeof items[j] === 'string') { + chunk += items[j]; + } else if (items[j] < 0) { + // making all negative offsets a space - better to have + // a space in incorrect place than not have them at all + chunk += ' '; + } + } + break; + case 'Tj': + chunk += args[0]; + break; + case "'": + chunk += args[0] + ' '; + break; + case '"': + chunk += args[2] + ' '; + break; + } // switch + if (chunk !== '') { + text += fontCharsToUnicode(chunk, font.translated.properties); + chunk = ''; + } + + args = []; + } else if (obj != null) { + assertWellFormed(args.length <= 33, 'Too many arguments'); + args.push(obj); + } + } + + return text; + }, + extractDataStructures: function partialEvaluatorExtractDataStructures(dict, baseDict, xref, properties) { @@ -12954,7 +13587,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode'); if (toUnicode) - properties.toUnicode = this.readToUnicode(toUnicode, xref); + properties.toUnicode = this.readToUnicode(toUnicode, xref, properties); if (properties.composite) { // CIDSystemInfo helps to match CID to glyphs @@ -13010,7 +13643,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { properties.hasEncoding = hasEncoding; }, - readToUnicode: function PartialEvaluator_readToUnicode(toUnicode, xref) { + readToUnicode: function PartialEvaluator_readToUnicode(toUnicode, xref, + properties) { var cmapObj = toUnicode; var charToUnicode = []; if (isName(cmapObj)) { @@ -13099,6 +13733,11 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } } else if (octet == 0x3E) { if (token.length) { + // Heuristic: guessing chars size by checking numbers sizes + // in the CMap entries. + if (token.length == 2 && properties.composite) + properties.wideChars = false; + if (token.length <= 4) { // parsing hex number tokens.push(parseInt(token, 16)); @@ -13316,6 +13955,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { length1: length1, length2: length2, composite: composite, + wideChars: composite, fixedPitch: false, fontMatrix: dict.get('FontMatrix') || IDENTITY_MATRIX, firstChar: firstChar || 0, @@ -13336,7 +13976,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { properties.coded = true; var charProcs = dict.get('CharProcs').getAll(); var fontResources = dict.get('Resources') || resources; - properties.resources = fontResources; properties.charProcOperatorList = {}; for (var key in charProcs) { var glyphStream = charProcs[key]; @@ -13380,6 +14019,7 @@ var EvalState = (function EvalStateClosure() { return EvalState; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -14135,6 +14775,736 @@ function isSpecialUnicode(unicode) { unicode < kCmapGlyphOffset + kSizeOfGlyphArea); } +// The normalization table is obtained by filtering the Unicode characters +// database with entries. +var NormalizedUnicodes = { + '\u00A8': '\u0020\u0308', + '\u00AF': '\u0020\u0304', + '\u00B4': '\u0020\u0301', + '\u00B5': '\u03BC', + '\u00B8': '\u0020\u0327', + '\u0132': '\u0049\u004A', + '\u0133': '\u0069\u006A', + '\u013F': '\u004C\u00B7', + '\u0140': '\u006C\u00B7', + '\u0149': '\u02BC\u006E', + '\u017F': '\u0073', + '\u01C4': '\u0044\u017D', + '\u01C5': '\u0044\u017E', + '\u01C6': '\u0064\u017E', + '\u01C7': '\u004C\u004A', + '\u01C8': '\u004C\u006A', + '\u01C9': '\u006C\u006A', + '\u01CA': '\u004E\u004A', + '\u01CB': '\u004E\u006A', + '\u01CC': '\u006E\u006A', + '\u01F1': '\u0044\u005A', + '\u01F2': '\u0044\u007A', + '\u01F3': '\u0064\u007A', + '\u02D8': '\u0020\u0306', + '\u02D9': '\u0020\u0307', + '\u02DA': '\u0020\u030A', + '\u02DB': '\u0020\u0328', + '\u02DC': '\u0020\u0303', + '\u02DD': '\u0020\u030B', + '\u037A': '\u0020\u0345', + '\u0384': '\u0020\u0301', + '\u03D0': '\u03B2', + '\u03D1': '\u03B8', + '\u03D2': '\u03A5', + '\u03D5': '\u03C6', + '\u03D6': '\u03C0', + '\u03F0': '\u03BA', + '\u03F1': '\u03C1', + '\u03F2': '\u03C2', + '\u03F4': '\u0398', + '\u03F5': '\u03B5', + '\u03F9': '\u03A3', + '\u0587': '\u0565\u0582', + '\u0675': '\u0627\u0674', + '\u0676': '\u0648\u0674', + '\u0677': '\u06C7\u0674', + '\u0678': '\u064A\u0674', + '\u0E33': '\u0E4D\u0E32', + '\u0EB3': '\u0ECD\u0EB2', + '\u0EDC': '\u0EAB\u0E99', + '\u0EDD': '\u0EAB\u0EA1', + '\u0F77': '\u0FB2\u0F81', + '\u0F79': '\u0FB3\u0F81', + '\u1E9A': '\u0061\u02BE', + '\u1FBD': '\u0020\u0313', + '\u1FBF': '\u0020\u0313', + '\u1FC0': '\u0020\u0342', + '\u1FFE': '\u0020\u0314', + '\u2002': '\u0020', + '\u2003': '\u0020', + '\u2004': '\u0020', + '\u2005': '\u0020', + '\u2006': '\u0020', + '\u2008': '\u0020', + '\u2009': '\u0020', + '\u200A': '\u0020', + '\u2017': '\u0020\u0333', + '\u2024': '\u002E', + '\u2025': '\u002E\u002E', + '\u2026': '\u002E\u002E\u002E', + '\u2033': '\u2032\u2032', + '\u2034': '\u2032\u2032\u2032', + '\u2036': '\u2035\u2035', + '\u2037': '\u2035\u2035\u2035', + '\u203C': '\u0021\u0021', + '\u203E': '\u0020\u0305', + '\u2047': '\u003F\u003F', + '\u2048': '\u003F\u0021', + '\u2049': '\u0021\u003F', + '\u2057': '\u2032\u2032\u2032\u2032', + '\u205F': '\u0020', + '\u20A8': '\u0052\u0073', + '\u2100': '\u0061\u002F\u0063', + '\u2101': '\u0061\u002F\u0073', + '\u2103': '\u00B0\u0043', + '\u2105': '\u0063\u002F\u006F', + '\u2106': '\u0063\u002F\u0075', + '\u2107': '\u0190', + '\u2109': '\u00B0\u0046', + '\u2116': '\u004E\u006F', + '\u2121': '\u0054\u0045\u004C', + '\u2135': '\u05D0', + '\u2136': '\u05D1', + '\u2137': '\u05D2', + '\u2138': '\u05D3', + '\u213B': '\u0046\u0041\u0058', + '\u2160': '\u0049', + '\u2161': '\u0049\u0049', + '\u2162': '\u0049\u0049\u0049', + '\u2163': '\u0049\u0056', + '\u2164': '\u0056', + '\u2165': '\u0056\u0049', + '\u2166': '\u0056\u0049\u0049', + '\u2167': '\u0056\u0049\u0049\u0049', + '\u2168': '\u0049\u0058', + '\u2169': '\u0058', + '\u216A': '\u0058\u0049', + '\u216B': '\u0058\u0049\u0049', + '\u216C': '\u004C', + '\u216D': '\u0043', + '\u216E': '\u0044', + '\u216F': '\u004D', + '\u2170': '\u0069', + '\u2171': '\u0069\u0069', + '\u2172': '\u0069\u0069\u0069', + '\u2173': '\u0069\u0076', + '\u2174': '\u0076', + '\u2175': '\u0076\u0069', + '\u2176': '\u0076\u0069\u0069', + '\u2177': '\u0076\u0069\u0069\u0069', + '\u2178': '\u0069\u0078', + '\u2179': '\u0078', + '\u217A': '\u0078\u0069', + '\u217B': '\u0078\u0069\u0069', + '\u217C': '\u006C', + '\u217D': '\u0063', + '\u217E': '\u0064', + '\u217F': '\u006D', + '\u222C': '\u222B\u222B', + '\u222D': '\u222B\u222B\u222B', + '\u222F': '\u222E\u222E', + '\u2230': '\u222E\u222E\u222E', + '\u2474': '\u0028\u0031\u0029', + '\u2475': '\u0028\u0032\u0029', + '\u2476': '\u0028\u0033\u0029', + '\u2477': '\u0028\u0034\u0029', + '\u2478': '\u0028\u0035\u0029', + '\u2479': '\u0028\u0036\u0029', + '\u247A': '\u0028\u0037\u0029', + '\u247B': '\u0028\u0038\u0029', + '\u247C': '\u0028\u0039\u0029', + '\u247D': '\u0028\u0031\u0030\u0029', + '\u247E': '\u0028\u0031\u0031\u0029', + '\u247F': '\u0028\u0031\u0032\u0029', + '\u2480': '\u0028\u0031\u0033\u0029', + '\u2481': '\u0028\u0031\u0034\u0029', + '\u2482': '\u0028\u0031\u0035\u0029', + '\u2483': '\u0028\u0031\u0036\u0029', + '\u2484': '\u0028\u0031\u0037\u0029', + '\u2485': '\u0028\u0031\u0038\u0029', + '\u2486': '\u0028\u0031\u0039\u0029', + '\u2487': '\u0028\u0032\u0030\u0029', + '\u2488': '\u0031\u002E', + '\u2489': '\u0032\u002E', + '\u248A': '\u0033\u002E', + '\u248B': '\u0034\u002E', + '\u248C': '\u0035\u002E', + '\u248D': '\u0036\u002E', + '\u248E': '\u0037\u002E', + '\u248F': '\u0038\u002E', + '\u2490': '\u0039\u002E', + '\u2491': '\u0031\u0030\u002E', + '\u2492': '\u0031\u0031\u002E', + '\u2493': '\u0031\u0032\u002E', + '\u2494': '\u0031\u0033\u002E', + '\u2495': '\u0031\u0034\u002E', + '\u2496': '\u0031\u0035\u002E', + '\u2497': '\u0031\u0036\u002E', + '\u2498': '\u0031\u0037\u002E', + '\u2499': '\u0031\u0038\u002E', + '\u249A': '\u0031\u0039\u002E', + '\u249B': '\u0032\u0030\u002E', + '\u249C': '\u0028\u0061\u0029', + '\u249D': '\u0028\u0062\u0029', + '\u249E': '\u0028\u0063\u0029', + '\u249F': '\u0028\u0064\u0029', + '\u24A0': '\u0028\u0065\u0029', + '\u24A1': '\u0028\u0066\u0029', + '\u24A2': '\u0028\u0067\u0029', + '\u24A3': '\u0028\u0068\u0029', + '\u24A4': '\u0028\u0069\u0029', + '\u24A5': '\u0028\u006A\u0029', + '\u24A6': '\u0028\u006B\u0029', + '\u24A7': '\u0028\u006C\u0029', + '\u24A8': '\u0028\u006D\u0029', + '\u24A9': '\u0028\u006E\u0029', + '\u24AA': '\u0028\u006F\u0029', + '\u24AB': '\u0028\u0070\u0029', + '\u24AC': '\u0028\u0071\u0029', + '\u24AD': '\u0028\u0072\u0029', + '\u24AE': '\u0028\u0073\u0029', + '\u24AF': '\u0028\u0074\u0029', + '\u24B0': '\u0028\u0075\u0029', + '\u24B1': '\u0028\u0076\u0029', + '\u24B2': '\u0028\u0077\u0029', + '\u24B3': '\u0028\u0078\u0029', + '\u24B4': '\u0028\u0079\u0029', + '\u24B5': '\u0028\u007A\u0029', + '\u2A0C': '\u222B\u222B\u222B\u222B', + '\u2A74': '\u003A\u003A\u003D', + '\u2A75': '\u003D\u003D', + '\u2A76': '\u003D\u003D\u003D', + '\u2E9F': '\u6BCD', + '\u2EF3': '\u9F9F', + '\u2F00': '\u4E00', + '\u2F01': '\u4E28', + '\u2F02': '\u4E36', + '\u2F03': '\u4E3F', + '\u2F04': '\u4E59', + '\u2F05': '\u4E85', + '\u2F06': '\u4E8C', + '\u2F07': '\u4EA0', + '\u2F08': '\u4EBA', + '\u2F09': '\u513F', + '\u2F0A': '\u5165', + '\u2F0B': '\u516B', + '\u2F0C': '\u5182', + '\u2F0D': '\u5196', + '\u2F0E': '\u51AB', + '\u2F0F': '\u51E0', + '\u2F10': '\u51F5', + '\u2F11': '\u5200', + '\u2F12': '\u529B', + '\u2F13': '\u52F9', + '\u2F14': '\u5315', + '\u2F15': '\u531A', + '\u2F16': '\u5338', + '\u2F17': '\u5341', + '\u2F18': '\u535C', + '\u2F19': '\u5369', + '\u2F1A': '\u5382', + '\u2F1B': '\u53B6', + '\u2F1C': '\u53C8', + '\u2F1D': '\u53E3', + '\u2F1E': '\u56D7', + '\u2F1F': '\u571F', + '\u2F20': '\u58EB', + '\u2F21': '\u5902', + '\u2F22': '\u590A', + '\u2F23': '\u5915', + '\u2F24': '\u5927', + '\u2F25': '\u5973', + '\u2F26': '\u5B50', + '\u2F27': '\u5B80', + '\u2F28': '\u5BF8', + '\u2F29': '\u5C0F', + '\u2F2A': '\u5C22', + '\u2F2B': '\u5C38', + '\u2F2C': '\u5C6E', + '\u2F2D': '\u5C71', + '\u2F2E': '\u5DDB', + '\u2F2F': '\u5DE5', + '\u2F30': '\u5DF1', + '\u2F31': '\u5DFE', + '\u2F32': '\u5E72', + '\u2F33': '\u5E7A', + '\u2F34': '\u5E7F', + '\u2F35': '\u5EF4', + '\u2F36': '\u5EFE', + '\u2F37': '\u5F0B', + '\u2F38': '\u5F13', + '\u2F39': '\u5F50', + '\u2F3A': '\u5F61', + '\u2F3B': '\u5F73', + '\u2F3C': '\u5FC3', + '\u2F3D': '\u6208', + '\u2F3E': '\u6236', + '\u2F3F': '\u624B', + '\u2F40': '\u652F', + '\u2F41': '\u6534', + '\u2F42': '\u6587', + '\u2F43': '\u6597', + '\u2F44': '\u65A4', + '\u2F45': '\u65B9', + '\u2F46': '\u65E0', + '\u2F47': '\u65E5', + '\u2F48': '\u66F0', + '\u2F49': '\u6708', + '\u2F4A': '\u6728', + '\u2F4B': '\u6B20', + '\u2F4C': '\u6B62', + '\u2F4D': '\u6B79', + '\u2F4E': '\u6BB3', + '\u2F4F': '\u6BCB', + '\u2F50': '\u6BD4', + '\u2F51': '\u6BDB', + '\u2F52': '\u6C0F', + '\u2F53': '\u6C14', + '\u2F54': '\u6C34', + '\u2F55': '\u706B', + '\u2F56': '\u722A', + '\u2F57': '\u7236', + '\u2F58': '\u723B', + '\u2F59': '\u723F', + '\u2F5A': '\u7247', + '\u2F5B': '\u7259', + '\u2F5C': '\u725B', + '\u2F5D': '\u72AC', + '\u2F5E': '\u7384', + '\u2F5F': '\u7389', + '\u2F60': '\u74DC', + '\u2F61': '\u74E6', + '\u2F62': '\u7518', + '\u2F63': '\u751F', + '\u2F64': '\u7528', + '\u2F65': '\u7530', + '\u2F66': '\u758B', + '\u2F67': '\u7592', + '\u2F68': '\u7676', + '\u2F69': '\u767D', + '\u2F6A': '\u76AE', + '\u2F6B': '\u76BF', + '\u2F6C': '\u76EE', + '\u2F6D': '\u77DB', + '\u2F6E': '\u77E2', + '\u2F6F': '\u77F3', + '\u2F70': '\u793A', + '\u2F71': '\u79B8', + '\u2F72': '\u79BE', + '\u2F73': '\u7A74', + '\u2F74': '\u7ACB', + '\u2F75': '\u7AF9', + '\u2F76': '\u7C73', + '\u2F77': '\u7CF8', + '\u2F78': '\u7F36', + '\u2F79': '\u7F51', + '\u2F7A': '\u7F8A', + '\u2F7B': '\u7FBD', + '\u2F7C': '\u8001', + '\u2F7D': '\u800C', + '\u2F7E': '\u8012', + '\u2F7F': '\u8033', + '\u2F80': '\u807F', + '\u2F81': '\u8089', + '\u2F82': '\u81E3', + '\u2F83': '\u81EA', + '\u2F84': '\u81F3', + '\u2F85': '\u81FC', + '\u2F86': '\u820C', + '\u2F87': '\u821B', + '\u2F88': '\u821F', + '\u2F89': '\u826E', + '\u2F8A': '\u8272', + '\u2F8B': '\u8278', + '\u2F8C': '\u864D', + '\u2F8D': '\u866B', + '\u2F8E': '\u8840', + '\u2F8F': '\u884C', + '\u2F90': '\u8863', + '\u2F91': '\u897E', + '\u2F92': '\u898B', + '\u2F93': '\u89D2', + '\u2F94': '\u8A00', + '\u2F95': '\u8C37', + '\u2F96': '\u8C46', + '\u2F97': '\u8C55', + '\u2F98': '\u8C78', + '\u2F99': '\u8C9D', + '\u2F9A': '\u8D64', + '\u2F9B': '\u8D70', + '\u2F9C': '\u8DB3', + '\u2F9D': '\u8EAB', + '\u2F9E': '\u8ECA', + '\u2F9F': '\u8F9B', + '\u2FA0': '\u8FB0', + '\u2FA1': '\u8FB5', + '\u2FA2': '\u9091', + '\u2FA3': '\u9149', + '\u2FA4': '\u91C6', + '\u2FA5': '\u91CC', + '\u2FA6': '\u91D1', + '\u2FA7': '\u9577', + '\u2FA8': '\u9580', + '\u2FA9': '\u961C', + '\u2FAA': '\u96B6', + '\u2FAB': '\u96B9', + '\u2FAC': '\u96E8', + '\u2FAD': '\u9751', + '\u2FAE': '\u975E', + '\u2FAF': '\u9762', + '\u2FB0': '\u9769', + '\u2FB1': '\u97CB', + '\u2FB2': '\u97ED', + '\u2FB3': '\u97F3', + '\u2FB4': '\u9801', + '\u2FB5': '\u98A8', + '\u2FB6': '\u98DB', + '\u2FB7': '\u98DF', + '\u2FB8': '\u9996', + '\u2FB9': '\u9999', + '\u2FBA': '\u99AC', + '\u2FBB': '\u9AA8', + '\u2FBC': '\u9AD8', + '\u2FBD': '\u9ADF', + '\u2FBE': '\u9B25', + '\u2FBF': '\u9B2F', + '\u2FC0': '\u9B32', + '\u2FC1': '\u9B3C', + '\u2FC2': '\u9B5A', + '\u2FC3': '\u9CE5', + '\u2FC4': '\u9E75', + '\u2FC5': '\u9E7F', + '\u2FC6': '\u9EA5', + '\u2FC7': '\u9EBB', + '\u2FC8': '\u9EC3', + '\u2FC9': '\u9ECD', + '\u2FCA': '\u9ED1', + '\u2FCB': '\u9EF9', + '\u2FCC': '\u9EFD', + '\u2FCD': '\u9F0E', + '\u2FCE': '\u9F13', + '\u2FCF': '\u9F20', + '\u2FD0': '\u9F3B', + '\u2FD1': '\u9F4A', + '\u2FD2': '\u9F52', + '\u2FD3': '\u9F8D', + '\u2FD4': '\u9F9C', + '\u2FD5': '\u9FA0', + '\u3036': '\u3012', + '\u3038': '\u5341', + '\u3039': '\u5344', + '\u303A': '\u5345', + '\u309B': '\u0020\u3099', + '\u309C': '\u0020\u309A', + '\u3131': '\u1100', + '\u3132': '\u1101', + '\u3133': '\u11AA', + '\u3134': '\u1102', + '\u3135': '\u11AC', + '\u3136': '\u11AD', + '\u3137': '\u1103', + '\u3138': '\u1104', + '\u3139': '\u1105', + '\u313A': '\u11B0', + '\u313B': '\u11B1', + '\u313C': '\u11B2', + '\u313D': '\u11B3', + '\u313E': '\u11B4', + '\u313F': '\u11B5', + '\u3140': '\u111A', + '\u3141': '\u1106', + '\u3142': '\u1107', + '\u3143': '\u1108', + '\u3144': '\u1121', + '\u3145': '\u1109', + '\u3146': '\u110A', + '\u3147': '\u110B', + '\u3148': '\u110C', + '\u3149': '\u110D', + '\u314A': '\u110E', + '\u314B': '\u110F', + '\u314C': '\u1110', + '\u314D': '\u1111', + '\u314E': '\u1112', + '\u314F': '\u1161', + '\u3150': '\u1162', + '\u3151': '\u1163', + '\u3152': '\u1164', + '\u3153': '\u1165', + '\u3154': '\u1166', + '\u3155': '\u1167', + '\u3156': '\u1168', + '\u3157': '\u1169', + '\u3158': '\u116A', + '\u3159': '\u116B', + '\u315A': '\u116C', + '\u315B': '\u116D', + '\u315C': '\u116E', + '\u315D': '\u116F', + '\u315E': '\u1170', + '\u315F': '\u1171', + '\u3160': '\u1172', + '\u3161': '\u1173', + '\u3162': '\u1174', + '\u3163': '\u1175', + '\u3164': '\u1160', + '\u3165': '\u1114', + '\u3166': '\u1115', + '\u3167': '\u11C7', + '\u3168': '\u11C8', + '\u3169': '\u11CC', + '\u316A': '\u11CE', + '\u316B': '\u11D3', + '\u316C': '\u11D7', + '\u316D': '\u11D9', + '\u316E': '\u111C', + '\u316F': '\u11DD', + '\u3170': '\u11DF', + '\u3171': '\u111D', + '\u3172': '\u111E', + '\u3173': '\u1120', + '\u3174': '\u1122', + '\u3175': '\u1123', + '\u3176': '\u1127', + '\u3177': '\u1129', + '\u3178': '\u112B', + '\u3179': '\u112C', + '\u317A': '\u112D', + '\u317B': '\u112E', + '\u317C': '\u112F', + '\u317D': '\u1132', + '\u317E': '\u1136', + '\u317F': '\u1140', + '\u3180': '\u1147', + '\u3181': '\u114C', + '\u3182': '\u11F1', + '\u3183': '\u11F2', + '\u3184': '\u1157', + '\u3185': '\u1158', + '\u3186': '\u1159', + '\u3187': '\u1184', + '\u3188': '\u1185', + '\u3189': '\u1188', + '\u318A': '\u1191', + '\u318B': '\u1192', + '\u318C': '\u1194', + '\u318D': '\u119E', + '\u318E': '\u11A1', + '\u3200': '\u0028\u1100\u0029', + '\u3201': '\u0028\u1102\u0029', + '\u3202': '\u0028\u1103\u0029', + '\u3203': '\u0028\u1105\u0029', + '\u3204': '\u0028\u1106\u0029', + '\u3205': '\u0028\u1107\u0029', + '\u3206': '\u0028\u1109\u0029', + '\u3207': '\u0028\u110B\u0029', + '\u3208': '\u0028\u110C\u0029', + '\u3209': '\u0028\u110E\u0029', + '\u320A': '\u0028\u110F\u0029', + '\u320B': '\u0028\u1110\u0029', + '\u320C': '\u0028\u1111\u0029', + '\u320D': '\u0028\u1112\u0029', + '\u320E': '\u0028\u1100\u1161\u0029', + '\u320F': '\u0028\u1102\u1161\u0029', + '\u3210': '\u0028\u1103\u1161\u0029', + '\u3211': '\u0028\u1105\u1161\u0029', + '\u3212': '\u0028\u1106\u1161\u0029', + '\u3213': '\u0028\u1107\u1161\u0029', + '\u3214': '\u0028\u1109\u1161\u0029', + '\u3215': '\u0028\u110B\u1161\u0029', + '\u3216': '\u0028\u110C\u1161\u0029', + '\u3217': '\u0028\u110E\u1161\u0029', + '\u3218': '\u0028\u110F\u1161\u0029', + '\u3219': '\u0028\u1110\u1161\u0029', + '\u321A': '\u0028\u1111\u1161\u0029', + '\u321B': '\u0028\u1112\u1161\u0029', + '\u321C': '\u0028\u110C\u116E\u0029', + '\u321D': '\u0028\u110B\u1169\u110C\u1165\u11AB\u0029', + '\u321E': '\u0028\u110B\u1169\u1112\u116E\u0029', + '\u3220': '\u0028\u4E00\u0029', + '\u3221': '\u0028\u4E8C\u0029', + '\u3222': '\u0028\u4E09\u0029', + '\u3223': '\u0028\u56DB\u0029', + '\u3224': '\u0028\u4E94\u0029', + '\u3225': '\u0028\u516D\u0029', + '\u3226': '\u0028\u4E03\u0029', + '\u3227': '\u0028\u516B\u0029', + '\u3228': '\u0028\u4E5D\u0029', + '\u3229': '\u0028\u5341\u0029', + '\u322A': '\u0028\u6708\u0029', + '\u322B': '\u0028\u706B\u0029', + '\u322C': '\u0028\u6C34\u0029', + '\u322D': '\u0028\u6728\u0029', + '\u322E': '\u0028\u91D1\u0029', + '\u322F': '\u0028\u571F\u0029', + '\u3230': '\u0028\u65E5\u0029', + '\u3231': '\u0028\u682A\u0029', + '\u3232': '\u0028\u6709\u0029', + '\u3233': '\u0028\u793E\u0029', + '\u3234': '\u0028\u540D\u0029', + '\u3235': '\u0028\u7279\u0029', + '\u3236': '\u0028\u8CA1\u0029', + '\u3237': '\u0028\u795D\u0029', + '\u3238': '\u0028\u52B4\u0029', + '\u3239': '\u0028\u4EE3\u0029', + '\u323A': '\u0028\u547C\u0029', + '\u323B': '\u0028\u5B66\u0029', + '\u323C': '\u0028\u76E3\u0029', + '\u323D': '\u0028\u4F01\u0029', + '\u323E': '\u0028\u8CC7\u0029', + '\u323F': '\u0028\u5354\u0029', + '\u3240': '\u0028\u796D\u0029', + '\u3241': '\u0028\u4F11\u0029', + '\u3242': '\u0028\u81EA\u0029', + '\u3243': '\u0028\u81F3\u0029', + '\u32C0': '\u0031\u6708', + '\u32C1': '\u0032\u6708', + '\u32C2': '\u0033\u6708', + '\u32C3': '\u0034\u6708', + '\u32C4': '\u0035\u6708', + '\u32C5': '\u0036\u6708', + '\u32C6': '\u0037\u6708', + '\u32C7': '\u0038\u6708', + '\u32C8': '\u0039\u6708', + '\u32C9': '\u0031\u0030\u6708', + '\u32CA': '\u0031\u0031\u6708', + '\u32CB': '\u0031\u0032\u6708', + '\u3358': '\u0030\u70B9', + '\u3359': '\u0031\u70B9', + '\u335A': '\u0032\u70B9', + '\u335B': '\u0033\u70B9', + '\u335C': '\u0034\u70B9', + '\u335D': '\u0035\u70B9', + '\u335E': '\u0036\u70B9', + '\u335F': '\u0037\u70B9', + '\u3360': '\u0038\u70B9', + '\u3361': '\u0039\u70B9', + '\u3362': '\u0031\u0030\u70B9', + '\u3363': '\u0031\u0031\u70B9', + '\u3364': '\u0031\u0032\u70B9', + '\u3365': '\u0031\u0033\u70B9', + '\u3366': '\u0031\u0034\u70B9', + '\u3367': '\u0031\u0035\u70B9', + '\u3368': '\u0031\u0036\u70B9', + '\u3369': '\u0031\u0037\u70B9', + '\u336A': '\u0031\u0038\u70B9', + '\u336B': '\u0031\u0039\u70B9', + '\u336C': '\u0032\u0030\u70B9', + '\u336D': '\u0032\u0031\u70B9', + '\u336E': '\u0032\u0032\u70B9', + '\u336F': '\u0032\u0033\u70B9', + '\u3370': '\u0032\u0034\u70B9', + '\u33E0': '\u0031\u65E5', + '\u33E1': '\u0032\u65E5', + '\u33E2': '\u0033\u65E5', + '\u33E3': '\u0034\u65E5', + '\u33E4': '\u0035\u65E5', + '\u33E5': '\u0036\u65E5', + '\u33E6': '\u0037\u65E5', + '\u33E7': '\u0038\u65E5', + '\u33E8': '\u0039\u65E5', + '\u33E9': '\u0031\u0030\u65E5', + '\u33EA': '\u0031\u0031\u65E5', + '\u33EB': '\u0031\u0032\u65E5', + '\u33EC': '\u0031\u0033\u65E5', + '\u33ED': '\u0031\u0034\u65E5', + '\u33EE': '\u0031\u0035\u65E5', + '\u33EF': '\u0031\u0036\u65E5', + '\u33F0': '\u0031\u0037\u65E5', + '\u33F1': '\u0031\u0038\u65E5', + '\u33F2': '\u0031\u0039\u65E5', + '\u33F3': '\u0032\u0030\u65E5', + '\u33F4': '\u0032\u0031\u65E5', + '\u33F5': '\u0032\u0032\u65E5', + '\u33F6': '\u0032\u0033\u65E5', + '\u33F7': '\u0032\u0034\u65E5', + '\u33F8': '\u0032\u0035\u65E5', + '\u33F9': '\u0032\u0036\u65E5', + '\u33FA': '\u0032\u0037\u65E5', + '\u33FB': '\u0032\u0038\u65E5', + '\u33FC': '\u0032\u0039\u65E5', + '\u33FD': '\u0033\u0030\u65E5', + '\u33FE': '\u0033\u0031\u65E5', + '\uFB00': '\u0066\u0066', + '\uFB01': '\u0066\u0069', + '\uFB02': '\u0066\u006C', + '\uFB03': '\u0066\u0066\u0069', + '\uFB04': '\u0066\u0066\u006C', + '\uFB05': '\u017F\u0074', + '\uFB06': '\u0073\u0074', + '\uFB13': '\u0574\u0576', + '\uFB14': '\u0574\u0565', + '\uFB15': '\u0574\u056B', + '\uFB16': '\u057E\u0576', + '\uFB17': '\u0574\u056D', + '\uFB4F': '\u05D0\u05DC', + '\uFE49': '\u203E', + '\uFE4A': '\u203E', + '\uFE4B': '\u203E', + '\uFE4C': '\u203E', + '\uFE4D': '\u005F', + '\uFE4E': '\u005F', + '\uFE4F': '\u005F' +}; + +function fontCharsToUnicode(charCodes, fontProperties) { + var toUnicode = fontProperties.toUnicode; + var composite = fontProperties.composite; + var encoding, differences, cidToUnicode; + var result = ''; + if (composite) { + cidToUnicode = fontProperties.cidToUnicode; + for (var i = 0, ii = charCodes.length; i < ii; i += 2) { + var charCode = (charCodes.charCodeAt(i) << 8) | + charCodes.charCodeAt(i + 1); + if (toUnicode && charCode in toUnicode) { + var unicode = toUnicode[charCode]; + result += typeof unicode !== 'number' ? unicode : + String.fromCharCode(unicode); + continue; + } + result += String.fromCharCode(!cidToUnicode ? charCode : + cidToUnicode[charCode] || charCode); + } + } else { + differences = fontProperties.differences; + encoding = fontProperties.baseEncoding; + for (var i = 0, ii = charCodes.length; i < ii; i++) { + var charCode = charCodes.charCodeAt(i); + var unicode; + if (toUnicode && charCode in toUnicode) { + var unicode = toUnicode[charCode]; + result += typeof unicode !== 'number' ? unicode : + String.fromCharCode(unicode); + continue; + } + + var glyphName = charCode in differences ? differences[charCode] : + encoding[charCode]; + if (glyphName in GlyphsUnicode) { + result += String.fromCharCode(GlyphsUnicode[glyphName]); + continue; + } + result += String.fromCharCode(charCode); + } + } + // normalizing the unicode characters + for (var i = 0, ii = result.length; i < ii; i++) { + if (!(result[i] in NormalizedUnicodes)) + continue; + result = result.substring(0, i) + NormalizedUnicodes[result[i]] + + result.substring(i + 1); + ii = result.length; + } + return result; +} + /** * 'Font' is the class the outside world should use, it encapsulate all the font * decoding logics whatever type it is (assuming the font type is supported). @@ -14148,7 +15518,6 @@ var Font = (function FontClosure() { this.name = name; this.coded = properties.coded; this.charProcOperatorList = properties.charProcOperatorList; - this.resources = properties.resources; this.sizes = []; var names = name.split('+'); @@ -14172,6 +15541,7 @@ var Font = (function FontClosure() { this.widths = properties.widths; this.defaultWidth = properties.defaultWidth; this.composite = properties.composite; + this.wideChars = properties.wideChars; this.hasEncoding = properties.hasEncoding; this.fontMatrix = properties.fontMatrix; @@ -15109,6 +16479,16 @@ var Font = (function FontClosure() { properties.glyphNames = glyphNames; } + function isOS2Valid(os2Table) { + var data = os2Table.data; + // usWinAscent == 0 makes font unreadable by windows + var usWinAscent = (data[74] << 8) | data[75]; + if (usWinAscent == 0) + return false; + + return true; + } + // Check that required tables are present var requiredTables = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'post']; @@ -15116,7 +16496,7 @@ var Font = (function FontClosure() { var header = readOpenTypeHeader(font); var numTables = header.numTables; - var cmap, post, maxp, hhea, hmtx, vhea, vmtx, head, loca, glyf; + var cmap, post, maxp, hhea, hmtx, vhea, vmtx, head, loca, glyf, os2; var tables = []; for (var i = 0; i < numTables; i++) { var table = readTableEntry(font); @@ -15134,6 +16514,8 @@ var Font = (function FontClosure() { hmtx = table; else if (table.tag == 'head') head = table; + else if (table.tag == 'OS/2') + os2 = table; requiredTables.splice(index, 1); } else { @@ -15149,7 +16531,7 @@ var Font = (function FontClosure() { tables.push(table); } - var numTables = header.numTables + requiredTables.length; + var numTables = tables.length + requiredTables.length; // header and new offsets. Table entry information is appended to the // end of file. The virtualOffset represents where to put the actual @@ -15163,21 +16545,10 @@ var Font = (function FontClosure() { // of missing tables createOpenTypeHeader(header.version, ttf, numTables); - if (requiredTables.indexOf('OS/2') != -1) { - // extract some more font properties from the OpenType head and - // hhea tables; yMin and descent value are always negative - var override = { - unitsPerEm: int16([head.data[18], head.data[19]]), - yMax: int16([head.data[42], head.data[43]]), - yMin: int16([head.data[38], head.data[39]]) - 0x10000, - ascent: int16([hhea.data[4], hhea.data[5]]), - descent: int16([hhea.data[6], hhea.data[7]]) - 0x10000 - }; - - tables.push({ - tag: 'OS/2', - data: stringToArray(createOS2Table(properties, null, override)) - }); + // Invalid OS/2 can break the font for the Windows + if (os2 && !isOS2Valid(os2)) { + tables.splice(tables.indexOf(os2), 1); + os2 = null; } // Ensure the [h/v]mtx tables contains the advance width and @@ -15357,9 +16728,9 @@ var Font = (function FontClosure() { this.isSymbolicFont = false; } - // heuristics: if removed more than 2 glyphs encoding WinAnsiEncoding - // does not set properly - if (glyphsRemoved > 2) { + // heuristics: if removed more than 10 glyphs encoding WinAnsiEncoding + // does not set properly (broken PDFs have about 100 removed glyphs) + if (glyphsRemoved > 10) { warn('Switching TrueType encoding to MacRomanEncoding for ' + this.name + ' font'); encoding = Encodings.MacRomanEncoding; @@ -15458,6 +16829,23 @@ var Font = (function FontClosure() { } this.unicodeIsEnabled = unicodeIsEnabled; + if (!os2) { + // extract some more font properties from the OpenType head and + // hhea tables; yMin and descent value are always negative + var override = { + unitsPerEm: int16([head.data[18], head.data[19]]), + yMax: int16([head.data[42], head.data[43]]), + yMin: int16([head.data[38], head.data[39]]) - 0x10000, + ascent: int16([hhea.data[4], hhea.data[5]]), + descent: int16([hhea.data[6], hhea.data[7]]) - 0x10000 + }; + + tables.push({ + tag: 'OS/2', + data: stringToArray(createOS2Table(properties, glyphs, override)) + }); + } + // Rewrite the 'post' table if needed if (requiredTables.indexOf('post') != -1) { tables.push({ @@ -15842,6 +17230,7 @@ var Font = (function FontClosure() { } // MacRoman encoding address by re-encoding the cmap table + fontCharCode = glyphName in this.glyphNameMap ? this.glyphNameMap[glyphName] : GlyphsUnicode[glyphName]; break; @@ -15885,7 +17274,7 @@ var Font = (function FontClosure() { glyphs = []; - if (this.composite) { + if (this.wideChars) { // composite fonts have multi-byte strings convert the string from // single-byte to multi-byte // XXX assuming CIDFonts are two-byte - later need to extract the @@ -16841,7 +18230,7 @@ var CFFFont = (function CFFFontClosure() { this.properties = properties; var parser = new CFFParser(file, properties); - var cff = parser.parse(); + var cff = parser.parse(true); var compiler = new CFFCompiler(cff); this.readExtra(cff); try { @@ -16932,7 +18321,7 @@ var CFFParser = (function CFFParserClosure() { this.properties = properties; } CFFParser.prototype = { - parse: function CFFParser_parse() { + parse: function CFFParser_parse(normalizeCIDData) { var properties = this.properties; var cff = new CFF(); this.cff = cff; @@ -16987,6 +18376,21 @@ var CFFParser = (function CFFParserClosure() { cff.charset = charset; cff.encoding = encoding; + if (!cff.isCIDFont || !normalizeCIDData) + return cff; + + // DirectWrite does not like CID fonts data. Trying to convert/flatten + // the font data and remove CID properties. + if (cff.fdArray.length !== 1) + error('Unable to normalize CID font in CFF data'); + + var fontDict = cff.fdArray[0]; + fontDict.setByKey(17, topDict.getByName('CharStrings')); + cff.topDict = fontDict; + cff.isCIDFont = false; + delete cff.fdArray; + delete cff.fdSelect; + return cff; }, parseHeader: function CFFParser_parseHeader() { @@ -16997,7 +18401,7 @@ var CFFParser = (function CFFParserClosure() { ++offset; if (offset != 0) { - warn('cff data is shifted'); + info('cff data is shifted'); bytes = bytes.subarray(offset); this.bytes = bytes; } @@ -17585,9 +18989,9 @@ var CFFPrivateDict = (function CFFPrivateDictClosure() { [[12, 17], 'LanguageGroup', 'num', 0], [[12, 18], 'ExpansionFactor', 'num', 0.06], [[12, 19], 'initialRandomSeed', 'num', 0], - [19, 'Subrs', 'offset', null], [20, 'defaultWidthX', 'num', 0], - [21, 'nominalWidthX', 'num', 0] + [21, 'nominalWidthX', 'num', 0], + [19, 'Subrs', 'offset', null] ]; var tables = null; function CFFPrivateDict(strings) { @@ -18045,6 +19449,7 @@ var CFFCompiler = (function CFFCompilerClosure() { return CFFCompiler; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -22257,6 +23662,7 @@ var GlyphsUnicode = { '.notdef': 0x0000 }; + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -22274,7 +23680,7 @@ var PDFImage = (function PDFImageClosure() { var colorSpace = dict.get('ColorSpace', 'CS'); colorSpace = ColorSpace.parse(colorSpace, xref, res); var numComps = colorSpace.numComps; - handler.send('jpeg_decode', [image.getIR(), numComps], function(message) { + handler.send('JpegDecode', [image.getIR(), numComps], function(message) { var data = message.data; var stream = new Stream(data, 0, data.length, image.dict); promise.resolve(stream); @@ -22632,6 +24038,7 @@ function loadJpegStream(id, imageData, objs) { img.src = 'data:image/jpeg;base64,' + window.btoa(imageData); } + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -25579,6 +26986,7 @@ var Metrics = { } }; + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -25830,9 +27238,12 @@ var Parser = (function ParserClosure() { if (name == 'CCITTFaxDecode' || name == 'CCF') { return new CCITTFaxStream(stream, params); } - if (name == 'RunLengthDecode') { + if (name == 'RunLengthDecode' || name == 'RL') { return new RunLengthStream(stream); } + if (name == 'JBIG2Decode') { + error('JBIG2 image format is not currently supprted.'); + } warn('filter "' + name + '" not supported yet'); return stream; } @@ -25842,8 +27253,16 @@ var Parser = (function ParserClosure() { })(); var Lexer = (function LexerClosure() { - function Lexer(stream) { + function Lexer(stream, knownCommands) { this.stream = stream; + // The PDFs might have "glued" commands with other commands, operands or + // literals, e.g. "q1". The knownCommands is a dictionary of the valid + // commands and their prefixes. The prefixes are built the following way: + // if there a command that is a prefix of the other valid command or + // literal (e.g. 'f' and 'false') the following prefixes must be included, + // 'fa', 'fal', 'fals'. The prefixes are not needed, if the command has no + // other commands or literals as a prefix. The knowCommands is optional. + this.knownCommands = knownCommands; } Lexer.isSpace = function Lexer_isSpace(ch) { @@ -26107,12 +27526,18 @@ var Lexer = (function LexerClosure() { // command var str = ch; + var knownCommands = this.knownCommands; + var knownCommandFound = knownCommands && (str in knownCommands); while (!!(ch = stream.lookChar()) && !specialChars[ch.charCodeAt(0)]) { + // stop if known command is found and next character does not make + // the str a command + if (knownCommandFound && !((str + ch) in knownCommands)) + break; stream.skip(); if (str.length == 128) error('Command token too long: ' + str.length); - str += ch; + knownCommandFound = knownCommands && (str in knownCommands); } if (str == 'true') return true; @@ -26218,6 +27643,7 @@ var Linearization = (function LinearizationClosure() { return Linearization; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -26330,7 +27756,7 @@ Shadings.RadialAxial = (function RadialAxialClosure() { var r1 = raw[6]; return { type: 'Pattern', - getPattern: function(ctx) { + getPattern: function RadialAxial_getPattern(ctx) { var curMatrix = ctx.mozCurrentTransform; if (curMatrix) { var userMatrix = ctx.mozCurrentTransformInverse; @@ -26521,6 +27947,7 @@ var TilingPattern = (function TilingPatternClosure() { return TilingPattern; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -28210,7 +29637,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (a1 > codingLine[codingPos]) { if (a1 > this.columns) { - warn('row is wrong length'); + info('row is wrong length'); this.err = true; a1 = this.columns; } @@ -28230,7 +29657,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (a1 > codingLine[codingPos]) { if (a1 > this.columns) { - warn('row is wrong length'); + info('row is wrong length'); this.err = true; a1 = this.columns; } @@ -28240,7 +29667,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { codingLine[codingPos] = a1; } else if (a1 < codingLine[codingPos]) { if (a1 < 0) { - warn('invalid code'); + info('invalid code'); this.err = true; a1 = 0; } @@ -28402,7 +29829,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { this.eof = true; break; default: - warn('bad 2d code'); + info('bad 2d code'); this.addPixels(columns, 0); this.err = true; } @@ -28465,7 +29892,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { for (var i = 0; i < 4; ++i) { code1 = this.lookBits(12); if (code1 != 1) - warn('bad rtc code: ' + code1); + info('bad rtc code: ' + code1); this.eatBits(12); if (this.encoding > 0) { this.lookBits(1); @@ -28588,7 +30015,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (result[0] && result[2]) return result[1]; } - warn('Bad two dim code'); + info('Bad two dim code'); return EOF; }; @@ -28621,7 +30048,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (result[0]) return result[1]; } - warn('bad white code'); + info('bad white code'); this.eatBits(1); return 1; }; @@ -28658,7 +30085,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { if (result[0]) return result[1]; } - warn('bad black code'); + info('bad black code'); this.eatBits(1); return 1; }; @@ -28815,6 +30242,7 @@ var LZWStream = (function LZWStreamClosure() { return LZWStream; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -28828,10 +30256,13 @@ function MessageHandler(name, comObj) { var ah = this.actionHandler = {}; ah['console_log'] = [function ahConsoleLog(data) { - console.log.apply(console, data); + console.log.apply(console, data); }]; ah['console_error'] = [function ahConsoleError(data) { - console.error.apply(console, data); + console.error.apply(console, data); + }]; + ah['_warn'] = [function ah_Warn(data) { + warn(data); }]; comObj.onmessage = function messageHandlerComObjOnMessage(event) { @@ -28902,15 +30333,68 @@ var WorkerMessageHandler = { handler.send('test', data instanceof Uint8Array); }); - handler.on('doc', function wphSetupDoc(data) { + handler.on('GetDocRequest', function wphSetupDoc(data) { // Create only the model of the PDFDoc, which is enough for // processing the content of the pdf. - pdfModel = new PDFDocModel(new Stream(data)); + var pdfData = data.data; + var pdfPassword = data.params.password; + try { + pdfModel = new PDFDocument(new Stream(pdfData), pdfPassword); + } catch (e) { + if (e instanceof PasswordException) { + if (e.code === 'needpassword') { + handler.send('NeedPassword', { + exception: e + }); + } else if (e.code === 'incorrectpassword') { + handler.send('IncorrectPassword', { + exception: e + }); + } + + return; + } else { + throw e; + } + } + var doc = { + numPages: pdfModel.numPages, + fingerprint: pdfModel.getFingerprint(), + destinations: pdfModel.catalog.destinations, + outline: pdfModel.catalog.documentOutline, + info: pdfModel.getDocumentInfo(), + metadata: pdfModel.catalog.metadata, + encrypted: !!pdfModel.xref.encrypt + }; + handler.send('GetDoc', {pdfInfo: doc}); }); - handler.on('page_request', function wphSetupPageRequest(pageNum) { - pageNum = parseInt(pageNum); + handler.on('GetPageRequest', function wphSetupGetPage(data) { + var pageNumber = data.pageIndex + 1; + var pdfPage = pdfModel.getPage(pageNumber); + var page = { + pageIndex: data.pageIndex, + rotate: pdfPage.rotate, + ref: pdfPage.ref, + view: pdfPage.view + }; + handler.send('GetPage', {pageInfo: page}); + }); + handler.on('GetData', function wphSetupGetData(data, promise) { + promise.resolve(pdfModel.stream.bytes); + }); + + handler.on('GetAnnotationsRequest', function wphSetupGetAnnotations(data) { + var pdfPage = pdfModel.getPage(data.pageIndex + 1); + handler.send('GetAnnotations', { + pageIndex: data.pageIndex, + annotations: pdfPage.getAnnotations() + }); + }); + + handler.on('RenderPageRequest', function wphSetupRenderPage(data) { + var pageNum = data.pageIndex + 1; // The following code does quite the same as // Page.prototype.startRendering, but stops at one point and sends the @@ -28947,7 +30431,7 @@ var WorkerMessageHandler = { }; } - handler.send('page_error', { + handler.send('PageError', { pageNum: pageNum, error: e }); @@ -28965,13 +30449,30 @@ var WorkerMessageHandler = { fonts[dep] = true; } } - - handler.send('page', { - pageNum: pageNum, + handler.send('RenderPage', { + pageIndex: data.pageIndex, operatorList: operatorList, depFonts: Object.keys(fonts) }); }, this); + + handler.on('GetTextContent', function wphExtractText(data, promise) { + var pageNum = data.pageIndex + 1; + var start = Date.now(); + + var textContent = ''; + try { + var page = pdfModel.getPage(pageNum); + textContent = page.extractTextContent(); + promise.resolve(textContent); + } catch (e) { + // Skip errored pages + promise.reject(e); + } + + console.log('text indexing: page=%d - time=%dms', + pageNum, Date.now() - start); + }); } }; @@ -29012,10 +30513,22 @@ var workerConsole = { if (typeof window === 'undefined') { globalScope.console = workerConsole; + // Add a logger so we can pass warnings on to the main thread, errors will + // throw an exception which will be forwarded on automatically. + PDFJS.LogManager.addLogger({ + warn: function(msg) { + postMessage({ + action: '_warn', + data: msg + }); + } + }); + var handler = new MessageHandler('worker_processor', this); WorkerMessageHandler.setup(handler); } + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -29655,7 +31168,10 @@ var JpegImage = (function jpegImage() { tableData[z] = data[offset++]; } } else if ((quantizationTableSpec >> 4) === 1) { //16 bit - tableData[j] = readUint16(); + for (j = 0; j < 64; j++) { + var z = dctZigZag[j]; + tableData[z] = readUint16(); + } } else throw "DQT: invalid table spec"; quantizationTables[quantizationTableSpec & 15] = tableData; @@ -29670,7 +31186,8 @@ var JpegImage = (function jpegImage() { frame.precision = data[offset++]; frame.scanLines = readUint16(); frame.samplesPerLine = readUint16(); - frame.components = []; + frame.components = {}; + frame.componentsOrder = []; var componentsCount = data[offset++], componentId; var maxH = 0, maxV = 0; for (i = 0; i < componentsCount; i++) { @@ -29678,6 +31195,7 @@ var JpegImage = (function jpegImage() { var h = data[offset + 1] >> 4; var v = data[offset + 1] & 15; var qId = data[offset + 2]; + frame.componentsOrder.push(componentId); frame.components[componentId] = { h: h, v: v, @@ -29746,14 +31264,13 @@ var JpegImage = (function jpegImage() { this.jfif = jfif; this.adobe = adobe; this.components = []; - for (var id in frame.components) { - if (frame.components.hasOwnProperty(id)) { - this.components.push({ - lines: buildComponentData(frame, frame.components[id]), - scaleX: frame.components[id].h / frame.maxH, - scaleY: frame.components[id].v / frame.maxV - }); - } + for (var i = 0; i < frame.componentsOrder.length; i++) { + var component = frame.components[frame.componentsOrder[i]]; + this.components.push({ + lines: buildComponentData(frame, component), + scaleX: component.h / frame.maxH, + scaleY: component.v / frame.maxV + }); } }, getData: function getData(width, height) { @@ -29926,7 +31443,8 @@ var JpegImage = (function jpegImage() { }; return constructor; -})();/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +})(); +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 'use strict'; @@ -31788,6 +33306,7 @@ var JpxImage = (function JpxImageClosure() { return JpxImage; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ @@ -32220,14 +33739,35 @@ var bidi = PDFJS.bidi = (function bidiClosure() { return bidi; })(); + /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 'use strict'; var Metadata = PDFJS.Metadata = (function MetadataClosure() { + function fixMetadata(meta) { + return meta.replace(/>\\376\\377([^<]+)/g, function(all, codes) { + var bytes = codes.replace(/\\([0-3])([0-7])([0-7])/g, + function(code, d1, d2, d3) { + return String.fromCharCode(d1 * 64 + d2 * 8 + d3 * 1); + }); + var chars = ''; + for (var i = 0; i < bytes.length; i += 2) { + var code = bytes.charCodeAt(i) * 256 + bytes.charCodeAt(i + 1); + chars += code >= 32 && code < 127 && code != 60 && code != 62 && + code != 38 && false ? String.fromCharCode(code) : + '&#x' + (0x10000 + code).toString(16).substring(1) + ';'; + } + return '>' + chars; + }); + } + function Metadata(meta) { if (typeof meta === 'string') { + // Ghostscript produces invalid metadata + meta = fixMetadata(meta); + var parser = new DOMParser(); meta = parser.parseFromString(meta, 'application/xml'); } else if (!(meta instanceof Document)) { diff --git a/apps/files_pdfviewer/js/pdfjs/compatibility.js b/apps/files_pdfviewer/js/pdfjs/compatibility.js new file mode 100644 index 0000000000..528841bb61 --- /dev/null +++ b/apps/files_pdfviewer/js/pdfjs/compatibility.js @@ -0,0 +1,340 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +// Checking if the typed arrays are supported +(function checkTypedArrayCompatibility() { + if (typeof Uint8Array !== 'undefined') { + // some mobile version might not support Float64Array + 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; + if (typeof arg1 === 'number') { + result = []; + for (var i = 0; i < arg1; ++i) + result[i] = 0; + } else + result = arg1.slice(0); + + 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; + + // 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; +})(); + +// Object.create() ? +(function checkObjectCreateCompatibility() { + if (typeof Object.create !== 'undefined') + return; + + Object.create = function objectCreate(proto) { + var constructor = function objectCreateConstructor() {}; + constructor.prototype = proto; + return new constructor(); + }; +})(); + +// Object.defineProperty() ? +(function checkObjectDefinePropertyCompatibility() { + if (typeof Object.defineProperty !== 'undefined') + 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; + } + }; +})(); + +// Object.keys() ? +(function checkObjectKeysCompatibility() { + if (typeof Object.keys !== 'undefined') + return; + + Object.keys = function objectKeys(obj) { + var result = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) + result.push(i); + } + return result; + }; +})(); + +// No XMLHttpRequest.response ? +(function checkXMLHttpRequestResponseCompatibility() { + var xhrPrototype = XMLHttpRequest.prototype; + if ('response' in xhrPrototype || + 'mozResponseArrayBuffer' in xhrPrototype || + 'mozResponse' in xhrPrototype || + 'responseArrayBuffer' in xhrPrototype) + return; + // IE ? + if (typeof VBArray !== 'undefined') { + Object.defineProperty(xhrPrototype, 'response', { + get: function xmlHttpRequestResponseGet() { + return new Uint8Array(new VBArray(this.responseBody).toArray()); + } + }); + Object.defineProperty(xhrPrototype, 'overrideMimeType', { + value: function xmlHttpRequestOverrideMimeType(mimeType) {} + }); + return; + } + + // other browsers + function responseTypeSetter() { + // will be only called to set "arraybuffer" + this.overrideMimeType('text/plain; charset=x-user-defined'); + } + if (typeof xhrPrototype.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; + } + Object.defineProperty(xhrPrototype, 'response', { get: responseGetter }); +})(); + +// window.btoa (base64 encode function) ? +(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; + }; +})(); + +// Function.prototype.bind ? +(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 = Array.prototype.concat.apply(headArgs, arguments); + return fn.apply(obj, args); + }; + return bound; + }; +})(); + +// IE9 text/html data URI +(function checkDocumentDocumentModeCompatibility() { + if (!('documentMode' in document) || document.documentMode !== 9) + return; + // overriding the src property + var originalSrcDescriptor = Object.getOwnPropertyDescriptor( + HTMLIFrameElement.prototype, 'src'); + Object.defineProperty(HTMLIFrameElement.prototype, 'src', { + get: function htmlIFrameElementPrototypeSrcGet() { return this.$src; }, + set: function htmlIFrameElementPrototypeSrcSet(src) { + this.$src = src; + if (src.substr(0, 14) != 'data:text/html') { + originalSrcDescriptor.set.call(this, src); + return; + } + // for text/html, using blank document and then + // document's open, write, and close operations + originalSrcDescriptor.set.call(this, 'about:blank'); + setTimeout((function htmlIFrameElementPrototypeSrcOpenWriteClose() { + var doc = this.contentDocument; + doc.open('text/html'); + doc.write(src.substr(src.indexOf(',') + 1)); + doc.close(); + }).bind(this), 0); + }, + enumerable: true + }); +})(); + +// HTMLElement dataset property +(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 +(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(' '); + } + + var classListPrototype = { + add: function(name) { + changeList(this.element, name, true, 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 compatability +(function checkConsoleCompatibility() { + if (typeof console == 'undefined') { + console = {log: function() {}}; + } +})(); + +// Check onclick compatibility in Opera +(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 navigator.language is supported +(function checkNavigatorLanguage() { + if ('language' in navigator) + return; + Object.defineProperty(navigator, 'language', { + get: function navigatorLanguage() { + var language = navigator.userLanguage || 'en-US'; + return language.substring(0, 2).toLowerCase() + + language.substring(2).toUpperCase(); + }, + enumerable: true + }); +})(); diff --git a/apps/files_pdfviewer/js/pdfjs/update.sh b/apps/files_pdfviewer/js/pdfjs/update.sh index 599f633860..df100e780b 100755 --- a/apps/files_pdfviewer/js/pdfjs/update.sh +++ b/apps/files_pdfviewer/js/pdfjs/update.sh @@ -1,3 +1,6 @@ cd build rm pdf.js wget http://mozilla.github.com/pdf.js/build/pdf.js +wget http://mozilla.github.com/pdf.js/web/compatibility.js +wget http://mozilla.github.com/pdf.js/web/viewer.js + diff --git a/apps/files_pdfviewer/js/pdfjs/viewer.js b/apps/files_pdfviewer/js/pdfjs/viewer.js new file mode 100644 index 0000000000..ff4f2452b6 --- /dev/null +++ b/apps/files_pdfviewer/js/pdfjs/viewer.js @@ -0,0 +1,1936 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +var kDefaultURL = 'compressed.tracemonkey-pldi-09.pdf'; +var kDefaultScale = 'auto'; +var kDefaultScaleDelta = 1.1; +var kUnknownScale = 0; +var kCacheSize = 20; +var kCssUnits = 96.0 / 72.0; +var kScrollbarPadding = 40; +var kMinScale = 0.25; +var kMaxScale = 4.0; +var kImageDirectory = './images/'; +var kSettingsMemory = 20; + +var mozL10n = document.mozL10n || document.webL10n; + +function getFileName(url) { + var anchor = url.indexOf('#'); + var query = url.indexOf('?'); + var end = Math.min( + anchor > 0 ? anchor : url.length, + query > 0 ? query : url.length); + return url.substring(url.lastIndexOf('/', end) + 1, end); +} + +var Cache = function cacheCache(size) { + var data = []; + this.push = function cachePush(view) { + var i = data.indexOf(view); + if (i >= 0) + data.splice(i); + data.push(view); + if (data.length > size) + data.shift().destroy(); + }; +}; + +var ProgressBar = (function ProgressBarClosure() { + + function clamp(v, min, max) { + return Math.min(Math.max(v, min), max); + } + + function ProgressBar(id, opts) { + + // Fetch the sub-elements for later + this.div = document.querySelector(id + ' .progress'); + + // Get options, with sensible defaults + this.height = opts.height || 100; + this.width = opts.width || 100; + this.units = opts.units || '%'; + this.percent = opts.percent || 0; + + // Initialize heights + this.div.style.height = this.height + this.units; + } + + ProgressBar.prototype = { + + updateBar: function ProgressBar_updateBar() { + var progressSize = this.width * this._percent / 100; + + if (this._percent > 95) + this.div.classList.add('full'); + + this.div.style.width = progressSize + this.units; + }, + + get percent() { + return this._percent; + }, + + set percent(val) { + this._percent = clamp(val, 0, 100); + this.updateBar(); + } + }; + + return ProgressBar; +})(); + +var RenderingQueue = (function RenderingQueueClosure() { + function RenderingQueue() { + this.items = []; + } + + RenderingQueue.prototype = { + enqueueDraw: function RenderingQueueEnqueueDraw(item) { + if (!item.drawingRequired()) + return; // as no redraw required, no need for queueing. + + this.items.push(item); + if (this.items.length > 1) + return; // not first item + + item.draw(this.continueExecution.bind(this)); + }, + continueExecution: function RenderingQueueContinueExecution() { + var item = this.items.shift(); + + if (this.items.length == 0) + return; // queue is empty + + item = this.items[0]; + item.draw(this.continueExecution.bind(this)); + } + }; + + return RenderingQueue; +})(); + +var FirefoxCom = (function FirefoxComClosure() { + return { + /** + * Creates an event that the extension is listening for and will + * synchronously respond to. + * NOTE: It is reccomended to use request() instead since one day we may not + * be able to synchronously reply. + * @param {String} action The action to trigger. + * @param {String} data Optional data to send. + * @return {*} The response. + */ + requestSync: function(action, data) { + var request = document.createTextNode(''); + request.setUserData('action', action, null); + request.setUserData('data', data, null); + request.setUserData('sync', true, null); + document.documentElement.appendChild(request); + + var sender = document.createEvent('Events'); + sender.initEvent('pdf.js.message', true, false); + request.dispatchEvent(sender); + var response = request.getUserData('response'); + document.documentElement.removeChild(request); + return response; + }, + /** + * Creates an event that the extension is listening for and will + * asynchronously respond by calling the callback. + * @param {String} action The action to trigger. + * @param {String} data Optional data to send. + * @param {Function} callback Optional response callback that will be called + * with one data argument. + */ + request: function(action, data, callback) { + var request = document.createTextNode(''); + request.setUserData('action', action, null); + request.setUserData('data', data, null); + request.setUserData('sync', false, null); + if (callback) { + request.setUserData('callback', callback, null); + + document.addEventListener('pdf.js.response', function listener(event) { + var node = event.target, + callback = node.getUserData('callback'), + response = node.getUserData('response'); + + document.documentElement.removeChild(node); + + document.removeEventListener('pdf.js.response', listener, false); + return callback(response); + }, false); + } + document.documentElement.appendChild(request); + + var sender = document.createEvent('HTMLEvents'); + sender.initEvent('pdf.js.message', true, false); + return request.dispatchEvent(sender); + } + }; +})(); + +// Settings Manager - This is a utility for saving settings +// First we see if localStorage is available +// If not, we use FUEL in FF +var Settings = (function SettingsClosure() { + var isLocalStorageEnabled = (function localStorageEnabledTest() { + // Feature test as per http://diveintohtml5.info/storage.html + // The additional localStorage call is to get around a FF quirk, see + // bug #495747 in bugzilla + try { + return 'localStorage' in window && window['localStorage'] !== null && + localStorage; + } catch (e) { + return false; + } + })(); + + var isFirefoxExtension = PDFJS.isFirefoxExtension; + + function Settings(fingerprint) { + var database = null; + var index; + if (isFirefoxExtension) + database = FirefoxCom.requestSync('getDatabase', null) || '{}'; + else if (isLocalStorageEnabled) + database = localStorage.getItem('database') || '{}'; + else + return false; + + database = JSON.parse(database); + if (!('files' in database)) + database.files = []; + if (database.files.length >= kSettingsMemory) + database.files.shift(); + for (var i = 0, length = database.files.length; i < length; i++) { + var branch = database.files[i]; + if (branch.fingerprint == fingerprint) { + index = i; + break; + } + } + if (typeof index != 'number') + index = database.files.push({fingerprint: fingerprint}) - 1; + this.file = database.files[index]; + this.database = database; + } + + Settings.prototype = { + set: function settingsSet(name, val) { + if (!('file' in this)) + return false; + + var file = this.file; + file[name] = val; + var database = JSON.stringify(this.database); + if (isFirefoxExtension) + FirefoxCom.requestSync('setDatabase', database); + else if (isLocalStorageEnabled) + localStorage.setItem('database', database); + }, + + get: function settingsGet(name, defaultValue) { + if (!('file' in this)) + return defaultValue; + + return this.file[name] || defaultValue; + } + }; + + return Settings; +})(); + +var cache = new Cache(kCacheSize); +var renderingQueue = new RenderingQueue(); +var currentPageNumber = 1; + +var PDFView = { + pages: [], + thumbnails: [], + currentScale: kUnknownScale, + currentScaleValue: null, + initialBookmark: document.location.hash.substring(1), + startedTextExtraction: false, + pageText: [], + container: null, + initialized: false, + fellback: false, + pdfDocument: null, + // called once when the document is loaded + initialize: function pdfViewInitialize() { + this.container = document.getElementById('viewer'); + this.initialized = true; + }, + + setScale: function pdfViewSetScale(val, resetAutoSettings) { + if (val == this.currentScale) + return; + + var pages = this.pages; + for (var i = 0; i < pages.length; i++) + pages[i].update(val * kCssUnits); + + if (this.currentScale != val) + this.pages[this.page - 1].scrollIntoView(); + this.currentScale = val; + + var event = document.createEvent('UIEvents'); + event.initUIEvent('scalechange', false, false, window, 0); + event.scale = val; + event.resetAutoSettings = resetAutoSettings; + window.dispatchEvent(event); + }, + + parseScale: function pdfViewParseScale(value, resetAutoSettings) { + if ('custom' == value) + return; + + var scale = parseFloat(value); + this.currentScaleValue = value; + if (scale) { + this.setScale(scale, true); + return; + } + + var container = this.container; + var currentPage = this.pages[this.page - 1]; + var pageWidthScale = (container.clientWidth - kScrollbarPadding) / + currentPage.width * currentPage.scale / kCssUnits; + var pageHeightScale = (container.clientHeight - kScrollbarPadding) / + currentPage.height * currentPage.scale / kCssUnits; + switch (value) { + case 'page-actual': + this.setScale(1, resetAutoSettings); + break; + case 'page-width': + this.setScale(pageWidthScale, resetAutoSettings); + break; + case 'page-height': + this.setScale(pageHeightScale, resetAutoSettings); + break; + case 'page-fit': + this.setScale( + Math.min(pageWidthScale, pageHeightScale), resetAutoSettings); + break; + case 'auto': + this.setScale(Math.min(1.0, pageWidthScale), resetAutoSettings); + break; + } + + selectScaleOption(value); + }, + + zoomIn: function pdfViewZoomIn() { + var newScale = (this.currentScale * kDefaultScaleDelta).toFixed(2); + newScale = Math.min(kMaxScale, newScale); + this.parseScale(newScale, true); + }, + + zoomOut: function pdfViewZoomOut() { + var newScale = (this.currentScale / kDefaultScaleDelta).toFixed(2); + newScale = Math.max(kMinScale, newScale); + this.parseScale(newScale, true); + }, + + set page(val) { + var pages = this.pages; + var input = document.getElementById('pageNumber'); + if (!(0 < val && val <= pages.length)) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('pagechange', false, false, window, 0); + event.pageNumber = this.page; + window.dispatchEvent(event); + return; + } + + pages[val - 1].updateStats(); + currentPageNumber = val; + var event = document.createEvent('UIEvents'); + event.initUIEvent('pagechange', false, false, window, 0); + event.pageNumber = val; + window.dispatchEvent(event); + + // checking if the this.page was called from the updateViewarea function: + // avoiding the creation of two "set page" method (internal and public) + if (updateViewarea.inProgress) + return; + + // Avoid scrolling the first page during loading + if (this.loading && val == 1) + return; + + pages[val - 1].scrollIntoView(); + }, + + get page() { + return currentPageNumber; + }, + + open: function pdfViewOpen(url, scale, password) { + var parameters = {password: password}; + if (typeof url === 'string') { // URL + this.url = url; + document.title = decodeURIComponent(getFileName(url)) || url; + parameters.url = url; + } else if (url && 'byteLength' in url) { // ArrayBuffer + parameters.data = url; + } + +// if (!PDFView.loadingBar) { +// PDFView.loadingBar = new ProgressBar('#loadingBar', {}); +// } + + this.container = document.getElementById('viewer'); + + this.pdfDocument = null; + var self = this; + self.loading = true; + PDFJS.getDocument(parameters).then( + function getDocumentCallback(pdfDocument) { + self.load(pdfDocument, scale); + self.loading = false; + }, + function getDocumentError(message, exception) { + if (exception.name === 'PasswordException') { + if (exception.code === 'needpassword') { + var promptString = mozL10n.get('request_password', null, + 'PDF is protected by a password:'); + password = prompt(promptString); + if (password && password.length > 0) { + return PDFView.open(url, scale, password); + } + } + } + + var loadingIndicator = document.getElementById('loading'); + loadingIndicator.textContent = mozL10n.get('loading_error_indicator', + null, 'Error'); + var moreInfo = { + message: message + }; + self.error(mozL10n.get('loading_error', null, + 'An error occurred while loading the PDF.'), moreInfo); + self.loading = false; + }, + function getDocumentProgress(progressData) { + self.progress(progressData.loaded / progressData.total); + } + ); + }, + + download: function pdfViewDownload() { + function noData() { + FirefoxCom.request('download', { originalUrl: url }); + } + + var url = this.url.split('#')[0]; + if (PDFJS.isFirefoxExtension) { + // Document isn't ready just try to download with the url. + if (!this.pdfDocument) { + noData(); + return; + } + this.pdfDocument.getData().then( + function getDataSuccess(data) { + var bb = new MozBlobBuilder(); + bb.append(data.buffer); + var blobUrl = window.URL.createObjectURL( + bb.getBlob('application/pdf')); + + FirefoxCom.request('download', { blobUrl: blobUrl, originalUrl: url }, + function response(err) { + if (err) { + // This error won't really be helpful because it's likely the + // fallback won't work either (or is already open). + PDFView.error('PDF failed to download.'); + } + window.URL.revokeObjectURL(blobUrl); + } + ); + }, + noData // Error ocurred try downloading with just the url. + ); + } else { + url += '#pdfjs.action=download', '_parent'; + window.open(url, '_parent'); + } + }, + + fallback: function pdfViewFallback() { + if (!PDFJS.isFirefoxExtension) + return; + // Only trigger the fallback once so we don't spam the user with messages + // for one PDF. + if (this.fellback) + return; + this.fellback = true; + var url = this.url.split('#')[0]; + FirefoxCom.request('fallback', url, function response(download) { + if (!download) + return; + PDFView.download(); + }); + }, + + navigateTo: function pdfViewNavigateTo(dest) { + if (typeof dest === 'string') + dest = this.destinations[dest]; + if (!(dest instanceof Array)) + return; // invalid destination + // dest array looks like that: + var destRef = dest[0]; + var pageNumber = destRef instanceof Object ? + this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : (destRef + 1); + if (pageNumber > this.pages.length) + pageNumber = this.pages.length; + if (pageNumber) { + this.page = pageNumber; + var currentPage = this.pages[pageNumber - 1]; + currentPage.scrollIntoView(dest); + } + }, + + getDestinationHash: function pdfViewGetDestinationHash(dest) { + if (typeof dest === 'string') + return PDFView.getAnchorUrl('#' + escape(dest)); + if (dest instanceof Array) { + var destRef = dest[0]; // see navigateTo method for dest format + var pageNumber = destRef instanceof Object ? + this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : + (destRef + 1); + if (pageNumber) { + var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); + var destKind = dest[1]; + if (typeof destKind === 'object' && 'name' in destKind && + destKind.name == 'XYZ') { + var scale = (dest[4] || this.currentScale); + pdfOpenParams += '&zoom=' + (scale * 100); + if (dest[2] || dest[3]) { + pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); + } + } + return pdfOpenParams; + } + } + return ''; + }, + + /** + * For the firefox extension we prefix the full url on anchor links so they + * don't come up as resource:// urls and so open in new tab/window works. + * @param {String} anchor The anchor hash include the #. + */ + getAnchorUrl: function getAnchorUrl(anchor) { + if (PDFJS.isFirefoxExtension) + return this.url.split('#')[0] + anchor; + return anchor; + }, + + /** + * Show the error box. + * @param {String} message A message that is human readable. + * @param {Object} moreInfo (optional) Further information about the error + * that is more technical. Should have a 'message' + * and optionally a 'stack' property. + */ + error: function pdfViewError(message, moreInfo) { + var moreInfoText = mozL10n.get('error_build', {build: PDFJS.build}, + 'PDF.JS Build: {{build}}') + '\n'; + if (moreInfo) { + moreInfoText += + mozL10n.get('error_message', {message: moreInfo.message}, + 'Message: {{message}}'); + if (moreInfo.stack) { + moreInfoText += '\n' + + mozL10n.get('error_stack', {stack: moreInfo.stack}, + 'Stack: {{stack}}'); + } else { + if (moreInfo.filename) { + moreInfoText += '\n' + + mozL10n.get('error_file', {file: moreInfo.filename}, + 'File: {{file}}'); + } + if (moreInfo.lineNumber) { + moreInfoText += '\n' + + mozL10n.get('error_line', {line: moreInfo.lineNumber}, + 'Line: {{line}}'); + } + } + } + if (PDFJS.isFirefoxExtension) { + console.error(message + '\n' + moreInfoText); + this.fallback(); + return; + } + var errorWrapper = document.getElementById('errorWrapper'); + errorWrapper.removeAttribute('hidden'); + + var errorMessage = document.getElementById('errorMessage'); + errorMessage.textContent = message; + + var closeButton = document.getElementById('errorClose'); + closeButton.onclick = function() { + errorWrapper.setAttribute('hidden', 'true'); + }; + + var errorMoreInfo = document.getElementById('errorMoreInfo'); + var moreInfoButton = document.getElementById('errorShowMore'); + var lessInfoButton = document.getElementById('errorShowLess'); + moreInfoButton.onclick = function() { + errorMoreInfo.removeAttribute('hidden'); + moreInfoButton.setAttribute('hidden', 'true'); + lessInfoButton.removeAttribute('hidden'); + }; + lessInfoButton.onclick = function() { + errorMoreInfo.setAttribute('hidden', 'true'); + moreInfoButton.removeAttribute('hidden'); + lessInfoButton.setAttribute('hidden', 'true'); + }; + moreInfoButton.removeAttribute('hidden'); + lessInfoButton.setAttribute('hidden', 'true'); + errorMoreInfo.value = moreInfoText; + + errorMoreInfo.rows = moreInfoText.split('\n').length - 1; + }, + + progress: function pdfViewProgress(level) { + var percent = Math.round(level * 100); + var loadingIndicator = document.getElementById('loading'); + loadingIndicator.textContent = mozL10n.get('loading', {percent: percent}, + 'Loading... {{percent}}%'); + +// PDFView.loadingBar.percent = percent; + }, + + load: function pdfViewLoad(pdfDocument, scale) { + function bindOnAfterDraw(pageView, thumbnailView) { + // when page is painted, using the image as thumbnail base + pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { + thumbnailView.setImage(pageView.canvas); + preDraw(); + }; + } + + this.pdfDocument = pdfDocument; + +// var errorWrapper = document.getElementById('errorWrapper'); +// errorWrapper.setAttribute('hidden', 'true'); + + var loadingBox = document.getElementById('loading'); + loadingBox.setAttribute('hidden', 'true'); + +// var loadingBox = document.getElementById('loadingBox'); +// loadingBox.setAttribute('hidden', 'true'); + +// var thumbsView = document.getElementById('thumbnailView'); +// thumbsView.parentNode.scrollTop = 0; + +// while (thumbsView.hasChildNodes()) +// thumbsView.removeChild(thumbsView.lastChild); + +// if ('_loadingInterval' in thumbsView) +// clearInterval(thumbsView._loadingInterval); + + var container = document.getElementById('viewer'); + while (container.hasChildNodes()) + container.removeChild(container.lastChild); + + var pagesCount = pdfDocument.numPages; + var id = pdfDocument.fingerprint; + var storedHash = null; +// document.getElementById('numPages').textContent = +// mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}'); + document.getElementById('numPages').innerHTML = pagesCount; + document.getElementById('pageNumber').max = pagesCount; + PDFView.documentFingerprint = id; + var store = PDFView.store = new Settings(id); + if (store.get('exists', false)) { + var page = store.get('page', '1'); + var zoom = store.get('zoom', PDFView.currentScale); + var left = store.get('scrollLeft', '0'); + var top = store.get('scrollTop', '0'); + + storedHash = 'page=' + page + '&zoom=' + zoom + ',' + left + ',' + top; + } + + var pages = this.pages = []; + this.pageText = []; + this.startedTextExtraction = false; + var pagesRefMap = {}; + var thumbnails = this.thumbnails = []; + var pagePromises = []; + for (var i = 1; i <= pagesCount; i++) + pagePromises.push(pdfDocument.getPage(i)); + var self = this; + var pagesPromise = PDFJS.Promise.all(pagePromises); + pagesPromise.then(function(promisedPages) { + for (var i = 1; i <= pagesCount; i++) { + var page = promisedPages[i - 1]; + var pageView = new PageView(container, page, i, scale, + page.stats, self.navigateTo.bind(self)); +// var thumbnailView = new ThumbnailView(thumbsView, page, i); +// bindOnAfterDraw(pageView, thumbnailView); + + pages.push(pageView); +// thumbnails.push(thumbnailView); + var pageRef = page.ref; + pagesRefMap[pageRef.num + ' ' + pageRef.gen + ' R'] = i; + } + + self.pagesRefMap = pagesRefMap; + }); + + var destinationsPromise = pdfDocument.getDestinations(); + destinationsPromise.then(function(destinations) { + self.destinations = destinations; + }); + + // outline and initial view depends on destinations and pagesRefMap + PDFJS.Promise.all([pagesPromise, destinationsPromise]).then(function() { + pdfDocument.getOutline().then(function(outline) { + self.outline = new DocumentOutlineView(outline); + }); + + self.setInitialView(storedHash, scale); + }); + + pdfDocument.getMetadata().then(function(data) { + var info = data.info, metadata = data.metadata; + self.documentInfo = info; + self.metadata = metadata; + + var pdfTitle; + if (metadata) { + if (metadata.has('dc:title')) + pdfTitle = metadata.get('dc:title'); + } + + if (!pdfTitle && info && info['Title']) + pdfTitle = info['Title']; + + if (pdfTitle) + document.title = pdfTitle + ' - ' + document.title; + }); + }, + + setInitialView: function pdfViewSetInitialView(storedHash, scale) { + // Reset the current scale, as otherwise the page's scale might not get + // updated if the zoom level stayed the same. + this.currentScale = 0; + this.currentScaleValue = null; + if (this.initialBookmark) { + this.setHash(this.initialBookmark); + this.initialBookmark = null; + } + else if (storedHash) + this.setHash(storedHash); + else if (scale) { + this.parseScale(scale, true); + this.page = 1; + } + + if (PDFView.currentScale === kUnknownScale) { + // Scale was not initialized: invalid bookmark or scale was not specified. + // Setting the default one. + this.parseScale(kDefaultScale, true); + } + }, + + search: function pdfViewStartSearch() { + // Limit this function to run every ms. + var SEARCH_TIMEOUT = 250; + var lastSeach = this.lastSearch; + var now = Date.now(); + if (lastSeach && (now - lastSeach) < SEARCH_TIMEOUT) { + if (!this.searchTimer) { + this.searchTimer = setTimeout(function resumeSearch() { + PDFView.search(); + }, + SEARCH_TIMEOUT - (now - lastSeach) + ); + } + return; + } + this.searchTimer = null; + this.lastSearch = now; + + function bindLink(link, pageNumber) { + link.href = '#' + pageNumber; + link.onclick = function searchBindLink() { + PDFView.page = pageNumber; + return false; + }; + } + + var searchResults = document.getElementById('searchResults'); + + var searchTermsInput = document.getElementById('searchTermsInput'); + searchResults.removeAttribute('hidden'); + searchResults.textContent = ''; + + var terms = searchTermsInput.value; + + if (!terms) + return; + + // simple search: removing spaces and hyphens, then scanning every + terms = terms.replace(/\s-/g, '').toLowerCase(); + var index = PDFView.pageText; + var pageFound = false; + for (var i = 0, ii = index.length; i < ii; i++) { + var pageText = index[i].replace(/\s-/g, '').toLowerCase(); + var j = pageText.indexOf(terms); + if (j < 0) + continue; + + var pageNumber = i + 1; + var textSample = index[i].substr(j, 50); + var link = document.createElement('a'); + bindLink(link, pageNumber); + link.textContent = 'Page ' + pageNumber + ': ' + textSample; + searchResults.appendChild(link); + + pageFound = true; + } + if (!pageFound) { + searchResults.textContent = mozL10n.get('search_terms_not_found', null, + '(Not found)'); + } + }, + + setHash: function pdfViewSetHash(hash) { + if (!hash) + return; + + if (hash.indexOf('=') >= 0) { + var params = PDFView.parseQueryString(hash); + // borrowing syntax from "Parameters for Opening PDF Files" + if ('nameddest' in params) { + PDFView.navigateTo(params.nameddest); + return; + } + if ('page' in params) { + var pageNumber = (params.page | 0) || 1; + this.page = pageNumber; + if ('zoom' in params) { + var zoomArgs = params.zoom.split(','); // scale,left,top + // building destination array + + // If the zoom value, it has to get divided by 100. If it is a string, + // it should stay as it is. + var zoomArg = zoomArgs[0]; + var zoomArgNumber = parseFloat(zoomArg); + if (zoomArgNumber) + zoomArg = zoomArgNumber / 100; + + var dest = [null, {name: 'XYZ'}, (zoomArgs[1] | 0), + (zoomArgs[2] | 0), zoomArg]; + var currentPage = this.pages[pageNumber - 1]; + currentPage.scrollIntoView(dest); + } else + this.page = params.page; // simple page + return; + } + } else if (/^\d+$/.test(hash)) // page number + this.page = hash; + else // named destination + PDFView.navigateTo(unescape(hash)); + }, + + switchSidebarView: function pdfViewSwitchSidebarView(view) { + var thumbsView = document.getElementById('thumbnailView'); + var outlineView = document.getElementById('outlineView'); + var searchView = document.getElementById('searchView'); + + var thumbsButton = document.getElementById('viewThumbnail'); + var outlineButton = document.getElementById('viewOutline'); + var searchButton = document.getElementById('viewSearch'); + + switch (view) { + case 'thumbs': + thumbsButton.classList.add('toggled'); + outlineButton.classList.remove('toggled'); + searchButton.classList.remove('toggled'); + thumbsView.classList.remove('hidden'); + outlineView.classList.add('hidden'); + searchView.classList.add('hidden'); + + updateThumbViewArea(); + break; + + case 'outline': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.add('toggled'); + searchButton.classList.remove('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.remove('hidden'); + searchView.classList.add('hidden'); + + if (outlineButton.getAttribute('disabled')) + return; + break; + + case 'search': + thumbsButton.classList.remove('toggled'); + outlineButton.classList.remove('toggled'); + searchButton.classList.add('toggled'); + thumbsView.classList.add('hidden'); + outlineView.classList.add('hidden'); + searchView.classList.remove('hidden'); + + var searchTermsInput = document.getElementById('searchTermsInput'); + searchTermsInput.focus(); + // Start text extraction as soon as the search gets displayed. + this.extractText(); + break; + } + }, + + extractText: function() { + if (this.startedTextExtraction) + return; + this.startedTextExtraction = true; + var self = this; + function extractPageText(pageIndex) { + self.pages[pageIndex].pdfPage.getTextContent().then( + function textContentResolved(textContent) { + self.pageText[pageIndex] = textContent; + self.search(); + if ((pageIndex + 1) < self.pages.length) + extractPageText(pageIndex + 1); + } + ); + }; + extractPageText(0); + }, + + getVisiblePages: function pdfViewGetVisiblePages() { + var pages = this.pages; + var kBottomMargin = 10; + var kTopPadding = 30; + var visiblePages = []; + + var currentHeight = kTopPadding + kBottomMargin; + //var container = this.container; + var container = document.getElementById('viewer'); + + // Add 1px to the scrolltop to give a little wiggle room if the math is off, + // this won't be needed if we calc current page number based off the middle + // of the screen instead of the top. + var containerTop = container.scrollTop + 1; + for (var i = 1; i <= pages.length; ++i) { + var page = pages[i - 1]; + var pageHeight = page.height + kBottomMargin; + if (currentHeight + pageHeight > containerTop) + break; + + currentHeight += pageHeight; + } + + var containerBottom = containerTop + container.clientHeight; + for (; i <= pages.length && currentHeight < containerBottom; ++i) { + var singlePage = pages[i - 1]; + visiblePages.push({ id: singlePage.id, y: currentHeight, + view: singlePage }); + currentHeight += page.height + kBottomMargin; + } + return visiblePages; + }, + + getVisibleThumbs: function pdfViewGetVisibleThumbs() { + var thumbs = this.thumbnails; + var kBottomMargin = 15; + var visibleThumbs = []; + + var view = document.getElementById('thumbnailView'); + var currentHeight = kBottomMargin; + + var top = view.scrollTop; + for (var i = 1; i <= thumbs.length; ++i) { + var thumb = thumbs[i - 1]; + var thumbHeight = thumb.height * thumb.scaleY + kBottomMargin; + if (currentHeight + thumbHeight > top) + break; + + currentHeight += thumbHeight; + } + + var bottom = top + view.clientHeight; + for (; i <= thumbs.length && currentHeight < bottom; ++i) { + var singleThumb = thumbs[i - 1]; + visibleThumbs.push({ id: singleThumb.id, y: currentHeight, + view: singleThumb }); + currentHeight += singleThumb.height * singleThumb.scaleY + kBottomMargin; + } + + return visibleThumbs; + }, + + // Helper function to parse query string (e.g. ?param1=value&parm2=...). + parseQueryString: function pdfViewParseQueryString(query) { + var parts = query.split('&'); + var params = {}; + for (var i = 0, ii = parts.length; i < parts.length; ++i) { + var param = parts[i].split('='); + var key = param[0]; + var value = param.length > 1 ? param[1] : null; + params[unescape(key)] = unescape(value); + } + return params; + } +}; + +var PageView = function pageView(container, pdfPage, id, scale, + stats, navigateTo) { + this.id = id; + this.pdfPage = pdfPage; + + this.scale = scale || 1.0; + this.viewport = this.pdfPage.getViewport(this.scale); + + var anchor = document.createElement('a'); + anchor.name = '' + this.id; + + var div = document.createElement('div'); + div.id = 'pageContainer' + this.id; + div.className = 'page'; + + container.appendChild(anchor); + container.appendChild(div); + + this.destroy = function pageViewDestroy() { + this.update(); + this.pdfPage.destroy(); + }; + + this.update = function pageViewUpdate(scale) { + this.scale = scale || this.scale; + var viewport = this.pdfPage.getViewport(this.scale); + + this.viewport = viewport; + div.style.width = viewport.width + 'px'; + div.style.height = viewport.height + 'px'; + + while (div.hasChildNodes()) + div.removeChild(div.lastChild); + div.removeAttribute('data-loaded'); + + delete this.canvas; + + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }; + + Object.defineProperty(this, 'width', { + get: function PageView_getWidth() { + return this.viewport.width; + }, + enumerable: true + }); + + Object.defineProperty(this, 'height', { + get: function PageView_getHeight() { + return this.viewport.height; + }, + enumerable: true + }); + + function setupAnnotations(pdfPage, viewport) { + function bindLink(link, dest) { + link.href = PDFView.getDestinationHash(dest); + link.onclick = function pageViewSetupLinksOnclick() { + if (dest) + PDFView.navigateTo(dest); + return false; + }; + } + function createElementWithStyle(tagName, item) { + var rect = viewport.convertToViewportRectangle(item.rect); + rect = PDFJS.Util.normalizeRect(rect); + var element = document.createElement(tagName); + 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'; + return element; + } + function createCommentAnnotation(type, item) { + var container = document.createElement('section'); + container.className = 'annotComment'; + + var image = createElementWithStyle('img', item); + var type = item.type; + var rect = viewport.convertToViewportRectangle(item.rect); + rect = PDFJS.Util.normalizeRect(rect); + image.src = kImageDirectory + 'annotation-' + type.toLowerCase() + '.svg'; + image.alt = mozL10n.get('text_annotation_type', {type: type}, + '[{{type}} Annotation]'); + var content = document.createElement('div'); + content.setAttribute('hidden', true); + var title = document.createElement('h1'); + var text = document.createElement('p'); + content.style.left = Math.floor(rect[2]) + 'px'; + content.style.top = Math.floor(rect[1]) + 'px'; + title.textContent = item.title; + + if (!item.content && !item.title) { + content.setAttribute('hidden', true); + } else { + var e = document.createElement('span'); + var lines = item.content.split('\n'); + for (var i = 0, ii = lines.length; i < ii; ++i) { + var line = lines[i]; + e.appendChild(document.createTextNode(line)); + if (i < (ii - 1)) + e.appendChild(document.createElement('br')); + } + text.appendChild(e); + image.addEventListener('mouseover', function annotationImageOver() { + content.removeAttribute('hidden'); + }, false); + + image.addEventListener('mouseout', function annotationImageOut() { + content.setAttribute('hidden', true); + }, false); + } + + content.appendChild(title); + content.appendChild(text); + container.appendChild(image); + container.appendChild(content); + + return container; + } + + pdfPage.getAnnotations().then(function(items) { + for (var i = 0; i < items.length; i++) { + var item = items[i]; + switch (item.type) { + case 'Link': + var link = createElementWithStyle('a', item); + link.href = item.url || ''; + if (!item.url) + bindLink(link, ('dest' in item) ? item.dest : null); + div.appendChild(link); + break; + case 'Text': + var comment = createCommentAnnotation(item.name, item); + if (comment) + div.appendChild(comment); + break; + case 'Widget': + // TODO: support forms + PDFView.fallback(); + break; + } + } + }); + } + + this.getPagePoint = function pageViewGetPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }; + + this.scrollIntoView = function pageViewScrollIntoView(dest) { + if (!dest) { + div.scrollIntoView(true); + return; + } + + var x = 0, y = 0; + var width = 0, height = 0, widthScale, heightScale; + var scale = 0; + switch (dest[1].name) { + case 'XYZ': + x = dest[2]; + y = dest[3]; + scale = dest[4]; + break; + case 'Fit': + case 'FitB': + scale = 'page-fit'; + break; + case 'FitH': + case 'FitBH': + y = dest[2]; + scale = 'page-width'; + break; + case 'FitV': + case 'FitBV': + x = dest[2]; + scale = 'page-height'; + break; + case 'FitR': + x = dest[2]; + y = dest[3]; + width = dest[4] - x; + height = dest[5] - y; + widthScale = (this.container.clientWidth - kScrollbarPadding) / + width / kCssUnits; + heightScale = (this.container.clientHeight - kScrollbarPadding) / + height / kCssUnits; + scale = Math.min(widthScale, heightScale); + break; + default: + return; + } + + if (scale && scale !== PDFView.currentScale) + PDFView.parseScale(scale, true); + else if (PDFView.currentScale === kUnknownScale) + PDFView.parseScale(kDefaultScale, true); + + var boundingRect = [ + this.viewport.convertToViewportPoint(x, y), + this.viewport.convertToViewportPoint(x + width, y + height) + ]; + setTimeout(function pageViewScrollIntoViewRelayout() { + // letting page to re-layout before scrolling + var scale = PDFView.currentScale; + var x = Math.min(boundingRect[0][0], boundingRect[1][0]); + var y = Math.min(boundingRect[0][1], boundingRect[1][1]); + var width = Math.abs(boundingRect[0][0] - boundingRect[1][0]); + var height = Math.abs(boundingRect[0][1] - boundingRect[1][1]); + + // using temporary div to scroll it into view + var tempDiv = document.createElement('div'); + tempDiv.style.position = 'absolute'; + tempDiv.style.left = Math.floor(x) + 'px'; + tempDiv.style.top = Math.floor(y) + 'px'; + tempDiv.style.width = Math.ceil(width) + 'px'; + tempDiv.style.height = Math.ceil(height) + 'px'; + div.appendChild(tempDiv); + tempDiv.scrollIntoView(true); + div.removeChild(tempDiv); + }, 0); + }; + + this.drawingRequired = function() { + return !div.querySelector('canvas'); + }; + + this.draw = function pageviewDraw(callback) { + if (!this.drawingRequired()) { + this.updateStats(); + callback(); + return; + } + + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + canvas.mozOpaque = true; + div.appendChild(canvas); + this.canvas = canvas; + + var textLayerDiv = null; + if (!PDFJS.disableTextLayer) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + div.appendChild(textLayerDiv); + } + var textLayer = textLayerDiv ? new TextLayerBuilder(textLayerDiv) : null; + + var scale = this.scale, viewport = this.viewport; + canvas.width = viewport.width; + canvas.height = viewport.height; + + var ctx = canvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + + // Rendering area + + var self = this; + function pageViewDrawCallback(error) { + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } + + if (error) { + PDFView.error(mozL10n.get('rendering_error', null, + 'An error occurred while rendering the page.'), error); + } + + self.stats = pdfPage.stats; + self.updateStats(); + if (self.onAfterDraw) + self.onAfterDraw(); + + cache.push(self); + callback(); + } + + var renderContext = { + canvasContext: ctx, + viewport: this.viewport, + textLayer: textLayer + }; + this.pdfPage.render(renderContext).then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); + } + ); + + setupAnnotations(this.pdfPage, this.viewport); + div.setAttribute('data-loaded', true); + }; + + this.updateStats = function pageViewUpdateStats() { + if (PDFJS.pdfBug && Stats.enabled) { + var stats = this.stats; + Stats.add(this.id, stats); + } + }; +}; + +var ThumbnailView = function thumbnailView(container, pdfPage, id) { + var anchor = document.createElement('a'); + anchor.href = PDFView.getAnchorUrl('#page=' + id); + anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); + anchor.onclick = function stopNavigation() { + PDFView.page = id; + return false; + }; + + var viewport = pdfPage.getViewport(1); + var pageWidth = this.width = viewport.width; + var pageHeight = this.height = viewport.height; + var pageRatio = pageWidth / pageHeight; + this.id = id; + + var canvasWidth = 98; + var canvasHeight = canvasWidth / this.width * this.height; + var scaleX = this.scaleX = (canvasWidth / pageWidth); + var scaleY = this.scaleY = (canvasHeight / pageHeight); + + var div = document.createElement('div'); + div.id = 'thumbnailContainer' + id; + div.className = 'thumbnail'; + + anchor.appendChild(div); + container.appendChild(anchor); + + this.hasImage = false; + + function getPageDrawContext() { + var canvas = document.createElement('canvas'); + canvas.id = 'thumbnail' + id; + canvas.mozOpaque = true; + + canvas.width = canvasWidth; + canvas.height = canvasHeight; + canvas.className = 'thumbnailImage'; + canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', + {page: id}, 'Thumbnail of Page {{page}}')); + + div.setAttribute('data-loaded', true); + + var ring = document.createElement('div'); + ring.className = 'thumbnailSelectionRing'; + ring.appendChild(canvas); + div.appendChild(ring); + + var ctx = canvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + ctx.restore(); + return ctx; + } + + this.drawingRequired = function thumbnailViewDrawingRequired() { + return !this.hasImage; + }; + + this.draw = function thumbnailViewDraw(callback) { + if (this.hasImage) { + callback(); + return; + } + + var ctx = getPageDrawContext(); + var drawViewport = pdfPage.getViewport(scaleX); + var renderContext = { + canvasContext: ctx, + viewport: drawViewport + }; + pdfPage.render(renderContext).then( + function pdfPageRenderCallback() { + callback(); + }, + function pdfPageRenderError(error) { + callback(); + } + ); + this.hasImage = true; + }; + + this.setImage = function thumbnailViewSetImage(img) { + if (this.hasImage || !img) + return; + + var ctx = getPageDrawContext(); + ctx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, ctx.canvas.width, ctx.canvas.height); + + this.hasImage = true; + }; +}; + +var DocumentOutlineView = function documentOutlineView(outline) { + var outlineView = document.getElementById('outlineView'); +// while (outlineView.firstChild) +// outlineView.removeChild(outlineView.firstChild); + + function bindItemLink(domObj, item) { + domObj.href = PDFView.getDestinationHash(item.dest); + domObj.onclick = function documentOutlineViewOnclick(e) { + PDFView.navigateTo(item.dest); + return false; + }; + } + + if (!outline) { + var noOutline = document.createElement('div'); + noOutline.classList.add('noOutline'); + //noOutline.textContent = mozL10n.get('no_outline', null, + // 'No Outline Available'); + noOutline.textContent = 'No Outline Available'; + //outlineView.appendChild(noOutline); + return; + } + + var queue = [{parent: outlineView, items: outline}]; + while (queue.length > 0) { + var levelData = queue.shift(); + var i, n = levelData.items.length; + for (i = 0; i < n; i++) { + var item = levelData.items[i]; + var div = document.createElement('div'); + div.className = 'outlineItem'; + var a = document.createElement('a'); + bindItemLink(a, item); + a.textContent = item.title; + div.appendChild(a); + + if (item.items.length > 0) { + var itemsDiv = document.createElement('div'); + itemsDiv.className = 'outlineItems'; + div.appendChild(itemsDiv); + queue.push({parent: itemsDiv, items: item.items}); + } + + levelData.parent.appendChild(div); + } + } +}; + +// optimised CSS custom property getter/setter +var CustomStyle = (function 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 + var prefixes = ['ms', 'Moz', 'Webkit', 'O']; + var _cache = { }; + + function CustomStyle() { + } + + CustomStyle.getProp = function get(propName, element) { + // check cache only when no element is given + if (arguments.length == 1 && typeof _cache[propName] == 'string') { + return _cache[propName]; + } + + element = element || document.documentElement; + var style = element.style, prefixed, uPropName; + + // test standard property first + if (typeof style[propName] == 'string') { + return (_cache[propName] = propName); + } + + // capitalize + uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + + // test vendor specific properties + for (var i = 0, l = prefixes.length; i < l; i++) { + prefixed = prefixes[i] + uPropName; + if (typeof style[prefixed] == 'string') { + return (_cache[propName] = prefixed); + } + } + + //if all fails then set to undefined + return (_cache[propName] = 'undefined'); + } + + CustomStyle.setProp = function set(propName, element, str) { + var prop = this.getProp(propName); + if (prop != 'undefined') + element.style[prop] = str; + } + + return CustomStyle; +})(); + +var TextLayerBuilder = function textLayerBuilder(textLayerDiv) { + this.textLayerDiv = textLayerDiv; + + this.beginLayout = function textLayerBuilderBeginLayout() { + this.textDivs = []; + this.textLayerQueue = []; + }; + + this.endLayout = function textLayerBuilderEndLayout() { + var self = this; + var textDivs = this.textDivs; + var textLayerDiv = this.textLayerDiv; + var renderTimer = null; + var renderingDone = false; + var renderInterval = 0; + var resumeInterval = 500; // in ms + + // Render the text layer, one div at a time + function renderTextLayer() { + if (textDivs.length === 0) { + clearInterval(renderTimer); + renderingDone = true; + return; + } + var textDiv = textDivs.shift(); + if (textDiv.dataset.textLength > 0) { + textLayerDiv.appendChild(textDiv); + + if (textDiv.dataset.textLength > 1) { // avoid div by zero + // Adjust div width to match canvas text + // Due to the .offsetWidth calls, this is slow + // This needs to come after appending to the DOM + var textScale = textDiv.dataset.canvasWidth / textDiv.offsetWidth; + CustomStyle.setProp('transform' , textDiv, + 'scale(' + textScale + ', 1)'); + CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%'); + } + } // textLength > 0 + } + renderTimer = setInterval(renderTextLayer, renderInterval); + + // Stop rendering when user scrolls. Resume after XXX milliseconds + // of no scroll events + var scrollTimer = null; + function textLayerOnScroll() { + if (renderingDone) { + window.removeEventListener('scroll', textLayerOnScroll, false); + return; + } + + // Immediately pause rendering + clearInterval(renderTimer); + + clearTimeout(scrollTimer); + scrollTimer = setTimeout(function textLayerScrollTimer() { + // Resume rendering + renderTimer = setInterval(renderTextLayer, renderInterval); + }, resumeInterval); + }; // textLayerOnScroll + + window.addEventListener('scroll', textLayerOnScroll, false); + }; // endLayout + + this.appendText = function textLayerBuilderAppendText(text, + fontName, fontSize) { + var textDiv = document.createElement('div'); + + // vScale and hScale already contain the scaling to pixel units + var fontHeight = fontSize * text.geom.vScale; + textDiv.dataset.canvasWidth = text.canvasWidth * text.geom.hScale; + textDiv.dataset.fontName = fontName; + + textDiv.style.fontSize = fontHeight + 'px'; + textDiv.style.left = text.geom.x + 'px'; + textDiv.style.top = (text.geom.y - fontHeight) + 'px'; + textDiv.textContent = PDFJS.bidi(text, -1); + textDiv.dir = text.direction; + textDiv.dataset.textLength = text.length; + this.textDivs.push(textDiv); + }; +}; + +window.addEventListener('load', function webViewerLoad(evt) { + PDFView.initialize(); + var params = PDFView.parseQueryString(document.location.search.substring(1)); + + var file = PDFJS.isFirefoxExtension ? + window.location.toString() : params.file || kDefaultURL; + +/* if (PDFJS.isFirefoxExtension || !window.File || !window.FileReader || + !window.FileList || !window.Blob) { + document.getElementById('openFile').setAttribute('hidden', 'true'); + } else { + document.getElementById('fileInput').value = null; + } +*/ + // Special debugging flags in the hash section of the URL. + var hash = document.location.hash.substring(1); + var hashParams = PDFView.parseQueryString(hash); + + if ('disableWorker' in hashParams) + PDFJS.disableWorker = (hashParams['disableWorker'] === 'true'); + +/* if (!PDFJS.isFirefoxExtension) { + var locale = navigator.language; + if ('locale' in hashParams) + locale = hashParams['locale']; + mozL10n.language.code = locale; + } +*/ + if ('disableTextLayer' in hashParams) + PDFJS.disableTextLayer = (hashParams['disableTextLayer'] === 'true'); + + if ('pdfBug' in hashParams && + (!PDFJS.isFirefoxExtension || FirefoxCom.requestSync('pdfBugEnabled'))) { + PDFJS.pdfBug = true; + var pdfBug = hashParams['pdfBug']; + var enabled = pdfBug.split(','); + PDFBug.enable(enabled); + PDFBug.init(); + } + +/* if (!PDFJS.isFirefoxExtension || + (PDFJS.isFirefoxExtension && FirefoxCom.requestSync('searchEnabled'))) { + document.querySelector('#viewSearch').classList.remove('hidden'); + } +*/ + // Listen for warnings to trigger the fallback UI. Errors should be caught + // and call PDFView.error() so we don't need to listen for those. + PDFJS.LogManager.addLogger({ + warn: function() { + PDFView.fallback(); + } + }); + +// var thumbsView = document.getElementById('thumbnailView'); +// thumbsView.addEventListener('scroll', updateThumbViewArea, true); +/* + var mainContainer = document.getElementById('mainContainer'); + var outerContainer = document.getElementById('outerContainer'); + mainContainer.addEventListener('transitionend', function(e) { + if (e.target == mainContainer) { + var event = document.createEvent('UIEvents'); + event.initUIEvent('resize', false, false, window, 0); + window.dispatchEvent(event); + outerContainer.classList.remove('sidebarMoving'); + } + }, true); + + document.getElementById('sidebarToggle').addEventListener('click', + function() { + this.classList.toggle('toggled'); + outerContainer.classList.add('sidebarMoving'); + outerContainer.classList.toggle('sidebarOpen'); + updateThumbViewArea(); + }); + + PDFView.open(file, 0); +*/ +}, true); + +/** + * Render the next not yet visible page already such that it is + * hopefully ready once the user scrolls to it. + */ +function preDraw() { + var pages = PDFView.pages; + var visible = PDFView.getVisiblePages(); + var last = visible[visible.length - 1]; + // PageView.id is the actual page number, which is + 1 compared + // to the index in `pages`. That means, pages[last.id] is the next + // PageView instance. + if (pages[last.id] && pages[last.id].drawingRequired()) { + renderingQueue.enqueueDraw(pages[last.id]); + return; + } + // If there is nothing to draw on the next page, maybe the user + // is scrolling up, so, let's try to render the next page *before* + // the first visible page + if (pages[visible[0].id - 2]) { + renderingQueue.enqueueDraw(pages[visible[0].id - 2]); + } +} + +function updateViewarea() { + if (!PDFView.initialized) + return; + var visiblePages = PDFView.getVisiblePages(); + var pageToDraw; + for (var i = 0; i < visiblePages.length; i++) { + var page = visiblePages[i]; + var pageObj = PDFView.pages[page.id - 1]; + + pageToDraw |= pageObj.drawingRequired(); + renderingQueue.enqueueDraw(pageObj); + } + + if (!visiblePages.length) + return; + + // If there is no need to draw a page that is currenlty visible, preDraw the + // next page the user might scroll to. + if (!pageToDraw) { + preDraw(); + } + + updateViewarea.inProgress = true; // used in "set page" + var currentId = PDFView.page; + var firstPage = visiblePages[0]; + PDFView.page = firstPage.id; + updateViewarea.inProgress = false; + + var currentScale = PDFView.currentScale; + var currentScaleValue = PDFView.currentScaleValue; + var normalizedScaleValue = currentScaleValue == currentScale ? + currentScale * 100 : currentScaleValue; + + var pageNumber = firstPage.id; + var pdfOpenParams = '#page=' + pageNumber; + pdfOpenParams += '&zoom=' + normalizedScaleValue; + var currentPage = PDFView.pages[pageNumber - 1]; + var topLeft = currentPage.getPagePoint(PDFView.container.scrollLeft, + (PDFView.container.scrollTop - firstPage.y)); + pdfOpenParams += ',' + Math.round(topLeft[0]) + ',' + Math.round(topLeft[1]); + + var store = PDFView.store; + store.set('exists', true); + store.set('page', pageNumber); + store.set('zoom', normalizedScaleValue); + store.set('scrollLeft', Math.round(topLeft[0])); + store.set('scrollTop', Math.round(topLeft[1])); +// var href = PDFView.getAnchorUrl(pdfOpenParams); +// document.getElementById('viewBookmark').href = href; +} + +window.addEventListener('scroll', function webViewerScroll(evt) { + updateViewarea(); +}, true); + +var thumbnailTimer; + +function updateThumbViewArea() { + // Only render thumbs after pausing scrolling for this amount of time + // (makes UI more responsive) + var delay = 50; // in ms + + if (thumbnailTimer) + clearTimeout(thumbnailTimer); + + thumbnailTimer = setTimeout(function() { + var visibleThumbs = PDFView.getVisibleThumbs(); + for (var i = 0; i < visibleThumbs.length; i++) { + var thumb = visibleThumbs[i]; + renderingQueue.enqueueDraw(PDFView.thumbnails[thumb.id - 1]); + } + }, delay); +} + +window.addEventListener('resize', function webViewerResize(evt) { + if (PDFView.initialized && + (document.getElementById('pageWidthOption').selected || + document.getElementById('pageFitOption').selected || + document.getElementById('pageAutoOption').selected)) + PDFView.parseScale(document.getElementById('scaleSelect').value); + updateViewarea(); +}); + +window.addEventListener('hashchange', function webViewerHashchange(evt) { + PDFView.setHash(document.location.hash.substring(1)); +}); + +window.addEventListener('change', function webViewerChange(evt) { + var files = evt.target.files; + if (!files || files.length == 0) + return; + + // Read the local file into a Uint8Array. + var fileReader = new FileReader(); + fileReader.onload = function webViewerChangeFileReaderOnload(evt) { + var data = evt.target.result; + var buffer = new ArrayBuffer(data.length); + var uint8Array = new Uint8Array(buffer); + + for (var i = 0; i < data.length; i++) + uint8Array[i] = data.charCodeAt(i); + + PDFView.open(uint8Array, 0); + }; + + // Read as a binary string since "readAsArrayBuffer" is not yet + // implemented in Firefox. + var file = files[0]; + fileReader.readAsBinaryString(file); + document.title = file.name; + + // URL does not reflect proper document location - hiding some icons. + document.getElementById('viewBookmark').setAttribute('hidden', 'true'); + document.getElementById('download').setAttribute('hidden', 'true'); +}, true); + +function selectScaleOption(value) { + var options = document.getElementById('scaleSelect').options; + var predefinedValueFound = false; + for (var i = 0; i < options.length; i++) { + var option = options[i]; + if (option.value != value) { + option.selected = false; + continue; + } + option.selected = true; + predefinedValueFound = true; + } + return predefinedValueFound; +} + +window.addEventListener('localized', function localized(evt) { + document.getElementsByTagName('html')[0].dir = mozL10n.language.direction; +}, true); + +window.addEventListener('scalechange', function scalechange(evt) { + var customScaleOption = document.getElementById('customScaleOption'); + customScaleOption.selected = false; + + if (!evt.resetAutoSettings && + (document.getElementById('pageWidthOption').selected || + document.getElementById('pageFitOption').selected || + document.getElementById('pageAutoOption').selected)) { + updateViewarea(); + return; + } + + var predefinedValueFound = selectScaleOption('' + evt.scale); + if (!predefinedValueFound) { + customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%'; + customScaleOption.selected = true; + } + + updateViewarea(); +}, true); + +window.addEventListener('pagechange', function pagechange(evt) { + var page = evt.pageNumber; + if (document.getElementById('pageNumber').value != page) { + document.getElementById('pageNumber').value = page; + var selected = document.querySelector('.thumbnail.selected'); + if (selected) + selected.classList.remove('selected'); +/* + var thumbnail = document.getElementById('thumbnailContainer' + page); + thumbnail.classList.add('selected'); + var visibleThumbs = PDFView.getVisibleThumbs(); + var numVisibleThumbs = visibleThumbs.length; + // If the thumbnail isn't currently visible scroll it into view. + if (numVisibleThumbs > 0) { + var first = visibleThumbs[0].id; + // Account for only one thumbnail being visible. + var last = numVisibleThumbs > 1 ? + visibleThumbs[numVisibleThumbs - 1].id : first; + if (page <= first || page >= last) + thumbnail.scrollIntoView(); + } +*/ + } + document.getElementById('previous').disabled = (page <= 1); + document.getElementById('next').disabled = (page >= PDFView.pages.length); +}, true); + +// Firefox specific event, so that we can prevent browser from zooming +window.addEventListener('DOMMouseScroll', function(evt) { + if (evt.ctrlKey) { + evt.preventDefault(); + + var ticks = evt.detail; + var direction = (ticks > 0) ? 'zoomOut' : 'zoomIn'; + for (var i = 0, length = Math.abs(ticks); i < length; i++) + PDFView[direction](); + } +}, false); + +window.addEventListener('keydown', function keydown(evt) { + var handled = false; + var cmd = (evt.ctrlKey ? 1 : 0) | + (evt.altKey ? 2 : 0) | + (evt.shiftKey ? 4 : 0) | + (evt.metaKey ? 8 : 0); + + // First, handle the key bindings that are independent whether an input + // control is selected or not. + if (cmd == 1 || cmd == 8) { // either CTRL or META key. + switch (evt.keyCode) { + case 61: // FF/Mac '=' + case 107: // FF '+' and '=' + case 187: // Chrome '+' + PDFView.zoomIn(); + handled = true; + break; + case 109: // FF '-' + case 189: // Chrome '-' + PDFView.zoomOut(); + handled = true; + break; + case 48: // '0' + PDFView.parseScale(kDefaultScale, true); + handled = true; + break; + } + } + + if (handled) { + evt.preventDefault(); + return; + } + + // Some shortcuts should not get handled if a control/input element + // is selected. + var curElement = document.activeElement; + if (curElement && curElement.tagName == 'INPUT') + return; + var controlsElement = document.getElementById('controls'); + while (curElement) { + if (curElement === controlsElement) + return; // ignoring if the 'controls' element is focused + curElement = curElement.parentNode; + } + + if (cmd == 0) { // no control key pressed at all. + switch (evt.keyCode) { + case 37: // left arrow + case 75: // 'k' + case 80: // 'p' + PDFView.page--; + handled = true; + break; + case 39: // right arrow + case 74: // 'j' + case 78: // 'n' + PDFView.page++; + handled = true; + break; + } + } + + if (handled) { + evt.preventDefault(); + } +}); diff --git a/apps/files_pdfviewer/js/viewer.js b/apps/files_pdfviewer/js/viewer.js index 85bbf09362..d98bedc5c0 100644 --- a/apps/files_pdfviewer/js/viewer.js +++ b/apps/files_pdfviewer/js/viewer.js @@ -24,21 +24,9 @@ function showPDFviewer(dir,filename){ var oldcontent = $("#content").html(); $("#content").html(oldcontent+'
Loading... 0%
'); showPDFviewer.lastTitle = document.title; - if(!showPDFviewer.loaded){ - OC.addScript( 'files_pdfviewer', 'pdfjs/build/pdf',function(){ - OC.addScript( 'files_pdfviewer', 'pdfview',function(){ - showPDFviewer.loaded=true; - PDFJS.workerSrc = OC.filePath('files_pdfviewer','js','pdfjs/build/pdf.js'); - PDFView.Ptitle = filename; - PDFView.open(url,1.00); - PDFView.active=true; - }); - }); - }else{ - PDFView.Ptitle = filename; - PDFView.open(url,1.00); - PDFView.active=true; - } + PDFView.Ptitle = filename; + PDFView.open(url,1.00); + PDFView.active=true; $("#pageWidthOption").attr("selected","selected"); showPDFviewer.shown = true; } @@ -46,7 +34,6 @@ function showPDFviewer(dir,filename){ showPDFviewer.shown=false; showPDFviewer.oldCode=''; showPDFviewer.lastTitle=''; -showPDFviewer.loaded=false; $(document).ready(function(){ if(!$.browser.msie){//doesnt work on IE