Implement progressive loading of PDFs

This commit is contained in:
Mack Duan 2013-02-06 15:19:29 -08:00
parent added3da8f
commit ef423ef30c
25 changed files with 2110 additions and 586 deletions

View File

@ -4,6 +4,9 @@
<head> <head>
<!-- In production, only one script (pdf.js) is necessary --> <!-- In production, only one script (pdf.js) is necessary -->
<!-- In production, change the content of PDFJS.workerSrc below --> <!-- In production, change the content of PDFJS.workerSrc below -->
<script type="text/javascript" src="../../src/network.js"></script>
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
<script type="text/javascript" src="../../src/core.js"></script> <script type="text/javascript" src="../../src/core.js"></script>
<script type="text/javascript" src="../../src/util.js"></script> <script type="text/javascript" src="../../src/util.js"></script>
<script type="text/javascript" src="../../src/api.js"></script> <script type="text/javascript" src="../../src/api.js"></script>

View File

@ -4,6 +4,9 @@
<head> <head>
<!-- In production, only one script (pdf.js) is necessary --> <!-- In production, only one script (pdf.js) is necessary -->
<!-- In production, change the content of PDFJS.workerSrc below --> <!-- In production, change the content of PDFJS.workerSrc below -->
<script type="text/javascript" src="../../src/network.js"></script>
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
<script type="text/javascript" src="../../src/core.js"></script> <script type="text/javascript" src="../../src/core.js"></script>
<script type="text/javascript" src="../../src/util.js"></script> <script type="text/javascript" src="../../src/util.js"></script>
<script type="text/javascript" src="../../src/api.js"></script> <script type="text/javascript" src="../../src/api.js"></script>

View File

@ -16,7 +16,7 @@
*/ */
/* jshint esnext:true */ /* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
dump */ dump, NetworkManager */
'use strict'; 'use strict';
@ -37,6 +37,7 @@ const MAX_DATABASE_LENGTH = 4096;
Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm'); Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/NetUtil.jsm'); Cu.import('resource://gre/modules/NetUtil.jsm');
Cu.import('resource://pdf.js/network.js');
XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
'resource://gre/modules/PrivateBrowsingUtils.jsm'); 'resource://gre/modules/PrivateBrowsingUtils.jsm');
@ -190,9 +191,8 @@ PdfDataListener.prototype = {
}; };
// All the priviledged actions. // All the priviledged actions.
function ChromeActions(domWindow, dataListener, contentDispositionFilename) { function ChromeActions(domWindow, contentDispositionFilename) {
this.domWindow = domWindow; this.domWindow = domWindow;
this.dataListener = dataListener;
this.contentDispositionFilename = contentDispositionFilename; this.contentDispositionFilename = contentDispositionFilename;
} }
@ -306,37 +306,7 @@ ChromeActions.prototype = {
return getStringPref('general.useragent.locale', 'en-US'); return getStringPref('general.useragent.locale', 'en-US');
}, },
getLoadingType: function() { getLoadingType: function() {
return this.dataListener ? 'passive' : 'active'; return 'passive';
},
initPassiveLoading: function() {
if (!this.dataListener)
return false;
var domWindow = this.domWindow;
this.dataListener.onprogress =
function ChromeActions_dataListenerProgress(loaded, total) {
domWindow.postMessage({
pdfjsLoadAction: 'progress',
loaded: loaded,
total: total
}, '*');
};
var self = this;
this.dataListener.oncomplete =
function ChromeActions_dataListenerComplete(data, errorCode) {
domWindow.postMessage({
pdfjsLoadAction: 'complete',
data: data,
errorCode: errorCode
}, '*');
delete self.dataListener;
};
return true;
}, },
getStrings: function(data) { getStrings: function(data) {
try { try {
@ -436,6 +406,140 @@ ChromeActions.prototype = {
} }
}; };
var RangedChromeActions = (function RangedChromeActionsClosure() {
/**
* This is for range requests
*/
function RangedChromeActions(
domWindow, contentDispositionFilename, originalRequest) {
ChromeActions.call(this, domWindow, contentDispositionFilename);
this.pdfUrl = originalRequest.URI.resolve('');
this.contentLength = originalRequest.contentLength;
// Pass all the headers from the original request through
var httpHeaderVisitor = {
headers: {},
visitHeader: function(aHeader, aValue) {
if (aHeader === 'Range') {
// When loading the PDF from cache, firefox seems to set the Range
// request header to fetch only the unfetched portions of the file
// (e.g. 'Range: bytes=1024-'). However, we want to set this header
// manually to fetch the PDF in chunks.
return;
}
this.headers[aHeader] = aValue;
}
};
originalRequest.visitRequestHeaders(httpHeaderVisitor);
var getXhr = function getXhr() {
const XMLHttpRequest = Components.Constructor(
'@mozilla.org/xmlextras/xmlhttprequest;1');
return new XMLHttpRequest();
};
this.networkManager = new NetworkManager(this.pdfUrl, {
httpHeaders: httpHeaderVisitor.headers,
getXhr: getXhr
});
var self = this;
// If we are in range request mode, this means we manually issued xhr
// requests, which we need to abort when we leave the page
domWindow.addEventListener('unload', function unload(e) {
self.networkManager.abortAllRequests();
domWindow.removeEventListener(e.type, unload);
});
}
RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
var proto = RangedChromeActions.prototype;
proto.constructor = RangedChromeActions;
proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() {
this.domWindow.postMessage({
pdfjsLoadAction: 'supportsRangedLoading',
pdfUrl: this.pdfUrl,
length: this.contentLength
}, '*');
return true;
};
proto.requestDataRange = function RangedChromeActions_requestDataRange(args) {
var begin = args.begin;
var end = args.end;
var domWindow = this.domWindow;
// TODO(mack): Support error handler. We're not currently not handling
// errors from chrome code for non-range requests, so this doesn't
// seem high-pri
this.networkManager.requestRange(begin, end, {
onDone: function RangedChromeActions_onDone(args) {
domWindow.postMessage({
pdfjsLoadAction: 'range',
begin: args.begin,
chunk: args.chunk
}, '*');
}
});
};
return RangedChromeActions;
})();
var StandardChromeActions = (function StandardChromeActionsClosure() {
/**
* This is for a single network stream
*/
function StandardChromeActions(domWindow, contentDispositionFilename,
dataListener) {
ChromeActions.call(this, domWindow, contentDispositionFilename);
this.dataListener = dataListener;
}
StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
var proto = StandardChromeActions.prototype;
proto.constructor = StandardChromeActions;
proto.initPassiveLoading =
function StandardChromeActions_initPassiveLoading() {
if (!this.dataListener) {
return false;
}
var self = this;
this.dataListener.onprogress = function ChromeActions_dataListenerProgress(
loaded, total) {
self.domWindow.postMessage({
pdfjsLoadAction: 'progress',
loaded: loaded,
total: total
}, '*');
};
this.dataListener.oncomplete = function ChromeActions_dataListenerComplete(
data, errorCode) {
self.domWindow.postMessage({
pdfjsLoadAction: 'complete',
data: data,
errorCode: errorCode
}, '*');
delete self.dataListener;
};
return true;
};
return StandardChromeActions;
})();
// Event listener to trigger chrome privedged code. // Event listener to trigger chrome privedged code.
function RequestListener(actions) { function RequestListener(actions) {
this.actions = actions; this.actions = actions;
@ -552,11 +656,17 @@ PdfStreamConverter.prototype = {
/* /*
* This component works as such: * This component works as such:
* 1. asyncConvertData stores the listener * 1. asyncConvertData stores the listener
* 2. onStartRequest creates a new channel, streams the viewer and cancels * 2. onStartRequest creates a new channel, streams the viewer
* the request so pdf.js can do the request * 3. If range requests are supported:
* Since the request is cancelled onDataAvailable should not be called. The * 3.1. Suspends and cancels the request so we can issue range
* onStopRequest does nothing. The convert function just returns the stream, * requests instead.
* it's just the synchronous version of asyncConvertData. *
* If range rquests are not supported:
* 3.1. Read the stream as it's loaded in onDataAvailable to send
* to the viewer
*
* The convert function just returns the stream, it's just the synchronous
* version of asyncConvertData.
*/ */
// nsIStreamConverter::convert // nsIStreamConverter::convert
@ -573,40 +683,57 @@ PdfStreamConverter.prototype = {
// nsIStreamListener::onDataAvailable // nsIStreamListener::onDataAvailable
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
if (!this.dataListener) { if (!this.dataListener) {
// Do nothing since all the data loading is handled by the viewer.
return; return;
} }
var binaryStream = this.binaryStream; var binaryStream = this.binaryStream;
binaryStream.setInputStream(aInputStream); binaryStream.setInputStream(aInputStream);
this.dataListener.append(binaryStream.readByteArray(aCount)); var chunk = binaryStream.readByteArray(aCount);
this.dataListener.append(chunk);
}, },
// nsIRequestObserver::onStartRequest // nsIRequestObserver::onStartRequest
onStartRequest: function(aRequest, aContext) { onStartRequest: function(aRequest, aContext) {
// Setup the request so we can use it below. // Setup the request so we can use it below.
var acceptRanges = false;
try {
aRequest.QueryInterface(Ci.nsIHttpChannel);
if (aRequest.getResponseHeader('Accept-Ranges') === 'bytes') {
var hash = aRequest.URI.ref;
acceptRanges = hash.indexOf('disableRange=true') < 0;
}
} catch (e) {}
aRequest.QueryInterface(Ci.nsIChannel); aRequest.QueryInterface(Ci.nsIChannel);
aRequest.QueryInterface(Ci.nsIWritablePropertyBag); aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
// Creating storage for PDF data
var contentLength = aRequest.contentLength;
var dataListener = new PdfDataListener(contentLength);
var contentDispositionFilename; var contentDispositionFilename;
try { try {
contentDispositionFilename = aRequest.contentDispositionFilename; contentDispositionFilename = aRequest.contentDispositionFilename;
} catch (e) {} } catch (e) {}
this.dataListener = dataListener;
this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
.createInstance(Ci.nsIBinaryInputStream);
// Change the content type so we don't get stuck in a loop. // Change the content type so we don't get stuck in a loop.
aRequest.setProperty('contentType', aRequest.contentType); aRequest.setProperty('contentType', aRequest.contentType);
aRequest.contentType = 'text/html'; aRequest.contentType = 'text/html';
if (!acceptRanges) {
// Creating storage for PDF data
var contentLength = aRequest.contentLength;
this.dataListener = new PdfDataListener(contentLength);
this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
.createInstance(Ci.nsIBinaryInputStream);
} else {
// Suspend the request so we're not consuming any of the stream,
// but we can't cancel the request yet. Otherwise, the original
// listener will think we do not want to go the new PDF url
aRequest.suspend();
}
// Create a new channel that is viewer loaded as a resource. // Create a new channel that is viewer loaded as a resource.
var ioService = Services.io; var ioService = Services.io;
var channel = ioService.newChannel( var channel = ioService.newChannel(
PDF_VIEWER_WEB_PAGE, null, null); PDF_VIEWER_WEB_PAGE, null, null);
var self = this;
var listener = this.listener; var listener = this.listener;
// Proxy all the request observer calls, when it gets to onStopRequest // Proxy all the request observer calls, when it gets to onStopRequest
// we can get the dom window. We also intentionally pass on the original // we can get the dom window. We also intentionally pass on the original
@ -625,8 +752,18 @@ PdfStreamConverter.prototype = {
var domWindow = getDOMWindow(channel); var domWindow = getDOMWindow(channel);
// Double check the url is still the correct one. // Double check the url is still the correct one.
if (domWindow.document.documentURIObject.equals(aRequest.URI)) { if (domWindow.document.documentURIObject.equals(aRequest.URI)) {
var actions = new ChromeActions(domWindow, dataListener, var actions;
contentDispositionFilename); if (acceptRanges) {
// We are going to be issuing range requests, so cancel the
// original request
aRequest.resume();
aRequest.cancel(Cr.NS_BINDING_ABORTED);
actions = new RangedChromeActions(domWindow,
contentDispositionFilename, aRequest);
} else {
actions = new StandardChromeActions(
domWindow, contentDispositionFilename, self.dataListener);
}
var requestListener = new RequestListener(actions); var requestListener = new RequestListener(actions);
domWindow.addEventListener(PDFJS_EVENT_ID, function(event) { domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
requestListener.receive(event); requestListener.receive(event);

32
make.js
View File

@ -27,6 +27,7 @@ var path = require('path');
var ROOT_DIR = __dirname + '/', // absolute path to project's root var ROOT_DIR = __dirname + '/', // absolute path to project's root
BUILD_DIR = 'build/', BUILD_DIR = 'build/',
SRC_DIR = 'src/',
BUILD_TARGET = BUILD_DIR + 'pdf.js', BUILD_TARGET = BUILD_DIR + 'pdf.js',
FIREFOX_BUILD_DIR = BUILD_DIR + '/firefox/', FIREFOX_BUILD_DIR = BUILD_DIR + '/firefox/',
CHROME_BUILD_DIR = BUILD_DIR + '/chrome/', CHROME_BUILD_DIR = BUILD_DIR + '/chrome/',
@ -219,16 +220,21 @@ target.locale = function() {
// make bundle // make bundle
// Bundles all source files into one wrapper 'pdf.js' file, in the given order. // Bundles all source files into one wrapper 'pdf.js' file, in the given order.
// //
target.bundle = function() { target.bundle = function(args) {
args = args || {};
var excludes = args.excludes || [];
target.buildnumber(); target.buildnumber();
cd(ROOT_DIR); cd(ROOT_DIR);
echo(); echo();
echo('### Bundling files into ' + BUILD_TARGET); echo('### Bundling files into ' + BUILD_TARGET);
// File order matters
var SRC_FILES = var SRC_FILES =
['core.js', ['network.js',
'chunked_stream.js',
'pdf_manager.js',
'core.js',
'util.js', 'util.js',
'api.js', 'api.js',
'canvas.js', 'canvas.js',
@ -252,13 +258,21 @@ target.bundle = function() {
'bidi.js', 'bidi.js',
'metadata.js']; 'metadata.js'];
for (var i = 0, length = excludes.length; i < length; ++i) {
var exclude = excludes[i];
var index = SRC_FILES.indexOf(exclude);
if (index >= 0) {
SRC_FILES.splice(index, 1);
}
}
var EXT_SRC_FILES = [ var EXT_SRC_FILES = [
'../external/jpgjs/jpg.js']; '../external/jpgjs/jpg.js'];
if (!test('-d', BUILD_DIR)) if (!test('-d', BUILD_DIR))
mkdir(BUILD_DIR); mkdir(BUILD_DIR);
cd('src'); cd(SRC_DIR);
var bundle = cat(SRC_FILES), var bundle = cat(SRC_FILES),
bundleVersion = EXTENSION_VERSION, bundleVersion = EXTENSION_VERSION,
bundleBuild = exec('git log --format="%h" -n 1', bundleBuild = exec('git log --format="%h" -n 1',
@ -356,7 +370,7 @@ target.firefox = function() {
FIREFOX_AMO_EXTENSION_NAME = 'pdf.js.amo.xpi'; FIREFOX_AMO_EXTENSION_NAME = 'pdf.js.amo.xpi';
target.locale(); target.locale();
target.bundle(); target.bundle({ excludes: ['network.js'] });
cd(ROOT_DIR); cd(ROOT_DIR);
// Clear out everything in the firefox extension build directory // Clear out everything in the firefox extension build directory
@ -382,7 +396,8 @@ target.firefox = function() {
], ],
preprocess: [ preprocess: [
[COMMON_WEB_FILES_PREPROCESS, FIREFOX_BUILD_CONTENT_DIR + '/web'], [COMMON_WEB_FILES_PREPROCESS, FIREFOX_BUILD_CONTENT_DIR + '/web'],
[BUILD_TARGET, FIREFOX_BUILD_CONTENT_DIR + BUILD_TARGET] [BUILD_TARGET, FIREFOX_BUILD_CONTENT_DIR + BUILD_TARGET],
[SRC_DIR + 'network.js', FIREFOX_BUILD_CONTENT_DIR]
] ]
}; };
builder.build(setup); builder.build(setup);
@ -461,7 +476,7 @@ target.mozcentral = function() {
'content', 'content',
'LICENSE']; 'LICENSE'];
target.bundle(); target.bundle({ excludes: ['network.js'] });
cd(ROOT_DIR); cd(ROOT_DIR);
// Clear out everything in the firefox extension build directory // Clear out everything in the firefox extension build directory
@ -489,7 +504,8 @@ target.mozcentral = function() {
], ],
preprocess: [ preprocess: [
[COMMON_WEB_FILES_PREPROCESS, MOZCENTRAL_CONTENT_DIR + '/web'], [COMMON_WEB_FILES_PREPROCESS, MOZCENTRAL_CONTENT_DIR + '/web'],
[BUILD_TARGET, MOZCENTRAL_CONTENT_DIR + BUILD_TARGET] [BUILD_TARGET, MOZCENTRAL_CONTENT_DIR + BUILD_TARGET],
[SRC_DIR + 'network.js', MOZCENTRAL_CONTENT_DIR]
] ]
}; };
builder.build(setup); builder.build(setup);

View File

@ -35,9 +35,13 @@
* - httpHeaders - Basic authentication headers. * - httpHeaders - Basic authentication headers.
* - password - For decrypting password-protected PDFs. * - password - For decrypting password-protected PDFs.
* *
* @param {object} pdfDataRangeTransport is optional. It is used if you want
* to manually serve range requests for data in the PDF. See viewer.js for
* an example of pdfDataRangeTransport's interface.
*
* @return {Promise} A promise that is resolved with {PDFDocumentProxy} object. * @return {Promise} A promise that is resolved with {PDFDocumentProxy} object.
*/ */
PDFJS.getDocument = function getDocument(source) { PDFJS.getDocument = function getDocument(source, pdfDataRangeTransport) {
var workerInitializedPromise, workerReadyPromise, transport; var workerInitializedPromise, workerReadyPromise, transport;
if (typeof source === 'string') { if (typeof source === 'string') {
@ -64,7 +68,8 @@ PDFJS.getDocument = function getDocument(source) {
workerInitializedPromise = new PDFJS.Promise(); workerInitializedPromise = new PDFJS.Promise();
workerReadyPromise = new PDFJS.Promise(); workerReadyPromise = new PDFJS.Promise();
transport = new WorkerTransport(workerInitializedPromise, workerReadyPromise); transport = new WorkerTransport(workerInitializedPromise,
workerReadyPromise, pdfDataRangeTransport);
workerInitializedPromise.then(function transportInitialized() { workerInitializedPromise.then(function transportInitialized() {
transport.fetchDocument(params); transport.fetchDocument(params);
}); });
@ -114,10 +119,7 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
* mapping named destinations to reference numbers. * mapping named destinations to reference numbers.
*/ */
getDestinations: function PDFDocumentProxy_getDestinations() { getDestinations: function PDFDocumentProxy_getDestinations() {
var promise = new PDFJS.Promise(); return this.transport.getDestinations();
var destinations = this.pdfInfo.destinations;
promise.resolve(destinations);
return promise;
}, },
/** /**
* @return {Promise} A promise that is resolved with an array of all the * @return {Promise} A promise that is resolved with an array of all the
@ -180,6 +182,13 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() {
this.transport.getData(promise); this.transport.getData(promise);
return promise; return promise;
}, },
/**
* @return {Promise} A promise that is resolved when the document's data
* is loaded
*/
dataLoaded: function PDFDocumentProxy_dataLoaded() {
return this.transport.dataLoaded();
},
destroy: function PDFDocumentProxy_destroy() { destroy: function PDFDocumentProxy_destroy() {
this.transport.destroy(); this.transport.destroy();
} }
@ -462,7 +471,10 @@ var PDFPageProxy = (function PDFPageProxyClosure() {
* For internal use only. * For internal use only.
*/ */
var WorkerTransport = (function WorkerTransportClosure() { var WorkerTransport = (function WorkerTransportClosure() {
function WorkerTransport(workerInitializedPromise, workerReadyPromise) { function WorkerTransport(workerInitializedPromise, workerReadyPromise,
pdfDataRangeTransport) {
this.pdfDataRangeTransport = pdfDataRangeTransport;
this.workerReadyPromise = workerReadyPromise; this.workerReadyPromise = workerReadyPromise;
this.commonObjs = new PDFObjects(); this.commonObjs = new PDFObjects();
@ -544,6 +556,21 @@ var WorkerTransport = (function WorkerTransportClosure() {
function WorkerTransport_setupMessageHandler(messageHandler) { function WorkerTransport_setupMessageHandler(messageHandler) {
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
var pdfDataRangeTransport = this.pdfDataRangeTransport;
if (pdfDataRangeTransport) {
pdfDataRangeTransport.addListener(function(begin, chunk) {
messageHandler.send('OnDataRange', {
begin: begin,
chunk: chunk
});
});
messageHandler.on('RequestDataRange',
function transportDataRange(data) {
pdfDataRangeTransport.requestDataRange(data.begin, data.end);
}, this);
}
messageHandler.on('GetDoc', function transportDoc(data) { messageHandler.on('GetDoc', function transportDoc(data) {
var pdfInfo = data.pdfInfo; var pdfInfo = data.pdfInfo;
var pdfDocument = new PDFDocumentProxy(pdfInfo, this); var pdfDocument = new PDFDocumentProxy(pdfInfo, this);
@ -647,6 +674,10 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, this); }, this);
messageHandler.on('DocProgress', function transportDocProgress(data) { messageHandler.on('DocProgress', function transportDocProgress(data) {
// TODO(mack): The progress event should be resolved on a different
// promise that tracks progress of whole file, since workerReadyPromise
// is for file being ready to render, not for when file is fully
// downloaded
this.workerReadyPromise.progress({ this.workerReadyPromise.progress({
loaded: data.loaded, loaded: data.loaded,
total: data.total total: data.total
@ -702,7 +733,11 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, },
fetchDocument: function WorkerTransport_fetchDocument(source) { fetchDocument: function WorkerTransport_fetchDocument(source) {
this.messageHandler.send('GetDocRequest', {source: source}); source.chunkedViewerLoading = !!this.pdfDataRangeTransport;
this.messageHandler.send('GetDocRequest', {
source: source,
disableRange: PDFJS.disableRange
});
}, },
getData: function WorkerTransport_getData(promise) { getData: function WorkerTransport_getData(promise) {
@ -711,6 +746,14 @@ var WorkerTransport = (function WorkerTransportClosure() {
}); });
}, },
dataLoaded: function WorkerTransport_dataLoaded() {
var promise = new PDFJS.Promise();
this.messageHandler.send('DataLoaded', null, function(args) {
promise.resolve(args);
});
return promise;
},
getPage: function WorkerTransport_getPage(pageNumber, promise) { getPage: function WorkerTransport_getPage(pageNumber, promise) {
var pageIndex = pageNumber - 1; var pageIndex = pageNumber - 1;
if (pageIndex in this.pagePromises) if (pageIndex in this.pagePromises)
@ -724,6 +767,16 @@ var WorkerTransport = (function WorkerTransportClosure() {
getAnnotations: function WorkerTransport_getAnnotations(pageIndex) { getAnnotations: function WorkerTransport_getAnnotations(pageIndex) {
this.messageHandler.send('GetAnnotationsRequest', this.messageHandler.send('GetAnnotationsRequest',
{ pageIndex: pageIndex }); { pageIndex: pageIndex });
},
getDestinations: function WorkerTransport_getDestinations() {
var promise = new PDFJS.Promise();
this.messageHandler.send('GetDestinations', null,
function transportDestinations(destinations) {
promise.resolve(destinations);
}
);
return promise;
} }
}; };
return WorkerTransport; return WorkerTransport;

440
src/chunked_stream.js Normal file
View File

@ -0,0 +1,440 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals assert, MissingDataException, isInt, NetworkManager, PDFJS,
isEmptyObj */
'use strict';
var ChunkedStream = (function ChunkedStreamClosure() {
function ChunkedStream(length, chunkSize) {
this.bytes = new Uint8Array(length);
this.start = 0;
this.pos = 0;
this.end = length;
this.chunkSize = chunkSize;
this.loadedChunks = [];
this.numChunksLoaded = 0;
this.numChunks = Math.ceil(length / chunkSize);
}
// required methods for a stream. if a particular stream does not
// implement these, an error should be thrown
ChunkedStream.prototype = {
getMissingChunks: function ChunkedStream_getMissingChunks() {
var chunks = [];
for (var chunk = 0, n = this.numChunks; chunk < n; ++chunk) {
if (!(chunk in this.loadedChunks)) {
chunks.push(chunk);
}
}
return chunks;
},
allChunksLoaded: function ChunkedStream_allChunksLoaded() {
return this.numChunksLoaded === this.numChunks;
},
onReceiveData: function(begin, chunk) {
var end = begin + chunk.byteLength;
assert(begin % this.chunkSize === 0, 'Bad begin offset: ' + begin);
// Using this.length is inaccurate here since this.start can be moved
// See ChunkedStream.moveStart()
var length = this.bytes.length;
assert(end % this.chunkSize === 0 || end === length,
'Bad end offset: ' + end);
this.bytes.set(new Uint8Array(chunk), begin);
var chunkSize = this.chunkSize;
var beginChunk = Math.floor(begin / chunkSize);
var endChunk = Math.floor((end - 1) / chunkSize) + 1;
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (!(chunk in this.loadedChunks)) {
this.loadedChunks[chunk] = true;
++this.numChunksLoaded;
}
}
},
ensureRange: function ChunkedStream_ensureRange(begin, end) {
if (begin >= end) {
return;
}
var chunkSize = this.chunkSize;
var beginChunk = Math.floor(begin / chunkSize);
var endChunk = Math.floor((end - 1) / chunkSize) + 1;
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (!(chunk in this.loadedChunks)) {
throw new MissingDataException(begin, end);
}
}
},
nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) {
for (var chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) {
if (!(chunk in this.loadedChunks)) {
return chunk;
}
}
// Wrap around to beginning
for (var chunk = 0; chunk < beginChunk; ++chunk) {
if (!(chunk in this.loadedChunks)) {
return chunk;
}
}
return null;
},
hasChunk: function ChunkedStream_hasChunk(chunk) {
return chunk in this.loadedChunks;
},
get length() {
return this.end - this.start;
},
getByte: function ChunkedStream_getByte() {
var pos = this.pos;
if (pos >= this.end) {
return null;
}
this.ensureRange(pos, pos + 1);
return this.bytes[this.pos++];
},
// returns subarray of original buffer
// should only be read
getBytes: function ChunkedStream_getBytes(length) {
var bytes = this.bytes;
var pos = this.pos;
var strEnd = this.end;
if (!length) {
this.ensureRange(pos, strEnd);
return bytes.subarray(pos, strEnd);
}
var end = pos + length;
if (end > strEnd)
end = strEnd;
this.ensureRange(pos, end);
this.pos = end;
return bytes.subarray(pos, end);
},
getByteRange: function ChunkedStream_getBytes(begin, end) {
this.ensureRange(begin, end);
return this.bytes.subarray(begin, end);
},
lookChar: function ChunkedStream_lookChar() {
var pos = this.pos;
if (pos >= this.end)
return null;
this.ensureRange(pos, pos + 1);
return String.fromCharCode(this.bytes[pos]);
},
getChar: function ChunkedStream_getChar() {
var pos = this.pos;
if (pos >= this.end)
return null;
this.ensureRange(pos, pos + 1);
return String.fromCharCode(this.bytes[this.pos++]);
},
skip: function ChunkedStream_skip(n) {
if (!n)
n = 1;
this.pos += n;
},
reset: function ChunkedStream_reset() {
this.pos = this.start;
},
moveStart: function ChunkedStream_moveStart() {
this.start = this.pos;
},
makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) {
function ChunkedStreamSubstream() {}
ChunkedStreamSubstream.prototype = Object.create(this);
var subStream = new ChunkedStreamSubstream();
subStream.pos = subStream.start = start;
subStream.end = start + length || this.end;
subStream.dict = dict;
return subStream;
},
isStream: true
};
return ChunkedStream;
})();
var ChunkedStreamManager = (function ChunkedStreamManagerClosure() {
function ChunkedStreamManager(length, chunkSize, url, args) {
this.stream = new ChunkedStream(length, chunkSize);
this.length = length;
this.chunkSize = chunkSize;
this.url = url;
var msgHandler = this.msgHandler = args.msgHandler;
if (args.chunkedViewerLoading) {
msgHandler.on('OnDataRange', this.onReceiveData.bind(this));
this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
msgHandler.send('RequestDataRange', { begin: begin, end: end });
};
} else {
var getXhr = function getXhr() {
//#if B2G
// return new XMLHttpRequest({ mozSystem: true });
//#else
return new XMLHttpRequest();
//#endif
};
this.networkManager = new NetworkManager(this.url, {
getXhr: getXhr,
httpHeaders: args.httpHeaders
});
var self = this;
this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
this.networkManager.requestRange(begin, end, {
onDone: this.onReceiveData.bind(this),
});
};
}
this.currRequestId = 0;
this.chunksNeededByRequest = {};
this.requestsByChunk = {};
this.callbacksByRequest = {};
this.loadedStream = new PDFJS.Promise();
}
ChunkedStreamManager.prototype = {
onLoadedStream: function ChunkedStreamManager_getLoadedStream() {
return this.loadedStream;
},
// Get all the chunks that are not yet loaded and groups them into
// contiguous ranges to load in as few requests as possible
requestAllChunks: function ChunkedStreamManager_requestAllChunks() {
var missingChunks = this.stream.getMissingChunks();
var chunksToRequest = [];
for (var i = 0, n = missingChunks.length; i < n; ++i) {
var chunk = missingChunks[i];
if (!(chunk in this.requestsByChunk)) {
this.requestsByChunk[chunk] = [];
chunksToRequest.push(chunk);
}
}
var groupedChunks = this.groupChunks(chunksToRequest);
for (var i = 0, n = groupedChunks.length; i < n; ++i) {
var groupedChunk = groupedChunks[i];
var begin = groupedChunk.beginChunk * this.chunkSize;
var end = groupedChunk.endChunk * this.chunkSize;
this.sendRequest(begin, end);
}
return this.loadedStream;
},
getStream: function ChunkedStreamManager_getStream() {
return this.stream;
},
// Loads any chunks in the requested range that are not yet loaded
requestRange: function ChunkedStreamManager_requestRange(
begin, end, callback) {
end = Math.min(end, this.length);
var beginChunk = this.getBeginChunk(begin);
var endChunk = this.getEndChunk(end);
var requestId = this.currRequestId++;
var chunksNeeded;
this.chunksNeededByRequest[requestId] = chunksNeeded = {};
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
if (!this.stream.hasChunk(chunk)) {
chunksNeeded[chunk] = true;
}
}
if (isEmptyObj(chunksNeeded)) {
callback();
return;
}
this.callbacksByRequest[requestId] = callback;
var chunksToRequest = [];
for (var chunk in chunksNeeded) {
chunk = chunk | 0;
if (!(chunk in this.requestsByChunk)) {
this.requestsByChunk[chunk] = [];
chunksToRequest.push(chunk);
}
this.requestsByChunk[chunk].push(requestId);
}
if (!chunksToRequest.length) {
return;
}
var groupedChunksToRequest = this.groupChunks(chunksToRequest);
for (var i = 0; i < groupedChunksToRequest.length; ++i) {
var groupedChunk = groupedChunksToRequest[i];
var begin = groupedChunk.beginChunk * this.chunkSize;
var end = groupedChunk.endChunk * this.chunkSize;
this.sendRequest(begin, end);
}
},
// Groups a sorted array of chunks into as few continguous larger
// chunks as possible
groupChunks: function ChunkedStreamManager_groupChunks(chunks) {
var groupedChunks = [];
var beginChunk;
var prevChunk;
for (var i = 0; i < chunks.length; ++i) {
var chunk = chunks[i];
if (!beginChunk) {
beginChunk = chunk;
}
if (prevChunk && prevChunk + 1 !== chunk) {
groupedChunks.push({
beginChunk: beginChunk, endChunk: prevChunk + 1});
beginChunk = chunk;
}
if (i + 1 === chunks.length) {
groupedChunks.push({
beginChunk: beginChunk, endChunk: chunk + 1});
}
prevChunk = chunk;
}
return groupedChunks;
},
onReceiveData: function ChunkedStreamManager_onReceiveData(args) {
var chunk = args.chunk;
var begin = args.begin;
var end = begin + chunk.byteLength;
var beginChunk = this.getBeginChunk(begin);
var endChunk = this.getEndChunk(end);
this.stream.onReceiveData(begin, chunk);
if (this.stream.allChunksLoaded()) {
this.loadedStream.resolve(this.stream);
}
var loadedRequests = [];
for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
var requestIds = this.requestsByChunk[chunk];
delete this.requestsByChunk[chunk];
for (var i = 0; i < requestIds.length; ++i) {
var requestId = requestIds[i];
var chunksNeeded = this.chunksNeededByRequest[requestId];
if (chunk in chunksNeeded) {
delete chunksNeeded[chunk];
}
if (!isEmptyObj(chunksNeeded)) {
continue;
}
loadedRequests.push(requestId);
}
}
// If there are no pending requests, automatically fetch the next
// unfetched chunk of the PDF
if (isEmptyObj(this.requestsByChunk)) {
var nextEmptyChunk;
if (this.stream.numChunksLoaded === 1) {
// This is a special optimization so that after fetching the first
// chunk, rather than fetching the second chunk, we fetch the last
// chunk.
var lastChunk = this.stream.numChunks - 1;
if (!this.stream.hasChunk(lastChunk)) {
nextEmptyChunk = lastChunk;
}
} else {
nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
}
if (isInt(nextEmptyChunk)) {
var nextEmptyByte = nextEmptyChunk * this.chunkSize;
this.requestRange(nextEmptyByte, nextEmptyByte + this.chunkSize,
function() {});
}
}
for (var i = 0; i < loadedRequests.length; ++i) {
var requestId = loadedRequests[i];
var callback = this.callbacksByRequest[requestId];
delete this.callbacksByRequest[requestId];
callback();
}
this.msgHandler.send('DocProgress', {
loaded: this.stream.numChunksLoaded * this.chunkSize,
total: this.length
});
},
getBeginChunk: function ChunkedStreamManager_getBeginChunk(begin) {
var chunk = Math.floor(begin / this.chunkSize);
return chunk;
},
getEndChunk: function ChunkedStreamManager_getEndChunk(end) {
if (end % this.chunkSize === 0) {
return end / this.chunkSize;
}
// 0 -> 0
// 1 -> 1
// 99 -> 1
// 100 -> 1
// 101 -> 2
var chunk = Math.floor((end - 1) / this.chunkSize) + 1;
return chunk;
}
};
return ChunkedStreamManager;
})();

View File

@ -17,7 +17,8 @@
/* globals assertWellFormed, calculateMD5, Catalog, error, info, isArray, /* globals assertWellFormed, calculateMD5, Catalog, error, info, isArray,
isArrayBuffer, isDict, isName, isStream, isString, Lexer, isArrayBuffer, isDict, isName, isStream, isString, Lexer,
Linearization, NullStream, PartialEvaluator, shadow, Stream, Linearization, NullStream, PartialEvaluator, shadow, Stream,
StreamsSequenceStream, stringToPDFString, TODO, Util, warn, XRef */ StreamsSequenceStream, stringToPDFString, TODO, Util, warn, XRef,
MissingDataException */
'use strict'; 'use strict';
@ -35,69 +36,6 @@ if (!globalScope.PDFJS) {
globalScope.PDFJS = {}; globalScope.PDFJS = {};
} }
// getPdf()
// Convenience function to perform binary Ajax GET
// Usage: getPdf('http://...', callback)
// getPdf({
// url:String ,
// [,progress:Function, error:Function]
// },
// callback)
function getPdf(arg, callback) {
var params = arg;
if (typeof arg === 'string')
params = { url: arg };
//#if !B2G
var xhr = new XMLHttpRequest();
//#else
//var xhr = new XMLHttpRequest({mozSystem: true});
//#endif
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.substring(0, params.url.indexOf(':') + 1);
xhr.expected = (protocol === 'http:' || protocol === 'https:') ? 200 : 0;
if ('progress' in params)
xhr.onprogress = params.progress || undefined;
var calledErrorBack = false;
if ('error' in params) {
xhr.onerror = function errorBack() {
if (!calledErrorBack) {
calledErrorBack = true;
params.error();
}
};
}
xhr.onreadystatechange = function getPdfOnreadystatechange(e) {
if (xhr.readyState === 4) {
if (xhr.status === xhr.expected) {
var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse ||
xhr.responseArrayBuffer || xhr.response);
callback(data);
} else if (params.error && !calledErrorBack) {
calledErrorBack = true;
params.error(e);
}
}
};
xhr.send(null);
}
globalScope.PDFJS.getPdf = getPdf;
globalScope.PDFJS.pdfBug = false; globalScope.PDFJS.pdfBug = false;
@ -496,8 +434,8 @@ var PDFDocument = (function PDFDocumentClosure() {
function init(stream, password) { function init(stream, password) {
assertWellFormed(stream.length > 0, 'stream must have data'); assertWellFormed(stream.length > 0, 'stream must have data');
this.stream = stream; this.stream = stream;
this.setup(password); var xref = new XRef(this.stream, password);
this.acroForm = this.catalog.catDict.get('AcroForm'); this.xref = xref;
} }
function find(stream, needle, limit, backwards) { function find(stream, needle, limit, backwards) {
@ -535,15 +473,25 @@ var PDFDocument = (function PDFDocumentClosure() {
}; };
PDFDocument.prototype = { PDFDocument.prototype = {
parse: function PDFDocument_parse(recoveryMode) {
this.setup(recoveryMode);
this.acroForm = this.catalog.catDict.get('AcroForm');
},
get linearization() { get linearization() {
var length = this.stream.length; var length = this.stream.length;
var linearization = false; var linearization = false;
if (length) { if (length) {
try { try {
linearization = new Linearization(this.stream); linearization = new Linearization(this.stream);
if (linearization.length != length) if (linearization.length != length) {
linearization = false; linearization = false;
}
} catch (err) { } catch (err) {
if (err instanceof MissingDataException) {
throw err;
}
warn('The linearization data is not available ' + warn('The linearization data is not available ' +
'or unreadable pdf data is found'); 'or unreadable pdf data is found');
linearization = false; linearization = false;
@ -622,14 +570,13 @@ var PDFDocument = (function PDFDocumentClosure() {
} }
// May not be a PDF file, continue anyway. // May not be a PDF file, continue anyway.
}, },
setup: function PDFDocument_setup(password) { parseStartXRef: function PDFDocument_parseStartXRef() {
this.checkHeader(); var startXRef = this.startXRef;
var xref = new XRef(this.stream, this.xref.setStartXRef(startXRef);
this.startXRef, },
this.mainXRefEntriesOffset, setup: function PDFDocument_setup(recoveryMode) {
password); this.xref.parse(recoveryMode);
this.xref = xref; this.catalog = new Catalog(this.xref);
this.catalog = new Catalog(xref);
}, },
get numPages() { get numPages() {
var linearization = this.linearization; var linearization = this.linearization;
@ -637,7 +584,7 @@ var PDFDocument = (function PDFDocumentClosure() {
// shadow the prototype getter // shadow the prototype getter
return shadow(this, 'numPages', num); return shadow(this, 'numPages', num);
}, },
getDocumentInfo: function PDFDocument_getDocumentInfo() { get documentInfo() {
var docInfo = { var docInfo = {
PDFFormatVersion: this.pdfFormatVersion, PDFFormatVersion: this.pdfFormatVersion,
IsAcroFormPresent: !!this.acroForm IsAcroFormPresent: !!this.acroForm
@ -660,9 +607,9 @@ var PDFDocument = (function PDFDocumentClosure() {
} }
} }
} }
return shadow(this, 'getDocumentInfo', docInfo); return shadow(this, 'documentInfo', docInfo);
}, },
getFingerprint: function PDFDocument_getFingerprint() { get fingerprint() {
var xref = this.xref, fileID; var xref = this.xref, fileID;
if (xref.trailer.has('ID')) { if (xref.trailer.has('ID')) {
fileID = ''; fileID = '';
@ -681,10 +628,15 @@ var PDFDocument = (function PDFDocumentClosure() {
} }
} }
return shadow(this, 'getFingerprint', fileID); return shadow(this, 'fingerprint', fileID);
}, },
getPage: function PDFDocument_getPage(n) {
return this.catalog.getPage(n); traversePages: function PDFDocument_traversePages() {
this.catalog.traversePages();
},
getPage: function PDFDocument_getPage(pageIndex) {
return this.catalog.getPage(pageIndex);
} }
}; };

View File

@ -19,7 +19,7 @@
IDENTITY_MATRIX, info, isArray, isCmd, isDict, isEOF, isName, isNum, IDENTITY_MATRIX, info, isArray, isCmd, isDict, isEOF, isName, isNum,
isStream, isString, JpegStream, Lexer, Metrics, Name, Parser, isStream, isString, JpegStream, Lexer, Metrics, Name, Parser,
Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts, Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts,
TilingPattern, TODO, warn, Util */ TilingPattern, TODO, warn, Util, MissingDataException */
'use strict'; 'use strict';
@ -155,10 +155,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
assert(fontRes, 'fontRes not available'); assert(fontRes, 'fontRes not available');
++this.fontIdCounter;
font = xref.fetchIfRef(font) || fontRes.get(fontName); font = xref.fetchIfRef(font) || fontRes.get(fontName);
if (!isDict(font)) { if (!isDict(font)) {
++this.fontIdCounter;
return { return {
translated: new ErrorFont('Font ' + fontName + ' is not available'), translated: new ErrorFont('Font ' + fontName + ' is not available'),
loadedName: 'g_font_' + this.uniquePrefix + this.fontIdCounter loadedName: 'g_font_' + this.uniquePrefix + this.fontIdCounter
@ -169,7 +168,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
if (!loadedName) { if (!loadedName) {
// keep track of each font we translated so the caller can // keep track of each font we translated so the caller can
// load them asynchronously before calling display on a page // load them asynchronously before calling display on a page
loadedName = 'g_font_' + this.uniquePrefix + this.fontIdCounter; loadedName = 'g_font_' + this.uniquePrefix + (this.fontIdCounter + 1);
font.loadedName = loadedName; font.loadedName = loadedName;
var translated; var translated;
@ -177,6 +176,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
translated = this.translateFont(font, xref, resources, translated = this.translateFont(font, xref, resources,
dependency); dependency);
} catch (e) { } catch (e) {
if (e instanceof MissingDataException) {
font.loadedName = undefined;
throw e;
}
translated = new ErrorFont(e instanceof Error ? e.message : e); translated = new ErrorFont(e instanceof Error ? e.message : e);
} }
font.translated = translated; font.translated = translated;
@ -196,6 +199,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
data.charProcOperatorList = charProcOperatorList; data.charProcOperatorList = charProcOperatorList;
} }
} }
++this.fontIdCounter;
return font; return font;
}, },

224
src/network.js Normal file
View File

@ -0,0 +1,224 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// NOTE: Be careful what goes in this file, as it is also used from the context
// of the addon. So using warn/error in here will break the addon.
'use strict';
//#if (FIREFOX || MOZCENTRAL)
//
//Components.utils.import('resource://gre/modules/Services.jsm');
//
//var EXPORTED_SYMBOLS = ['NetworkManager'];
//
//function log(aMsg) {
// var msg = 'network.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
// Services.console.logStringMessage(msg);
// // TODO(mack): dump() doesn't seem to work here...
// dump(msg + '\n');
//}
//#else
function log(aMsg) {
console.log(aMsg);
}
//#endif
var NetworkManager = (function NetworkManagerClosure() {
function NetworkManager(url, args) {
this.url = url;
args = args || {};
this.httpHeaders = args.httpHeaders || {};
this.getXhr = args.getXhr ||
function NetworkManager_getXhr() {
return new XMLHttpRequest();
};
this.currXhrId = 0;
this.pendingRequests = {};
this.loadedRequests = {};
}
function getArrayBuffer(xhr) {
var data = (xhr.mozResponseArrayBuffer || xhr.mozResponse ||
xhr.responseArrayBuffer || xhr.response);
if (typeof data !== 'string') {
return data;
}
var length = data.length;
var buffer = new Uint8Array(length);
for (var i = 0; i < length; i++) {
buffer[i] = data.charCodeAt(i) & 0xFF;
}
return buffer;
}
NetworkManager.prototype = {
requestRange: function NetworkManager_requestRange(begin, end, listeners) {
var args = {
begin: begin,
end: end
};
for (var prop in listeners) {
args[prop] = listeners[prop];
}
return this.request(args);
},
requestFull: function NetworkManager_requestRange(listeners) {
return this.request(listeners);
},
request: function NetworkManager_requestRange(args) {
var xhr = this.getXhr();
var xhrId = this.currXhrId++;
var pendingRequest = this.pendingRequests[xhrId] = {
xhr: xhr
};
xhr.open('GET', this.url);
for (var property in this.httpHeaders) {
var value = this.httpHeaders[property];
if (typeof value === 'undefined') {
continue;
}
xhr.setRequestHeader(property, value);
}
if ('begin' in args && 'end' in args) {
var rangeStr = args.begin + '-' + (args.end - 1);
xhr.setRequestHeader('Range', 'bytes=' + rangeStr);
pendingRequest.expectedStatus = 206;
} else {
pendingRequest.expectedStatus = 200;
}
xhr.mozResponseType = xhr.responseType = 'arraybuffer';
if (args.onProgress) {
xhr.onprogress = args.onProgress;
}
if (args.onError) {
xhr.onerror = function(evt) {
args.onError(xhr.status);
};
}
xhr.onreadystatechange = this.onStateChange.bind(this, xhrId);
pendingRequest.onHeadersReceived = args.onHeadersReceived;
pendingRequest.onDone = args.onDone;
pendingRequest.onError = args.onError;
xhr.send(null);
return xhrId;
},
onStateChange: function NetworkManager_onStateChange(xhrId, evt) {
var pendingRequest = this.pendingRequests[xhrId];
if (!pendingRequest) {
// Maybe abortRequest was called...
return;
}
var xhr = pendingRequest.xhr;
if (xhr.readyState >= 2 && pendingRequest.onHeadersReceived) {
pendingRequest.onHeadersReceived();
delete pendingRequest.onHeadersReceived;
}
if (xhr.readyState !== 4) {
return;
}
if (!(xhrId in this.pendingRequests)) {
// The XHR request might have been aborted in onHeadersReceived()
// callback, in which case we should abort request
return;
}
delete this.pendingRequests[xhrId];
if (xhr.status === 0) {
if (pendingRequest.onError) {
pendingRequest.onError(xhr.status);
}
return;
}
if (xhr.status !== pendingRequest.expectedStatus) {
if (pendingRequest.onError) {
pendingRequest.onError(xhr.status);
}
return;
}
this.loadedRequests[xhrId] = true;
var chunk = getArrayBuffer(xhr);
if (pendingRequest.expectedStatus === 206) {
var rangeHeader = xhr.getResponseHeader('Content-Range');
var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader);
var begin = parseInt(matches[1], 10);
var end = parseInt(matches[2], 10) + 1;
pendingRequest.onDone({
begin: begin,
end: end,
chunk: chunk
});
} else {
pendingRequest.onDone({
chunk: chunk
});
}
},
hasPendingRequests: function NetworkManager_hasPendingRequests() {
for (var xhrId in this.pendingRequests) {
return true;
}
return false;
},
getRequestXhr: function NetworkManager_getXhr(xhrId) {
return this.pendingRequests[xhrId].xhr;
},
isPendingRequest: function NetworkManager_isPendingRequest(xhrId) {
return xhrId in this.pendingRequests;
},
isLoadedRequest: function NetworkManager_isLoadedRequest(xhrId) {
return xhrId in this.loadedRequests;
},
abortAllRequests: function NetworkManager_abortAllRequests() {
for (var xhrId in this.pendingRequests) {
this.abortRequest(xhrId | 0);
}
},
abortRequest: function NetworkManager_abortRequest(xhrId) {
var xhr = this.pendingRequests[xhrId].xhr;
delete this.pendingRequests[xhrId];
xhr.abort();
}
};
return NetworkManager;
})();

View File

@ -17,7 +17,8 @@
/* globals assertWellFormed, bytesToString, CipherTransformFactory, error, info, /* globals assertWellFormed, bytesToString, CipherTransformFactory, error, info,
InvalidPDFException, isArray, isCmd, isDict, isInt, isName, isRef, InvalidPDFException, isArray, isCmd, isDict, isInt, isName, isRef,
isStream, JpegStream, Lexer, log, Page, Parser, Promise, shadow, isStream, JpegStream, Lexer, log, Page, Parser, Promise, shadow,
stringToPDFString, stringToUTF8String, warn, isString */ stringToPDFString, stringToUTF8String, warn, isString, assert, PDFJS,
MissingDataException, XRefParseException */
'use strict'; 'use strict';
@ -152,9 +153,18 @@ var RefSet = (function RefSetClosure() {
var Catalog = (function CatalogClosure() { var Catalog = (function CatalogClosure() {
function Catalog(xref) { function Catalog(xref) {
this.xref = xref; this.xref = xref;
var obj = xref.getCatalogObj(); this.catDict = xref.getCatalogObj();
assertWellFormed(isDict(obj), 'catalog object is not a dictionary'); assertWellFormed(isDict(this.catDict),
this.catDict = obj; 'catalog object is not a dictionary');
// Stores state as we traverse the pages catalog so that we can resume
// parsing if an exception is thrown
this.traversePagesQueue = [{
pagesDict: this.toplevelPagesDict,
posInKids: 0
}];
this.pagePromises = [];
this.currPageIndex = 0;
} }
Catalog.prototype = { Catalog.prototype = {
@ -258,27 +268,6 @@ var Catalog = (function CatalogClosure() {
// shadow the prototype getter // shadow the prototype getter
return shadow(this, 'num', obj); return shadow(this, 'num', obj);
}, },
traverseKids: function Catalog_traverseKids(pagesDict) {
var pageCache = this.pageCache;
var kids = pagesDict.get('Kids');
assertWellFormed(isArray(kids),
'page dictionary kids object is not an array');
for (var i = 0, ii = kids.length; i < ii; ++i) {
var kid = kids[i];
assertWellFormed(isRef(kid),
'page dictionary kid is not a reference');
var obj = this.xref.fetch(kid);
if (isDict(obj, 'Page') || (isDict(obj) && !obj.has('Kids'))) {
pageCache.push(new Page(this.xref, pageCache.length, obj, kid));
} else { // must be a child page dictionary
assertWellFormed(
isDict(obj),
'page dictionary kid reference points to wrong type of object'
);
this.traverseKids(obj);
}
}
},
get destinations() { get destinations() {
function fetchDestination(dest) { function fetchDestination(dest) {
return isDict(dest) ? dest.get('D') : dest; return isDict(dest) ? dest.get('D') : dest;
@ -346,13 +335,53 @@ var Catalog = (function CatalogClosure() {
} }
return shadow(this, 'javaScript', javaScript); return shadow(this, 'javaScript', javaScript);
}, },
getPage: function Catalog_getPage(n) {
var pageCache = this.pageCache; getPage: function Catalog_getPage(pageIndex) {
if (!pageCache) { if (!(pageIndex in this.pagePromises)) {
pageCache = this.pageCache = []; this.pagePromises[pageIndex] = new PDFJS.Promise();
this.traverseKids(this.toplevelPagesDict); }
return this.pagePromises[pageIndex];
},
// Traverses pages in DFS order so that pages are processed in increasing
// order
traversePages: function Catalog_traversePages() {
var queue = this.traversePagesQueue;
while (queue.length) {
var queueItem = queue[queue.length - 1];
var pagesDict = queueItem.pagesDict;
var kids = pagesDict.get('Kids');
assert(isArray(kids), 'page dictionary kids object is not an array');
if (queueItem.posInKids >= kids.length) {
queue.pop();
continue;
}
var kidRef = kids[queueItem.posInKids];
assert(isRef(kidRef), 'page dictionary kid is not a reference');
var kid = this.xref.fetch(kidRef);
if (isDict(kid, 'Page') || (isDict(kid) && !kid.has('Kids'))) {
var pageIndex = this.currPageIndex++;
var page = new Page(this.xref, pageIndex, kid, kidRef);
if (!(pageIndex in this.pagePromises)) {
this.pagePromises[pageIndex] = new PDFJS.Promise();
}
this.pagePromises[pageIndex].resolve(page);
} else { // must be a child page dictionary
assert(
isDict(kid),
'page dictionary kid reference points to wrong type of object'
);
queue.push({
pagesDict: kid,
posInKids: 0
});
}
++queueItem.posInKids;
} }
return this.pageCache[n - 1];
} }
}; };
@ -360,75 +389,60 @@ var Catalog = (function CatalogClosure() {
})(); })();
var XRef = (function XRefClosure() { var XRef = (function XRefClosure() {
function XRef(stream, startXRef, mainXRefEntriesOffset, password) { function XRef(stream, password) {
this.stream = stream; this.stream = stream;
this.entries = []; this.entries = [];
this.xrefstms = {}; this.xrefstms = {};
var trailerDict = this.readXRef(startXRef);
trailerDict.assignXref(this);
this.trailer = trailerDict;
// prepare the XRef cache // prepare the XRef cache
this.cache = []; this.cache = [];
this.password = password;
var encrypt = trailerDict.get('Encrypt');
if (encrypt) {
var ids = trailerDict.get('ID');
var fileId = (ids && ids.length) ? ids[0] : '';
this.encrypt = new CipherTransformFactory(encrypt, fileId, password);
}
// get the root dictionary (catalog) object
if (!(this.root = trailerDict.get('Root')))
error('Invalid root reference');
} }
XRef.prototype = { XRef.prototype = {
readXRefTable: function XRef_readXRefTable(parser) { setStartXRef: function XRef_setStartXRef(startXRef) {
// Example of cross-reference table: // Store the starting positions of xref tables as we process them
// xref // so we can recover from missing data errors
// 0 1 <-- subsection header (first obj #, obj count) this.startXRefQueue = [startXRef];
// 0000000000 65535 f <-- actual object (offset, generation #, f/n) },
// 23 2 <-- subsection header ... and so on ...
// 0000025518 00002 n
// 0000025635 00000 n
// trailer
// ...
// Outer loop is over subsection headers parse: function XRef_parse(recoveryMode) {
var obj; var trailerDict;
while (!isCmd(obj = parser.getObj(), 'trailer')) { if (!recoveryMode) {
var first = obj, trailerDict = this.readXRef();
count = parser.getObj(); } else {
warn('Indexing all PDF objects');
if (!isInt(first) || !isInt(count)) trailerDict = this.indexObjects();
error('Invalid XRef table: wrong types in subsection header'); }
trailerDict.assignXref(this);
// Inner loop is over objects themselves this.trailer = trailerDict;
for (var i = 0; i < count; i++) { var encrypt = trailerDict.get('Encrypt');
var entry = {}; if (encrypt) {
entry.offset = parser.getObj(); var ids = trailerDict.get('ID');
entry.gen = parser.getObj(); var fileId = (ids && ids.length) ? ids[0] : '';
var type = parser.getObj(); this.encrypt = new CipherTransformFactory(
encrypt, fileId, this.password);
if (isCmd(type, 'f'))
entry.free = true;
else if (isCmd(type, 'n'))
entry.uncompressed = true;
// Validate entry obj
if (!isInt(entry.offset) || !isInt(entry.gen) ||
!(entry.free || entry.uncompressed)) {
error('Invalid entry in XRef subsection: ' + first + ', ' + count);
}
if (!this.entries[i + first])
this.entries[i + first] = entry;
}
} }
// Sanity check: as per spec, first object must be free // get the root dictionary (catalog) object
if (this.entries[0] && !this.entries[0].free) if (!(this.root = trailerDict.get('Root'))) {
error('Invalid XRef table: unexpected first object'); error('Invalid root reference');
}
},
processXRefTable: function XRef_processXRefTable(parser) {
if (!('tableState' in this)) {
// Stores state of the table as we process it so we can resume
// from middle of table in case of missing data error
this.tableState = {
entryNum: 0,
streamPos: parser.lexer.stream.pos,
parserBuf1: parser.buf1,
parserBuf2: parser.buf2
};
}
var obj = this.readXRefTable(parser);
// Sanity check // Sanity check
if (!isCmd(obj, 'trailer')) if (!isCmd(obj, 'trailer'))
@ -447,27 +461,140 @@ var XRef = (function XRefClosure() {
if (!isDict(dict)) if (!isDict(dict))
error('Invalid XRef table: could not parse trailer dictionary'); error('Invalid XRef table: could not parse trailer dictionary');
delete this.tableState;
return dict; return dict;
}, },
readXRefTable: function XRef_readXRefTable(parser) {
// Example of cross-reference table:
// xref
// 0 1 <-- subsection header (first obj #, obj count)
// 0000000000 65535 f <-- actual object (offset, generation #, f/n)
// 23 2 <-- subsection header ... and so on ...
// 0000025518 00002 n
// 0000025635 00000 n
// trailer
// ...
var stream = parser.lexer.stream;
var tableState = this.tableState;
stream.pos = tableState.streamPos;
parser.buf1 = tableState.parserBuf1;
parser.buf2 = tableState.parserBuf2;
// Outer loop is over subsection headers
var obj;
while (true) {
if (!('firstEntryNum' in tableState) || !('entryCount' in tableState)) {
if (isCmd(obj = parser.getObj(), 'trailer')) {
break;
}
tableState.firstEntryNum = obj;
tableState.entryCount = parser.getObj();
}
var first = tableState.firstEntryNum;
var count = tableState.entryCount;
if (!isInt(first) || !isInt(count))
error('Invalid XRef table: wrong types in subsection header');
// Inner loop is over objects themselves
for (var i = tableState.entryNum; i < count; i++) {
tableState.streamPos = stream.pos;
tableState.entryNum = i;
tableState.parserBuf1 = parser.buf1;
tableState.parserBuf2 = parser.buf2;
var entry = {};
entry.offset = parser.getObj();
entry.gen = parser.getObj();
var type = parser.getObj();
if (isCmd(type, 'f'))
entry.free = true;
else if (isCmd(type, 'n'))
entry.uncompressed = true;
// Validate entry obj
if (!isInt(entry.offset) || !isInt(entry.gen) ||
!(entry.free || entry.uncompressed)) {
console.log(entry.offset, entry.gen, entry.free,
entry.uncompressed);
error('Invalid entry in XRef subsection: ' + first + ', ' + count);
}
if (!this.entries[i + first])
this.entries[i + first] = entry;
}
tableState.entryNum = 0;
tableState.streamPos = stream.pos;
tableState.parserBuf1 = parser.buf1;
tableState.parserBuf2 = parser.buf2;
delete tableState.firstEntryNum;
delete tableState.entryCount;
}
// 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');
return obj;
},
processXRefStream: function XRef_processXRefStream(stream) {
if (!('streamState' in this)) {
// Stores state of the stream as we process it so we can resume
// from middle of stream in case of missing data error
var streamParameters = stream.parameters;
var byteWidths = streamParameters.get('W');
var range = streamParameters.get('Index');
if (!range) {
range = [0, streamParameters.get('Size')];
}
this.streamState = {
entryRanges: range,
byteWidths: byteWidths,
entryNum: 0,
streamPos: stream.pos
};
}
this.readXRefStream(stream);
delete this.streamState;
return stream.parameters;
},
readXRefStream: function XRef_readXRefStream(stream) { readXRefStream: function XRef_readXRefStream(stream) {
var streamParameters = stream.parameters;
var byteWidths = streamParameters.get('W');
var range = streamParameters.get('Index');
if (!range)
range = [0, streamParameters.get('Size')];
var i, j; var i, j;
while (range.length > 0) { var streamState = this.streamState;
var first = range[0], n = range[1]; stream.pos = streamState.streamPos;
var byteWidths = streamState.byteWidths;
var typeFieldWidth = byteWidths[0];
var offsetFieldWidth = byteWidths[1];
var generationFieldWidth = byteWidths[2];
var entryRanges = streamState.entryRanges;
while (entryRanges.length > 0) {
var first = entryRanges[0];
var n = entryRanges[1];
if (!isInt(first) || !isInt(n)) if (!isInt(first) || !isInt(n))
error('Invalid XRef range fields: ' + first + ', ' + n); error('Invalid XRef range fields: ' + first + ', ' + n);
var typeFieldWidth = byteWidths[0];
var offsetFieldWidth = byteWidths[1];
var generationFieldWidth = byteWidths[2];
if (!isInt(typeFieldWidth) || !isInt(offsetFieldWidth) || if (!isInt(typeFieldWidth) || !isInt(offsetFieldWidth) ||
!isInt(generationFieldWidth)) { !isInt(generationFieldWidth)) {
error('Invalid XRef entry fields length: ' + first + ', ' + n); error('Invalid XRef entry fields length: ' + first + ', ' + n);
} }
for (i = 0; i < n; ++i) { for (i = streamState.entryNum; i < n; ++i) {
streamState.entryNum = i;
streamState.streamPos = stream.pos;
var type = 0, offset = 0, generation = 0; var type = 0, offset = 0, generation = 0;
for (j = 0; j < typeFieldWidth; ++j) for (j = 0; j < typeFieldWidth; ++j)
type = (type << 8) | stream.getByte(); type = (type << 8) | stream.getByte();
@ -496,9 +623,11 @@ var XRef = (function XRefClosure() {
if (!this.entries[first + i]) if (!this.entries[first + i])
this.entries[first + i] = entry; this.entries[first + i] = entry;
} }
range.splice(0, 2);
streamState.entryNum = 0;
streamState.streamPos = stream.pos;
entryRanges.splice(0, 2);
} }
return streamParameters;
}, },
indexObjects: function XRef_indexObjects() { indexObjects: function XRef_indexObjects() {
// Simple scan through the PDF content to find objects, // Simple scan through the PDF content to find objects,
@ -586,7 +715,8 @@ var XRef = (function XRefClosure() {
} }
// reading XRef streams // reading XRef streams
for (var i = 0, ii = xrefStms.length; i < ii; ++i) { for (var i = 0, ii = xrefStms.length; i < ii; ++i) {
this.readXRef(xrefStms[i], true); this.startXRefQueue.push(xrefStms[i]);
this.readXRef(/* recoveryMode */ true);
} }
// finding main trailer // finding main trailer
var dict; var dict;
@ -610,64 +740,84 @@ var XRef = (function XRefClosure() {
// calling error() would reject worker with an UnknownErrorException. // calling error() would reject worker with an UnknownErrorException.
throw new InvalidPDFException('Invalid PDF structure'); throw new InvalidPDFException('Invalid PDF structure');
}, },
readXRef: function XRef_readXRef(startXRef, recoveryMode) {
readXRef: function XRef_readXRef(recoveryMode) {
var stream = this.stream; var stream = this.stream;
stream.pos = startXRef;
try { try {
var parser = new Parser(new Lexer(stream), true, null); while (this.startXRefQueue.length) {
var obj = parser.getObj(); var startXRef = this.startXRefQueue[0];
var dict;
// Get dictionary stream.pos = startXRef;
if (isCmd(obj, 'xref')) {
// Parse end-of-file XRef
dict = this.readXRefTable(parser);
// Recursively get other XRefs 'XRefStm', if any var parser = new Parser(new Lexer(stream), true, null);
obj = dict.get('XRefStm'); var obj = parser.getObj();
if (isInt(obj)) { var dict;
var pos = obj;
// ignore previously loaded xref streams // Get dictionary
// (possible infinite recursion) if (isCmd(obj, 'xref')) {
if (!(pos in this.xrefstms)) {
this.xrefstms[pos] = 1; // Parse end-of-file XRef
this.readXRef(pos); dict = this.processXRefTable(parser);
if (!this.topDict) {
this.topDict = dict;
} }
// Recursively get other XRefs 'XRefStm', if any
obj = dict.get('XRefStm');
if (isInt(obj)) {
var pos = obj;
// ignore previously loaded xref streams
// (possible infinite recursion)
if (!(pos in this.xrefstms)) {
this.xrefstms[pos] = 1;
this.startXRefQueue.push(pos);
}
}
} else if (isInt(obj)) {
// Parse in-stream XRef
if (!isInt(parser.getObj()) ||
!isCmd(parser.getObj(), 'obj') ||
!isStream(obj = parser.getObj())) {
error('Invalid XRef stream');
}
dict = this.processXRefStream(obj);
if (!this.topDict) {
this.topDict = dict;
}
if (!dict)
error('Failed to read XRef stream');
} }
} else if (isInt(obj)) {
// Parse in-stream XRef // Recursively get previous dictionary, if any
if (!isInt(parser.getObj()) || obj = dict.get('Prev');
!isCmd(parser.getObj(), 'obj') || if (isInt(obj)) {
!isStream(obj = parser.getObj())) { this.startXRefQueue.push(obj);
error('Invalid XRef stream'); } 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.startXRefQueue.push(obj.num);
} }
dict = this.readXRefStream(obj);
if (!dict) this.startXRefQueue.shift();
error('Failed to read XRef stream');
} }
// Recursively get previous dictionary, if any return this.topDict;
obj = dict.get('Prev');
if (isInt(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, recoveryMode);
}
return dict;
} catch (e) { } catch (e) {
if (e instanceof MissingDataException) {
throw e;
}
log('(while reading XRef): ' + e); log('(while reading XRef): ' + e);
} }
if (recoveryMode) if (recoveryMode)
return; return;
throw new XRefParseException();
warn('Indexing all PDF objects');
return this.indexObjects();
}, },
getEntry: function XRef_getEntry(i) { getEntry: function XRef_getEntry(i) {
var e = this.entries[i]; var e = this.entries[i];
if (e === null) if (e === null)

180
src/pdf_manager.js Normal file
View File

@ -0,0 +1,180 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals NotImplementedException, MissingDataException, PDFJS, Stream,
PDFDocument, ChunkedStream, ChunkedStreamManager */
'use strict';
// TODO(mack): Make use of PDFJS.Util.inherit() when it becomes available
var BasePdfManager = (function BasePdfManagerClosure() {
function BasePdfManager() {
throw new Error('Cannot initialize BaseManagerManager');
}
BasePdfManager.prototype = {
onLoadedStream: function BasePdfManager_onLoadedStream() {
throw new NotImplementedException();
},
ensureModel: function BasePdfManager_ensureModel(prop) {
var args = [].slice.call(arguments);
args.unshift(this.pdfModel);
return this.ensure.apply(this, args);
},
ensureXRef: function BasePdfManager_ensureXRef(prop) {
var args = [].slice.call(arguments);
args.unshift(this.pdfModel.xref);
return this.ensure.apply(this, args);
},
ensureCatalog: function BasePdfManager_ensureCatalog(prop) {
var args = [].slice.call(arguments);
args.unshift(this.pdfModel.catalog);
return this.ensure.apply(this, args);
},
getPage: function BasePdfManager_pagePage(pageIndex) {
return this.pdfModel.getPage(pageIndex);
},
ensure: function BasePdfManager_ensure(obj, prop) {
return new NotImplementedException();
},
requestLoadedStream: function BasePdfManager_requestLoadedStream() {
return new NotImplementedException();
}
};
return BasePdfManager;
})();
var LocalPdfManager = (function LocalPdfManagerClosure() {
function LocalPdfManager(data, password) {
var stream = new Stream(data);
this.pdfModel = new PDFDocument(stream, password);
this.loadedStream = new PDFJS.Promise();
this.loadedStream.resolve(stream);
}
LocalPdfManager.prototype = Object.create(BasePdfManager.prototype);
LocalPdfManager.prototype.constructor = LocalPdfManager;
LocalPdfManager.prototype.ensure =
function LocalPdfManager_ensure(obj, prop) {
var promise = new PDFJS.Promise();
var result;
var value = obj[prop];
try {
if (typeof(value) === 'function') {
var args = [].slice.call(arguments, 2);
result = value.apply(obj, args);
} else {
result = value;
}
promise.resolve(result);
} catch (e) {
console.log(e.stack);
promise.reject(e);
}
return promise;
};
LocalPdfManager.prototype.requestLoadedStream =
function LocalPdfManager_requestLoadedStream() {
};
LocalPdfManager.prototype.onLoadedStream =
function LocalPdfManager_getLoadedStream() {
return this.loadedStream;
};
return LocalPdfManager;
})();
var NetworkPdfManager = (function NetworkPdfManagerClosure() {
var CHUNK_SIZE = 64000;
function NetworkPdfManager(args, msgHandler) {
this.msgHandler = msgHandler;
var params = {
msgHandler: msgHandler,
httpHeaders: args.httpHeaders,
chunkedViewerLoading: args.chunkedViewerLoading
};
this.streamManager = new ChunkedStreamManager(args.length, CHUNK_SIZE,
args.url, params);
this.pdfModel = new PDFDocument(this.streamManager.getStream(),
args.password);
}
NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype);
NetworkPdfManager.prototype.constructor = NetworkPdfManager;
NetworkPdfManager.prototype.ensure =
function NetworkPdfManager_ensure(obj, prop) {
var promise = new PDFJS.Promise();
var args = [].slice.call(arguments);
args.unshift(promise);
this.ensureHelper.apply(this, args);
return promise;
};
NetworkPdfManager.prototype.ensureHelper =
function NetworkPdfManager_ensureHelper(promise, obj, prop) {
try {
var result;
var value = obj[prop];
if (typeof(value) === 'function') {
var args = [].slice.call(arguments, 3);
result = value.apply(obj, args);
} else {
result = value;
}
promise.resolve(result);
} catch(e) {
if (!(e instanceof MissingDataException)) {
console.log(e.stack);
promise.reject(e);
return;
}
var allArgs = Array.prototype.slice.call(arguments);
this.streamManager.requestRange(e.begin, e.end, function() {
this.ensureHelper.apply(this, allArgs);
}.bind(this));
}
};
NetworkPdfManager.prototype.requestLoadedStream =
function NetworkPdfManager_requestLoadedStream() {
this.streamManager.requestAllChunks();
};
NetworkPdfManager.prototype.onLoadedStream =
function NetworkPdfManager_getLoadedStream() {
return this.streamManager.onLoadedStream();
};
return NetworkPdfManager;
})();

View File

@ -189,6 +189,45 @@ var MissingPDFException = (function MissingPDFExceptionClosure() {
return MissingPDFException; return MissingPDFException;
})(); })();
var NotImplementedException = (function NotImplementedExceptionClosure() {
function NotImplementedException(msg) {
this.message = msg;
}
NotImplementedException.prototype = new Error();
NotImplementedException.prototype.name = 'NotImplementedException';
NotImplementedException.constructor = NotImplementedException;
return NotImplementedException;
})();
var MissingDataException = (function MissingDataExceptionClosure() {
function MissingDataException(begin, end) {
this.begin = begin;
this.end = end;
this.message = 'Missing data [begin, end)';
}
MissingDataException.prototype = new Error();
MissingDataException.prototype.name = 'MissingDataException';
MissingDataException.constructor = MissingDataException;
return MissingDataException;
})();
var XRefParseException = (function XRefParseExceptionClosure() {
function XRefParseException(msg) {
this.message = msg;
}
XRefParseException.prototype = new Error();
XRefParseException.prototype.name = 'XRefParseException';
XRefParseException.constructor = XRefParseException;
return XRefParseException;
})();
function bytesToString(bytes) { function bytesToString(bytes) {
var str = ''; var str = '';
var length = bytes.length; var length = bytes.length;
@ -482,6 +521,13 @@ function stringToUTF8String(str) {
return decodeURIComponent(escape(str)); return decodeURIComponent(escape(str));
} }
function isEmptyObj(obj) {
for (var key in obj) {
return false;
}
return true;
}
function isBool(v) { function isBool(v) {
return typeof v == 'boolean'; return typeof v == 'boolean';
} }

View File

@ -16,7 +16,9 @@
*/ */
/* globals error, globalScope, InvalidPDFException, log, /* globals error, globalScope, InvalidPDFException, log,
MissingPDFException, PasswordException, PDFDocument, PDFJS, Promise, MissingPDFException, PasswordException, PDFDocument, PDFJS, Promise,
Stream, UnknownErrorException, warn */ Stream, UnknownErrorException, warn, NetworkManager, LocalPdfManager,
NetworkPdfManager, XRefParseException, NotImplementedException,
isInt */
'use strict'; 'use strict';
@ -107,58 +109,124 @@ MessageHandler.prototype = {
var WorkerMessageHandler = { var WorkerMessageHandler = {
setup: function wphSetup(handler) { setup: function wphSetup(handler) {
var pdfModel = null; var pdfManager;
function loadDocument(pdfData, pdfModelSource) { function loadDocument(recoveryMode) {
// Create only the model of the PDFDoc, which is enough for var loadDocumentPromise = new PDFJS.Promise();
// processing the content of the pdf.
var pdfPassword = pdfModelSource.password; var parseSuccess = function parseSuccess() {
try { var numPagesPromise = pdfManager.ensureModel('numPages');
pdfModel = new PDFDocument(new Stream(pdfData), pdfPassword); var fingerprintPromise = pdfManager.ensureModel('fingerprint');
} catch (e) { var outlinePromise = pdfManager.ensureCatalog('documentOutline');
if (e instanceof PasswordException) { var infoPromise = pdfManager.ensureModel('documentInfo');
if (e.code === 'needpassword') { var metadataPromise = pdfManager.ensureCatalog('metadata');
handler.send('NeedPassword', { var encryptedPromise = pdfManager.ensureXRef('encrypt');
exception: e var javaScriptPromise = pdfManager.ensureCatalog('javaScript');
}); PDFJS.Promise.all([numPagesPromise, fingerprintPromise, outlinePromise,
} else if (e.code === 'incorrectpassword') { infoPromise, metadataPromise, encryptedPromise,
handler.send('IncorrectPassword', { javaScriptPromise]).then(
exception: e function onDocReady(results) {
});
var doc = {
numPages: results[0],
fingerprint: results[1],
outline: results[2],
info: results[3],
metadata: results[4],
encrypted: !!results[5],
javaScript: results[6]
};
loadDocumentPromise.resolve(doc);
});
};
var parseFailure = function parseFailure(e) {
loadDocumentPromise.reject(e);
};
pdfManager.ensureModel('checkHeader').then(function() {
pdfManager.ensureModel('parseStartXRef').then(function() {
pdfManager.ensureModel('parse', recoveryMode).then(
parseSuccess, parseFailure);
});
});
return loadDocumentPromise;
}
function getPdfManager(data) {
var pdfManagerPromise = new PDFJS.Promise();
var source = data.source;
var disableRange = data.disableRange;
if (source.data) {
pdfManager = new LocalPdfManager(source.data, source.password);
pdfManagerPromise.resolve();
return pdfManagerPromise;
} else if (source.chunkedViewerLoading) {
pdfManager = new NetworkPdfManager(source, handler);
pdfManagerPromise.resolve();
return pdfManagerPromise;
}
var networkManager = new NetworkManager(source.url, {
httpHeaders: source.httpHeaders
});
var fullRequestXhrId = networkManager.requestFull({
onHeadersReceived: function onHeadersReceived() {
if (disableRange) {
return;
} }
return; var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId);
} else if (e instanceof InvalidPDFException) { if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') {
handler.send('InvalidPDF', { return;
exception: e }
});
return; var length = fullRequestXhr.getResponseHeader('Content-Length');
} else if (e instanceof MissingPDFException) { length = parseInt(length, 10);
handler.send('MissingPDF', { if (!isInt(length)) {
exception: e return;
}); }
return; // NOTE: by cancelling the full request, and then issuing range
} else { // requests, there will be an issue for sites where you can only
handler.send('UnknownError', { // request the pdf once. However, if this is the case, then the
exception: new UnknownErrorException(e.message, e.toString()) // server should not be returning that it can support range requests.
}); networkManager.abortRequest(fullRequestXhrId);
return; source.length = length;
pdfManager = new NetworkPdfManager(source, handler);
pdfManagerPromise.resolve(pdfManager);
},
onDone: function onDone(args) {
// the data is array, instantiating directly from it
pdfManager = new LocalPdfManager(args.chunk, source.password);
pdfManagerPromise.resolve();
},
onError: function onError(status) {
if (status == 404) {
var exception = new MissingPDFException( 'Missing PDF "' +
source.url + '".');
handler.send('MissingPDF', { exception: exception });
} else {
handler.send('DocError', 'Unexpected server response (' +
status + ') while retrieving PDF "' +
source.url + '".');
}
},
onProgress: function onProgress(evt) {
handler.send('DocProgress', {
loaded: evt.loaded,
total: evt.lengthComputable ? evt.total : void(0)
});
} }
} });
var doc = {
numPages: pdfModel.numPages, return pdfManagerPromise;
fingerprint: pdfModel.getFingerprint(),
destinations: pdfModel.catalog.destinations,
javaScript: pdfModel.catalog.javaScript,
outline: pdfModel.catalog.documentOutline,
info: pdfModel.getDocumentInfo(),
metadata: pdfModel.catalog.metadata,
encrypted: !!pdfModel.xref.encrypt
};
handler.send('GetDoc', {pdfInfo: doc});
} }
handler.on('test', function wphSetupTest(data) { handler.on('test', function wphSetupTest(data) {
@ -184,140 +252,183 @@ var WorkerMessageHandler = {
}); });
handler.on('GetDocRequest', function wphSetupDoc(data) { handler.on('GetDocRequest', function wphSetupDoc(data) {
var source = data.source;
if (source.data) {
// the data is array, instantiating directly from it
loadDocument(source.data, source);
return;
}
PDFJS.getPdf( var onSuccess = function(doc) {
{ handler.send('GetDoc', { pdfInfo: doc });
url: source.url, pdfManager.ensureModel('traversePages');
progress: function getPDFProgress(evt) { };
handler.send('DocProgress', {
loaded: evt.loaded, var onFailure = function(e) {
total: evt.lengthComputable ? evt.total : void(0) if (e instanceof PasswordException) {
if (e.code === 'needpassword') {
handler.send('NeedPassword', {
exception: e
}); });
}, } else if (e.code === 'incorrectpassword') {
error: function getPDFError(e) { handler.send('IncorrectPassword', {
if (e.target.status == 404) { exception: e
handler.send('MissingPDF', { });
exception: new MissingPDFException( }
'Missing PDF \"' + source.url + '\".')}); } else if (e instanceof InvalidPDFException) {
} else { handler.send('InvalidPDF', {
handler.send('DocError', 'Unexpected server response (' + exception: e
e.target.status + ') while retrieving PDF \"' + });
source.url + '\".'); } else if (e instanceof MissingPDFException) {
} handler.send('MissingPDF', {
}, exception: e
headers: source.httpHeaders });
}, } else {
function getPDFLoad(data) { handler.send('UnknownError', {
loadDocument(data, source); exception: new UnknownErrorException(e.message, e.toString())
});
}
};
getPdfManager(data).then(function() {
loadDocument(false).then(onSuccess, function(ex) {
// Try again with recoveryMode == true
if (!(ex instanceof XRefParseException)) {
onFailure(ex);
return;
}
pdfManager.onLoadedStream().then(function() {
loadDocument(true).then(onSuccess, onFailure);
});
}); });
});
}); });
handler.on('GetPageRequest', function wphSetupGetPage(data) { handler.on('GetPageRequest', function wphSetupGetPage(data) {
var pageNumber = data.pageIndex + 1; var pageIndex = data.pageIndex;
var pdfPage = pdfModel.getPage(pageNumber); pdfManager.getPage(pageIndex).then(function(page) {
var page = { var rotatePromise = pdfManager.ensure(page, 'rotate');
pageIndex: data.pageIndex, var refPromise = pdfManager.ensure(page, 'ref');
rotate: pdfPage.rotate, var viewPromise = pdfManager.ensure(page, 'view');
ref: pdfPage.ref,
view: pdfPage.view PDFJS.Promise.all([rotatePromise, refPromise, viewPromise]).then(
}; function(results) {
handler.send('GetPage', {pageInfo: page}); var page = {
pageIndex: data.pageIndex,
rotate: results[0],
ref: results[1],
view: results[2]
};
handler.send('GetPage', { pageInfo: page });
});
});
}); });
handler.on('GetDestinations',
function wphSetupGetDestinations(data, promise) {
pdfManager.ensureCatalog('destinations').then(function(destinations) {
promise.resolve(destinations);
});
}
);
handler.on('GetData', function wphSetupGetData(data, promise) { handler.on('GetData', function wphSetupGetData(data, promise) {
promise.resolve(pdfModel.stream.bytes); pdfManager.requestLoadedStream();
pdfManager.onLoadedStream().then(function(stream) {
promise.resolve(stream.bytes);
});
});
handler.on('DataLoaded', function wphSetupDataLoaded(data, promise) {
pdfManager.onLoadedStream().then(function(stream) {
promise.resolve({ length: stream.bytes.byteLength });
});
}); });
handler.on('GetAnnotationsRequest', function wphSetupGetAnnotations(data) { handler.on('GetAnnotationsRequest', function wphSetupGetAnnotations(data) {
var pdfPage = pdfModel.getPage(data.pageIndex + 1); pdfManager.getPage(data.pageIndex).then(function(page) {
handler.send('GetAnnotations', { pdfManager.ensure(page, 'getAnnotations').then(function(annotations) {
pageIndex: data.pageIndex, handler.send('GetAnnotations', {
annotations: pdfPage.getAnnotations() pageIndex: data.pageIndex,
annotations: annotations
});
});
}); });
}); });
handler.on('RenderPageRequest', function wphSetupRenderPage(data) { handler.on('RenderPageRequest', function wphSetupRenderPage(data) {
var pageNum = data.pageIndex + 1; pdfManager.getPage(data.pageIndex).then(function(page) {
var start = Date.now(); var pageNum = data.pageIndex + 1;
var start = Date.now();
var dependency = []; var dependency = [];
var operatorList = null;
try {
var page = pdfModel.getPage(pageNum);
// Pre compile the pdf page and fetch the fonts/images. // Pre compile the pdf page and fetch the fonts/images.
operatorList = page.getOperatorList(handler, dependency); pdfManager.ensure(page, 'getOperatorList', handler,
} catch (e) { dependency).then(function(operatorList) {
var minimumStackMessage =
'worker.js: while trying to getPage() and getOperatorList()';
var wrappedException; // The following code does quite the same as
// Page.prototype.startRendering, but stops at one point and sends the
// result back to the main thread.
// Turn the error into an obj that can be serialized log('page=%d - getOperatorList: time=%dms, len=%d', pageNum,
if (typeof e === 'string') { Date.now() - start, operatorList.fnArray.length);
wrappedException = {
message: e,
stack: minimumStackMessage
};
} else if (typeof e === 'object') {
wrappedException = {
message: e.message || e.toString(),
stack: e.stack || minimumStackMessage
};
} else {
wrappedException = {
message: 'Unknown exception type: ' + (typeof e),
stack: minimumStackMessage
};
}
handler.send('PageError', { // Filter the dependecies for fonts.
pageNum: pageNum, var fonts = {};
error: wrappedException for (var i = 0, ii = dependency.length; i < ii; i++) {
var dep = dependency[i];
if (dep.indexOf('g_font_') === 0) {
fonts[dep] = true;
}
}
handler.send('RenderPage', {
pageIndex: data.pageIndex,
operatorList: operatorList,
depFonts: Object.keys(fonts)
});
}, function(e) {
var minimumStackMessage =
'worker.js: while trying to getPage() and getOperatorList()';
var wrappedException;
// Turn the error into an obj that can be serialized
if (typeof e === 'string') {
wrappedException = {
message: e,
stack: minimumStackMessage
};
} else if (typeof e === 'object') {
wrappedException = {
message: e.message || e.toString(),
stack: e.stack || minimumStackMessage
};
} else {
wrappedException = {
message: 'Unknown exception type: ' + (typeof e),
stack: minimumStackMessage
};
}
handler.send('PageError', {
pageNum: pageNum,
error: wrappedException
});
}); });
return;
}
log('page=%d - getOperatorList: time=%dms, len=%d', pageNum,
Date.now() - start, operatorList.fnArray.length);
// Filter the dependecies for fonts.
var fonts = {};
for (var i = 0, ii = dependency.length; i < ii; i++) {
var dep = dependency[i];
if (dep.indexOf('g_font_') === 0) {
fonts[dep] = true;
}
}
handler.send('RenderPage', {
pageIndex: data.pageIndex,
operatorList: operatorList,
depFonts: Object.keys(fonts)
}); });
}, this); }, this);
handler.on('GetTextContent', function wphExtractText(data, promise) { handler.on('GetTextContent', function wphExtractText(data, promise) {
var pageNum = data.pageIndex + 1; pdfManager.getPage(data.pageIndex).then(function(page) {
var start = Date.now(); var pageNum = data.pageIndex + 1;
var start = Date.now();
var textContent = ''; pdfManager.ensure(page,
try { 'extractTextContent').then(function(textContent) {
var page = pdfModel.getPage(pageNum); promise.resolve(textContent);
textContent = page.extractTextContent(); log('text indexing: page=%d - time=%dms', pageNum,
promise.resolve(textContent); Date.now() - start);
} catch (e) { }, function (e) {
// Skip errored pages // Skip errored pages
promise.reject(e); promise.reject(e);
} });
});
log('text indexing: page=%d - time=%dms',
pageNum, Date.now() - start);
}); });
} }
}; };

View File

@ -19,6 +19,9 @@
// List of files to include; // List of files to include;
var files = [ var files = [
'network.js',
'chunked_stream.js',
'pdf_manager.js',
'core.js', 'core.js',
'util.js', 'util.js',
'canvas.js', 'canvas.js',

View File

@ -28,7 +28,8 @@
// PDFJS.disableWorker = true; // PDFJS.disableWorker = true;
PDFJS.enableStats = true; PDFJS.enableStats = true;
var appPath, browser, canvas, dummyCanvas, currentTaskIdx, manifest, stdout; var appPath, masterMode, browser, canvas, dummyCanvas, currentTaskIdx,
manifest, stdout;
var inFlightRequests = 0; var inFlightRequests = 0;
function queryParams() { function queryParams() {
@ -47,6 +48,7 @@ function load() {
browser = params.browser; browser = params.browser;
var manifestFile = params.manifestFile; var manifestFile = params.manifestFile;
appPath = params.path; appPath = params.path;
masterMode = params.masterMode === 'True';
var delay = params.delay || 0; var delay = params.delay || 0;
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
@ -124,27 +126,28 @@ function nextTask() {
log('Loading file "' + task.file + '"\n'); log('Loading file "' + task.file + '"\n');
var absoluteUrl = combineUrl(window.location.href, task.file); var absoluteUrl = combineUrl(window.location.href, task.file);
getPdf(absoluteUrl, function nextTaskGetPdf(data) { var failure;
var failure; function continuation() {
function continuation() { task.pageNum = task.firstPage || 1;
task.pageNum = task.firstPage || 1; nextPage(task, failure);
nextPage(task, failure); }
}
try { // When generating reference images in masterMode, disable range requests
var promise = PDFJS.getDocument(data); PDFJS.disableRange = !task.rangeRequest || masterMode;
promise.then(function(doc) { try {
task.pdfDoc = doc; var promise = PDFJS.getDocument(absoluteUrl);
continuation(); promise.then(function(doc) {
}, function(e) { task.pdfDoc = doc;
failure = 'load PDF doc : ' + e; continuation();
continuation(); }, function(e) {
}); failure = 'load PDF doc : ' + e;
return; continuation();
} catch (e) { });
failure = 'load PDF doc : ' + exceptionToString(e); return;
} } catch (e) {
continuation(); failure = 'load PDF doc : ' + exceptionToString(e);
}); }
continuation();
} }
function getLastPageNum(task) { function getLastPageNum(task) {

View File

@ -12,6 +12,9 @@
<script type="text/javascript" src="fontutils.js"></script> <script type="text/javascript" src="fontutils.js"></script>
<!-- include source files here... --> <!-- include source files here... -->
<script type="text/javascript" src="../../src/network.js"></script>
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
<script type="text/javascript" src="../../src/core.js"></script> <script type="text/javascript" src="../../src/core.js"></script>
<script type="text/javascript" src="../../src/api.js"></script> <script type="text/javascript" src="../../src/api.js"></script>
<script type="text/javascript" src="../../src/util.js"></script> <script type="text/javascript" src="../../src/util.js"></script>

View File

@ -3,6 +3,7 @@
!tracemonkey.pdf !tracemonkey.pdf
!issue2391-1.pdf !issue2391-1.pdf
!issue2391-2.pdf !issue2391-2.pdf
!filled-background.pdf
!ArabicCIDTrueType.pdf !ArabicCIDTrueType.pdf
!ThuluthFeatures.pdf !ThuluthFeatures.pdf
!arial_unicode_ab_cidfont.pdf !arial_unicode_ab_cidfont.pdf

File diff suppressed because one or more lines are too long

View File

@ -88,6 +88,7 @@ class TestOptions(OptionParser):
return options return options
def prompt(question): def prompt(question):
'''Return True iff the user answered "yes" to |question|.''' '''Return True iff the user answered "yes" to |question|.'''
inp = raw_input(question +' [yes/no] > ') inp = raw_input(question +' [yes/no] > ')
@ -157,12 +158,32 @@ class TestHandlerBase(BaseHTTPRequestHandler):
def sendFile(self, path, ext): def sendFile(self, path, ext):
self.send_response(200) self.send_response(200)
self.send_header("Accept-Ranges", "bytes")
self.send_header("Content-Type", MIMEs[ext]) self.send_header("Content-Type", MIMEs[ext])
self.send_header("Content-Length", os.path.getsize(path)) self.send_header("Content-Length", os.path.getsize(path))
self.end_headers() self.end_headers()
with open(path, "rb") as f: with open(path, "rb") as f:
self.wfile.write(f.read()) self.wfile.write(f.read())
def sendFileRange(self, path, ext, start, end):
file_len = os.path.getsize(path)
if (end is None) or (file_len < end):
end = file_len
if (file_len < start) or (end <= start):
self.send_error(416)
return
chunk_len = end - start
self.send_response(206)
self.send_header("Accept-Ranges", "bytes")
self.send_header("Content-Type", MIMEs[ext])
self.send_header("Content-Length", chunk_len)
self.send_header("Content-Range", 'bytes ' + str(start) + '-' + str(end - 1) + '/' + str(file_len))
self.end_headers()
time.sleep(chunk_len / 500000.0)
with open(path, "rb") as f:
f.seek(start)
self.wfile.write(f.read(chunk_len))
def do_GET(self): def do_GET(self):
url = urlparse(self.path) url = urlparse(self.path)
@ -188,8 +209,18 @@ class TestHandlerBase(BaseHTTPRequestHandler):
return return
if 'Range' in self.headers: if 'Range' in self.headers:
# TODO for fetch-as-you-go range_re = re.compile(r"^bytes=(\d+)\-(\d+)?")
self.send_error(501) parsed_range = range_re.search(self.headers.getheader("Range"))
if parsed_range is None:
self.send_error(501)
return
print 'Range requested ' + parsed_range.group(1) + '-' + parsed_range.group(2)
start = int(parsed_range.group(1))
if parsed_range.group(2) is None:
self.sendFileRange(path, ext, start, None)
else:
end = int(parsed_range.group(2)) + 1
self.sendFileRange(path, ext, start, end)
return return
self.sendFile(path, ext) self.sendFile(path, ext)
@ -606,6 +637,7 @@ def startBrowsers(browsers, options, path):
qs = '?browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile) qs = '?browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile)
qs += '&path=' + b.path qs += '&path=' + b.path
qs += '&delay=' + str(options.statsDelay) qs += '&delay=' + str(options.statsDelay)
qs += '&masterMode=' + str(options.masterMode)
b.start(host + path + qs) b.start(host + path + qs)
def teardownBrowsers(browsers): def teardownBrowsers(browsers):

View File

@ -1,4 +1,11 @@
[ [
{ "id": "filled-background-range",
"file": "pdfs/filled-background.pdf",
"md5": "2e3120255d9c3e79b96d2543b12d2589",
"rounds": 1,
"rangeRequest": true,
"type": "eq"
},
{ "id": "tracemonkey-eq", { "id": "tracemonkey-eq",
"file": "pdfs/tracemonkey.pdf", "file": "pdfs/tracemonkey.pdf",
"md5": "9a192d8b1a7dc652a19835f6f08098bd", "md5": "9a192d8b1a7dc652a19835f6f08098bd",

View File

@ -19,6 +19,9 @@ limitations under the License.
<head> <head>
<title>pdf.js test slave</title> <title>pdf.js test slave</title>
<style type="text/css"></style> <style type="text/css"></style>
<script type="text/javascript" src="/src/network.js"></script>
<script type="text/javascript" src="/src/chunked_stream.js"></script>
<script type="text/javascript" src="/src/pdf_manager.js"></script>
<script type="text/javascript" src="/src/core.js"></script> <script type="text/javascript" src="/src/core.js"></script>
<script type="text/javascript" src="/src/util.js"></script> <script type="text/javascript" src="/src/util.js"></script>
<script type="text/javascript" src="/src/api.js"></script> <script type="text/javascript" src="/src/api.js"></script>

View File

@ -11,6 +11,9 @@
<script type="text/javascript" src="testreporter.js"></script> <script type="text/javascript" src="testreporter.js"></script>
<!-- include source files here... --> <!-- include source files here... -->
<script type="text/javascript" src="../../src/network.js"></script>
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
<script type="text/javascript" src="../../src/core.js"></script> <script type="text/javascript" src="../../src/core.js"></script>
<script type="text/javascript" src="../../src/api.js"></script> <script type="text/javascript" src="../../src/api.js"></script>
<script type="text/javascript" src="../../src/util.js"></script> <script type="text/javascript" src="../../src/util.js"></script>

View File

@ -237,12 +237,14 @@ html[dir='rtl'] #sidebarContent {
bottom: 0; bottom: 0;
left: 0; left: 0;
} }
.loadingInProgress #viewerContainer {
top: 39px;
}
.toolbar { .toolbar {
position: absolute; position: relative;
left: 0; left: 0;
right: 0; right: 0;
height: 32px;
z-index: 9999; z-index: 9999;
cursor: default; cursor: default;
} }
@ -271,9 +273,8 @@ html[dir='rtl'] #sidebarContent {
0 0 1px hsla(0,0%,0%,.1); 0 0 1px hsla(0,0%,0%,.1);
} }
#toolbarViewer, .findbar { #toolbarContainer, .findbar {
position: relative; position: relative;
height: 32px;
background-color: #474747; /* IE9 */ background-color: #474747; /* IE9 */
background-image: url(images/texture.png), background-image: url(images/texture.png),
-webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95)); -webkit-linear-gradient(hsla(0,0%,32%,.99), hsla(0,0%,27%,.95));
@ -292,6 +293,83 @@ html[dir='rtl'] #sidebarContent {
0 1px 1px hsla(0,0%,0%,.1); 0 1px 1px hsla(0,0%,0%,.1);
} }
#toolbarViewer {
height: 32px;
}
#loadingBar {
position: relative;
width: 100%;
height: 6px;
background-color: #333;
border-bottom: 1px solid #333;
}
#loadingBar .progress {
position: absolute;
top: 0;
left: 0;
width: 0%;
height: 100%;
background-color: #ddd;
overflow: hidden;
-moz-transition: width 200ms;
-ms-transition: width 200ms;
-webkit-transition: width 200ms;
transition: width 200ms;
}
@-moz-keyframes progressIndeterminate {
0% { left: 0%; }
50% { left: 100%; }
100% { left: 100%; }
}
@-ms-keyframes progressIndeterminate {
0% { left: 0%; }
50% { left: 100%; }
100% { left: 100%; }
}
@-webkit-keyframes progressIndeterminate {
0% { left: 0%; }
50% { left: 100%; }
100% { left: 100%; }
}
@keyframes progressIndeterminate {
0% { left: 0%; }
50% { left: 100%; }
100% { left: 100%; }
}
#loadingBar .progress.indeterminate {
background-color: #999;
-moz-transition: none;
-ms-transition: none;
-webkit-transition: none;
transition: none;
}
#loadingBar .indeterminate .glimmer {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 50px;
background-image: -moz-linear-gradient(left, #999 0%, #fff 50%, #999 100%);
background-image: -ms-linear-gradient(left, #999 0%, #fff 50%, #999 100%);
background-image: -webkit-linear-gradient(left, #999 0%, #fff 50%, #999 100%);
background-image: linear-gradient(left, #999 0%, #fff 50%, #999 100%);
background-size: 100% 100% no-repeat;
-moz-animation: progressIndeterminate 2s linear infinite;
-ms-animation: progressIndeterminate 2s linear infinite;
-webkit-animation: progressIndeterminate 2s linear infinite;
animation: progressIndeterminate 2s linear infinite;
}
.findbar { .findbar {
top: 32px; top: 32px;
position: absolute; position: absolute;
@ -1123,82 +1201,6 @@ canvas {
background: url('images/loading-icon.gif') center no-repeat; background: url('images/loading-icon.gif') center no-repeat;
} }
#loadingBox {
position: absolute;
top: 50%;
margin-top: -25px;
left: 0;
right: 0;
text-align: center;
color: #ddd;
font-size: 14px;
}
#loadingBar {
display: inline-block;
clear: both;
margin: 0px;
margin-top: 5px;
line-height: 0;
border-radius: 2px;
width: 200px;
height: 25px;
background-color: hsla(0,0%,0%,.3);
background-image: -moz-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
background-image: -webkit-linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0));
border: 1px solid #000;
box-shadow: 0 1px 1px hsla(0,0%,0%,.1) inset,
0 0 1px hsla(0,0%,0%,.2) inset,
0 0 1px 1px rgba(255, 255, 255, 0.1);
}
#loadingBar .progress {
display: inline-block;
float: left;
background: #666;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2b2b2), color-stop(100%,#898989));
background: -webkit-linear-gradient(top, #b2b2b2 0%,#898989 100%);
background: -moz-linear-gradient(top, #b2b2b2 0%,#898989 100%);
background: -ms-linear-gradient(top, #b2b2b2 0%,#898989 100%);
background: -o-linear-gradient(top, #b2b2b2 0%,#898989 100%);
background: linear-gradient(top, #b2b2b2 0%,#898989 100%);
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
width: 0%;
height: 100%;
}
#loadingBar .progress.full {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
#loadingBar .progress.indeterminate {
width: 100%;
height: 25px;
background-image: -moz-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
background-image: -webkit-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
background-image: -ms-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
background-image: -o-linear-gradient( 30deg, #404040, #404040 15%, #898989, #404040 85%, #404040);
background-size: 75px 25px;
-moz-animation: progressIndeterminate 1s linear infinite;
-webkit-animation: progressIndeterminate 1s linear infinite;
}
@-moz-keyframes progressIndeterminate {
from { background-position: 0px 0px; }
to { background-position: 75px 0px; }
}
@-webkit-keyframes progressIndeterminate {
from { background-position: 0px 0px; }
to { background-position: 75px 0px; }
}
.textLayer { .textLayer {
position: absolute; position: absolute;
left: 0; left: 0;
@ -1280,7 +1282,6 @@ canvas {
left: 0; left: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 32px;
z-index: 1000; z-index: 1000;
padding: 3px; padding: 3px;
font-size: 0.8em; font-size: 0.8em;
@ -1430,9 +1431,12 @@ canvas {
@media all and (max-width: 770px) { @media all and (max-width: 770px) {
#sidebarContainer { #sidebarContainer {
top: 33px; top: 32px;
z-index: 100; z-index: 100;
} }
.loadingInProgress #sidebarContainer {
top: 39px;
}
#sidebarContent { #sidebarContent {
top: 32px; top: 32px;
background-color: hsla(0,0%,0%,.7); background-color: hsla(0,0%,0%,.7);

View File

@ -38,6 +38,9 @@ limitations under the License.
<!--#endif--> <!--#endif-->
<!--#if !PRODUCTION--> <!--#if !PRODUCTION-->
<script type="text/javascript" src="../src/network.js"></script>
<script type="text/javascript" src="../src/chunked_stream.js"></script>
<script type="text/javascript" src="../src/pdf_manager.js"></script>
<script type="text/javascript" src="../src/core.js"></script> <script type="text/javascript" src="../src/core.js"></script>
<script type="text/javascript" src="../src/util.js"></script> <script type="text/javascript" src="../src/util.js"></script>
<script type="text/javascript" src="../src/api.js"></script> <script type="text/javascript" src="../src/api.js"></script>
@ -74,7 +77,7 @@ limitations under the License.
</head> </head>
<body> <body>
<div id="outerContainer"> <div id="outerContainer" class="loadingInProgress">
<div id="sidebarContainer"> <div id="sidebarContainer">
<div id="toolbarSidebar"> <div id="toolbarSidebar">
@ -189,6 +192,12 @@ limitations under the License.
</div> </div>
</div> </div>
</div> </div>
<div id="loadingBar">
<div class="progress">
<div class="glimmer">
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -207,11 +216,6 @@ limitations under the License.
<div id="viewer" contextmenu="viewerContextMenu"></div> <div id="viewer" contextmenu="viewerContextMenu"></div>
</div> </div>
<div id="loadingBox">
<div id="loading"></div>
<div id="loadingBar"><div class="progress"></div></div>
</div>
<div id="errorWrapper" hidden='true'> <div id="errorWrapper" hidden='true'>
<div id="errorMessageLeft"> <div id="errorMessageLeft">
<span id="errorMessage"></span> <span id="errorMessage"></span>

View File

@ -117,17 +117,12 @@ var ProgressBar = (function ProgressBarClosure() {
updateBar: function ProgressBar_updateBar() { updateBar: function ProgressBar_updateBar() {
if (this._indeterminate) { if (this._indeterminate) {
this.div.classList.add('indeterminate'); this.div.classList.add('indeterminate');
this.div.style.width = this.width + this.units;
return; return;
} }
var progressSize = this.width * this._percent / 100;
if (this._percent > 95)
this.div.classList.add('full');
else
this.div.classList.remove('full');
this.div.classList.remove('indeterminate'); this.div.classList.remove('indeterminate');
var progressSize = this.width * this._percent / 100;
this.div.style.width = progressSize + this.units; this.div.style.width = progressSize + this.units;
}, },
@ -945,12 +940,39 @@ var PDFView = {
PDFView.loadingBar = new ProgressBar('#loadingBar', {}); PDFView.loadingBar = new ProgressBar('#loadingBar', {});
} }
window.addEventListener('message', function window_message(e) { var pdfDataRangeTransport = {
listeners: [],
addListener: function PdfDataRangeTransport_addListener(listener) {
this.listeners.push(listener);
},
onDataRange: function PdfDataRangeTransport_onDataRange(begin, chunk) {
for (var i = 0, n = this.listeners.length; i < n; ++i) {
this.listeners[i](begin, chunk);
}
},
requestDataRange: function PdfDataRangeTransport_requestDataRange(
begin, end) {
FirefoxCom.request('requestDataRange', { begin: begin, end: end });
}
};
window.addEventListener('message', function windowMessage(e) {
var args = e.data; var args = e.data;
if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) if (typeof args !== 'object' || !('pdfjsLoadAction' in args))
return; return;
switch (args.pdfjsLoadAction) { switch (args.pdfjsLoadAction) {
case 'supportsRangedLoading':
PDFView.open(args.pdfUrl, 0, undefined, pdfDataRangeTransport, {
length: args.length
});
break;
case 'range':
pdfDataRangeTransport.onDataRange(args.begin, args.chunk);
break;
case 'progress': case 'progress':
PDFView.progress(args.loaded / args.total); PDFView.progress(args.loaded / args.total);
break; break;
@ -985,7 +1007,9 @@ var PDFView = {
//#endif //#endif
}, },
open: function pdfViewOpen(url, scale, password) { // TODO(mack): This function signature should really be pdfViewOpen(url, args)
open: function pdfViewOpen(url, scale, password,
pdfDataRangeTransport, args) {
var parameters = {password: password}; var parameters = {password: password};
if (typeof url === 'string') { // URL if (typeof url === 'string') { // URL
this.setTitleUsingUrl(url); this.setTitleUsingUrl(url);
@ -993,6 +1017,11 @@ var PDFView = {
} else if (url && 'byteLength' in url) { // ArrayBuffer } else if (url && 'byteLength' in url) { // ArrayBuffer
parameters.data = url; parameters.data = url;
} }
if (args) {
for (var prop in args) {
parameters[prop] = args[prop];
}
}
if (!PDFView.loadingBar) { if (!PDFView.loadingBar) {
PDFView.loadingBar = new ProgressBar('#loadingBar', {}); PDFView.loadingBar = new ProgressBar('#loadingBar', {});
@ -1001,7 +1030,7 @@ var PDFView = {
this.pdfDocument = null; this.pdfDocument = null;
var self = this; var self = this;
self.loading = true; self.loading = true;
PDFJS.getDocument(parameters).then( PDFJS.getDocument(parameters, pdfDataRangeTransport).then(
function getDocumentCallback(pdfDocument) { function getDocumentCallback(pdfDocument) {
self.load(pdfDocument, scale); self.load(pdfDocument, scale);
self.loading = false; self.loading = false;
@ -1042,9 +1071,6 @@ var PDFView = {
//#endif //#endif
} }
var loadingIndicator = document.getElementById('loading');
loadingIndicator.textContent = mozL10n.get('loading_error_indicator',
null, 'Error');
var moreInfo = { var moreInfo = {
message: message message: message
}; };
@ -1251,9 +1277,6 @@ var PDFView = {
} }
} }
var loadingBox = document.getElementById('loadingBox');
loadingBox.setAttribute('hidden', 'true');
//#if !(FIREFOX || MOZCENTRAL) //#if !(FIREFOX || MOZCENTRAL)
var errorWrapper = document.getElementById('errorWrapper'); var errorWrapper = document.getElementById('errorWrapper');
errorWrapper.removeAttribute('hidden'); errorWrapper.removeAttribute('hidden');
@ -1308,10 +1331,12 @@ var PDFView = {
var errorWrapper = document.getElementById('errorWrapper'); var errorWrapper = document.getElementById('errorWrapper');
errorWrapper.setAttribute('hidden', 'true'); errorWrapper.setAttribute('hidden', 'true');
var loadingBox = document.getElementById('loadingBox'); pdfDocument.dataLoaded().then(function() {
loadingBox.setAttribute('hidden', 'true'); var loadingBar = document.getElementById('loadingBar');
var loadingIndicator = document.getElementById('loading'); loadingBar.classList.add('hidden');
loadingIndicator.textContent = ''; var outerContainer = document.getElementById('outerContainer');
outerContainer.classList.remove('loadingInProgress');
});
var thumbsView = document.getElementById('thumbnailView'); var thumbsView = document.getElementById('thumbnailView');
thumbsView.parentNode.scrollTop = 0; thumbsView.parentNode.scrollTop = 0;
@ -3070,8 +3095,13 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
var hash = document.location.hash.substring(1); var hash = document.location.hash.substring(1);
var hashParams = PDFView.parseQueryString(hash); var hashParams = PDFView.parseQueryString(hash);
if ('disableWorker' in hashParams) if ('disableWorker' in hashParams) {
PDFJS.disableWorker = (hashParams['disableWorker'] === 'true'); PDFJS.disableWorker = (hashParams['disableWorker'] === 'true');
}
if ('disableRange' in hashParams) {
PDFJS.disableRange = (hashParams['disableRange'] === 'true');
}
//#if !(FIREFOX || MOZCENTRAL) //#if !(FIREFOX || MOZCENTRAL)
var locale = navigator.language; var locale = navigator.language;
@ -3237,11 +3267,9 @@ document.addEventListener('DOMContentLoaded', function webViewerLoad(evt) {
}); });
//#if (FIREFOX || MOZCENTRAL) //#if (FIREFOX || MOZCENTRAL)
//if (FirefoxCom.requestSync('getLoadingType') == 'passive') { //PDFView.setTitleUsingUrl(file);
// PDFView.setTitleUsingUrl(file); //PDFView.initPassiveLoading();
// PDFView.initPassiveLoading(); //return;
// return;
//}
//#endif //#endif
//#if !B2G //#if !B2G