Skip to content

Commit

Permalink
Update: Tweak pdf.js range requests for improved performance (#84)
Browse files Browse the repository at this point in the history
- Re-enable range requests for Safari and iOS that was previously disabled by pdf.js' new built-in compatability checking
- Increase non-US locale chunk size to 512KB
- Disable range requests for files with original size <4MB except for Excel files, see code for details
- Add polyfill for Array.includes and update other polyfills to MDN ones
  • Loading branch information
tonyjin authored Apr 19, 2017
1 parent 85337bc commit 0e7f358
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .stylelintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"value-no-vendor-prefix": true,
"number-leading-zero": never,
"declaration-no-important": true,
"order/declaration-block-properties-alphabetical-order": [true, { "severity": "warning" }]
"order/properties-alphabetical-order": [true, { "severity": "warning" }]
}
}
147 changes: 120 additions & 27 deletions src/lib/polyfill.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable */
(function() {
(function () {
var testObject = {};

if (!(Object.setPrototypeOf || testObject.__proto__)) {
var nativeGetPrototypeOf = Object.getPrototypeOf;

Object.getPrototypeOf = function(object) {
Object.getPrototypeOf = function (object) {
if (object.__proto__) {
return object.__proto__;
} else {
Expand All @@ -16,8 +16,8 @@
})();

if (typeof Object.assign != 'function') {
(function() {
Object.assign = function(target) {
(function () {
Object.assign = function (target) {
'use strict';
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
Expand All @@ -39,70 +39,163 @@ if (typeof Object.assign != 'function') {
})();
}

// https://tc39.github.io/ecma262/#sec-array.prototype.find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
configurable: true,
enumerable: false,
writable: true,
value: function(predicate) {
'use strict';
if (this === null) {
throw new TypeError('Array.prototype.find called on null or undefined');
value: function (predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}

var o = Object(this);

// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;

// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;

// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
var value;

for (var i = 0; i < length; i++) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return value;
// 5. Let k be 0.
var k = 0;

// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return kValue.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return kValue;
}
// e. Increase k by 1.
k++;
}

// 7. Return undefined.
return undefined;
}
});
}

// https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
configurable: true,
enumerable: false,
writable: true,
value: function(predicate) {
'use strict';
if (this === null) {
throw new TypeError('Array.prototype.findIndex called on null or undefined');
value: function (predicate) {
// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}

var o = Object(this);

// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;

// 3. If IsCallable(predicate) is false, throw a TypeError exception.
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;

// 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
var thisArg = arguments[1];
var value;

for (var i = 0; i < length; i++) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return i;
// 5. Let k be 0.
var k = 0;

// 6. Repeat, while k < len
while (k < len) {
// a. Let Pk be ! ToString(k).
// b. Let kValue be ? Get(O, Pk).
// c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
// d. If testResult is true, return k.
var kValue = o[k];
if (predicate.call(thisArg, kValue, k, o)) {
return k;
}
// e. Increase k by 1.
k++;
}

// 7. Return -1.
return -1;
}
});
}

// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
configurable: true,
enumerable: false,
writable: true,
value: function (searchElement, fromIndex) {

// 1. Let O be ? ToObject(this value).
if (this == null) {
throw new TypeError('"this" is null or not defined');
}

var o = Object(this);

// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;

// 3. If len is 0, return false.
if (len === 0) {
return false;
}

// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;

// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);

function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}

// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
// c. Increase k by 1.
if (sameValueZero(o[k], searchElement)) {
return true;
}
k++;
}

// 8. Return false
return false;
}
});
}

if (!String.prototype.endsWith) {
Object.defineProperty(String.prototype, 'endsWith', {
configurable: true,
enumerable: false,
writable: true,
value: function(searchString, position) {
value: function (searchString, position) {
var subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
Expand Down
18 changes: 13 additions & 5 deletions src/lib/viewers/doc/DocBaseViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ const SAFARI_PRINT_TIMEOUT_MS = 1000; // Wait 1s before trying to print
const PRINT_DIALOG_TIMEOUT_MS = 500;
const MAX_SCALE = 10.0;
const MIN_SCALE = 0.1;
const DEFAULT_RANGE_REQUEST_CHUNK_SIZE = 393216; // 384KB
const DEFAULT_RANGE_REQUEST_CHUNK_SIZE = 524288; // 512KB
const LARGE_RANGE_REQUEST_CHUNK_SIZE = 1048576; // 1MB
const MINIMUM_RANGE_REQUEST_FILE_SIZE = 4194304; // 4MB
const DISABLE_RANGE_REQUEST_EXENSIONS = ['xls', 'xlsm', 'xlsx'];
const MOBILE_MAX_CANVAS_SIZE = 2949120; // ~3MP 1920x1536
const SHOW_PAGE_NUM_INPUT_CLASS = 'show-page-number-input';
const IS_SAFARI_CLASS = 'is-safari';
Expand Down Expand Up @@ -226,6 +228,7 @@ class DocBaseViewer extends BaseViewer {
this.findBarEl = this.containerEl.appendChild(document.createElement('div'));
this.findBarEl.classList.add(CLASS_BOX_PREVIEW_FIND_BAR);

/* global PDFJS */
this.findController = new PDFJS.PDFFindController({
pdfViewer: this.pdfViewer
});
Expand Down Expand Up @@ -651,6 +654,7 @@ class DocBaseViewer extends BaseViewer {
setupPdfjs() {
// Set PDFJS worker & character maps
const { file, location } = this.options;
const { size, extension, watermark_info: watermarkInfo } = file;
const assetUrlCreator = createAssetUrlCreator(location);
PDFJS.workerSrc = assetUrlCreator(`third-party/doc/${DOC_STATIC_ASSETS_VERSION}/pdf.worker.min.js`);
PDFJS.cMapUrl = `${location.staticBaseURI}third-party/doc/${DOC_STATIC_ASSETS_VERSION}/cmaps/`;
Expand All @@ -659,12 +663,16 @@ class DocBaseViewer extends BaseViewer {
// Open links in new tab
PDFJS.externalLinkTarget = PDFJS.LinkTarget.BLANK;

// Disable range requests for iOS Safari - mobile Safari caches ranges incorrectly
PDFJS.disableRange = PDFJS.disableRange || (Browser.isIOS() && Browser.getName() === 'Safari');
// Disable range requests for files smaller than MINIMUM_RANGE_REQUEST_FILE_SIZE (4MB) except for
// Excel files since their representations can be many times larger than the original file. Remove
// the Excel check once WinExcel starts generating appropriately-sized representations. This
// also overrides any range request disabling that may be set by pdf.js's compatbility checking
// since the browsers we support should all be able to properly handle range requests
PDFJS.disableRange = size < MINIMUM_RANGE_REQUEST_FILE_SIZE &&
!DISABLE_RANGE_REQUEST_EXENSIONS.includes(extension);

// Disable range requests for watermarked files since they are streamed
PDFJS.disableRange = PDFJS.disableRange ||
(file.watermark_info && file.watermark_info.is_watermarked);
PDFJS.disableRange = PDFJS.disableRange || (watermarkInfo && watermarkInfo.is_watermarked);

// Disable text layer if user doesn't have download permissions
PDFJS.disableTextLayer = !checkPermission(file, PERMISSION_DOWNLOAD) ||
Expand Down
24 changes: 16 additions & 8 deletions src/lib/viewers/doc/__tests__/DocBaseViewer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {

it('should set a default chunk size if no viewer option set and locale is not en-US', () => {
const url = 'url';
const defaultChunkSize = 393216; // 384KB
const defaultChunkSize = 524288; // 512KB

docBase.options.location = {
locale: 'not-en-US'
Expand Down Expand Up @@ -1048,6 +1048,8 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
staticBaseURI: 'test/'
},
file: {
size: 10000000,
extension: 'pdf',
watermark_info: {
is_watermarked: false
},
Expand All @@ -1071,23 +1073,29 @@ describe('src/lib/viewers/doc/DocBaseViewer', () => {
expect(PDFJS.externalLinkRel).to.equal('noopener noreferrer nofollow');
});

it('should disable range requests if the browser is Mobile Safari', () => {
sandbox.stub(Browser, 'isIOS').returns(true);
it('should disable range requests if the file is smaller than 4MB and is not an Excel file', () => {
docBase.options.file.size = 100;
docBase.options.extension = 'pdf';
docBase.setupPdfjs();
expect(PDFJS.disableRange).to.be.true;
});

it('should not disable range requests if the file is an Excel file', () => {
docBase.options.extension = 'xlsx';
docBase.setupPdfjs();
expect(PDFJS.disableRange).to.be.false;
});

it('should disable range requests if the file is watermarked', () => {
// file is watermarked
stubs.browser.returns('Chrome');
docBase.options.file.watermark_info.is_watermarked = true;

docBase.setupPdfjs();
expect(PDFJS.disableRange).to.be.true;
});

it('should enable range requests if the file and browser meet the conditions', () => {
stubs.browser.returns('Chrome');
it('should enable range requests if the file is greater than 4MB, is not Excel, and is not watermarked', () => {
docBase.options.size = 10000000;
docBase.options.extension = 'pdf';
docBase.options.file.watermark_info.is_watermarked = false;
docBase.setupPdfjs();
expect(PDFJS.disableRange).to.be.false;
});
Expand Down

0 comments on commit 0e7f358

Please sign in to comment.