From f486051d7716a771d1771c5586af3f470aebc656 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:52:43 +0200 Subject: [PATCH 1/7] Explicitly list the ignored files in the `src/shared/` folder, for the `gulp lib` build target To avoid having to manually update the "shared files" list in `gulpfile.js`, whenever any changes are made to the file structure in the `src/shared/` folder, let's simply list the files we do *not* want instead. Given that the excluded files have been consistent ever since the `lib` build target was added, this patch should help avoid unnecessary churn in `gulpfile.js` in the future. --- gulpfile.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 588ffc9dc6788..da2da4ab0c468 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -877,24 +877,16 @@ gulp.task('lib', ['buildnumber'], function () { var licenseHeaderLibre = fs.readFileSync('./src/license_header_libre.js').toString(); var preprocessor2 = require('./external/builder/preprocessor2.js'); - var sharedFiles = [ - 'compatibility', - 'global_scope', - 'is_node', - 'streams_polyfill', - 'util', - ]; var buildLib = merge([ gulp.src([ - 'src/{core,display}/*.js', - 'src/shared/{' + sharedFiles.join() + '}.js', + 'src/{core,display,shared}/*.js', + '!src/shared/{cffStandardStrings,fonts_utils}.js', 'src/{pdf,pdf.worker}.js', ], { base: 'src/', }), gulp.src([ 'examples/node/domstubs.js', 'web/*.js', - '!web/pdfjs.js', - '!web/viewer.js', + '!web/{pdfjs,viewer}.js', ], { base: '.', }), gulp.src('test/unit/*.js', { base: '.', }), ]).pipe(transform('utf8', preprocess)) From 08c8f8733db4cd6629e6b7173292f38f6ef8bea8 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:36:26 +0200 Subject: [PATCH 2/7] Move `PageViewport` from `src/shared/util.js` to `src/display/dom_utils.js` Since the `PageViewport` is not used in the worker, duplicating this code on both the main and worker sides seems completely unnecessary. --- src/display/api.js | 6 +- src/display/dom_utils.js | 139 ++++++++++++++++++++++++++++++++++++++- src/shared/util.js | 137 -------------------------------------- 3 files changed, 141 insertions(+), 141 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 0003b8090550b..9791229172b82 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -17,12 +17,12 @@ import { assert, createPromiseCapability, getVerbosityLevel, info, InvalidPDFException, isArrayBuffer, isNum, isSameOrigin, MessageHandler, MissingPDFException, - NativeImageDecoding, PageViewport, PasswordException, setVerbosityLevel, - shadow, stringToBytes, UnexpectedResponseException, UnknownErrorException, + NativeImageDecoding, PasswordException, setVerbosityLevel, shadow, + stringToBytes, UnexpectedResponseException, UnknownErrorException, unreachable, Util, warn } from '../shared/util'; import { - DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, + DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, PageViewport, RenderingCancelledException, StatTimer } from './dom_utils'; import { FontFaceObject, FontLoader } from './font_loader'; diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js index adf0c8e71f544..ca9b820ead789 100644 --- a/src/display/dom_utils.js +++ b/src/display/dom_utils.js @@ -15,7 +15,7 @@ import { assert, CMapCompressionType, removeNullCharacters, stringToBytes, - unreachable, warn + unreachable, Util, warn } from '../shared/util'; const DEFAULT_LINK_REL = 'noopener noreferrer nofollow'; @@ -135,6 +135,142 @@ class DOMSVGFactory { } } +/** + * PDF page viewport created based on scale, rotation and offset. + * @class + * @alias PageViewport + */ +var PageViewport = (function PageViewportClosure() { + /** + * @constructor + * @private + * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. + * @param scale {number} scale of the viewport. + * @param rotation {number} rotations of the viewport in degrees. + * @param offsetX {number} offset X + * @param offsetY {number} offset Y + * @param dontFlip {boolean} if true, axis Y will not be flipped. + */ + function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { + this.viewBox = viewBox; + this.scale = scale; + this.rotation = rotation; + this.offsetX = offsetX; + this.offsetY = offsetY; + + // creating transform to convert pdf coordinate system to the normal + // canvas like coordinates taking in account scale and rotation + var centerX = (viewBox[2] + viewBox[0]) / 2; + var centerY = (viewBox[3] + viewBox[1]) / 2; + var rotateA, rotateB, rotateC, rotateD; + rotation = rotation % 360; + rotation = rotation < 0 ? rotation + 360 : rotation; + switch (rotation) { + case 180: + rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; + break; + case 90: + rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; + break; + case 270: + rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; + break; + // case 0: + default: + rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; + break; + } + + if (dontFlip) { + rotateC = -rotateC; rotateD = -rotateD; + } + + var offsetCanvasX, offsetCanvasY; + var width, height; + if (rotateA === 0) { + offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; + offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; + width = Math.abs(viewBox[3] - viewBox[1]) * scale; + height = Math.abs(viewBox[2] - viewBox[0]) * scale; + } else { + offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; + offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; + width = Math.abs(viewBox[2] - viewBox[0]) * scale; + height = Math.abs(viewBox[3] - viewBox[1]) * scale; + } + // creating transform for the following operations: + // translate(-centerX, -centerY), rotate and flip vertically, + // scale, and translate(offsetCanvasX, offsetCanvasY) + this.transform = [ + rotateA * scale, + rotateB * scale, + rotateC * scale, + rotateD * scale, + offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, + offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY + ]; + + this.width = width; + this.height = height; + this.fontScale = scale; + } + PageViewport.prototype = /** @lends PageViewport.prototype */ { + /** + * Clones viewport with additional properties. + * @param args {Object} (optional) If specified, may contain the 'scale' or + * 'rotation' properties to override the corresponding properties in + * the cloned viewport. + * @returns {PageViewport} Cloned viewport. + */ + clone: function PageViewPort_clone(args) { + args = args || {}; + var scale = 'scale' in args ? args.scale : this.scale; + var rotation = 'rotation' in args ? args.rotation : this.rotation; + return new PageViewport(this.viewBox.slice(), scale, rotation, + this.offsetX, this.offsetY, args.dontFlip); + }, + /** + * Converts PDF point to the viewport coordinates. For examples, useful for + * converting PDF location into canvas pixel coordinates. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the viewport coordinate space. + * @see {@link convertToPdfPoint} + * @see {@link convertToViewportRectangle} + */ + convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + }, + /** + * Converts PDF rectangle to the viewport coordinates. + * @param rect {Array} xMin, yMin, xMax and yMax coordinates. + * @returns {Array} Contains corresponding coordinates of the rectangle + * in the viewport coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToViewportRectangle: + function PageViewport_convertToViewportRectangle(rect) { + var tl = Util.applyTransform([rect[0], rect[1]], this.transform); + var br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + }, + /** + * Converts viewport coordinates to the PDF location. For examples, useful + * for converting canvas pixel location into PDF one. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the PDF coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + }, + }; + return PageViewport; +})(); + var RenderingCancelledException = (function RenderingCancelledException() { function RenderingCancelledException(msg, type) { this.message = msg; @@ -277,6 +413,7 @@ class DummyStatTimer { } export { + PageViewport, RenderingCancelledException, addLinkAttributes, getFilenameFromUrl, diff --git a/src/shared/util.js b/src/shared/util.js index a7b670ba863a6..5330cd61d66d3 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -926,142 +926,6 @@ var Util = (function UtilClosure() { return Util; })(); -/** - * PDF page viewport created based on scale, rotation and offset. - * @class - * @alias PageViewport - */ -var PageViewport = (function PageViewportClosure() { - /** - * @constructor - * @private - * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. - * @param scale {number} scale of the viewport. - * @param rotation {number} rotations of the viewport in degrees. - * @param offsetX {number} offset X - * @param offsetY {number} offset Y - * @param dontFlip {boolean} if true, axis Y will not be flipped. - */ - function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { - this.viewBox = viewBox; - this.scale = scale; - this.rotation = rotation; - this.offsetX = offsetX; - this.offsetY = offsetY; - - // creating transform to convert pdf coordinate system to the normal - // canvas like coordinates taking in account scale and rotation - var centerX = (viewBox[2] + viewBox[0]) / 2; - var centerY = (viewBox[3] + viewBox[1]) / 2; - var rotateA, rotateB, rotateC, rotateD; - rotation = rotation % 360; - rotation = rotation < 0 ? rotation + 360 : rotation; - switch (rotation) { - case 180: - rotateA = -1; rotateB = 0; rotateC = 0; rotateD = 1; - break; - case 90: - rotateA = 0; rotateB = 1; rotateC = 1; rotateD = 0; - break; - case 270: - rotateA = 0; rotateB = -1; rotateC = -1; rotateD = 0; - break; - // case 0: - default: - rotateA = 1; rotateB = 0; rotateC = 0; rotateD = -1; - break; - } - - if (dontFlip) { - rotateC = -rotateC; rotateD = -rotateD; - } - - var offsetCanvasX, offsetCanvasY; - var width, height; - if (rotateA === 0) { - offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; - offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; - width = Math.abs(viewBox[3] - viewBox[1]) * scale; - height = Math.abs(viewBox[2] - viewBox[0]) * scale; - } else { - offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX; - offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY; - width = Math.abs(viewBox[2] - viewBox[0]) * scale; - height = Math.abs(viewBox[3] - viewBox[1]) * scale; - } - // creating transform for the following operations: - // translate(-centerX, -centerY), rotate and flip vertically, - // scale, and translate(offsetCanvasX, offsetCanvasY) - this.transform = [ - rotateA * scale, - rotateB * scale, - rotateC * scale, - rotateD * scale, - offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY, - offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY - ]; - - this.width = width; - this.height = height; - this.fontScale = scale; - } - PageViewport.prototype = /** @lends PageViewport.prototype */ { - /** - * Clones viewport with additional properties. - * @param args {Object} (optional) If specified, may contain the 'scale' or - * 'rotation' properties to override the corresponding properties in - * the cloned viewport. - * @returns {PageViewport} Cloned viewport. - */ - clone: function PageViewPort_clone(args) { - args = args || {}; - var scale = 'scale' in args ? args.scale : this.scale; - var rotation = 'rotation' in args ? args.rotation : this.rotation; - return new PageViewport(this.viewBox.slice(), scale, rotation, - this.offsetX, this.offsetY, args.dontFlip); - }, - /** - * Converts PDF point to the viewport coordinates. For examples, useful for - * converting PDF location into canvas pixel coordinates. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the viewport coordinate space. - * @see {@link convertToPdfPoint} - * @see {@link convertToViewportRectangle} - */ - convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { - return Util.applyTransform([x, y], this.transform); - }, - /** - * Converts PDF rectangle to the viewport coordinates. - * @param rect {Array} xMin, yMin, xMax and yMax coordinates. - * @returns {Array} Contains corresponding coordinates of the rectangle - * in the viewport coordinate space. - * @see {@link convertToViewportPoint} - */ - convertToViewportRectangle: - function PageViewport_convertToViewportRectangle(rect) { - var tl = Util.applyTransform([rect[0], rect[1]], this.transform); - var br = Util.applyTransform([rect[2], rect[3]], this.transform); - return [tl[0], tl[1], br[0], br[1]]; - }, - /** - * Converts viewport coordinates to the PDF location. For examples, useful - * for converting canvas pixel location into PDF one. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the PDF coordinate space. - * @see {@link convertToViewportPoint} - */ - convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { - return Util.applyInverseTransform([x, y], this.transform); - }, - }; - return PageViewport; -})(); - var PDFStringTranslateTable = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2D8, 0x2C7, 0x2C6, 0x2D9, 0x2DD, 0x2DB, 0x2DA, 0x2DC, 0, 0, 0, 0, 0, 0, 0, @@ -1627,7 +1491,6 @@ export { MissingPDFException, NativeImageDecoding, NotImplementedException, - PageViewport, PasswordException, PasswordResponses, StreamType, From 5917b21702b5006d48fd637ee87d50223a34ce02 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:36:34 +0200 Subject: [PATCH 3/7] Remove completely unused `fontScale` property from `PageViewport` The `fontScale` property was added in PR 1531, see commit https://github.com/mozilla/pdf.js/commit/b312719d7e0cf1a45dda0b4ad871a45162ed6f5b in particular, apparently for the sole purpose of supporting the "acroforms" example. However, the `fontScale` property was never used anywhere else in the code-base, and after the modernization of the "acroforms" example in PR 8030 it's been completely unused. Finally, note that there's also a (more suitably named) `scale` property on `PageViewport` instances, which contains the exact same information as the property being removed here. --- src/display/dom_utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js index ca9b820ead789..a6f7ec624e55d 100644 --- a/src/display/dom_utils.js +++ b/src/display/dom_utils.js @@ -212,7 +212,6 @@ var PageViewport = (function PageViewportClosure() { this.width = width; this.height = height; - this.fontScale = scale; } PageViewport.prototype = /** @lends PageViewport.prototype */ { /** From 51673dbc5a752520571f0ce6094608f49293f4d0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:36:53 +0200 Subject: [PATCH 4/7] Convert the `PageViewport` to a proper ES6 class Also converts all `var` to `let` for good measure. --- src/display/dom_utils.js | 130 +++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 67 deletions(-) diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js index a6f7ec624e55d..ccb6607d0c65e 100644 --- a/src/display/dom_utils.js +++ b/src/display/dom_utils.js @@ -137,13 +137,9 @@ class DOMSVGFactory { /** * PDF page viewport created based on scale, rotation and offset. - * @class - * @alias PageViewport */ -var PageViewport = (function PageViewportClosure() { +class PageViewport { /** - * @constructor - * @private * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. * @param scale {number} scale of the viewport. * @param rotation {number} rotations of the viewport in degrees. @@ -151,7 +147,7 @@ var PageViewport = (function PageViewportClosure() { * @param offsetY {number} offset Y * @param dontFlip {boolean} if true, axis Y will not be flipped. */ - function PageViewport(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { + constructor(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { this.viewBox = viewBox; this.scale = scale; this.rotation = rotation; @@ -160,9 +156,9 @@ var PageViewport = (function PageViewportClosure() { // creating transform to convert pdf coordinate system to the normal // canvas like coordinates taking in account scale and rotation - var centerX = (viewBox[2] + viewBox[0]) / 2; - var centerY = (viewBox[3] + viewBox[1]) / 2; - var rotateA, rotateB, rotateC, rotateD; + let centerX = (viewBox[2] + viewBox[0]) / 2; + let centerY = (viewBox[3] + viewBox[1]) / 2; + let rotateA, rotateB, rotateC, rotateD; rotation = rotation % 360; rotation = rotation < 0 ? rotation + 360 : rotation; switch (rotation) { @@ -185,8 +181,8 @@ var PageViewport = (function PageViewportClosure() { rotateC = -rotateC; rotateD = -rotateD; } - var offsetCanvasX, offsetCanvasY; - var width, height; + let offsetCanvasX, offsetCanvasY; + let width, height; if (rotateA === 0) { offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX; offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY; @@ -213,62 +209,62 @@ var PageViewport = (function PageViewportClosure() { this.width = width; this.height = height; } - PageViewport.prototype = /** @lends PageViewport.prototype */ { - /** - * Clones viewport with additional properties. - * @param args {Object} (optional) If specified, may contain the 'scale' or - * 'rotation' properties to override the corresponding properties in - * the cloned viewport. - * @returns {PageViewport} Cloned viewport. - */ - clone: function PageViewPort_clone(args) { - args = args || {}; - var scale = 'scale' in args ? args.scale : this.scale; - var rotation = 'rotation' in args ? args.rotation : this.rotation; - return new PageViewport(this.viewBox.slice(), scale, rotation, - this.offsetX, this.offsetY, args.dontFlip); - }, - /** - * Converts PDF point to the viewport coordinates. For examples, useful for - * converting PDF location into canvas pixel coordinates. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the viewport coordinate space. - * @see {@link convertToPdfPoint} - * @see {@link convertToViewportRectangle} - */ - convertToViewportPoint: function PageViewport_convertToViewportPoint(x, y) { - return Util.applyTransform([x, y], this.transform); - }, - /** - * Converts PDF rectangle to the viewport coordinates. - * @param rect {Array} xMin, yMin, xMax and yMax coordinates. - * @returns {Array} Contains corresponding coordinates of the rectangle - * in the viewport coordinate space. - * @see {@link convertToViewportPoint} - */ - convertToViewportRectangle: - function PageViewport_convertToViewportRectangle(rect) { - var tl = Util.applyTransform([rect[0], rect[1]], this.transform); - var br = Util.applyTransform([rect[2], rect[3]], this.transform); - return [tl[0], tl[1], br[0], br[1]]; - }, - /** - * Converts viewport coordinates to the PDF location. For examples, useful - * for converting canvas pixel location into PDF one. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the PDF coordinate space. - * @see {@link convertToViewportPoint} - */ - convertToPdfPoint: function PageViewport_convertToPdfPoint(x, y) { - return Util.applyInverseTransform([x, y], this.transform); - }, - }; - return PageViewport; -})(); + + /** + * Clones viewport with additional properties. + * @param args {Object} (optional) If specified, may contain the 'scale' or + * 'rotation' properties to override the corresponding properties in + * the cloned viewport. + * @returns {PageViewport} Cloned viewport. + */ + clone(args) { + args = args || {}; + let scale = 'scale' in args ? args.scale : this.scale; + let rotation = 'rotation' in args ? args.rotation : this.rotation; + return new PageViewport(this.viewBox.slice(), scale, rotation, + this.offsetX, this.offsetY, args.dontFlip); + } + + /** + * Converts PDF point to the viewport coordinates. For examples, useful for + * converting PDF location into canvas pixel coordinates. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the viewport coordinate space. + * @see {@link convertToPdfPoint} + * @see {@link convertToViewportRectangle} + */ + convertToViewportPoint(x, y) { + return Util.applyTransform([x, y], this.transform); + } + + /** + * Converts PDF rectangle to the viewport coordinates. + * @param rect {Array} xMin, yMin, xMax and yMax coordinates. + * @returns {Array} Contains corresponding coordinates of the rectangle + * in the viewport coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToViewportRectangle(rect) { + let tl = Util.applyTransform([rect[0], rect[1]], this.transform); + let br = Util.applyTransform([rect[2], rect[3]], this.transform); + return [tl[0], tl[1], br[0], br[1]]; + } + + /** + * Converts viewport coordinates to the PDF location. For examples, useful + * for converting canvas pixel location into PDF one. + * @param x {number} X coordinate. + * @param y {number} Y coordinate. + * @returns {Object} Object that contains 'x' and 'y' properties of the + * point in the PDF coordinate space. + * @see {@link convertToViewportPoint} + */ + convertToPdfPoint(x, y) { + return Util.applyInverseTransform([x, y], this.transform); + } +} var RenderingCancelledException = (function RenderingCancelledException() { function RenderingCancelledException(msg, type) { From 69f2a77543421b2a2476d6c840c0d72b3f60be59 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:37:00 +0200 Subject: [PATCH 5/7] Update the signature of the `PageViewport` constructor, and improve the JSDoc comments for the class This changes the constructor to take a parameter object, rather than a string of parameters. --- src/display/api.js | 7 +++- src/display/dom_utils.js | 79 +++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 9791229172b82..24dee8c779299 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -885,7 +885,12 @@ var PDFPageProxy = (function PDFPageProxyClosure() { * along with transforms required for rendering. */ getViewport(scale, rotate = this.rotate, dontFlip = false) { - return new PageViewport(this.view, scale, rotate, 0, 0, dontFlip); + return new PageViewport({ + viewBox: this.view, + scale, + rotation: rotate, + dontFlip, + }); }, /** * @param {GetAnnotationsParameters} params - Annotation parameters. diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js index ccb6607d0c65e..94e3b10f20e3f 100644 --- a/src/display/dom_utils.js +++ b/src/display/dom_utils.js @@ -135,19 +135,38 @@ class DOMSVGFactory { } } +/** + * @typedef {Object} PageViewportParameters + * @property {Array} viewBox - The xMin, yMin, xMax and yMax coordinates. + * @property {number} scale - The scale of the viewport. + * @property {number} rotation - The rotation, in degrees, of the viewport. + * @property {number} offsetX - (optional) The vertical, i.e. x-axis, offset. + * The default value is `0`. + * @property {number} offsetY - (optional) The horizontal, i.e. y-axis, offset. + * The default value is `0`. + * @property {boolean} dontFlip - (optional) If true, the x-axis will not be + * flipped. The default value is `false`. + */ + +/** + * @typedef {Object} PageViewportCloneParameters + * @property {number} scale - (optional) The scale, overriding the one in the + * cloned viewport. The default value is `this.scale`. + * @property {number} rotation - (optional) The rotation, in degrees, overriding + * the one in the cloned viewport. The default value is `this.rotation`. + * @property {boolean} dontFlip - (optional) If true, the x-axis will not be + * flipped. The default value is `false`. + */ + /** * PDF page viewport created based on scale, rotation and offset. */ class PageViewport { /** - * @param viewBox {Array} xMin, yMin, xMax and yMax coordinates. - * @param scale {number} scale of the viewport. - * @param rotation {number} rotations of the viewport in degrees. - * @param offsetX {number} offset X - * @param offsetY {number} offset Y - * @param dontFlip {boolean} if true, axis Y will not be flipped. + * @param {PageViewportParameters} */ - constructor(viewBox, scale, rotation, offsetX, offsetY, dontFlip) { + constructor({ viewBox, scale, rotation, offsetX = 0, offsetY = 0, + dontFlip = false, }) { this.viewBox = viewBox; this.scale = scale; this.rotation = rotation; @@ -211,27 +230,29 @@ class PageViewport { } /** - * Clones viewport with additional properties. - * @param args {Object} (optional) If specified, may contain the 'scale' or - * 'rotation' properties to override the corresponding properties in - * the cloned viewport. - * @returns {PageViewport} Cloned viewport. + * Clones viewport, with optional additional properties. + * @param {PageViewportCloneParameters} - (optional) + * @return {PageViewport} Cloned viewport. */ - clone(args) { - args = args || {}; - let scale = 'scale' in args ? args.scale : this.scale; - let rotation = 'rotation' in args ? args.rotation : this.rotation; - return new PageViewport(this.viewBox.slice(), scale, rotation, - this.offsetX, this.offsetY, args.dontFlip); + clone({ scale = this.scale, rotation = this.rotation, + dontFlip = false, } = {}) { + return new PageViewport({ + viewBox: this.viewBox.slice(), + scale, + rotation, + offsetX: this.offsetX, + offsetY: this.offsetY, + dontFlip, + }); } /** * Converts PDF point to the viewport coordinates. For examples, useful for * converting PDF location into canvas pixel coordinates. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the viewport coordinate space. + * @param {number} x - The x-coordinate. + * @param {number} y - The y-coordinate. + * @return {Object} Object containing `x` and `y` properties of the + * point in the viewport coordinate space. * @see {@link convertToPdfPoint} * @see {@link convertToViewportRectangle} */ @@ -241,9 +262,9 @@ class PageViewport { /** * Converts PDF rectangle to the viewport coordinates. - * @param rect {Array} xMin, yMin, xMax and yMax coordinates. - * @returns {Array} Contains corresponding coordinates of the rectangle - * in the viewport coordinate space. + * @param {Array} rect - The xMin, yMin, xMax and yMax coordinates. + * @return {Array} Array containing corresponding coordinates of the rectangle + * in the viewport coordinate space. * @see {@link convertToViewportPoint} */ convertToViewportRectangle(rect) { @@ -255,10 +276,10 @@ class PageViewport { /** * Converts viewport coordinates to the PDF location. For examples, useful * for converting canvas pixel location into PDF one. - * @param x {number} X coordinate. - * @param y {number} Y coordinate. - * @returns {Object} Object that contains 'x' and 'y' properties of the - * point in the PDF coordinate space. + * @param {number} x - The x-coordinate. + * @param {number} y - The y-coordinate. + * @return {Object} Object containing `x` and `y` properties of the + * point in the PDF coordinate space. * @see {@link convertToViewportPoint} */ convertToPdfPoint(x, y) { From 44d8afd46b451101d79cad2e06810ddaa5a717e0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:37:54 +0200 Subject: [PATCH 6/7] Move `MessageHandler` into a separate `src/shared/message_handler.js` file The `MessageHandler` itself, and its assorted helper functions, are currently the single largest[1] piece of code in the `src/shared/util.js` file. By moving this code into its own file, `src/shared/util.js` thus becomes smaller and more manageable. --- src/core/worker.js | 3 +- src/display/api.js | 8 +- src/shared/message_handler.js | 446 ++++++++++++++++++ src/shared/util.js | 424 ----------------- test/unit/clitests.json | 4 +- test/unit/jasmine-boot.js | 2 +- ...stream_spec.js => message_handler_spec.js} | 5 +- 7 files changed, 458 insertions(+), 434 deletions(-) create mode 100644 src/shared/message_handler.js rename test/unit/{util_stream_spec.js => message_handler_spec.js} (98%) diff --git a/src/core/worker.js b/src/core/worker.js index 72c381a603e61..45c8e33cc1a89 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -15,12 +15,13 @@ import { arrayByteLength, arraysToBytes, assert, createPromiseCapability, info, - InvalidPDFException, MessageHandler, MissingPDFException, PasswordException, + InvalidPDFException, MissingPDFException, PasswordException, setVerbosityLevel, UnexpectedResponseException, UnknownErrorException, UNSUPPORTED_FEATURES, warn, XRefParseException } from '../shared/util'; import { LocalPdfManager, NetworkPdfManager } from './pdf_manager'; import isNodeJS from '../shared/is_node'; +import { MessageHandler } from '../shared/message_handler'; import { Ref } from './primitives'; var WorkerTask = (function WorkerTaskClosure() { diff --git a/src/display/api.js b/src/display/api.js index 24dee8c779299..4c23c77417820 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -16,10 +16,9 @@ import { assert, createPromiseCapability, getVerbosityLevel, info, InvalidPDFException, - isArrayBuffer, isNum, isSameOrigin, MessageHandler, MissingPDFException, - NativeImageDecoding, PasswordException, setVerbosityLevel, shadow, - stringToBytes, UnexpectedResponseException, UnknownErrorException, - unreachable, Util, warn + isArrayBuffer, isNum, isSameOrigin, MissingPDFException, NativeImageDecoding, + PasswordException, setVerbosityLevel, shadow, stringToBytes, + UnexpectedResponseException, UnknownErrorException, unreachable, Util, warn } from '../shared/util'; import { DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, PageViewport, @@ -30,6 +29,7 @@ import { apiCompatibilityParams } from './api_compatibility'; import { CanvasGraphics } from './canvas'; import globalScope from '../shared/global_scope'; import { GlobalWorkerOptions } from './worker_options'; +import { MessageHandler } from '../shared/message_handler'; import { Metadata } from './metadata'; import { PDFDataTransportStream } from './transport_stream'; import { WebGLContext } from './webgl'; diff --git a/src/shared/message_handler.js b/src/shared/message_handler.js new file mode 100644 index 0000000000000..e24cacae4b30f --- /dev/null +++ b/src/shared/message_handler.js @@ -0,0 +1,446 @@ +/* Copyright 2018 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 { + AbortException, assert, createPromiseCapability, MissingPDFException, + ReadableStream, UnexpectedResponseException, UnknownErrorException +} from './util'; + +function resolveCall(fn, args, thisArg = null) { + if (!fn) { + return Promise.resolve(undefined); + } + return new Promise((resolve, reject) => { + resolve(fn.apply(thisArg, args)); + }); +} + +function wrapReason(reason) { + if (typeof reason !== 'object') { + return reason; + } + switch (reason.name) { + case 'AbortException': + return new AbortException(reason.message); + case 'MissingPDFException': + return new MissingPDFException(reason.message); + case 'UnexpectedResponseException': + return new UnexpectedResponseException(reason.message, reason.status); + default: + return new UnknownErrorException(reason.message, reason.details); + } +} + +function makeReasonSerializable(reason) { + if (!(reason instanceof Error) || + reason instanceof AbortException || + reason instanceof MissingPDFException || + reason instanceof UnexpectedResponseException || + reason instanceof UnknownErrorException) { + return reason; + } + return new UnknownErrorException(reason.message, reason.toString()); +} + +function resolveOrReject(capability, success, reason) { + if (success) { + capability.resolve(); + } else { + capability.reject(reason); + } +} + +function finalize(promise) { + return Promise.resolve(promise).catch(() => {}); +} + +function MessageHandler(sourceName, targetName, comObj) { + this.sourceName = sourceName; + this.targetName = targetName; + this.comObj = comObj; + this.callbackId = 1; + this.streamId = 1; + this.postMessageTransfers = true; + this.streamSinks = Object.create(null); + this.streamControllers = Object.create(null); + let callbacksCapabilities = this.callbacksCapabilities = Object.create(null); + let ah = this.actionHandler = Object.create(null); + + this._onComObjOnMessage = (event) => { + let data = event.data; + if (data.targetName !== this.sourceName) { + return; + } + if (data.stream) { + this._processStreamMessage(data); + } else if (data.isReply) { + let callbackId = data.callbackId; + if (data.callbackId in callbacksCapabilities) { + let callback = callbacksCapabilities[callbackId]; + delete callbacksCapabilities[callbackId]; + if ('error' in data) { + callback.reject(wrapReason(data.error)); + } else { + callback.resolve(data.data); + } + } else { + throw new Error(`Cannot resolve callback ${callbackId}`); + } + } else if (data.action in ah) { + let action = ah[data.action]; + if (data.callbackId) { + let sourceName = this.sourceName; + let targetName = data.sourceName; + Promise.resolve().then(function () { + return action[0].call(action[1], data.data); + }).then((result) => { + comObj.postMessage({ + sourceName, + targetName, + isReply: true, + callbackId: data.callbackId, + data: result, + }); + }, (reason) => { + comObj.postMessage({ + sourceName, + targetName, + isReply: true, + callbackId: data.callbackId, + error: makeReasonSerializable(reason), + }); + }); + } else if (data.streamId) { + this._createStreamSink(data); + } else { + action[0].call(action[1], data.data); + } + } else { + throw new Error(`Unknown action from worker: ${data.action}`); + } + }; + comObj.addEventListener('message', this._onComObjOnMessage); +} + +MessageHandler.prototype = { + on(actionName, handler, scope) { + var ah = this.actionHandler; + if (ah[actionName]) { + throw new Error(`There is already an actionName called "${actionName}"`); + } + ah[actionName] = [handler, scope]; + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * @param {String} actionName - Action to call. + * @param {JSON} data - JSON data to send. + * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers + */ + send(actionName, data, transfers) { + var message = { + sourceName: this.sourceName, + targetName: this.targetName, + action: actionName, + data, + }; + this.postMessage(message, transfers); + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * Expects that the other side will callback with the response. + * @param {String} actionName - Action to call. + * @param {JSON} data - JSON data to send. + * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers. + * @returns {Promise} Promise to be resolved with response data. + */ + sendWithPromise(actionName, data, transfers) { + var callbackId = this.callbackId++; + var message = { + sourceName: this.sourceName, + targetName: this.targetName, + action: actionName, + data, + callbackId, + }; + var capability = createPromiseCapability(); + this.callbacksCapabilities[callbackId] = capability; + try { + this.postMessage(message, transfers); + } catch (e) { + capability.reject(e); + } + return capability.promise; + }, + /** + * Sends a message to the comObj to invoke the action with the supplied data. + * Expect that the other side will callback to signal 'start_complete'. + * @param {String} actionName - Action to call. + * @param {JSON} data - JSON data to send. + * @param {Object} queueingStrategy - strategy to signal backpressure based on + * internal queue. + * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers. + * @return {ReadableStream} ReadableStream to read data in chunks. + */ + sendWithStream(actionName, data, queueingStrategy, transfers) { + let streamId = this.streamId++; + let sourceName = this.sourceName; + let targetName = this.targetName; + + return new ReadableStream({ + start: (controller) => { + let startCapability = createPromiseCapability(); + this.streamControllers[streamId] = { + controller, + startCall: startCapability, + isClosed: false, + }; + this.postMessage({ + sourceName, + targetName, + action: actionName, + streamId, + data, + desiredSize: controller.desiredSize, + }); + // Return Promise for Async process, to signal success/failure. + return startCapability.promise; + }, + + pull: (controller) => { + let pullCapability = createPromiseCapability(); + this.streamControllers[streamId].pullCall = pullCapability; + this.postMessage({ + sourceName, + targetName, + stream: 'pull', + streamId, + desiredSize: controller.desiredSize, + }); + // Returning Promise will not call "pull" + // again until current pull is resolved. + return pullCapability.promise; + }, + + cancel: (reason) => { + let cancelCapability = createPromiseCapability(); + this.streamControllers[streamId].cancelCall = cancelCapability; + this.streamControllers[streamId].isClosed = true; + this.postMessage({ + sourceName, + targetName, + stream: 'cancel', + reason, + streamId, + }); + // Return Promise to signal success or failure. + return cancelCapability.promise; + }, + }, queueingStrategy); + }, + + _createStreamSink(data) { + let self = this; + let action = this.actionHandler[data.action]; + let streamId = data.streamId; + let desiredSize = data.desiredSize; + let sourceName = this.sourceName; + let targetName = data.sourceName; + let capability = createPromiseCapability(); + + let sendStreamRequest = ({ stream, chunk, transfers, + success, reason, }) => { + this.postMessage({ sourceName, targetName, stream, streamId, + chunk, success, reason, }, transfers); + }; + + let streamSink = { + enqueue(chunk, size = 1, transfers) { + if (this.isCancelled) { + return; + } + let lastDesiredSize = this.desiredSize; + this.desiredSize -= size; + // Enqueue decreases the desiredSize property of sink, + // so when it changes from positive to negative, + // set ready as unresolved promise. + if (lastDesiredSize > 0 && this.desiredSize <= 0) { + this.sinkCapability = createPromiseCapability(); + this.ready = this.sinkCapability.promise; + } + sendStreamRequest({ stream: 'enqueue', chunk, transfers, }); + }, + + close() { + if (this.isCancelled) { + return; + } + this.isCancelled = true; + sendStreamRequest({ stream: 'close', }); + delete self.streamSinks[streamId]; + }, + + error(reason) { + if (this.isCancelled) { + return; + } + this.isCancelled = true; + sendStreamRequest({ stream: 'error', reason, }); + }, + + sinkCapability: capability, + onPull: null, + onCancel: null, + isCancelled: false, + desiredSize, + ready: null, + }; + + streamSink.sinkCapability.resolve(); + streamSink.ready = streamSink.sinkCapability.promise; + this.streamSinks[streamId] = streamSink; + resolveCall(action[0], [data.data, streamSink], action[1]).then(() => { + sendStreamRequest({ stream: 'start_complete', success: true, }); + }, (reason) => { + sendStreamRequest({ stream: 'start_complete', success: false, reason, }); + }); + }, + + _processStreamMessage(data) { + let sourceName = this.sourceName; + let targetName = data.sourceName; + let streamId = data.streamId; + + let sendStreamResponse = ({ stream, success, reason, }) => { + this.comObj.postMessage({ sourceName, targetName, stream, + success, streamId, reason, }); + }; + + let deleteStreamController = () => { + // Delete streamController only when start, pull and + // cancel callbacks are resolved, to avoid "TypeError". + Promise.all([ + this.streamControllers[data.streamId].startCall, + this.streamControllers[data.streamId].pullCall, + this.streamControllers[data.streamId].cancelCall + ].map(function(capability) { + return capability && finalize(capability.promise); + })).then(() => { + delete this.streamControllers[data.streamId]; + }); + }; + + switch (data.stream) { + case 'start_complete': + resolveOrReject(this.streamControllers[data.streamId].startCall, + data.success, wrapReason(data.reason)); + break; + case 'pull_complete': + resolveOrReject(this.streamControllers[data.streamId].pullCall, + data.success, wrapReason(data.reason)); + break; + case 'pull': + // Ignore any pull after close is called. + if (!this.streamSinks[data.streamId]) { + sendStreamResponse({ stream: 'pull_complete', success: true, }); + break; + } + // Pull increases the desiredSize property of sink, + // so when it changes from negative to positive, + // set ready property as resolved promise. + if (this.streamSinks[data.streamId].desiredSize <= 0 && + data.desiredSize > 0) { + this.streamSinks[data.streamId].sinkCapability.resolve(); + } + // Reset desiredSize property of sink on every pull. + this.streamSinks[data.streamId].desiredSize = data.desiredSize; + resolveCall(this.streamSinks[data.streamId].onPull).then(() => { + sendStreamResponse({ stream: 'pull_complete', success: true, }); + }, (reason) => { + sendStreamResponse({ stream: 'pull_complete', + success: false, reason, }); + }); + break; + case 'enqueue': + assert(this.streamControllers[data.streamId], + 'enqueue should have stream controller'); + if (!this.streamControllers[data.streamId].isClosed) { + this.streamControllers[data.streamId].controller.enqueue(data.chunk); + } + break; + case 'close': + assert(this.streamControllers[data.streamId], + 'close should have stream controller'); + if (this.streamControllers[data.streamId].isClosed) { + break; + } + this.streamControllers[data.streamId].isClosed = true; + this.streamControllers[data.streamId].controller.close(); + deleteStreamController(); + break; + case 'error': + assert(this.streamControllers[data.streamId], + 'error should have stream controller'); + this.streamControllers[data.streamId].controller. + error(wrapReason(data.reason)); + deleteStreamController(); + break; + case 'cancel_complete': + resolveOrReject(this.streamControllers[data.streamId].cancelCall, + data.success, wrapReason(data.reason)); + deleteStreamController(); + break; + case 'cancel': + if (!this.streamSinks[data.streamId]) { + break; + } + resolveCall(this.streamSinks[data.streamId].onCancel, + [wrapReason(data.reason)]).then(() => { + sendStreamResponse({ stream: 'cancel_complete', success: true, }); + }, (reason) => { + sendStreamResponse({ stream: 'cancel_complete', + success: false, reason, }); + }); + this.streamSinks[data.streamId].sinkCapability. + reject(wrapReason(data.reason)); + this.streamSinks[data.streamId].isCancelled = true; + delete this.streamSinks[data.streamId]; + break; + default: + throw new Error('Unexpected stream case'); + } + }, + + /** + * Sends raw message to the comObj. + * @private + * @param {Object} message - Raw message. + * @param transfers List of transfers/ArrayBuffers, or undefined. + */ + postMessage(message, transfers) { + if (transfers && this.postMessageTransfers) { + this.comObj.postMessage(message, transfers); + } else { + this.comObj.postMessage(message); + } + }, + + destroy() { + this.comObj.removeEventListener('message', this._onComObjOnMessage); + }, +}; + +export { + MessageHandler, +}; diff --git a/src/shared/util.js b/src/shared/util.js index 5330cd61d66d3..6ffbedf608887 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1048,429 +1048,6 @@ var createObjectURL = (function createObjectURLClosure() { }; })(); -function resolveCall(fn, args, thisArg = null) { - if (!fn) { - return Promise.resolve(undefined); - } - return new Promise((resolve, reject) => { - resolve(fn.apply(thisArg, args)); - }); -} - -function wrapReason(reason) { - if (typeof reason !== 'object') { - return reason; - } - switch (reason.name) { - case 'AbortException': - return new AbortException(reason.message); - case 'MissingPDFException': - return new MissingPDFException(reason.message); - case 'UnexpectedResponseException': - return new UnexpectedResponseException(reason.message, reason.status); - default: - return new UnknownErrorException(reason.message, reason.details); - } -} - -function makeReasonSerializable(reason) { - if (!(reason instanceof Error) || - reason instanceof AbortException || - reason instanceof MissingPDFException || - reason instanceof UnexpectedResponseException || - reason instanceof UnknownErrorException) { - return reason; - } - return new UnknownErrorException(reason.message, reason.toString()); -} - -function resolveOrReject(capability, success, reason) { - if (success) { - capability.resolve(); - } else { - capability.reject(reason); - } -} - -function finalize(promise) { - return Promise.resolve(promise).catch(() => {}); -} - -function MessageHandler(sourceName, targetName, comObj) { - this.sourceName = sourceName; - this.targetName = targetName; - this.comObj = comObj; - this.callbackId = 1; - this.streamId = 1; - this.postMessageTransfers = true; - this.streamSinks = Object.create(null); - this.streamControllers = Object.create(null); - let callbacksCapabilities = this.callbacksCapabilities = Object.create(null); - let ah = this.actionHandler = Object.create(null); - - this._onComObjOnMessage = (event) => { - let data = event.data; - if (data.targetName !== this.sourceName) { - return; - } - if (data.stream) { - this._processStreamMessage(data); - } else if (data.isReply) { - let callbackId = data.callbackId; - if (data.callbackId in callbacksCapabilities) { - let callback = callbacksCapabilities[callbackId]; - delete callbacksCapabilities[callbackId]; - if ('error' in data) { - callback.reject(wrapReason(data.error)); - } else { - callback.resolve(data.data); - } - } else { - throw new Error(`Cannot resolve callback ${callbackId}`); - } - } else if (data.action in ah) { - let action = ah[data.action]; - if (data.callbackId) { - let sourceName = this.sourceName; - let targetName = data.sourceName; - Promise.resolve().then(function () { - return action[0].call(action[1], data.data); - }).then((result) => { - comObj.postMessage({ - sourceName, - targetName, - isReply: true, - callbackId: data.callbackId, - data: result, - }); - }, (reason) => { - comObj.postMessage({ - sourceName, - targetName, - isReply: true, - callbackId: data.callbackId, - error: makeReasonSerializable(reason), - }); - }); - } else if (data.streamId) { - this._createStreamSink(data); - } else { - action[0].call(action[1], data.data); - } - } else { - throw new Error(`Unknown action from worker: ${data.action}`); - } - }; - comObj.addEventListener('message', this._onComObjOnMessage); -} - -MessageHandler.prototype = { - on(actionName, handler, scope) { - var ah = this.actionHandler; - if (ah[actionName]) { - throw new Error(`There is already an actionName called "${actionName}"`); - } - ah[actionName] = [handler, scope]; - }, - /** - * Sends a message to the comObj to invoke the action with the supplied data. - * @param {String} actionName - Action to call. - * @param {JSON} data - JSON data to send. - * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers - */ - send(actionName, data, transfers) { - var message = { - sourceName: this.sourceName, - targetName: this.targetName, - action: actionName, - data, - }; - this.postMessage(message, transfers); - }, - /** - * Sends a message to the comObj to invoke the action with the supplied data. - * Expects that the other side will callback with the response. - * @param {String} actionName - Action to call. - * @param {JSON} data - JSON data to send. - * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers. - * @returns {Promise} Promise to be resolved with response data. - */ - sendWithPromise(actionName, data, transfers) { - var callbackId = this.callbackId++; - var message = { - sourceName: this.sourceName, - targetName: this.targetName, - action: actionName, - data, - callbackId, - }; - var capability = createPromiseCapability(); - this.callbacksCapabilities[callbackId] = capability; - try { - this.postMessage(message, transfers); - } catch (e) { - capability.reject(e); - } - return capability.promise; - }, - /** - * Sends a message to the comObj to invoke the action with the supplied data. - * Expect that the other side will callback to signal 'start_complete'. - * @param {String} actionName - Action to call. - * @param {JSON} data - JSON data to send. - * @param {Object} queueingStrategy - strategy to signal backpressure based on - * internal queue. - * @param {Array} [transfers] - Optional list of transfers/ArrayBuffers. - * @return {ReadableStream} ReadableStream to read data in chunks. - */ - sendWithStream(actionName, data, queueingStrategy, transfers) { - let streamId = this.streamId++; - let sourceName = this.sourceName; - let targetName = this.targetName; - - return new ReadableStream({ - start: (controller) => { - let startCapability = createPromiseCapability(); - this.streamControllers[streamId] = { - controller, - startCall: startCapability, - isClosed: false, - }; - this.postMessage({ - sourceName, - targetName, - action: actionName, - streamId, - data, - desiredSize: controller.desiredSize, - }); - // Return Promise for Async process, to signal success/failure. - return startCapability.promise; - }, - - pull: (controller) => { - let pullCapability = createPromiseCapability(); - this.streamControllers[streamId].pullCall = pullCapability; - this.postMessage({ - sourceName, - targetName, - stream: 'pull', - streamId, - desiredSize: controller.desiredSize, - }); - // Returning Promise will not call "pull" - // again until current pull is resolved. - return pullCapability.promise; - }, - - cancel: (reason) => { - let cancelCapability = createPromiseCapability(); - this.streamControllers[streamId].cancelCall = cancelCapability; - this.streamControllers[streamId].isClosed = true; - this.postMessage({ - sourceName, - targetName, - stream: 'cancel', - reason, - streamId, - }); - // Return Promise to signal success or failure. - return cancelCapability.promise; - }, - }, queueingStrategy); - }, - - _createStreamSink(data) { - let self = this; - let action = this.actionHandler[data.action]; - let streamId = data.streamId; - let desiredSize = data.desiredSize; - let sourceName = this.sourceName; - let targetName = data.sourceName; - let capability = createPromiseCapability(); - - let sendStreamRequest = ({ stream, chunk, transfers, - success, reason, }) => { - this.postMessage({ sourceName, targetName, stream, streamId, - chunk, success, reason, }, transfers); - }; - - let streamSink = { - enqueue(chunk, size = 1, transfers) { - if (this.isCancelled) { - return; - } - let lastDesiredSize = this.desiredSize; - this.desiredSize -= size; - // Enqueue decreases the desiredSize property of sink, - // so when it changes from positive to negative, - // set ready as unresolved promise. - if (lastDesiredSize > 0 && this.desiredSize <= 0) { - this.sinkCapability = createPromiseCapability(); - this.ready = this.sinkCapability.promise; - } - sendStreamRequest({ stream: 'enqueue', chunk, transfers, }); - }, - - close() { - if (this.isCancelled) { - return; - } - this.isCancelled = true; - sendStreamRequest({ stream: 'close', }); - delete self.streamSinks[streamId]; - }, - - error(reason) { - if (this.isCancelled) { - return; - } - this.isCancelled = true; - sendStreamRequest({ stream: 'error', reason, }); - }, - - sinkCapability: capability, - onPull: null, - onCancel: null, - isCancelled: false, - desiredSize, - ready: null, - }; - - streamSink.sinkCapability.resolve(); - streamSink.ready = streamSink.sinkCapability.promise; - this.streamSinks[streamId] = streamSink; - resolveCall(action[0], [data.data, streamSink], action[1]).then(() => { - sendStreamRequest({ stream: 'start_complete', success: true, }); - }, (reason) => { - sendStreamRequest({ stream: 'start_complete', success: false, reason, }); - }); - }, - - _processStreamMessage(data) { - let sourceName = this.sourceName; - let targetName = data.sourceName; - let streamId = data.streamId; - - let sendStreamResponse = ({ stream, success, reason, }) => { - this.comObj.postMessage({ sourceName, targetName, stream, - success, streamId, reason, }); - }; - - let deleteStreamController = () => { - // Delete streamController only when start, pull and - // cancel callbacks are resolved, to avoid "TypeError". - Promise.all([ - this.streamControllers[data.streamId].startCall, - this.streamControllers[data.streamId].pullCall, - this.streamControllers[data.streamId].cancelCall - ].map(function(capability) { - return capability && finalize(capability.promise); - })).then(() => { - delete this.streamControllers[data.streamId]; - }); - }; - - switch (data.stream) { - case 'start_complete': - resolveOrReject(this.streamControllers[data.streamId].startCall, - data.success, wrapReason(data.reason)); - break; - case 'pull_complete': - resolveOrReject(this.streamControllers[data.streamId].pullCall, - data.success, wrapReason(data.reason)); - break; - case 'pull': - // Ignore any pull after close is called. - if (!this.streamSinks[data.streamId]) { - sendStreamResponse({ stream: 'pull_complete', success: true, }); - break; - } - // Pull increases the desiredSize property of sink, - // so when it changes from negative to positive, - // set ready property as resolved promise. - if (this.streamSinks[data.streamId].desiredSize <= 0 && - data.desiredSize > 0) { - this.streamSinks[data.streamId].sinkCapability.resolve(); - } - // Reset desiredSize property of sink on every pull. - this.streamSinks[data.streamId].desiredSize = data.desiredSize; - resolveCall(this.streamSinks[data.streamId].onPull).then(() => { - sendStreamResponse({ stream: 'pull_complete', success: true, }); - }, (reason) => { - sendStreamResponse({ stream: 'pull_complete', - success: false, reason, }); - }); - break; - case 'enqueue': - assert(this.streamControllers[data.streamId], - 'enqueue should have stream controller'); - if (!this.streamControllers[data.streamId].isClosed) { - this.streamControllers[data.streamId].controller.enqueue(data.chunk); - } - break; - case 'close': - assert(this.streamControllers[data.streamId], - 'close should have stream controller'); - if (this.streamControllers[data.streamId].isClosed) { - break; - } - this.streamControllers[data.streamId].isClosed = true; - this.streamControllers[data.streamId].controller.close(); - deleteStreamController(); - break; - case 'error': - assert(this.streamControllers[data.streamId], - 'error should have stream controller'); - this.streamControllers[data.streamId].controller. - error(wrapReason(data.reason)); - deleteStreamController(); - break; - case 'cancel_complete': - resolveOrReject(this.streamControllers[data.streamId].cancelCall, - data.success, wrapReason(data.reason)); - deleteStreamController(); - break; - case 'cancel': - if (!this.streamSinks[data.streamId]) { - break; - } - resolveCall(this.streamSinks[data.streamId].onCancel, - [wrapReason(data.reason)]).then(() => { - sendStreamResponse({ stream: 'cancel_complete', success: true, }); - }, (reason) => { - sendStreamResponse({ stream: 'cancel_complete', - success: false, reason, }); - }); - this.streamSinks[data.streamId].sinkCapability. - reject(wrapReason(data.reason)); - this.streamSinks[data.streamId].isCancelled = true; - delete this.streamSinks[data.streamId]; - break; - default: - throw new Error('Unexpected stream case'); - } - }, - - /** - * Sends raw message to the comObj. - * @private - * @param {Object} message - Raw message. - * @param transfers List of transfers/ArrayBuffers, or undefined. - */ - postMessage(message, transfers) { - if (transfers && this.postMessageTransfers) { - this.comObj.postMessage(message, transfers); - } else { - this.comObj.postMessage(message); - } - }, - - destroy() { - this.comObj.removeEventListener('message', this._onComObjOnMessage); - }, -}; - export { FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, @@ -1486,7 +1063,6 @@ export { CMapCompressionType, AbortException, InvalidPDFException, - MessageHandler, MissingDataException, MissingPDFException, NativeImageDecoding, diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 01510af6797cc..a993d796de76c 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -20,6 +20,7 @@ "evaluator_spec.js", "fonts_spec.js", "function_spec.js", + "message_handler_spec.js", "metadata_spec.js", "murmurhash3_spec.js", "network_utils_spec.js", @@ -31,7 +32,6 @@ "type1_parser_spec.js", "ui_utils_spec.js", "unicode_spec.js", - "util_spec.js", - "util_stream_spec.js" + "util_spec.js" ] } diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index f239f0ec70bbe..b95a92f6ed7d3 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -62,6 +62,7 @@ function initializePDFJS(callback) { 'pdfjs-test/unit/evaluator_spec', 'pdfjs-test/unit/fonts_spec', 'pdfjs-test/unit/function_spec', + 'pdfjs-test/unit/message_handler_spec', 'pdfjs-test/unit/metadata_spec', 'pdfjs-test/unit/murmurhash3_spec', 'pdfjs-test/unit/network_spec', @@ -74,7 +75,6 @@ function initializePDFJS(callback) { 'pdfjs-test/unit/ui_utils_spec', 'pdfjs-test/unit/unicode_spec', 'pdfjs-test/unit/util_spec', - 'pdfjs-test/unit/util_stream_spec', ].map(function (moduleName) { return SystemJS.import(moduleName); })).then(function(modules) { diff --git a/test/unit/util_stream_spec.js b/test/unit/message_handler_spec.js similarity index 98% rename from test/unit/util_stream_spec.js rename to test/unit/message_handler_spec.js index d0b2b92e3420a..9db31ac1f977c 100644 --- a/test/unit/util_stream_spec.js +++ b/test/unit/message_handler_spec.js @@ -13,9 +13,10 @@ * limitations under the License. */ -import { createPromiseCapability, MessageHandler } from '../../src/shared/util'; +import { createPromiseCapability } from '../../src/shared/util'; +import { MessageHandler } from '../../src/shared/message_handler'; -describe('util_stream', function () { +describe('message_handler', function () { // Temporary fake port for sending messages between main and worker. class FakePort { constructor() { From 89caaf407120fea9f7eebe8b995d5dce179943c2 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 4 Jun 2018 12:38:05 +0200 Subject: [PATCH 7/7] Use `LoopbackPort` in the "message_handler" unit-tests There's no good reason, as far as I can tell, to duplicate the functionality of the `LoopbackPort` in the unit-tests. The only difference between the implementations is that `LoopbackPort` mimics the (native) structured cloning, however that shouldn't matter here since the tests are only sending "simple" data (strings respectively arrays with numbers). Furthermore the patch also changes `LoopbackPort` to default to using "structured cloning" and deferred invocation of the listeners, since native typed array support is now a requirement for using the PDF.js library. --- src/display/api.js | 2 +- test/unit/message_handler_spec.js | 45 ++++++------------------------- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 4c23c77417820..b121661f8995f 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1205,7 +1205,7 @@ var PDFPageProxy = (function PDFPageProxyClosure() { })(); class LoopbackPort { - constructor(defer) { + constructor(defer = true) { this._listeners = []; this._defer = defer; this._deferred = Promise.resolve(undefined); diff --git a/test/unit/message_handler_spec.js b/test/unit/message_handler_spec.js index 9db31ac1f977c..72735300b91ce 100644 --- a/test/unit/message_handler_spec.js +++ b/test/unit/message_handler_spec.js @@ -14,39 +14,10 @@ */ import { createPromiseCapability } from '../../src/shared/util'; +import { LoopbackPort } from '../../src/display/api'; import { MessageHandler } from '../../src/shared/message_handler'; describe('message_handler', function () { - // Temporary fake port for sending messages between main and worker. - class FakePort { - constructor() { - this._listeners = []; - this._deferred = Promise.resolve(undefined); - } - - postMessage(obj) { - let event = { data: obj, }; - this._deferred.then(() => { - this._listeners.forEach(function (listener) { - listener.call(this, event); - }, this); - }); - } - - addEventListener(name, listener) { - this._listeners.push(listener); - } - - removeEventListener(name, listener) { - let i = this._listeners.indexOf(listener); - this._listeners.splice(i, 1); - } - - terminate() { - this._listeners = []; - } - } - // Sleep function to wait for sometime, similar to setTimeout but faster. function sleep(ticks) { return Promise.resolve().then(() => { @@ -56,7 +27,7 @@ describe('message_handler', function () { describe('sendWithStream', function () { it('should return a ReadableStream', function () { - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler1 = new MessageHandler('main', 'worker', port); let readable = messageHandler1.sendWithStream('fakeHandler'); // Check if readable is an instance of ReadableStream. @@ -66,7 +37,7 @@ describe('message_handler', function () { it('should read using a reader', function (done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler1 = new MessageHandler('main', 'worker', port); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => { @@ -110,7 +81,7 @@ describe('message_handler', function () { it('should not read any data when cancelled', function (done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => { sink.onPull = function () { @@ -162,7 +133,7 @@ describe('message_handler', function () { it('should not read when errored', function(done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => { sink.onPull = function () { @@ -205,7 +176,7 @@ describe('message_handler', function () { it('should read data with blocking promise', function (done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => { sink.onPull = function () { @@ -266,7 +237,7 @@ describe('message_handler', function () { it('should read data with blocking promise and buffer whole data' + ' into stream', function (done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => { sink.onPull = function () { @@ -326,7 +297,7 @@ describe('message_handler', function () { it('should ignore any pull after close is called', function (done) { let log = ''; - let port = new FakePort(); + let port = new LoopbackPort(); let capability = createPromiseCapability(); let messageHandler2 = new MessageHandler('worker', 'main', port); messageHandler2.on('fakeHandler', (data, sink) => {