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/node_stream.js b/src/display/node_stream.js index 0091573382d35d..a55b8f1655fbd6 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,10 @@ class BaseFullReader { this._length = stream.source.length; this._loaded = 0; - this._fullRequest = null; + this._isStreamingSupported = !stream.source.disableStream; + this._isRangeSupported = !stream.options.disableRange; + + this._readableStream = null; this._readCapability = createPromiseCapability(); this._headersCapability = createPromiseCapability(); } @@ -104,7 +107,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(); @@ -121,8 +124,7 @@ class BaseFullReader { } cancel(reason) { - this._fullRequest.close(reason); - this._fullRequest.destroy(reason); + this._readableStream.destroy(reason); } } @@ -133,38 +135,14 @@ class BaseRangeReader { this._errored = false; this._reason = null; this.onProgress = null; - this._length = stream.source.length; this._loaded = 0; this._readCapability = createPromiseCapability(); - } - get isStreamingSupported() { - return false; + this._isStreamingSupported = !stream.source.disableStream; } - read() { - return this._readCapability.promise.then(() => { - if (this._done) { - return Promise.resolve({ value: undefined, done: true, }); - } - if (this._errored) { - return Promise.reject(this._reason); - } - - let chunk = this._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, - }); - } - return Promise.resolve({ value: chunk, done: false, }); - }); + get isStreamingSupported() { + return this._isStreamingSupported; } } @@ -178,9 +156,6 @@ class PDFNodeStreamFullReader extends BaseFullReader { this._disableRange = true; } - this._isStreamingSupported = !stream.source.disableStream; - this._isRangeSupported = false; - let options = { host: this._url.host, path: this._url.path, @@ -190,7 +165,7 @@ class PDFNodeStreamFullReader extends BaseFullReader { let handleResponse = (response) => { this._headersCapability.resolve(); - this._fullRequest = response; + this._readableStream = response; response.on('readable', () => { this._readCapability.resolve(); @@ -208,20 +183,7 @@ class PDFNodeStreamFullReader extends BaseFullReader { this._reason = reason; this._readCapability.resolve(); }); - }; - - this._request = this._url.protocol === 'http:' ? - http.request(options, handleResponse) : - https.request(options, handleResponse); - - this._request.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._headersCapability.reject(reason); - }); - this._request.end(); - this._headersCapability.promise.then(() => { let { allowRangeRequests, suggestedLength, } = validateRangeRequestCapabilities({ getResponseHeader: this.getResponseHeader.bind(this), @@ -234,11 +196,22 @@ class PDFNodeStreamFullReader extends BaseFullReader { this._isRangeSupported = true; } this._length = suggestedLength; + }; + + this._request = this._url.protocol === 'http:' ? + http.request(options, handleResponse) : + https.request(options, handleResponse); + + this._request.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._headersCapability.reject(reason); }); + this._request.end(); } - getReasponseHeader(name) { - return this._fullRequest.headers[name]; + getResponseHeader(name) { + return this._readableStream.headers[name]; } } @@ -246,8 +219,7 @@ class PDFNodeStreamRangeReader extends BaseRangeReader { constructor(stream, start, end) { super(stream); - this._rangeRequest = null; - this._read = null; + this._readableStream = null; let rangeStr = start + '-' + (end - 1); stream.httpHeaders['Range'] = 'bytes=' + rangeStr; @@ -258,8 +230,7 @@ class PDFNodeStreamRangeReader extends BaseRangeReader { headers: stream.httpHeaders, }; let handleResponse = (response) => { - this._rangeRequest = response; - this._read = this._rangeRequest.read; + this._readableStream = response; response.on('readable', () => { this._readCapability.resolve(); @@ -288,9 +259,30 @@ class PDFNodeStreamRangeReader extends BaseRangeReader { this._request.end(); } + read() { + return this._readCapability.promise.then(() => { + if (this._done) { + return Promise.resolve({ value: undefined, done: true, }); + } + if (this._errored) { + return Promise.reject(this._reason); + } + + 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, }); + } + return Promise.resolve({ value: chunk, done: false, }); + }); + } + cancel(reason) { - this._rangeRequest.close(reason); - this._rangeRequest.destroy(reason); + this._readableStream.destroy(reason); } } @@ -298,9 +290,7 @@ class PDFNodeStreamFsFullReader extends BaseFullReader { constructor(stream) { super(stream); - this._isRangeSupported = true; - this._isStreamingSupported = true; - this._fullRequest = fs.createReadStream(this._url.path); + this._readableStream = fs.createReadStream(this._url.path); fs.lstat(this._url.path, (error, stat) => { if (error) { @@ -313,17 +303,17 @@ class PDFNodeStreamFsFullReader extends BaseFullReader { this._headersCapability.resolve(); }); - this._fullRequest.on('readable', () => { + this._readableStream.on('readable', () => { this._readCapability.resolve(); }); - this._fullRequest.on('end', () => { - this._fullRequest.destroy(); + this._readableStream.on('end', () => { + this._readableStream.destroy(); this._done = true; this._readCapability.resolve(); }); - this._fullRequest.on('error', (reason) => { + this._readableStream.on('error', (reason) => { this._errored = true; this._reason = reason; this._readCapability.resolve(); @@ -335,7 +325,7 @@ class PDFNodeStreamFsRangeReader extends BaseRangeReader { constructor(stream, start, end) { super(stream); - this._rangeRequest = fs.createReadStream(this._url.path, { start, end, }); + this._readableStream = fs.createReadStream(this._url.path, { start, end, }); fs.lstat(this._url.path, (error, stat) => { if (error) { this._errored = true; @@ -344,28 +334,48 @@ class PDFNodeStreamFsRangeReader extends BaseRangeReader { } this._length = stat.size; }); - this._read = this._rangeRequest.read; - this._rangeRequest.on('readable', () => { + this._readableStream.on('readable', () => { this._readCapability.resolve(); }); - this._rangeRequest.on('end', () => { - this._rangeRequest.destroy(); + this._readableStream.on('end', () => { + this._readableStream.destroy(); this._done = true; this._readCapability.resolve(); }); - this._rangeRequest.on('error', (reason) => { + this._readableStream.on('error', (reason) => { this._errored = true; this._reason = reason; this._readCapability.resolve(); }); } + read() { + return this._readCapability.promise.then(() => { + if (this._done) { + return Promise.resolve({ value: undefined, done: true, }); + } + if (this._errored) { + return Promise.reject(this._reason); + } + + 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, }); + } + return Promise.resolve({ value: chunk, done: false, }); + }); + } + cancel(reason) { - this._rangeRequest.close(reason); - this._rangeRequest.destroy(reason); + this._readableStream.destroy(reason); } } 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); + }); + }); +});