Browse Source

Merge pull request #2719 from mduan/chunked

Implement progressive loading of PDFs
Brendan Dahl 12 years ago
parent
commit
49ff029f5f
  1. 3
      examples/acroforms/index.html
  2. 3
      examples/helloworld/index.html
  3. 236
      extensions/firefox/components/PdfStreamConverter.js
  4. 32
      make.js
  5. 79
      src/api.js
  6. 441
      src/chunked_stream.js
  7. 254
      src/core.js
  8. 1549
      src/evaluator.js
  9. 224
      src/network.js
  10. 417
      src/obj.js
  11. 15
      src/parser.js
  12. 190
      src/pdf_manager.js
  13. 13
      src/stream.js
  14. 59
      src/util.js
  15. 423
      src/worker.js
  16. 3
      src/worker_loader.js
  17. 48
      test/driver.js
  18. 3
      test/font/font_test.html
  19. 1
      test/pdfs/.gitignore
  20. 114
      test/pdfs/filled-background.pdf
  21. 59
      test/test.py
  22. 21
      test/test_manifest.json
  23. 3
      test/test_slave.html
  24. 2
      test/unit/api_spec.js
  25. 189
      test/unit/evaluator_spec.js
  26. 3
      test/unit/unit_test.html
  27. 168
      web/viewer.css
  28. 16
      web/viewer.html
  29. 84
      web/viewer.js

3
examples/acroforms/index.html

@ -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>

3
examples/helloworld/index.html

@ -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>

236
extensions/firefox/components/PdfStreamConverter.js

@ -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;
} }
@ -305,39 +305,6 @@ ChromeActions.prototype = {
getLocale: function() { getLocale: function() {
return getStringPref('general.useragent.locale', 'en-US'); return getStringPref('general.useragent.locale', 'en-US');
}, },
getLoadingType: function() {
return this.dataListener ? 'passive' : 'active';
},
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 {
// Lazy initialization of localizedStrings // Lazy initialization of localizedStrings
@ -436,6 +403,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 +653,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 +680,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 +749,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

@ -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);

79
src/api.js

@ -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();
@ -516,11 +528,14 @@ var WorkerTransport = (function WorkerTransportClosure() {
} }
WorkerTransport.prototype = { WorkerTransport.prototype = {
destroy: function WorkerTransport_destroy() { destroy: function WorkerTransport_destroy() {
if (this.worker)
this.worker.terminate();
this.pageCache = []; this.pageCache = [];
this.pagePromises = []; this.pagePromises = [];
var self = this;
this.messageHandler.send('Terminate', null, function () {
if (self.worker) {
self.worker.terminate();
}
});
}, },
setupFakeWorker: function WorkerTransport_setupFakeWorker() { setupFakeWorker: function WorkerTransport_setupFakeWorker() {
warn('Setting up fake worker.'); warn('Setting up fake worker.');
@ -544,6 +559,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 +677,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 +736,12 @@ var WorkerTransport = (function WorkerTransportClosure() {
}, },
fetchDocument: function WorkerTransport_fetchDocument(source) { fetchDocument: function WorkerTransport_fetchDocument(source) {
this.messageHandler.send('GetDocRequest', {source: source}); source.disableAutoFetch = PDFJS.disableAutoFetch;
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 +750,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 +771,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;

441
src/chunked_stream.js

@ -0,0 +1,441 @@
/* -*- 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, Promise,
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;
this.disableAutoFetch = args.disableAutoFetch;
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 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 (!this.disableAutoFetch && 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;
})();

254
src/core.js

@ -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, Promise */
'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;
@ -122,7 +60,8 @@ var Page = (function PageClosure() {
return appearance; return appearance;
} }
function Page(xref, pageIndex, pageDict, ref) { function Page(pdfManager, xref, pageIndex, pageDict, ref) {
this.pdfManager = pdfManager;
this.pageIndex = pageIndex; this.pageIndex = pageIndex;
this.pageDict = pageDict; this.pageDict = pageDict;
this.xref = xref; this.xref = xref;
@ -208,28 +147,72 @@ var Page = (function PageClosure() {
} }
return content; return content;
}, },
getOperatorList: function Page_getOperatorList(handler, dependency) { getOperatorList: function Page_getOperatorList(handler) {
var xref = this.xref; var self = this;
var contentStream = this.getContentStream(); var promise = new Promise();
var resources = this.resources;
var pe = this.pe = new PartialEvaluator( var pageListPromise = new Promise();
xref, handler, this.pageIndex, var annotationListPromise = new Promise();
'p' + this.pageIndex + '_');
var pdfManager = this.pdfManager;
var list = pe.getOperatorList(contentStream, resources, dependency); var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
[]);
var annotations = this.getAnnotationsForDraw(); var resourcesPromise = pdfManager.ensure(this, 'resources');
var annotationEvaluator = new PartialEvaluator( var dataPromises = Promise.all(
xref, handler, this.pageIndex, [contentStreamPromise, resourcesPromise]);
'p' + this.pageIndex + '_annotation'); dataPromises.then(function(data) {
var annotationsList = annotationEvaluator.getAnnotationsOperatorList( var contentStream = data[0];
annotations, dependency); var resources = data[1];
var pe = self.pe = new PartialEvaluator(
Util.concatenateToArray(list.fnArray, annotationsList.fnArray); pdfManager,
Util.concatenateToArray(list.argsArray, annotationsList.argsArray); self.xref, handler, self.pageIndex,
'p' + self.pageIndex + '_');
pe.optimizeQueue(list);
return list; pdfManager.ensure(pe, 'getOperatorList',
[contentStream, resources]).then(
function(opListPromise) {
opListPromise.then(function(data) {
pageListPromise.resolve(data);
});
}
);
});
pdfManager.ensure(this, 'getAnnotationsForDraw', []).then(
function(annotations) {
var annotationEvaluator = new PartialEvaluator(
pdfManager, self.xref, handler, self.pageIndex,
'p' + self.pageIndex + '_annotation');
pdfManager.ensure(annotationEvaluator, 'getAnnotationsOperatorList',
[annotations]).then(
function(opListPromise) {
opListPromise.then(function(data) {
annotationListPromise.resolve(data);
});
}
);
}
);
Promise.all([pageListPromise, annotationListPromise]).then(
function(datas) {
var pageData = datas[0];
var pageQueue = pageData.queue;
var annotationData = datas[1];
var annotationQueue = annotationData.queue;
Util.concatenateToArray(pageQueue.fnArray, annotationQueue.fnArray);
Util.concatenateToArray(pageQueue.argsArray,
annotationQueue.argsArray);
PartialEvaluator.optimizeQueue(pageQueue);
Util.extendObj(pageData.dependencies, annotationData.dependencies);
promise.resolve(pageData);
}
);
return promise;
}, },
extractTextContent: function Page_extractTextContent() { extractTextContent: function Page_extractTextContent() {
var handler = { var handler = {
@ -237,14 +220,40 @@ var Page = (function PageClosure() {
send: function nullHandlerSend() {} send: function nullHandlerSend() {}
}; };
var xref = this.xref; var self = this;
var contentStream = this.getContentStream();
var resources = xref.fetchIfRef(this.resources); var textContentPromise = new Promise();
var pdfManager = this.pdfManager;
var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
[]);
var resourcesPromise = new Promise();
pdfManager.ensure(this, 'resources').then(function(resources) {
pdfManager.ensure(self.xref, 'fetchIfRef', [resources]).then(
function(resources) {
resourcesPromise.resolve(resources);
}
);
});
var dataPromises = Promise.all([contentStreamPromise,
resourcesPromise]);
dataPromises.then(function(data) {
var contentStream = data[0];
var resources = data[1];
var pe = new PartialEvaluator(
pdfManager,
self.xref, handler, self.pageIndex,
'p' + self.pageIndex + '_');
pe.getTextContent(contentStream, resources).then(function(bidiTexts) {
textContentPromise.resolve({
bidiTexts: bidiTexts
});
});
});
var pe = new PartialEvaluator( return textContentPromise;
xref, handler, this.pageIndex,
'p' + this.pageIndex + '_');
return pe.getTextContent(contentStream, resources);
}, },
getLinks: function Page_getLinks() { getLinks: function Page_getLinks() {
var links = []; var links = [];
@ -484,20 +493,21 @@ var Page = (function PageClosure() {
* `PDFDocument` objects on the main thread created. * `PDFDocument` objects on the main thread created.
*/ */
var PDFDocument = (function PDFDocumentClosure() { var PDFDocument = (function PDFDocumentClosure() {
function PDFDocument(arg, password) { function PDFDocument(pdfManager, arg, password) {
if (isStream(arg)) if (isStream(arg))
init.call(this, arg, password); init.call(this, pdfManager, arg, password);
else if (isArrayBuffer(arg)) else if (isArrayBuffer(arg))
init.call(this, new Stream(arg), password); init.call(this, pdfManager, new Stream(arg), password);
else else
error('PDFDocument: Unknown argument type'); error('PDFDocument: Unknown argument type');
} }
function init(stream, password) { function init(pdfManager, stream, password) {
assertWellFormed(stream.length > 0, 'stream must have data'); assertWellFormed(stream.length > 0, 'stream must have data');
this.pdfManager = pdfManager;
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 +545,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 +642,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.pdfManager, this.xref);
this.catalog = new Catalog(xref);
}, },
get numPages() { get numPages() {
var linearization = this.linearization; var linearization = this.linearization;
@ -637,7 +656,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 +679,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 +700,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);
} }
}; };

1549
src/evaluator.js

File diff suppressed because it is too large Load Diff

224
src/network.js

@ -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;
})();

417
src/obj.js

@ -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,
Promise, MissingDataException, XRefParseException, Stream */
'use strict'; 'use strict';
@ -150,11 +151,21 @@ var RefSet = (function RefSetClosure() {
})(); })();
var Catalog = (function CatalogClosure() { var Catalog = (function CatalogClosure() {
function Catalog(xref) { function Catalog(pdfManager, xref) {
this.pdfManager = pdfManager;
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 +269,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 +336,54 @@ 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 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.pdfManager, this.xref, pageIndex, kid,
kidRef);
if (!(pageIndex in this.pagePromises)) {
this.pagePromises[pageIndex] = new 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,29 +391,83 @@ 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 = {
setStartXRef: function XRef_setStartXRef(startXRef) {
// Store the starting positions of xref tables as we process them
// so we can recover from missing data errors
this.startXRefQueue = [startXRef];
},
parse: function XRef_parse(recoveryMode) {
var trailerDict;
if (!recoveryMode) {
trailerDict = this.readXRef();
} else {
warn('Indexing all PDF objects');
trailerDict = this.indexObjects();
}
trailerDict.assignXref(this);
this.trailer = trailerDict;
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, this.password);
}
// get the root dictionary (catalog) object
if (!(this.root = trailerDict.get('Root'))) {
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
if (!isCmd(obj, 'trailer'))
error('Invalid XRef table: could not find trailer dictionary');
// Read trailer dictionary, e.g.
// trailer
// << /Size 22
// /Root 20R
// /Info 10R
// /ID [ <81b14aafa313db63dbd6f981e49f94f4> ]
// >>
// The parser goes through the entire stream << ... >> and provides
// a getter interface for the key-value table
var dict = parser.getObj();
if (!isDict(dict))
error('Invalid XRef table: could not parse trailer dictionary');
delete this.tableState;
return dict;
},
readXRefTable: function XRef_readXRefTable(parser) { readXRefTable: function XRef_readXRefTable(parser) {
// Example of cross-reference table: // Example of cross-reference table:
// xref // xref
@ -394,17 +479,36 @@ var XRef = (function XRefClosure() {
// trailer // 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 // Outer loop is over subsection headers
var obj; var obj;
while (!isCmd(obj = parser.getObj(), 'trailer')) {
var first = obj,
count = parser.getObj();
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)) if (!isInt(first) || !isInt(count))
error('Invalid XRef table: wrong types in subsection header'); error('Invalid XRef table: wrong types in subsection header');
// Inner loop is over objects themselves // Inner loop is over objects themselves
for (var i = 0; i < count; i++) { 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 = {}; var entry = {};
entry.offset = parser.getObj(); entry.offset = parser.getObj();
entry.gen = parser.getObj(); entry.gen = parser.getObj();
@ -418,56 +522,81 @@ var XRef = (function XRefClosure() {
// Validate entry obj // Validate entry obj
if (!isInt(entry.offset) || !isInt(entry.gen) || if (!isInt(entry.offset) || !isInt(entry.gen) ||
!(entry.free || entry.uncompressed)) { !(entry.free || entry.uncompressed)) {
console.log(entry.offset, entry.gen, entry.free,
entry.uncompressed);
error('Invalid entry in XRef subsection: ' + first + ', ' + count); error('Invalid entry in XRef subsection: ' + first + ', ' + count);
} }
if (!this.entries[i + first]) if (!this.entries[i + first])
this.entries[i + first] = entry; 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 // Sanity check: as per spec, first object must be free
if (this.entries[0] && !this.entries[0].free) if (this.entries[0] && !this.entries[0].free)
error('Invalid XRef table: unexpected first object'); error('Invalid XRef table: unexpected first object');
// Sanity check return obj;
if (!isCmd(obj, 'trailer')) },
error('Invalid XRef table: could not find trailer dictionary');
// Read trailer dictionary, e.g. processXRefStream: function XRef_processXRefStream(stream) {
// trailer if (!('streamState' in this)) {
// << /Size 22 // Stores state of the stream as we process it so we can resume
// /Root 20R // from middle of stream in case of missing data error
// /Info 10R var streamParameters = stream.parameters;
// /ID [ <81b14aafa313db63dbd6f981e49f94f4> ] var byteWidths = streamParameters.get('W');
// >> var range = streamParameters.get('Index');
// The parser goes through the entire stream << ... >> and provides if (!range) {
// a getter interface for the key-value table range = [0, streamParameters.get('Size')];
var dict = parser.getObj(); }
if (!isDict(dict))
error('Invalid XRef table: could not parse trailer dictionary');
return dict; 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 +625,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 +717,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 +742,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)
error('Failed to read XRef stream');
}
// Recursively get previous dictionary, if any this.startXRefQueue.shift();
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; return this.topDict;
} 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)
@ -682,10 +834,16 @@ var XRef = (function XRefClosure() {
fetch: function XRef_fetch(ref, suppressEncryption) { fetch: function XRef_fetch(ref, suppressEncryption) {
assertWellFormed(isRef(ref), 'ref object is not a reference'); assertWellFormed(isRef(ref), 'ref object is not a reference');
var num = ref.num; var num = ref.num;
if (num in this.cache) var e;
return this.cache[num]; if (num in this.cache) {
e = this.cache[num];
if (e instanceof Stream) {
return e.makeSubStream(e.start, e.length, e.dict);
}
return e;
}
var e = this.getEntry(num); e = this.getEntry(num);
// the referenced entry can be free // the referenced entry can be free
if (e === null) if (e === null)
@ -727,9 +885,16 @@ var XRef = (function XRefClosure() {
} else { } else {
e = parser.getObj(); e = parser.getObj();
} }
// Don't cache streams since they are mutable (except images). if (!isStream(e) || e instanceof JpegStream) {
if (!isStream(e) || e instanceof JpegStream)
this.cache[num] = e; this.cache[num] = e;
} else if (e instanceof Stream) {
e = e.makeSubStream(e.start, e.length, e.dict);
this.cache[num] = e;
} else if ('readBlock' in e) {
e.getBytes();
e = e.makeSubStream(0, e.bufferLength, e.dict);
this.cache[num] = e;
}
return e; return e;
} }

15
src/parser.js

@ -36,6 +36,21 @@ var Parser = (function ParserClosure() {
} }
Parser.prototype = { Parser.prototype = {
saveState: function Parser_saveState() {
this.state = {
buf1: this.buf1,
buf2: this.buf2,
streamPos: this.lexer.stream.pos
};
},
restoreState: function Parser_restoreState() {
var state = this.state;
this.buf1 = state.buf1;
this.buf2 = state.buf2;
this.lexer.stream.pos = state.streamPos;
},
refill: function Parser_refill() { refill: function Parser_refill() {
this.buf1 = this.lexer.getObj(); this.buf1 = this.lexer.getObj();
this.buf2 = this.lexer.getObj(); this.buf2 = this.lexer.getObj();

190
src/pdf_manager.js

@ -0,0 +1,190 @@
/* -*- 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, Promise, 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, args) {
return this.ensure(this.pdfModel, prop, args);
},
ensureXRef: function BasePdfManager_ensureXRef(prop, args) {
return this.ensure(this.pdfModel.xref, prop, args);
},
ensureCatalog: function BasePdfManager_ensureCatalog(prop, args) {
return this.ensure(this.pdfModel.catalog, prop, args);
},
getPage: function BasePdfManager_pagePage(pageIndex) {
return this.pdfModel.getPage(pageIndex);
},
ensure: function BasePdfManager_ensure(obj, prop, args) {
return new NotImplementedException();
},
requestRange: function BasePdfManager_ensure(begin, end) {
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(this, stream, password);
this.loadedStream = new Promise();
this.loadedStream.resolve(stream);
}
LocalPdfManager.prototype = Object.create(BasePdfManager.prototype);
LocalPdfManager.prototype.constructor = LocalPdfManager;
LocalPdfManager.prototype.ensure =
function LocalPdfManager_ensure(obj, prop, args) {
var promise = new Promise();
try {
var value = obj[prop];
var result;
if (typeof(value) === 'function') {
result = value.apply(obj, args);
} else {
result = value;
}
promise.resolve(result);
} catch (e) {
console.log(e.stack);
promise.reject(e);
}
return promise;
};
LocalPdfManager.prototype.requestRange =
function LocalPdfManager_requestRange(begin, end) {
var promise = new Promise();
promise.resolve();
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 = 65536;
function NetworkPdfManager(args, msgHandler) {
this.msgHandler = msgHandler;
var params = {
msgHandler: msgHandler,
httpHeaders: args.httpHeaders,
chunkedViewerLoading: args.chunkedViewerLoading,
disableAutoFetch: args.disableAutoFetch
};
this.streamManager = new ChunkedStreamManager(args.length, CHUNK_SIZE,
args.url, params);
this.pdfModel = new PDFDocument(this, this.streamManager.getStream(),
args.password);
}
NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype);
NetworkPdfManager.prototype.constructor = NetworkPdfManager;
NetworkPdfManager.prototype.ensure =
function NetworkPdfManager_ensure(obj, prop, args) {
var promise = new Promise();
this.ensureHelper(promise, obj, prop, args);
return promise;
};
NetworkPdfManager.prototype.ensureHelper =
function NetworkPdfManager_ensureHelper(promise, obj, prop, args) {
try {
var result;
var value = obj[prop];
if (typeof(value) === 'function') {
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;
}
this.streamManager.requestRange(e.begin, e.end, function() {
this.ensureHelper(promise, obj, prop, args);
}.bind(this));
}
};
NetworkPdfManager.prototype.requestRange =
function NetworkPdfManager_requestRange(begin, end) {
var promise = new Promise();
this.streamManager.requestRange(begin, end, function() {
promise.resolve();
});
return promise;
};
NetworkPdfManager.prototype.requestLoadedStream =
function NetworkPdfManager_requestLoadedStream() {
this.streamManager.requestAllChunks();
};
NetworkPdfManager.prototype.onLoadedStream =
function NetworkPdfManager_getLoadedStream() {
return this.streamManager.onLoadedStream();
};
return NetworkPdfManager;
})();

13
src/stream.js

@ -26,7 +26,7 @@ var Stream = (function StreamClosure() {
this.start = start || 0; this.start = start || 0;
this.pos = this.start; this.pos = this.start;
this.end = (start + length) || this.bytes.length; this.end = (start + length) || this.bytes.length;
this.dict = dict; this.parameters = this.dict = dict;
} }
// required methods for a stream. if a particular stream does not // required methods for a stream. if a particular stream does not
@ -645,6 +645,10 @@ var PredictorStream = (function PredictorStreamClosure() {
var colors = this.colors; var colors = this.colors;
var rawBytes = this.stream.getBytes(rowBytes); var rawBytes = this.stream.getBytes(rowBytes);
this.eof = !rawBytes.length;
if (this.eof) {
return;
}
var inbuf = 0, outbuf = 0; var inbuf = 0, outbuf = 0;
var inbits = 0, outbits = 0; var inbits = 0, outbits = 0;
@ -705,6 +709,10 @@ var PredictorStream = (function PredictorStreamClosure() {
var predictor = this.stream.getByte(); var predictor = this.stream.getByte();
var rawBytes = this.stream.getBytes(rowBytes); var rawBytes = this.stream.getBytes(rowBytes);
this.eof = !rawBytes.length;
if (this.eof) {
return;
}
var bufferLength = this.bufferLength; var bufferLength = this.bufferLength;
var buffer = this.ensureBuffer(bufferLength + rowBytes); var buffer = this.ensureBuffer(bufferLength + rowBytes);
@ -853,6 +861,7 @@ var JpegStream = (function JpegStreamClosure() {
var data = jpegImage.getData(width, height); var data = jpegImage.getData(width, height);
this.buffer = data; this.buffer = data;
this.bufferLength = data.length; this.bufferLength = data.length;
this.eof = true;
} catch (e) { } catch (e) {
error('JPEG error: ' + e); error('JPEG error: ' + e);
} }
@ -988,6 +997,7 @@ var JpxStream = (function JpxStreamClosure() {
this.buffer = data; this.buffer = data;
this.bufferLength = data.length; this.bufferLength = data.length;
this.eof = true;
}; };
JpxStream.prototype.getChar = function JpxStream_getChar() { JpxStream.prototype.getChar = function JpxStream_getChar() {
error('internal error: getChar is not valid on JpxStream'); error('internal error: getChar is not valid on JpxStream');
@ -1032,6 +1042,7 @@ var Jbig2Stream = (function Jbig2StreamClosure() {
this.buffer = data; this.buffer = data;
this.bufferLength = dataLength; this.bufferLength = dataLength;
this.eof = true;
}; };
Jbig2Stream.prototype.getChar = function Jbig2Stream_getChar() { Jbig2Stream.prototype.getChar = function Jbig2Stream_getChar() {
error('internal error: getChar is not valid on Jbig2Stream'); error('internal error: getChar is not valid on Jbig2Stream');

59
src/util.js

@ -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;
@ -358,8 +397,19 @@ var Util = PDFJS.Util = (function UtilClosure() {
return num < 0 ? -1 : 1; return num < 0 ? -1 : 1;
}; };
// TODO(mack): Rename appendToArray
Util.concatenateToArray = function concatenateToArray(arr1, arr2) { Util.concatenateToArray = function concatenateToArray(arr1, arr2) {
return Array.prototype.push.apply(arr1, arr2); Array.prototype.push.apply(arr1, arr2);
};
Util.prependToArray = function concatenateToArray(arr1, arr2) {
Array.prototype.unshift.apply(arr1, arr2);
};
Util.extendObj = function extendObj(obj1, obj2) {
for (var key in obj2) {
obj1[key] = obj2[key];
}
}; };
return Util; return Util;
@ -482,6 +532,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';
} }

423
src/worker.js

@ -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(recoveryMode) {
var loadDocumentPromise = new Promise();
var parseSuccess = function parseSuccess() {
var numPagesPromise = pdfManager.ensureModel('numPages');
var fingerprintPromise = pdfManager.ensureModel('fingerprint');
var outlinePromise = pdfManager.ensureCatalog('documentOutline');
var infoPromise = pdfManager.ensureModel('documentInfo');
var metadataPromise = pdfManager.ensureCatalog('metadata');
var encryptedPromise = pdfManager.ensureXRef('encrypt');
var javaScriptPromise = pdfManager.ensureCatalog('javaScript');
Promise.all([numPagesPromise, fingerprintPromise, outlinePromise,
infoPromise, metadataPromise, encryptedPromise,
javaScriptPromise]).then(
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);
});
};
function loadDocument(pdfData, pdfModelSource) { var parseFailure = function parseFailure(e) {
// Create only the model of the PDFDoc, which is enough for loadDocumentPromise.reject(e);
// processing the content of the pdf. };
var pdfPassword = pdfModelSource.password;
try { pdfManager.ensureModel('checkHeader', []).then(function() {
pdfModel = new PDFDocument(new Stream(pdfData), pdfPassword); pdfManager.ensureModel('parseStartXRef', []).then(function() {
} catch (e) { pdfManager.ensureModel('parse', [recoveryMode]).then(
if (e instanceof PasswordException) { parseSuccess, parseFailure);
if (e.code === 'needpassword') { });
handler.send('NeedPassword', { });
exception: e
}); return loadDocumentPromise;
} else if (e.code === 'incorrectpassword') { }
handler.send('IncorrectPassword', {
exception: e function getPdfManager(data) {
}); var pdfManagerPromise = new 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,191 @@ 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.requestLoadedStream();
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 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(
pageIndex: data.pageIndex, function(annotations) {
annotations: pdfPage.getAnnotations() handler.send('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 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); page.getOperatorList(handler).then(function(opListData) {
} catch (e) {
var minimumStackMessage =
'worker.js: while trying to getPage() and getOperatorList()';
var wrappedException; var operatorList = opListData.queue;
var dependency = Object.keys(opListData.dependencies);
// Turn the error into an obj that can be serialized // The following code does quite the same as
if (typeof e === 'string') { // Page.prototype.startRendering, but stops at one point and sends the
wrappedException = { // result back to the main thread.
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', { log('page=%d - getOperatorList: time=%dms, len=%d', pageNum,
pageNum: pageNum, Date.now() - start, operatorList.fnArray.length);
error: wrappedException
});
return;
}
log('page=%d - getOperatorList: time=%dms, len=%d', pageNum, // Filter the dependecies for fonts.
Date.now() - start, operatorList.fnArray.length); 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)
});
}, 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
};
}
// Filter the dependecies for fonts. handler.send('PageError', {
var fonts = {}; pageNum: pageNum,
for (var i = 0, ii = dependency.length; i < ii; i++) { error: wrappedException
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 = ''; page.extractTextContent().then(function(textContent) {
try { promise.resolve(textContent);
var page = pdfModel.getPage(pageNum); log('text indexing: page=%d - time=%dms', pageNum,
textContent = page.extractTextContent(); Date.now() - start);
promise.resolve(textContent); }, function (e) {
} catch (e) { // Skip errored pages
// Skip errored pages promise.reject(e);
promise.reject(e); });
} });
});
log('text indexing: page=%d - time=%dms', handler.on('Terminate', function wphTerminate(data, promise) {
pageNum, Date.now() - start); pdfManager.streamManager.networkManager.abortAllRequests();
promise.resolve();
}); });
} }
}; };

3
src/worker_loader.js

@ -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',

48
test/driver.js

@ -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,29 @@ 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.disableRange || masterMode;
promise.then(function(doc) { PDFJS.disableAutoFetch = !task.enableAutoFetch || masterMode;
task.pdfDoc = doc; try {
continuation(); var promise = PDFJS.getDocument(absoluteUrl);
}, function(e) { promise.then(function(doc) {
failure = 'load PDF doc : ' + e; task.pdfDoc = doc;
continuation(); continuation();
}); }, function(e) {
return; failure = 'load PDF doc : ' + e;
} catch (e) { continuation();
failure = 'load PDF doc : ' + exceptionToString(e); });
} return;
continuation(); } catch (e) {
}); failure = 'load PDF doc : ' + exceptionToString(e);
}
continuation();
} }
function getLastPageNum(task) { function getLastPageNum(task) {

3
test/font/font_test.html

@ -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>

1
test/pdfs/.gitignore vendored

@ -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

114
test/pdfs/filled-background.pdf

File diff suppressed because one or more lines are too long

59
test/test.py

@ -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] > ')
@ -151,18 +152,55 @@ class TestHandlerBase(BaseHTTPRequestHandler):
try: try:
BaseHTTPRequestHandler.handle_one_request(self) BaseHTTPRequestHandler.handle_one_request(self)
except socket.error, v: except socket.error, v:
# Ignoring connection reset by peer exceptions if v[0] == errno.ECONNRESET:
if v[0] != errno.ECONNRESET: # Ignoring connection reset by peer exceptions
print 'Detected connection reset'
elif v[0] == errno.EPIPE:
print 'Detected remote peer disconnected'
elif v[0] == 10053:
print 'An established connection was aborted by the' \
' software in your host machine'
else:
raise raise
def finish(self,*args,**kw):
# From http://stackoverflow.com/a/14355079/1834797
try:
if not self.wfile.closed:
self.wfile.flush()
self.wfile.close()
except socket.error:
pass
self.rfile.close()
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
time.sleep(chunk_len / 1000000.0)
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()
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 +226,20 @@ 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
if VERBOSE:
print 'Range requested %s - %s: %s' % (
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 +656,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):

21
test/test_manifest.json

@ -1,4 +1,10 @@
[ [
{ "id": "filled-background-range",
"file": "pdfs/filled-background.pdf",
"md5": "2e3120255d9c3e79b96d2543b12d2589",
"rounds": 1,
"type": "eq"
},
{ "id": "tracemonkey-eq", { "id": "tracemonkey-eq",
"file": "pdfs/tracemonkey.pdf", "file": "pdfs/tracemonkey.pdf",
"md5": "9a192d8b1a7dc652a19835f6f08098bd", "md5": "9a192d8b1a7dc652a19835f6f08098bd",
@ -304,6 +310,14 @@
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "usmanm-bad-auto-fetch",
"file": "pdfs/usmanm-bad.pdf",
"md5": "38afb822433aaf07fc8f54807cd4f61a",
"link": true,
"rounds": 1,
"type": "eq",
"enableAutoFetch": true
},
{ "id": "vesta-bad", { "id": "vesta-bad",
"file": "pdfs/vesta.pdf", "file": "pdfs/vesta.pdf",
"md5": "0afebc109b7c17b95619ea3fab5eafe6", "md5": "0afebc109b7c17b95619ea3fab5eafe6",
@ -1052,13 +1066,6 @@
"type": "eq", "type": "eq",
"about": "Image with indexed colorspace that has a base lab colorspace." "about": "Image with indexed colorspace that has a base lab colorspace."
}, },
{ "id": "yo01",
"file": "pdfs/yo01.pdf",
"md5": "7d42435c20fe0d32de4ea3d7e4727ac1",
"rounds": 1,
"link": true,
"type": "eq"
},
{ "id": "20130226130259", { "id": "20130226130259",
"file": "pdfs/20130226130259.pdf", "file": "pdfs/20130226130259.pdf",
"md5": "c33e90a1b369c508573023d2434b950f", "md5": "c33e90a1b369c508573023d2434b950f",

3
test/test_slave.html

@ -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>

2
test/unit/api_spec.js

@ -11,7 +11,7 @@ describe('api', function() {
function waitsForPromise(promise) { function waitsForPromise(promise) {
waitsFor(function() { waitsFor(function() {
return promise.isResolved || promise.isRejected; return promise.isResolved || promise.isRejected;
}, 4000); }, 10000);
} }
function expectAfterPromise(promise, successCallback) { function expectAfterPromise(promise, successCallback) {
waitsForPromise(promise); waitsForPromise(promise);

189
test/unit/evaluator_spec.js

@ -28,137 +28,170 @@ describe('evaluator', function() {
} }
}; };
function PdfManagerMock() { }
describe('splitCombinedOperations', function() { describe('splitCombinedOperations', function() {
it('should reject unknown operations', function() { it('should reject unknown operations', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('qTT'); var stream = new StringStream('qTT');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(1); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('save'); expect(result.fnArray.length).toEqual(1);
expect(result.argsArray[0].length).toEqual(0); expect(result.fnArray[0]).toEqual('save');
expect(result.argsArray[0].length).toEqual(0);
});
}); });
it('should handle one operations', function() { it('should handle one operations', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('Q'); var stream = new StringStream('Q');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(1); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('restore'); expect(result.fnArray.length).toEqual(1);
expect(result.fnArray[0]).toEqual('restore');
});
}); });
it('should handle two glued operations', function() { it('should handle two glued operations', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var resources = new ResourcesMock(); var resources = new ResourcesMock();
resources.Res1 = {}; resources.Res1 = {};
var stream = new StringStream('/Res1 DoQ'); var stream = new StringStream('/Res1 DoQ');
var result = evaluator.getOperatorList(stream, resources, []); var promise = evaluator.getOperatorList(stream, resources);
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(2); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('paintXObject'); expect(result.fnArray.length).toEqual(2);
expect(result.fnArray[1]).toEqual('restore'); expect(result.fnArray[0]).toEqual('paintXObject');
expect(result.fnArray[1]).toEqual('restore');
});
}); });
it('should handle tree glued operations', function() { it('should handle tree glued operations', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('qqq'); var stream = new StringStream('qqq');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(3); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('save'); expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[1]).toEqual('save'); expect(result.fnArray[0]).toEqual('save');
expect(result.fnArray[2]).toEqual('save'); expect(result.fnArray[1]).toEqual('save');
expect(result.fnArray[2]).toEqual('save');
});
}); });
it('should handle three glued operations #2', function() { it('should handle three glued operations #2', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var resources = new ResourcesMock(); var resources = new ResourcesMock();
resources.Res1 = {}; resources.Res1 = {};
var stream = new StringStream('B*Bf*'); var stream = new StringStream('B*Bf*');
var result = evaluator.getOperatorList(stream, resources, []); var promise = evaluator.getOperatorList(stream, resources);
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(3); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('eoFillStroke'); expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[1]).toEqual('fillStroke'); expect(result.fnArray[0]).toEqual('eoFillStroke');
expect(result.fnArray[2]).toEqual('eoFill'); expect(result.fnArray[1]).toEqual('fillStroke');
expect(result.fnArray[2]).toEqual('eoFill');
});
}); });
it('should handle glued operations and operands', function() { it('should handle glued operations and operands', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('q5 Ts'); var stream = new StringStream('q5 Ts');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(2); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('save'); expect(result.fnArray.length).toEqual(2);
expect(result.fnArray[1]).toEqual('setTextRise'); expect(result.fnArray[0]).toEqual('save');
expect(result.argsArray.length).toEqual(2); expect(result.fnArray[1]).toEqual('setTextRise');
expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray.length).toEqual(2);
expect(result.argsArray[1][0]).toEqual(5); expect(result.argsArray[1].length).toEqual(1);
expect(result.argsArray[1][0]).toEqual(5);
});
}); });
it('should handle glued operations and literals', function() { it('should handle glued operations and literals', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('trueifalserinullq'); var stream = new StringStream('trueifalserinullq');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(!!result.fnArray && !!result.argsArray).toEqual(true); var result = data.queue;
expect(result.fnArray.length).toEqual(3); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray[0]).toEqual('setFlatness'); expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[1]).toEqual('setRenderingIntent'); expect(result.fnArray[0]).toEqual('setFlatness');
expect(result.fnArray[2]).toEqual('save'); expect(result.fnArray[1]).toEqual('setRenderingIntent');
expect(result.argsArray.length).toEqual(3); expect(result.fnArray[2]).toEqual('save');
expect(result.argsArray[0].length).toEqual(1); expect(result.argsArray.length).toEqual(3);
expect(result.argsArray[0][0]).toEqual(true); expect(result.argsArray[0].length).toEqual(1);
expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray[0][0]).toEqual(true);
expect(result.argsArray[1][0]).toEqual(false); expect(result.argsArray[1].length).toEqual(1);
expect(result.argsArray[2].length).toEqual(0); expect(result.argsArray[1][0]).toEqual(false);
expect(result.argsArray[2].length).toEqual(0);
});
}); });
}); });
describe('validateNumberOfArgs', function() { describe('validateNumberOfArgs', function() {
it('should execute if correct number of arguments', function() { it('should execute if correct number of arguments', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('5 1 d0'); var stream = new StringStream('5 1 d0');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); console.log('here!');
var promise = evaluator.getOperatorList(stream, new ResourcesMock());
expect(result.argsArray[0][0]).toEqual(5); promise.then(function(data) {
expect(result.argsArray[0][1]).toEqual(1); var result = data.queue;
expect(result.fnArray[0]).toEqual('setCharWidth'); expect(result.argsArray[0][0]).toEqual(5);
expect(result.argsArray[0][1]).toEqual(1);
expect(result.fnArray[0]).toEqual('setCharWidth');
});
}); });
it('should execute if too many arguments', function() { it('should execute if too many arguments', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('5 1 4 d0'); var stream = new StringStream('5 1 4 d0');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(result.argsArray[0][0]).toEqual(5); var result = data.queue;
expect(result.argsArray[0][1]).toEqual(1); expect(result.argsArray[0][0]).toEqual(5);
expect(result.argsArray[0][2]).toEqual(4); expect(result.argsArray[0][1]).toEqual(1);
expect(result.fnArray[0]).toEqual('setCharWidth'); expect(result.argsArray[0][2]).toEqual(4);
expect(result.fnArray[0]).toEqual('setCharWidth');
});
}); });
it('should skip if too few arguments', function() { it('should skip if too few arguments', function() {
var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), var evaluator = new PartialEvaluator(new PdfManagerMock(),
new XrefMock(), new HandlerMock(),
'prefix'); 'prefix');
var stream = new StringStream('5 d0'); var stream = new StringStream('5 d0');
var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); var promise = evaluator.getOperatorList(stream, new ResourcesMock());
promise.then(function(data) {
expect(result.argsArray).toEqual([]); var result = data.queue;
expect(result.fnArray).toEqual([]); expect(result.argsArray).toEqual([]);
expect(result.fnArray).toEqual([]);
});
}); });
}); });
}); });

3
test/unit/unit_test.html

@ -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>

168
web/viewer.css

@ -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);

16
web/viewer.html

@ -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>

84
web/viewer.js

@ -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,17 @@ 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 ('disableAutoFetch' in hashParams) {
PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true');
}
//#if !(FIREFOX || MOZCENTRAL) //#if !(FIREFOX || MOZCENTRAL)
var locale = navigator.language; var locale = navigator.language;
@ -3237,11 +3271,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

Loading…
Cancel
Save