From 4451a9784d8eb6ed6e1e40f2c9ab162a46cfb09b Mon Sep 17 00:00:00 2001 From: Mukul Mishra Date: Thu, 10 Aug 2017 13:00:54 +0530 Subject: [PATCH] Adds fetch stream logic for networking part of PDF.js --- src/display/fetch_stream.js | 211 +++++++++++++++++++++++++++++++++++ src/display/network.js | 17 +-- src/display/network_utils.js | 13 ++- src/pdf.js | 5 +- test/unit/jasmine-boot.js | 9 +- 5 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 src/display/fetch_stream.js diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js new file mode 100644 index 00000000000000..9916d5fe50c073 --- /dev/null +++ b/src/display/fetch_stream.js @@ -0,0 +1,211 @@ +/* Copyright 2012 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. + */ + +import { assert, createPromiseCapability } from '../shared/util'; +import { validateRangeRequestCapabilities, validateResponseStatus +} from './network_utils'; + +class PDFFetchStream { + constructor(options) { + this.options = options; + this.source = options.source; + this.isHttp = /^https?:/i.test(this.source.url); + this.httpHeaders = (this.isHttp && this.source.httpHeaders) || {}; + + this._fullRequestReader = null; + this._rangeRequestReaders = []; + } + + getFullReader() { + assert(!this._fullRequestReader); + this._fullRequestReader = new PDFFetchStreamReader(this); + return this._fullRequestReader; + } + + getRangeReader(begin, end) { + let reader = new PDFFetchStreamRangeReader(this, begin, end); + this._rangeRequestReaders.push(reader); + return reader; + } + + cancelAllRequests(reason) { + if (this._fullRequestReader) { + this._fullRequestReader.cancel(reason); + } + let readers = this._rangeRequestReaders.slice(0); + readers.forEach(function(reader) { + reader.cancel(reason); + }); + } +} + +class PDFFetchStreamReader { + constructor(stream) { + this._stream = stream; + this._reader = null; + this._loaded = 0; + this._withCredentials = stream.source.withCredentials; + this._contentLength = this._stream.source.length; + this._headersCapability = createPromiseCapability(); + this._disableRange = this._stream.options.disableRange; + this._rangeChunkSize = this._stream.source.rangeChunkSize; + if (!this._rangeChunkSize && !this._disableRange) { + this._disableRange = true; + } + + this._isRangeSupported = !this._stream.options.disableRange; + this._isStreamingSupported = !this._stream.source.disableStream; + + this._headers = new Headers(); + for (let property in this._stream.httpHeaders) { + let value = this._stream.httpHeaders[property]; + if (typeof value === 'undefined') { + continue; + } + this._headers.append(property, value); + } + + let url = this._stream.source.url; + fetch(url, { + method: 'GET', + headers: this._headers, + mode: 'cors', + credentials: this._withCredentials ? 'omit' : 'include', + redirect: 'follow', + }).then((response) => { + if (response.status !== 200) { + throw validateResponseStatus(response.status, url); + } + this._headersCapability.resolve(); + this._reader = response.body.getReader(); + + let { allowRangeRequests, suggestedLength, } = + validateRangeRequestCapabilities({ + getResponseHeader: (name) => { + return response.headers.get(name); + }, + isHttp: this._stream.isHttp, + rangeChunkSize: this._rangeChunkSize, + disableRange: this._disableRange, + }); + + this._contentLength = suggestedLength; + this._isRangeSupported = allowRangeRequests; + }).catch(this._headersCapability.reject); + + this.onProgress = null; + } + + get headersReady() { + return this._headersCapability.promise; + } + + get contentLength() { + return this._contentLength; + } + + get isRangeSupported() { + return this._isRangeSupported; + } + + get isStreamingSupported() { + return this._isStreamingSupported; + } + + read() { + return this._headersCapability.promise.then(() => { + return this._reader.read().then(({ value, done, }) => { + if (done) { + return Promise.resolve({ value, done, }); + } + this._loaded += value.byteLength; + if (this.onProgress) { + this.onProgress({ + loaded: this._loaded, + total: this._contentLength, + }); + } + let buffer = new Uint8Array(value).buffer; + return Promise.resolve({ value: buffer, done: false, }); + }); + }); + } + + cancel(reason) { + if (this._reader) { + this._reader.cancel(reason); + } + } +} + +class PDFFetchStreamRangeReader { + constructor(stream, begin, end) { + this._stream = stream; + this._reader = null; + this._loaded = 0; + + this._readCapability = createPromiseCapability(); + this._isStreamingSupported = !stream.source.disableStream; + + this._headers = new Headers(); + for (let property in this._stream.httpHeaders) { + let value = this._stream.httpHeaders[property]; + if (typeof value === 'undefined') { + continue; + } + this._headers.append(property, value); + } + + let rangeStr = begin + '-' + (end - 1); + this._headers.append('Range', 'bytes=' + rangeStr); + fetch(this._stream.source.url, { + method: 'GET', + headers: this._headers, + }).then((response) => { + this._readCapability.resolve(); + this._reader = response.body.getReader(); + }); + + this.onProgress = null; + } + + get isStreamingSupported() { + return this._isStreamingSupported; + } + + read() { + return this._readCapability.promise.then(() => { + return this._reader.read().then(({ value, done, }) => { + if (done) { + return Promise.resolve({ value, done, }); + } + this._loaded += value.byteLength; + if (this.onProgress) { + this.onProgress({ loaded: this._loaded, }); + } + let buffer = new Uint8Array(value).buffer; + return Promise.resolve({ value: buffer, done: false, }); + }); + }); + } + + cancel(reason) { + this._reader.cancel(reason); + } +} + +export { + PDFFetchStream, +}; diff --git a/src/display/network.js b/src/display/network.js index 5d4f39314c8f6f..c63731617cc906 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -13,12 +13,10 @@ * limitations under the License. */ -import { - assert, createPromiseCapability, MissingPDFException, - UnexpectedResponseException -} from '../shared/util'; +import { assert, createPromiseCapability } from '../shared/util'; +import { validateRangeRequestCapabilities, validateResponseStatus +} from './network_utils'; import globalScope from '../shared/global_scope'; -import { validateRangeRequestCapabilities } from './network_utils'; if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('FIREFOX || MOZCENTRAL')) { throw new Error('Module "./network" shall not ' + @@ -417,14 +415,7 @@ PDFNetworkStreamFullRequestReader.prototype = { _onError: function PDFNetworkStreamFullRequestReader_onError(status) { var url = this._url; - var exception; - if (status === 404 || status === 0 && /^file:/.test(url)) { - exception = new MissingPDFException('Missing PDF "' + url + '".'); - } else { - exception = new UnexpectedResponseException( - 'Unexpected server response (' + status + - ') while retrieving PDF "' + url + '".', status); - } + var exception = validateResponseStatus(status, url); this._storedError = exception; this._headersReceivedCapability.reject(exception); this._requests.forEach(function (requestCapability) { diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 721afa4ccb8827..4427eb717f6b36 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -13,7 +13,8 @@ * limitations under the License. */ -import { assert, isInt } from '../shared/util'; +import { assert, isInt, MissingPDFException, UnexpectedResponseException +} from '../shared/util'; function validateRangeRequestCapabilities({ getResponseHeader, isHttp, rangeChunkSize, disableRange, }) { @@ -52,6 +53,16 @@ function validateRangeRequestCapabilities({ getResponseHeader, isHttp, return returnValues; } +function validateResponseStatus(statusCode, url) { + if (statusCode === 404 || statusCode === 0 && /^file:/.test(url)) { + return new MissingPDFException('Missing PDF "' + url + '".'); + } + return new UnexpectedResponseException( + 'Unexpected server response (' + statusCode + + ') while retrieving PDF "' + url + '".', statusCode); +} + export { validateRangeRequestCapabilities, + validateResponseStatus, }; diff --git a/src/pdf.js b/src/pdf.js index 9c0b1488a6986b..c2c02a06a73024 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -34,7 +34,10 @@ if (typeof PDFJSDev === 'undefined' || if (pdfjsSharedUtil.isNodeJS()) { var PDFNodeStream = require('./display/node_stream.js').PDFNodeStream; pdfjsDisplayAPI.setPDFNetworkStreamClass(PDFNodeStream); - } else { + } else if (typeof Response !== 'undefined' && 'body' in Response.prototype) { + var PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream; + pdfjsDisplayAPI.setPDFNetworkStreamClass(PDFFetchStream); + } else { var PDFNetworkStream = require('./display/network.js').PDFNetworkStream; pdfjsDisplayAPI.setPDFNetworkStreamClass(PDFNetworkStream); } diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 8820e6c7d8bf48..6a0d80d93de5d3 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -45,6 +45,7 @@ function initializePDFJS(callback) { 'pdfjs/display/global', 'pdfjs/display/api', 'pdfjs/display/network', + 'pdfjs/display/fetch_stream', 'pdfjs-test/unit/annotation_spec', 'pdfjs-test/unit/api_spec', 'pdfjs-test/unit/bidi_spec', @@ -76,9 +77,15 @@ function initializePDFJS(callback) { var displayGlobal = modules[0]; var displayApi = modules[1]; var PDFNetworkStream = modules[2].PDFNetworkStream; + var PDFFetchStream = modules[3].PDFFetchStream; // Set network stream class for unit tests. - displayApi.setPDFNetworkStreamClass(PDFNetworkStream); + if (typeof Response !== 'undefined' && 'body' in Response.prototype) { + displayApi.setPDFNetworkStreamClass(PDFFetchStream); + } else { + displayApi.setPDFNetworkStreamClass(PDFNetworkStream); + } + // Configure the worker. displayGlobal.PDFJS.workerSrc = '../../build/generic/build/pdf.worker.js'; // Opt-in to using the latest API.