Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement progressive loading of PDFs #2719

Merged
merged 5 commits into from
Apr 18, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/acroforms/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<head>
<!-- In production, only one script (pdf.js) is necessary -->
<!-- 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/util.js"></script>
<script type="text/javascript" src="../../src/api.js"></script>
Expand Down
3 changes: 3 additions & 0 deletions examples/helloworld/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<head>
<!-- In production, only one script (pdf.js) is necessary -->
<!-- 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/util.js"></script>
<script type="text/javascript" src="../../src/api.js"></script>
Expand Down
236 changes: 185 additions & 51 deletions extensions/firefox/components/PdfStreamConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
dump */
dump, NetworkManager */

'use strict';

Expand All @@ -37,6 +37,7 @@ const MAX_DATABASE_LENGTH = 4096;
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/NetUtil.jsm');
Cu.import('resource://pdf.js/network.js');

XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
'resource://gre/modules/PrivateBrowsingUtils.jsm');
Expand Down Expand Up @@ -190,9 +191,8 @@ PdfDataListener.prototype = {
};

// All the priviledged actions.
function ChromeActions(domWindow, dataListener, contentDispositionFilename) {
function ChromeActions(domWindow, contentDispositionFilename) {
this.domWindow = domWindow;
this.dataListener = dataListener;
this.contentDispositionFilename = contentDispositionFilename;
}

Expand Down Expand Up @@ -305,39 +305,6 @@ ChromeActions.prototype = {
getLocale: function() {
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) {
try {
// Lazy initialization of localizedStrings
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't remove the event listener because unload points to the function not the instance that was added. e.g. https://github.com/mozilla/pdf.js/blob/master/extensions/firefox/components/PdfStreamConverter.js#L497

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested. I believe this works.

});
}

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.
function RequestListener(actions) {
this.actions = actions;
Expand Down Expand Up @@ -552,11 +653,17 @@ PdfStreamConverter.prototype = {
/*
* This component works as such:
* 1. asyncConvertData stores the listener
* 2. onStartRequest creates a new channel, streams the viewer and cancels
* the request so pdf.js can do the request
* Since the request is cancelled onDataAvailable should not be called. The
* onStopRequest does nothing. The convert function just returns the stream,
* it's just the synchronous version of asyncConvertData.
* 2. onStartRequest creates a new channel, streams the viewer
* 3. If range requests are supported:
* 3.1. Suspends and cancels the request so we can issue range
* requests instead.
*
* 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
Expand All @@ -573,40 +680,57 @@ PdfStreamConverter.prototype = {
// nsIStreamListener::onDataAvailable
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
if (!this.dataListener) {
// Do nothing since all the data loading is handled by the viewer.
return;
}

var binaryStream = this.binaryStream;
binaryStream.setInputStream(aInputStream);
this.dataListener.append(binaryStream.readByteArray(aCount));
var chunk = binaryStream.readByteArray(aCount);
this.dataListener.append(chunk);
},

// nsIRequestObserver::onStartRequest
onStartRequest: function(aRequest, aContext) {
// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's only allow disabling this if pdfBugEnabled. We generally try not to let the url affect behaviour. If someone really wants to disable range request then they shouldn't have it in the http headers.

}
} catch (e) {}
aRequest.QueryInterface(Ci.nsIChannel);

aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
// Creating storage for PDF data
var contentLength = aRequest.contentLength;
var dataListener = new PdfDataListener(contentLength);
var contentDispositionFilename;
try {
contentDispositionFilename = aRequest.contentDispositionFilename;
} 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.
aRequest.setProperty('contentType', aRequest.contentType);
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.
var ioService = Services.io;
var channel = ioService.newChannel(
PDF_VIEWER_WEB_PAGE, null, null);

var self = this;
var listener = this.listener;
// Proxy all the request observer calls, when it gets to onStopRequest
// we can get the dom window. We also intentionally pass on the original
Expand All @@ -625,8 +749,18 @@ PdfStreamConverter.prototype = {
var domWindow = getDOMWindow(channel);
// Double check the url is still the correct one.
if (domWindow.document.documentURIObject.equals(aRequest.URI)) {
var actions = new ChromeActions(domWindow, dataListener,
contentDispositionFilename);
var actions;
if (acceptRanges) {
// We are going to be issuing range requests, so cancel the
// original request
aRequest.resume();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to resume before cancelling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure if there was an issue without this line, so I think this is necessary. I'll test without it and let you know what the issue is.

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);
domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
requestListener.receive(event);
Expand Down
Loading