Skip to content

Commit 49ff029

Browse files
committed
Merge pull request mozilla#2719 from mduan/chunked
Implement progressive loading of PDFs
2 parents 369b81b + 2ce0027 commit 49ff029

29 files changed

+3228
-1310
lines changed

examples/acroforms/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<head>
55
<!-- In production, only one script (pdf.js) is necessary -->
66
<!-- In production, change the content of PDFJS.workerSrc below -->
7+
<script type="text/javascript" src="../../src/network.js"></script>
8+
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
9+
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
710
<script type="text/javascript" src="../../src/core.js"></script>
811
<script type="text/javascript" src="../../src/util.js"></script>
912
<script type="text/javascript" src="../../src/api.js"></script>

examples/helloworld/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<head>
55
<!-- In production, only one script (pdf.js) is necessary -->
66
<!-- In production, change the content of PDFJS.workerSrc below -->
7+
<script type="text/javascript" src="../../src/network.js"></script>
8+
<script type="text/javascript" src="../../src/chunked_stream.js"></script>
9+
<script type="text/javascript" src="../../src/pdf_manager.js"></script>
710
<script type="text/javascript" src="../../src/core.js"></script>
811
<script type="text/javascript" src="../../src/util.js"></script>
912
<script type="text/javascript" src="../../src/api.js"></script>

extensions/firefox/components/PdfStreamConverter.js

Lines changed: 185 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
/* jshint esnext:true */
1818
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
19-
dump */
19+
dump, NetworkManager */
2020

2121
'use strict';
2222

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

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

192193
// All the priviledged actions.
193-
function ChromeActions(domWindow, dataListener, contentDispositionFilename) {
194+
function ChromeActions(domWindow, contentDispositionFilename) {
194195
this.domWindow = domWindow;
195-
this.dataListener = dataListener;
196196
this.contentDispositionFilename = contentDispositionFilename;
197197
}
198198

@@ -305,39 +305,6 @@ ChromeActions.prototype = {
305305
getLocale: function() {
306306
return getStringPref('general.useragent.locale', 'en-US');
307307
},
308-
getLoadingType: function() {
309-
return this.dataListener ? 'passive' : 'active';
310-
},
311-
initPassiveLoading: function() {
312-
if (!this.dataListener)
313-
return false;
314-
315-
var domWindow = this.domWindow;
316-
this.dataListener.onprogress =
317-
function ChromeActions_dataListenerProgress(loaded, total) {
318-
319-
domWindow.postMessage({
320-
pdfjsLoadAction: 'progress',
321-
loaded: loaded,
322-
total: total
323-
}, '*');
324-
};
325-
326-
var self = this;
327-
this.dataListener.oncomplete =
328-
function ChromeActions_dataListenerComplete(data, errorCode) {
329-
330-
domWindow.postMessage({
331-
pdfjsLoadAction: 'complete',
332-
data: data,
333-
errorCode: errorCode
334-
}, '*');
335-
336-
delete self.dataListener;
337-
};
338-
339-
return true;
340-
},
341308
getStrings: function(data) {
342309
try {
343310
// Lazy initialization of localizedStrings
@@ -436,6 +403,140 @@ ChromeActions.prototype = {
436403
}
437404
};
438405

406+
var RangedChromeActions = (function RangedChromeActionsClosure() {
407+
/**
408+
* This is for range requests
409+
*/
410+
function RangedChromeActions(
411+
domWindow, contentDispositionFilename, originalRequest) {
412+
413+
ChromeActions.call(this, domWindow, contentDispositionFilename);
414+
415+
this.pdfUrl = originalRequest.URI.resolve('');
416+
this.contentLength = originalRequest.contentLength;
417+
418+
// Pass all the headers from the original request through
419+
var httpHeaderVisitor = {
420+
headers: {},
421+
visitHeader: function(aHeader, aValue) {
422+
if (aHeader === 'Range') {
423+
// When loading the PDF from cache, firefox seems to set the Range
424+
// request header to fetch only the unfetched portions of the file
425+
// (e.g. 'Range: bytes=1024-'). However, we want to set this header
426+
// manually to fetch the PDF in chunks.
427+
return;
428+
}
429+
this.headers[aHeader] = aValue;
430+
}
431+
};
432+
originalRequest.visitRequestHeaders(httpHeaderVisitor);
433+
434+
var getXhr = function getXhr() {
435+
const XMLHttpRequest = Components.Constructor(
436+
'@mozilla.org/xmlextras/xmlhttprequest;1');
437+
return new XMLHttpRequest();
438+
};
439+
440+
this.networkManager = new NetworkManager(this.pdfUrl, {
441+
httpHeaders: httpHeaderVisitor.headers,
442+
getXhr: getXhr
443+
});
444+
445+
var self = this;
446+
// If we are in range request mode, this means we manually issued xhr
447+
// requests, which we need to abort when we leave the page
448+
domWindow.addEventListener('unload', function unload(e) {
449+
self.networkManager.abortAllRequests();
450+
domWindow.removeEventListener(e.type, unload);
451+
});
452+
}
453+
454+
RangedChromeActions.prototype = Object.create(ChromeActions.prototype);
455+
var proto = RangedChromeActions.prototype;
456+
proto.constructor = RangedChromeActions;
457+
458+
proto.initPassiveLoading = function RangedChromeActions_initPassiveLoading() {
459+
this.domWindow.postMessage({
460+
pdfjsLoadAction: 'supportsRangedLoading',
461+
pdfUrl: this.pdfUrl,
462+
length: this.contentLength
463+
}, '*');
464+
465+
return true;
466+
};
467+
468+
proto.requestDataRange = function RangedChromeActions_requestDataRange(args) {
469+
var begin = args.begin;
470+
var end = args.end;
471+
var domWindow = this.domWindow;
472+
// TODO(mack): Support error handler. We're not currently not handling
473+
// errors from chrome code for non-range requests, so this doesn't
474+
// seem high-pri
475+
this.networkManager.requestRange(begin, end, {
476+
onDone: function RangedChromeActions_onDone(args) {
477+
domWindow.postMessage({
478+
pdfjsLoadAction: 'range',
479+
begin: args.begin,
480+
chunk: args.chunk
481+
}, '*');
482+
}
483+
});
484+
};
485+
486+
return RangedChromeActions;
487+
})();
488+
489+
var StandardChromeActions = (function StandardChromeActionsClosure() {
490+
491+
/**
492+
* This is for a single network stream
493+
*/
494+
function StandardChromeActions(domWindow, contentDispositionFilename,
495+
dataListener) {
496+
497+
ChromeActions.call(this, domWindow, contentDispositionFilename);
498+
this.dataListener = dataListener;
499+
}
500+
501+
StandardChromeActions.prototype = Object.create(ChromeActions.prototype);
502+
var proto = StandardChromeActions.prototype;
503+
proto.constructor = StandardChromeActions;
504+
505+
proto.initPassiveLoading =
506+
function StandardChromeActions_initPassiveLoading() {
507+
508+
if (!this.dataListener) {
509+
return false;
510+
}
511+
512+
var self = this;
513+
514+
this.dataListener.onprogress = function ChromeActions_dataListenerProgress(
515+
loaded, total) {
516+
self.domWindow.postMessage({
517+
pdfjsLoadAction: 'progress',
518+
loaded: loaded,
519+
total: total
520+
}, '*');
521+
};
522+
523+
this.dataListener.oncomplete = function ChromeActions_dataListenerComplete(
524+
data, errorCode) {
525+
self.domWindow.postMessage({
526+
pdfjsLoadAction: 'complete',
527+
data: data,
528+
errorCode: errorCode
529+
}, '*');
530+
531+
delete self.dataListener;
532+
};
533+
534+
return true;
535+
};
536+
537+
return StandardChromeActions;
538+
})();
539+
439540
// Event listener to trigger chrome privedged code.
440541
function RequestListener(actions) {
441542
this.actions = actions;
@@ -552,11 +653,17 @@ PdfStreamConverter.prototype = {
552653
/*
553654
* This component works as such:
554655
* 1. asyncConvertData stores the listener
555-
* 2. onStartRequest creates a new channel, streams the viewer and cancels
556-
* the request so pdf.js can do the request
557-
* Since the request is cancelled onDataAvailable should not be called. The
558-
* onStopRequest does nothing. The convert function just returns the stream,
559-
* it's just the synchronous version of asyncConvertData.
656+
* 2. onStartRequest creates a new channel, streams the viewer
657+
* 3. If range requests are supported:
658+
* 3.1. Suspends and cancels the request so we can issue range
659+
* requests instead.
660+
*
661+
* If range rquests are not supported:
662+
* 3.1. Read the stream as it's loaded in onDataAvailable to send
663+
* to the viewer
664+
*
665+
* The convert function just returns the stream, it's just the synchronous
666+
* version of asyncConvertData.
560667
*/
561668

562669
// nsIStreamConverter::convert
@@ -573,40 +680,57 @@ PdfStreamConverter.prototype = {
573680
// nsIStreamListener::onDataAvailable
574681
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
575682
if (!this.dataListener) {
576-
// Do nothing since all the data loading is handled by the viewer.
577683
return;
578684
}
579685

580686
var binaryStream = this.binaryStream;
581687
binaryStream.setInputStream(aInputStream);
582-
this.dataListener.append(binaryStream.readByteArray(aCount));
688+
var chunk = binaryStream.readByteArray(aCount);
689+
this.dataListener.append(chunk);
583690
},
584691

585692
// nsIRequestObserver::onStartRequest
586693
onStartRequest: function(aRequest, aContext) {
587694
// Setup the request so we can use it below.
695+
var acceptRanges = false;
696+
try {
697+
aRequest.QueryInterface(Ci.nsIHttpChannel);
698+
if (aRequest.getResponseHeader('Accept-Ranges') === 'bytes') {
699+
var hash = aRequest.URI.ref;
700+
acceptRanges = hash.indexOf('disableRange=true') < 0;
701+
}
702+
} catch (e) {}
588703
aRequest.QueryInterface(Ci.nsIChannel);
704+
589705
aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
590-
// Creating storage for PDF data
591-
var contentLength = aRequest.contentLength;
592-
var dataListener = new PdfDataListener(contentLength);
593706
var contentDispositionFilename;
594707
try {
595708
contentDispositionFilename = aRequest.contentDispositionFilename;
596709
} catch (e) {}
597-
this.dataListener = dataListener;
598-
this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
599-
.createInstance(Ci.nsIBinaryInputStream);
600710

601711
// Change the content type so we don't get stuck in a loop.
602712
aRequest.setProperty('contentType', aRequest.contentType);
603713
aRequest.contentType = 'text/html';
604714

715+
if (!acceptRanges) {
716+
// Creating storage for PDF data
717+
var contentLength = aRequest.contentLength;
718+
this.dataListener = new PdfDataListener(contentLength);
719+
this.binaryStream = Cc['@mozilla.org/binaryinputstream;1']
720+
.createInstance(Ci.nsIBinaryInputStream);
721+
} else {
722+
// Suspend the request so we're not consuming any of the stream,
723+
// but we can't cancel the request yet. Otherwise, the original
724+
// listener will think we do not want to go the new PDF url
725+
aRequest.suspend();
726+
}
727+
605728
// Create a new channel that is viewer loaded as a resource.
606729
var ioService = Services.io;
607730
var channel = ioService.newChannel(
608731
PDF_VIEWER_WEB_PAGE, null, null);
609732

733+
var self = this;
610734
var listener = this.listener;
611735
// Proxy all the request observer calls, when it gets to onStopRequest
612736
// we can get the dom window. We also intentionally pass on the original
@@ -625,8 +749,18 @@ PdfStreamConverter.prototype = {
625749
var domWindow = getDOMWindow(channel);
626750
// Double check the url is still the correct one.
627751
if (domWindow.document.documentURIObject.equals(aRequest.URI)) {
628-
var actions = new ChromeActions(domWindow, dataListener,
629-
contentDispositionFilename);
752+
var actions;
753+
if (acceptRanges) {
754+
// We are going to be issuing range requests, so cancel the
755+
// original request
756+
aRequest.resume();
757+
aRequest.cancel(Cr.NS_BINDING_ABORTED);
758+
actions = new RangedChromeActions(domWindow,
759+
contentDispositionFilename, aRequest);
760+
} else {
761+
actions = new StandardChromeActions(
762+
domWindow, contentDispositionFilename, self.dataListener);
763+
}
630764
var requestListener = new RequestListener(actions);
631765
domWindow.addEventListener(PDFJS_EVENT_ID, function(event) {
632766
requestListener.receive(event);

0 commit comments

Comments
 (0)