diff --git a/src/display/network.js b/src/display/network.js index be347b32e60452..37cbf906eb114b 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -351,18 +351,16 @@ function PDFNetworkStreamFullRequestReader(manager, options) { } PDFNetworkStreamFullRequestReader.prototype = { - getResponseHeader(name) { - let fullRequestXhrId = this._fullRequestId; - let fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); - - return fullRequestXhr.getResponseHeader(name); - }, - _onHeadersReceived: function PDFNetworkStreamFullRequestReader_onHeadersReceived() { + var fullRequestXhrId = this._fullRequestId; + var fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); + let { allowRangeRequests, suggestedLength, } = validateRangeRequestCapabilities({ - getResponseHeader: this.getResponseHeader.bind(this), + getResponseHeader: (name) => { + return fullRequestXhr.getResponseHeader(name); + }, isHttp: this._manager.isHttp, rangeChunkSize: this._rangeChunkSize, disableRange: this._disableRange, @@ -376,7 +374,6 @@ PDFNetworkStreamFullRequestReader.prototype = { } var networkManager = this._manager; - var fullRequestXhrId = this._fullRequestId; if (networkManager.isStreamingRequest(fullRequestXhrId)) { // We can continue fetching when progressive loading is enabled, // and we don't need the autoFetch feature. diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 9d8ebe1c0a9cd5..721afa4ccb8827 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -13,10 +13,11 @@ * limitations under the License. */ -import { isInt } from '../shared/util'; +import { assert, isInt } from '../shared/util'; function validateRangeRequestCapabilities({ getResponseHeader, isHttp, rangeChunkSize, disableRange, }) { + assert(rangeChunkSize > 0); let returnValues = { allowRangeRequests: false, suggestedLength: undefined, diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 0091573382d35d..964aff05b268fc 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -29,7 +29,7 @@ class PDFNodeStream { this.url = url.parse(this.source.url); this.isHttp = this.url.protocol === 'http:' || this.url.protocol === 'https:'; - this.isFsUrl = !this.url.host; + this.isFsUrl = this.url.protocol === 'file:' || !this.url.host; this.httpHeaders = (this.isHttp && this.source.httpHeaders) || {}; this._fullRequest = null; @@ -74,7 +74,16 @@ class BaseFullReader { this._length = stream.source.length; this._loaded = 0; - this._fullRequest = null; + this._disableRange = stream.options.disableRange || false; + this._rangeChunkSize = stream.source.rangeChunkSize; + if (!this._rangeChunkSize && !this._disableRange) { + this._disableRange = true; + } + + this._isStreamingSupported = !stream.source.disableStream; + this._isRangeSupported = !stream.options.disableRange; + + this._readableStream = null; this._readCapability = createPromiseCapability(); this._headersCapability = createPromiseCapability(); } @@ -104,7 +113,7 @@ class BaseFullReader { return Promise.reject(this._reason); } - let chunk = this._fullRequest.read(); + let chunk = this._readableStream.read(); if (chunk === null) { this._readCapability = createPromiseCapability(); return this.read(); @@ -116,13 +125,46 @@ class BaseFullReader { total: this._length, }); } - return Promise.resolve({ value: chunk, done: false, }); + let buffer = new Uint8Array(chunk).buffer; + return Promise.resolve({ value: buffer, done: false, }); }); } cancel(reason) { - this._fullRequest.close(reason); - this._fullRequest.destroy(reason); + if (!this._readableStream) { + this._error(reason); + return; + } + this._readableStream.destroy(reason); + } + + _error(reason) { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + } + + _setReadableStream(readableStream) { + this._readableStream = readableStream; + readableStream.on('readable', () => { + this._readCapability.resolve(); + }); + + readableStream.on('end', () => { + // Destroy readable to minimize resource usage. + readableStream.destroy(); + this._done = true; + this._readCapability.resolve(); + }); + + readableStream.on('error', (reason) => { + this._error(reason); + }); + + // Destroy ReadableStream if it already in errored state. + if (this._errored) { + this._readableStream.destroy(this._reason); + } } } @@ -133,13 +175,15 @@ class BaseRangeReader { this._errored = false; this._reason = null; this.onProgress = null; - this._length = stream.source.length; this._loaded = 0; + this._readableStream = null; this._readCapability = createPromiseCapability(); + + this._isStreamingSupported = !stream.source.disableStream; } get isStreamingSupported() { - return false; + return this._isStreamingSupported; } read() { @@ -151,80 +195,81 @@ class BaseRangeReader { return Promise.reject(this._reason); } - let chunk = this._read(); + let chunk = this._readableStream.read(); if (chunk === null) { this._readCapability = createPromiseCapability(); return this.read(); } this._loaded += chunk.length; if (this.onProgress) { - this.onProgress({ - loaded: this._loaded, - total: this._length, - }); + this.onProgress({ loaded: this._loaded, }); } - return Promise.resolve({ value: chunk, done: false, }); + let buffer = new Uint8Array(chunk).buffer; + return Promise.resolve({ value: buffer, done: false, }); }); } -} -class PDFNodeStreamFullReader extends BaseFullReader { - constructor(stream) { - super(stream); - - this._disableRange = stream.options.disableRange || false; - this._rangeChunkSize = stream.source.rangeChunkSize; - if (!this._rangeChunkSize && !this._disableRange) { - this._disableRange = true; + cancel(reason) { + if (!this._readableStream) { + this._error(reason); + return; } + this._readableStream.destroy(reason); + } - this._isStreamingSupported = !stream.source.disableStream; - this._isRangeSupported = false; + _error(reason) { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + } - let options = { - host: this._url.host, - path: this._url.path, - method: 'GET', - headers: stream.httpHeaders, - }; + _setReadableStream(readableStream) { + this._readableStream = readableStream; + readableStream.on('readable', () => { + this._readCapability.resolve(); + }); - let handleResponse = (response) => { - this._headersCapability.resolve(); - this._fullRequest = response; + readableStream.on('end', () => { + readableStream.destroy(); + this._done = true; + this._readCapability.resolve(); + }); - response.on('readable', () => { - this._readCapability.resolve(); - }); + readableStream.on('error', (reason) => { + this._error(reason); + }); - response.on('end', () => { - // Destroy response to minimize resource usage. - response.destroy(); - this._done = true; - this._readCapability.resolve(); - }); + if (this._errored) { + this._readableStream.destroy(this._reason); + } + } +} - response.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); - }; +function createRequestOptions(url, headers) { + return { + protocol: url.protocol, + auth: url.auth, + host: url.host, + port: url.port, + path: url.path, + method: 'GET', + headers, + }; +} - this._request = this._url.protocol === 'http:' ? - http.request(options, handleResponse) : - https.request(options, handleResponse); +class PDFNodeStreamFullReader extends BaseFullReader { + constructor(stream) { + super(stream); - this._request.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._headersCapability.reject(reason); - }); - this._request.end(); + let handleResponse = (response) => { + this._headersCapability.resolve(); + this._setReadableStream(response); - this._headersCapability.promise.then(() => { let { allowRangeRequests, suggestedLength, } = validateRangeRequestCapabilities({ - getResponseHeader: this.getResponseHeader.bind(this), + getResponseHeader: (name) => { + return this._readableStream.headers[name]; + }, isHttp: stream.isHttp, rangeChunkSize: this._rangeChunkSize, disableRange: this._disableRange, @@ -234,11 +279,23 @@ class PDFNodeStreamFullReader extends BaseFullReader { this._isRangeSupported = true; } this._length = suggestedLength; - }); - } + }; + + this._request = null; + if (this._url.protocol === 'http:') { + this._request = http.request(createRequestOptions( + this._url, stream.httpHeaders), handleResponse); + } else { + this._request = https.request(createRequestOptions( + this._url, stream.httpHeaders), handleResponse); + } - getReasponseHeader(name) { - return this._fullRequest.headers[name]; + this._request.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._headersCapability.reject(reason); + }); + this._request.end(); } } @@ -246,40 +303,29 @@ class PDFNodeStreamRangeReader extends BaseRangeReader { constructor(stream, start, end) { super(stream); - this._rangeRequest = null; - this._read = null; - let rangeStr = start + '-' + (end - 1); - stream.httpHeaders['Range'] = 'bytes=' + rangeStr; - - let options = { - host: this._url.host, - path: this._url.path, - method: 'GET', - headers: stream.httpHeaders, - }; - let handleResponse = (response) => { - this._rangeRequest = response; - this._read = this._rangeRequest.read; - - response.on('readable', () => { - this._readCapability.resolve(); - }); - - response.on('end', () => { - response.destroy(); - this._done = true; - this._readCapability.resolve(); - }); + this._readableStream = null; + this._httpHeaders = {}; + for (let property in stream.httpHeaders) { + let value = stream.httpHeaders[property]; + if (typeof value === 'undefined') { + continue; + } + this._httpHeaders[property] = value; + } + this._httpHeaders['Range'] = `bytes=${start}-${end - 1}`; - response.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); - }; - this._request = this._url.protocol === 'http:' ? - http.request(options, handleResponse) : - https.request(options, handleResponse); + this._request = null; + if (this._url.protocol === 'http:') { + this._request = http.request(createRequestOptions( + this._url, this._httpHeaders), (response) => { + this._setReadableStream(response); + }); + } else { + this._request = https.request(createRequestOptions( + this._url, this._httpHeaders), (response) => { + this._setReadableStream(response); + }); + } this._request.on('error', (reason) => { this._errored = true; @@ -287,20 +333,13 @@ class PDFNodeStreamRangeReader extends BaseRangeReader { }); this._request.end(); } - - cancel(reason) { - this._rangeRequest.close(reason); - this._rangeRequest.destroy(reason); - } } class PDFNodeStreamFsFullReader extends BaseFullReader { constructor(stream) { super(stream); - this._isRangeSupported = true; - this._isStreamingSupported = true; - this._fullRequest = fs.createReadStream(this._url.path); + this._setReadableStream(fs.createReadStream(this._url.path)); fs.lstat(this._url.path, (error, stat) => { if (error) { @@ -312,22 +351,6 @@ class PDFNodeStreamFsFullReader extends BaseFullReader { this._length = stat.size; this._headersCapability.resolve(); }); - - this._fullRequest.on('readable', () => { - this._readCapability.resolve(); - }); - - this._fullRequest.on('end', () => { - this._fullRequest.destroy(); - this._done = true; - this._readCapability.resolve(); - }); - - this._fullRequest.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); } } @@ -335,37 +358,8 @@ class PDFNodeStreamFsRangeReader extends BaseRangeReader { constructor(stream, start, end) { super(stream); - this._rangeRequest = fs.createReadStream(this._url.path, { start, end, }); - fs.lstat(this._url.path, (error, stat) => { - if (error) { - this._errored = true; - this._reason = error; - return; - } - this._length = stat.size; - }); - this._read = this._rangeRequest.read; - - this._rangeRequest.on('readable', () => { - this._readCapability.resolve(); - }); - - this._rangeRequest.on('end', () => { - this._rangeRequest.destroy(); - this._done = true; - this._readCapability.resolve(); - }); - - this._rangeRequest.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); - } - - cancel(reason) { - this._rangeRequest.close(reason); - this._rangeRequest.destroy(reason); + this._setReadableStream( + fs.createReadStream(this._url.path, { start, end, })); } } diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 59751ddb2a8723..6ec9ef943fb4e4 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -14,6 +14,7 @@ "fonts_spec.js", "function_spec.js", "murmurhash3_spec.js", + "node_stream_spec.js", "parser_spec.js", "primitives_spec.js", "stream_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 2bd0149b221357..9f20761ee528f1 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -43,6 +43,8 @@ function initializePDFJS(callback) { Promise.all([ 'pdfjs/display/global', + 'pdfjs/display/api', + 'pdfjs/display/network', 'pdfjs-test/unit/annotation_spec', 'pdfjs-test/unit/api_spec', 'pdfjs-test/unit/bidi_spec', @@ -71,7 +73,11 @@ function initializePDFJS(callback) { return SystemJS.import(moduleName); })).then(function (modules) { var displayGlobal = modules[0]; + var displayApi = modules[1]; + var PDFNetworkStream = modules[2].PDFNetworkStream; + // Set network stream class for unit tests. + displayApi.setPDFNetworkStreamClass(PDFNetworkStream); // Configure the worker. displayGlobal.PDFJS.workerSrc = '../../build/generic/build/pdf.worker.js'; // Opt-in to using the latest API. diff --git a/test/unit/node_stream_spec.js b/test/unit/node_stream_spec.js new file mode 100644 index 00000000000000..6b2fd955d49556 --- /dev/null +++ b/test/unit/node_stream_spec.js @@ -0,0 +1,191 @@ +/* Copyright 2017 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 __non_webpack_require__ */ + +import { isNodeJS } from '../../src/shared/util'; +import { PDFNodeStream } from '../../src/display/node_stream'; + +describe('node_stream', function() { + let pdf1 = + 'http://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'; + let pdfLength = 1016315; + let pdf2; + if (!isNodeJS()) { + pdf2 = new URL('../pdfs/tracemonkey.pdf', window.location).href; + } else { + let path = __non_webpack_require__('path'); + let url = __non_webpack_require__('url'); + pdf2 = + url.parse(path.join(process.cwd(), './test/pdfs/tracemonkey.pdf')).href; + } + + it('read both http(s) and filesystem pdf files', function(done) { + let stream1 = new PDFNodeStream({ + source: { + url: pdf1, + rangeChunkSize: 65536, + disableStream: true, + }, + disableRange: true, + }); + + let stream2 = new PDFNodeStream({ + source: { + url: pdf2, + rangeChunkSize: 65536, + disableStream: true, + }, + disableRange: true, + }); + + let fullReader1 = stream1.getFullReader(); + let fullReader2 = stream2.getFullReader(); + + let isStreamingSupported1, isRangeSupported1; + let promise1 = fullReader1.headersReady.then(() => { + isStreamingSupported1 = fullReader1.isStreamingSupported; + isRangeSupported1 = fullReader1.isRangeSupported; + }); + + let isStreamingSupported2, isRangeSupported2; + let promise2 = fullReader2.headersReady.then(() => { + isStreamingSupported2 = fullReader2.isStreamingSupported; + isRangeSupported2 = fullReader2.isRangeSupported; + }); + + let len1 = 0, len2 = 0; + let read1 = function () { + return fullReader1.read().then(function (result) { + if (result.done) { + return; + } + len1 += result.value.byteLength; + return read1(); + }); + }; + let read2 = function () { + return fullReader2.read().then(function (result) { + if (result.done) { + return; + } + len2 += result.value.byteLength; + return read2(); + }); + }; + + let readPromise = Promise.all([read1(), read2(), promise1, promise2]); + readPromise.then((result) => { + expect(isStreamingSupported1).toEqual(false); + expect(isRangeSupported1).toEqual(false); + expect(isStreamingSupported2).toEqual(false); + expect(isRangeSupported2).toEqual(false); + expect(len1).toEqual(pdfLength); + expect(len1).toEqual(len2); + done(); + }).catch((reason) => { + done.fail(reason); + }); + }); + + it('read custom ranges for both http(s) and filesystem urls', + function(done) { + let rangeSize = 32768; + let stream1 = new PDFNodeStream({ + source: { + url: pdf1, + length: pdfLength, + rangeChunkSize: rangeSize, + disableStream: true, + }, + disableRange: false, + }); + let stream2 = new PDFNodeStream({ + source: { + url: pdf2, + length: pdfLength, + rangeChunkSize: rangeSize, + disableStream: true, + }, + disableRange: false, + }); + + let fullReader1 = stream1.getFullReader(); + let fullReader2 = stream2.getFullReader(); + + let isStreamingSupported1, isRangeSupported1, fullReaderCancelled1; + let isStreamingSupported2, isRangeSupported2, fullReaderCancelled2; + + let promise1 = fullReader1.headersReady.then(function () { + isStreamingSupported1 = fullReader1.isStreamingSupported; + isRangeSupported1 = fullReader1.isRangeSupported; + // we shall be able to close the full reader without issues + fullReader1.cancel('Don\'t need full reader'); + fullReaderCancelled1 = true; + }); + + let promise2 = fullReader2.headersReady.then(function () { + isStreamingSupported2 = fullReader2.isStreamingSupported; + isRangeSupported2 = fullReader2.isRangeSupported; + fullReader2.cancel('Don\'t need full reader'); + fullReaderCancelled2 = true; + }); + + // Skipping fullReader results, requesting something from the PDF end. + let tailSize = (pdfLength % rangeSize) || rangeSize; + + let range11Reader = stream1.getRangeReader(pdfLength - tailSize - rangeSize, + pdfLength - tailSize); + let range12Reader = stream1.getRangeReader(pdfLength - tailSize, pdfLength); + + let range21Reader = stream2.getRangeReader(pdfLength - tailSize - rangeSize, + pdfLength - tailSize - 1); + let range22Reader = stream2.getRangeReader(pdfLength - tailSize, pdfLength); + + let result11 = { value: 0, }, result12 = { value: 0, }; + let result21 = { value: 0, }, result22 = { value: 0, }; + + let read = function (reader, lenResult) { + return reader.read().then(function (result) { + if (result.done) { + return; + } + lenResult.value += result.value.byteLength; + return read(reader, lenResult); + }); + }; + + let readPromises = Promise.all([read(range11Reader, result11), + read(range12Reader, result12), + read(range21Reader, result21), + read(range22Reader, result22), + promise1, promise2]); + + readPromises.then(function () { + expect(result11.value).toEqual(rangeSize); + expect(result12.value).toEqual(tailSize); + expect(result21.value).toEqual(rangeSize); + expect(result22.value).toEqual(tailSize); + expect(isStreamingSupported1).toEqual(false); + expect(isRangeSupported1).toEqual(true); + expect(fullReaderCancelled1).toEqual(true); + expect(isStreamingSupported2).toEqual(false); + expect(isRangeSupported2).toEqual(true); + expect(fullReaderCancelled2).toEqual(true); + done(); + }).catch(function (reason) { + done.fail(reason); + }); + }); +});