-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Changes from all commits
ef423ef
6b2c6fc
dbccbaa
27fe18e
2ce0027
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ | |
*/ | ||
/* jshint esnext:true */ | ||
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils, | ||
dump */ | ||
dump, NetworkManager */ | ||
|
||
'use strict'; | ||
|
||
|
@@ -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'); | ||
|
@@ -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; | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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. | ||
function RequestListener(actions) { | ||
this.actions = actions; | ||
|
@@ -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 | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have to resume before cancelling? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.