From 1559a9856216858b31a90d67a64751f9e5dc436a Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 16 Nov 2020 14:27:27 +0100 Subject: [PATCH] JS -- Implement app object * https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/AcrobatDC_js_api_reference.pdf * Add color, fullscreen objects + few constants. --- src/display/annotation_layer.js | 7 + src/display/display_utils.js | 62 ++++ src/scripting_api/app.js | 515 ++++++++++++++++++++++++++- src/scripting_api/color.js | 129 +++++++ src/scripting_api/constants.js | 113 +++++- src/scripting_api/field.js | 40 ++- src/scripting_api/fullscreen.js | 145 ++++++++ src/scripting_api/initialization.js | 42 ++- src/scripting_api/quickjs-sandbox.js | 6 +- src/scripting_api/thermometer.js | 69 ++++ test/unit/scripting_spec.js | 154 +++++++- web/app.js | 4 + 12 files changed, 1267 insertions(+), 19 deletions(-) create mode 100644 src/scripting_api/color.js create mode 100644 src/scripting_api/fullscreen.js create mode 100644 src/scripting_api/thermometer.js diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 26271cbe681931..86ede98ebac793 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -15,6 +15,7 @@ import { addLinkAttributes, + ColorConverters, DOMSVGFactory, getFilenameFromUrl, LinkTarget, @@ -567,6 +568,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { event.target.setSelectionRange(selStart, selEnd); } }, + strokeColor() { + const color = detail.strokeColor; + event.target.style.color = ColorConverters[`${color[0]}_HTML`]( + color.slice(1) + ); + }, }; for (const name of Object.keys(detail)) { if (name in actions) { diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 302dd024404651..e629a98b8b7e01 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -635,6 +635,67 @@ class PDFDateString { } } +function makeColorComp(n) { + return Math.floor(Math.max(0, Math.min(1, n)) * 255) + .toString(16) + .padStart(2, "0"); +} + +const ColorConverters = { + // PDF specifications section 10.3 + CMYK_G(components) { + const [c, y, m, k] = components; + return ["G", 1 - Math.min(1, 0.3 * c + 0.59 * m + 0.11 * y + k)]; + }, + G_CMYK(components) { + const [g] = components; + return ["CMYK", 0, 0, 0, 1 - g]; + }, + G_RGB(components) { + const [g] = components; + return ["RGB", g, g, g]; + }, + G_HTML(components) { + const [g] = components; + const G = makeColorComp(g); + return `#${G}${G}${G}`; + }, + RGB_G(components) { + const [r, g, b] = components; + return ["G", 0.3 * r + 0.59 * g + 0.11 * b]; + }, + RGB_HTML(components) { + const [r, g, b] = components; + const R = makeColorComp(r); + const G = makeColorComp(g); + const B = makeColorComp(b); + return `#${R}${G}${B}`; + }, + T_HTML() { + return "#00000000"; + }, + CMYK_RGB(components) { + const [c, y, m, k] = components; + return [ + "RGB", + 1 - Math.min(1, c + k), + 1 - Math.min(1, m + k), + 1 - Math.min(1, y + k), + ]; + }, + CMYK_HTML(components) { + return ColorConverters.RGB_HTML(ColorConverters.CMYK_RGB(components)); + }, + RGB_CMYK(components) { + const [r, g, b] = components; + const c = 1 - r; + const m = 1 - g; + const y = 1 - b; + const k = Math.min(c, m, y); + return ["CMYK", c, m, y, k]; + }, +}; + export { PageViewport, RenderingCancelledException, @@ -645,6 +706,7 @@ export { BaseCanvasFactory, DOMCanvasFactory, BaseCMapReaderFactory, + ColorConverters, DOMCMapReaderFactory, DOMSVGFactory, StatTimer, diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index 2a823b168994e7..2cd09bf3110ac1 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -13,22 +13,50 @@ * limitations under the License. */ +import { Color } from "./color.js"; import { EventDispatcher } from "./event.js"; -import { NotSupportedError } from "./error.js"; +import { FullScreen } from "./fullscreen.js"; import { PDFObject } from "./pdf_object.js"; +import { Thermometer } from "./thermometer.js"; + +const VIEWER_TYPE = "PDF.js"; +const VIEWER_VARIATION = "Full"; +const VIEWER_VERSION = "10.0"; +const FORMS_VERSION = undefined; class App extends PDFObject { constructor(data) { super(data); + + this.calculate = true; + + this._constants = null; + this._focusRect = true; + this._fs = null; + this._language = App._getLanguage(data.language); + this._openInPlace = false; + this._platform = App._getPlatform(data.platform); + this._runtimeHighlight = false; + this._runtimeHighlightColor = ["T"]; + this._thermometer = null; + this._toolbar = false; + this._document = data._document; + this._proxyHandler = data.proxyHandler; this._objects = Object.create(null); this._eventDispatcher = new EventDispatcher( this._document, data.calculationOrder, this._objects ); + this._setTimeout = data.setTimeout; + this._clearTimeout = data.clearTimeout; + this._setInterval = data.setInterval; + this._clearInterval = data.clearInterval; + this._timeoutIds = null; + this._timeoutIdsRegistry = null; - // used in proxy.js to check that this the object with the backdoor + // used in proxy.js to check that this is the object with the backdoor this._isApp = true; } @@ -38,12 +66,339 @@ class App extends PDFObject { this._eventDispatcher.dispatch(pdfEvent); } + _registerTimeout(timeout, id, interval) { + if (!this._timeoutIds) { + this._timeoutIds = new WeakMap(); + // FinalizationRegistry isn't implemented in QuickJS + // eslint-disable-next-line no-undef + if (typeof FinalizationRegistry !== "undefined") { + // About setTimeOut/setInterval return values (specs): + // The return value of this method must be held in a + // JavaScript variable. + // Otherwise, the timeout object is subject to garbage-collection, + // which would cause the clock to stop. + + // eslint-disable-next-line no-undef + this._timeoutIdsRegistry = new FinalizationRegistry( + ([timeoutId, isInterval]) => { + if (isInterval) { + this._clearInterval(timeoutId); + } else { + this._clearTimeout(timeoutId); + } + } + ); + } + } + this._timeoutIds.set(timeout, [id, interval]); + if (this._timeoutIdsRegistry) { + this._timeoutIdsRegistry.register(timeout, [id, interval]); + } + } + + _unregisterTimeout(timeout) { + if (!this._timeoutIds || !this._timeoutIds.has(timeout)) { + return; + } + const [id, interval] = this._timeoutIds.get(timeout); + if (this._timeoutIdsRegistry) { + this._timeoutIdsRegistry.unregister(timeout); + } + this._timeoutIds.delete(timeout); + + if (interval) { + this._clearInterval(id); + } else { + this._clearTimeout(id); + } + } + + static _getPlatform(platform) { + if (typeof platform === "string") { + platform = platform.toLowerCase(); + if (platform.includes("win")) { + return "WIN"; + } else if (platform.includes("mac")) { + return "MAC"; + } + } + return "UNIX"; + } + + static _getLanguage(language) { + const [main, sub] = language.toLowerCase().split(/[-_]/); + switch (main) { + case "zh": + if (sub === "cn" || sub === "sg") { + return "CHS"; + } + return "CHT"; + case "da": + return "DAN"; + case "de": + return "DEU"; + case "es": + return "ESP"; + case "fr": + return "FRA"; + case "it": + return "ITA"; + case "ko": + return "KOR"; + case "ja": + return "JPN"; + case "nl": + return "NLD"; + case "no": + return "NOR"; + case "pt": + if (sub === "br") { + return "PTB"; + } + return "ENU"; + case "fi": + return "SUO"; + case "SV": + return "SVE"; + default: + return "ENU"; + } + } + get activeDocs() { return [this._document.wrapped]; } set activeDocs(_) { - throw new NotSupportedError("app.activeDocs"); + throw new Error("app.activeDocs is read-only"); + } + + get constants() { + if (!this._constants) { + this._constants = Object.freeze({ + align: Object.freeze({ + left: 0, + center: 1, + right: 2, + top: 3, + bottom: 4, + }), + }); + } + return this._constants; + } + + set constants(_) { + throw new Error("app.constants is read-only"); + } + + get focusRect() { + return this._focusRect; + } + + set focusRect(val) { + /* TODO or not */ + this._focusRect = val; + } + + get formsVersion() { + return FORMS_VERSION; + } + + set formsVersion(_) { + throw new Error("app.formsVersion is read-only"); + } + + get fromPDFConverters() { + return []; + } + + set fromPDFConverters(_) { + throw new Error("app.fromPDFConverters is read-only"); + } + + get fs() { + if (this._fs === null) { + this._fs = new Proxy( + new FullScreen({ send: this._send }), + this._proxyHandler + ); + } + return this._fs; + } + + set fs(_) { + throw new Error("app.fs is read-only"); + } + + get language() { + return this._language; + } + + set language(_) { + throw new Error("app.language is read-only"); + } + + get media() { + return undefined; + } + + set media(_) { + throw new Error("app.media is read-only"); + } + + get monitors() { + return []; + } + + set monitors(_) { + throw new Error("app.monitors is read-only"); + } + + get numPlugins() { + return 0; + } + + set numPlugins(_) { + throw new Error("app.numPlugins is read-only"); + } + + get openInPlace() { + return this._openInPlace; + } + + set openInPlace(val) { + this._openInPlace = val; + /* TODO */ + } + + get platform() { + return this._platform; + } + + set platform(_) { + throw new Error("app.platform is read-only"); + } + + get plugins() { + return []; + } + + set plugins(_) { + throw new Error("app.plugins is read-only"); + } + + get printColorProfiles() { + return []; + } + + set printColorProfiles(_) { + throw new Error("app.printColorProfiles is read-only"); + } + + get printerNames() { + return []; + } + + set printerNames(_) { + throw new Error("app.printerNames is read-only"); + } + + get runtimeHighlight() { + return this._runtimeHighlight; + } + + set runtimeHighlight(val) { + this._runtimeHighlight = val; + /* TODO */ + } + + get runtimeHighlightColor() { + return this._runtimeHighlightColor; + } + + set runtimeHighlightColor(val) { + if (Color._isValidColor(val)) { + this._runtimeHighlightColor = val; + /* TODO */ + } + } + + get thermometer() { + if (this._thermometer === null) { + this._thermometer = new Proxy( + new Thermometer({ send: this._send }), + this._proxyHandler + ); + } + return this._thermometer; + } + + set thermometer(_) { + throw new Error("app.thermometer is read-only"); + } + + get toolbar() { + return this._toolbar; + } + + set toolbar(val) { + this._toolbar = val; + /* TODO */ + } + + get toolbarHorizontal() { + return this.toolbar; + } + + set toolbarHorizontal(value) { + /* has been deprecated and it's now equivalent to toolbar */ + this.toolbar = value; + } + + get toolbarVertical() { + return this.toolbar; + } + + set toolbarVertical(value) { + /* has been deprecated and it's now equivalent to toolbar */ + this.toolbar = value; + } + + get viewerType() { + return VIEWER_TYPE; + } + + set viewerType(_) { + throw new Error("app.viewerType is read-only"); + } + + get viewerVariation() { + return VIEWER_VARIATION; + } + + set viewerVariation(_) { + throw new Error("app.viewerVariation is read-only"); + } + + get viewerVersion() { + return VIEWER_VERSION; + } + + set viewerVersion(_) { + throw new Error("app.viewerVersion is read-only"); + } + + addMenuItem() { + /* Not implemented */ + } + + addSubMenu() { + /* Not implemented */ + } + + addToolButton() { + /* Not implemented */ } alert( @@ -56,6 +411,160 @@ class App extends PDFObject { ) { this._send({ command: "alert", value: cMsg }); } + + beep() { + /* Not implemented */ + } + + beginPriv() { + /* Not implemented */ + } + + browseForDoc() { + /* Not implemented */ + } + + clearInterval(oInterval) { + this.unregisterTimeout(oInterval); + } + + clearTimeOut(oTime) { + this.unregisterTimeout(oTime); + } + + endPriv() { + /* Not implemented */ + } + + execDialog() { + /* Not implemented */ + } + + execMenuItem() { + /* Not implemented */ + } + + getNthPlugInName() { + /* Not implemented */ + } + + getPath() { + /* Not implemented */ + } + + goBack() { + /* TODO */ + } + + goForward() { + /* TODO */ + } + + hideMenuItem() { + /* Not implemented */ + } + + hideToolbarButton() { + /* Not implemented */ + } + + launchURL() { + /* Unsafe */ + } + + listMenuItems() { + /* Not implemented */ + } + + listToolbarButtons() { + /* Not implemented */ + } + + loadPolicyFile() { + /* Not implemented */ + } + + mailGetAddrs() { + /* Not implemented */ + } + + mailMsg() { + /* TODO or not ? */ + } + + newDoc() { + /* Not implemented */ + } + + newCollection() { + /* Not implemented */ + } + + newFDF() { + /* Not implemented */ + } + + openDoc() { + /* Not implemented */ + } + + openFDF() { + /* Not implemented */ + } + + popUpMenu() { + /* Not implemented */ + } + + popUpMenuEx() { + /* Not implemented */ + } + + removeToolButton() { + /* Not implemented */ + } + + response() { + /* TODO or not */ + } + + setInterval(cExpr, nMilliseconds) { + if (typeof cExpr !== "string") { + throw new TypeError("First argument of app.setInterval must be a string"); + } + if (typeof nMilliseconds !== "number") { + throw new TypeError( + "Second argument of app.setInterval must be a number" + ); + } + + const id = this._setInterval(cExpr, nMilliseconds); + const timeout = Object.create(null); + this._registerTimeout(timeout, id, true); + return timeout; + } + + setTimeOut(cExpr, nMilliseconds) { + if (typeof cExpr !== "string") { + throw new TypeError("First argument of app.setTimeOut must be a string"); + } + if (typeof nMilliseconds !== "number") { + throw new TypeError("Second argument of app.setTimeOut must be a number"); + } + + const id = this._setTimeout(cExpr, nMilliseconds); + const timeout = Object.create(null); + this._registerTimeout(timeout, id, false); + return timeout; + } + + trustedFunction() { + /* Not implemented */ + } + + trustPropagatorFunction() { + /* Not implemented */ + } } export { App }; diff --git a/src/scripting_api/color.js b/src/scripting_api/color.js new file mode 100644 index 00000000000000..1f163d6eb6c5ba --- /dev/null +++ b/src/scripting_api/color.js @@ -0,0 +1,129 @@ +/* Copyright 2020 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 { ColorConverters } from "../display/display_utils.js"; +import { PDFObject } from "./pdf_object.js"; + +class Color extends PDFObject { + constructor() { + super({}); + + this.transparent = ["T"]; + this.black = ["G", 0]; + this.white = ["G", 1]; + this.red = ["RGB", 1, 0, 0]; + this.green = ["RGB", 0, 1, 0]; + this.blue = ["RGB", 0, 0, 1]; + this.cyan = ["CMYK", 1, 0, 0, 0]; + this.magenta = ["CMYK", 0, 1, 0, 0]; + this.yellow = ["CMYK", 0, 0, 1, 0]; + this.dkGray = ["G", 0.25]; + this.gray = ["G", 0.5]; + this.ltGray = ["G", 0.75]; + } + + static _isValidSpace(cColorSpace) { + return ( + typeof cColorSpace === "string" && + (cColorSpace === "T" || + cColorSpace === "G" || + cColorSpace === "RGB" || + cColorSpace === "CMYK") + ); + } + + static _isValidColor(colorArray) { + if (!Array.isArray(colorArray) || colorArray.length === 0) { + return false; + } + const space = colorArray[0]; + if (!Color._isValidSpace(space)) { + return false; + } + + switch (space) { + case "T": + if (colorArray.length !== 1) { + return false; + } + break; + case "G": + if (colorArray.length !== 2) { + return false; + } + break; + case "RGB": + if (colorArray.length !== 4) { + return false; + } + break; + case "CMYK": + if (colorArray.length !== 5) { + return false; + } + break; + default: + return false; + } + + return colorArray + .slice(1) + .every(c => typeof c === "number" && c >= 0 && c <= 1); + } + + static _getCorrectColor(colorArray) { + return Color._isValidColor(colorArray) ? colorArray : ["G", 0]; + } + + convert(colorArray, cColorSpace) { + if (!Color._isValidSpace(cColorSpace)) { + return this.black; + } + + if (cColorSpace === "T") { + return ["T"]; + } + + colorArray = Color._getCorrectColor(colorArray); + if (colorArray[0] === cColorSpace) { + return colorArray; + } + + if (colorArray[0] === "T") { + return this.convert(this.black, cColorSpace); + } + + return ColorConverters[`${colorArray[0]}_${cColorSpace}`]( + colorArray.slice(1) + ); + } + + equal(colorArray1, colorArray2) { + colorArray1 = Color._getCorrectColor(colorArray1); + colorArray2 = Color._getCorrectColor(colorArray2); + + if (colorArray1[0] === "T" || colorArray2[0] === "T") { + return colorArray1[0] === "T" && colorArray2[0] === "T"; + } + + if (colorArray1[0] !== colorArray2[0]) { + colorArray2 = this.convert(colorArray2, colorArray1[0]); + } + + return colorArray1.slice(1).every((c, i) => c === colorArray2[i + 1]); + } +} + +export { Color }; diff --git a/src/scripting_api/constants.js b/src/scripting_api/constants.js index 683e37324a1630..6fb77808d1492a 100644 --- a/src/scripting_api/constants.js +++ b/src/scripting_api/constants.js @@ -13,6 +13,105 @@ * limitations under the License. */ +const Border = Object.freeze({ + s: "solid", + d: "dashed", + b: "beveled", + i: "inset", + u: "underline", +}); + +const Cursor = Object.freeze({ + visible: 0, + hidden: 1, + delay: 2, +}); + +const Display = Object.freeze({ + visible: 0, + hidden: 1, + noPrint: 2, + noView: 3, +}); + +const Font = Object.freeze({ + Times: "Times-Roman", + TimesB: "Times-Bold", + TimesI: "Times-Italic", + TimesBI: "Times-BoldItalic", + Helv: "Helvetica", + HelvB: "Helvetica-Bold", + HelvI: "Helvetica-Oblique", + HelvBI: "Helvetica-BoldOblique", + Cour: "Courier", + CourB: "Courier-Bold", + CourI: "Courier-Oblique", + CourBI: "Courier-BoldOblique", + Symbol: "Symbol", + ZapfD: "ZapfDingbats", + KaGo: "HeiseiKakuGo-W5-UniJIS-UCS2-H", + KaMi: "HeiseiMin-W3-UniJIS-UCS2-H", +}); + +const Highlight = Object.freeze({ + n: "none", + i: "invert", + p: "push", + o: "outline", +}); + +const Position = Object.freeze({ + textOnly: 0, + iconOnly: 1, + iconTextV: 2, + textIconV: 3, + iconTextH: 4, + textIconH: 5, + overlay: 6, +}); + +const ScaleHow = Object.freeze({ + proportional: 0, + anamorphic: 1, +}); + +const ScaleWhen = Object.freeze({ + always: 0, + never: 1, + tooBig: 2, + tooSmall: 3, +}); + +const Style = Object.freeze({ + ch: "check", + cr: "cross", + di: "diamond", + ci: "circle", + st: "star", + sq: "square", +}); + +const Trans = Object.freeze({ + blindsH: "BlindsHorizontal", + blindsV: "BlindsVertical", + boxI: "BoxIn", + boxO: "BoxOut", + dissolve: "Dissolve", + glitterD: "GlitterDown", + glitterR: "GlitterRight", + glitterRD: "GlitterRightDown", + random: "Random", + replace: "Replace", + splitHI: "SplitHorizontalIn", + splitHO: "SplitHorizontalOut", + splitVI: "SplitVerticalIn", + splitVO: "SplitVerticalOut", + wipeD: "WipeDown", + wipeL: "WipeLeft", + wipeR: "WipeRight", + wipeU: "WipeUp", +}); + const ZoomType = Object.freeze({ none: "NoVary", fitP: "FitPage", @@ -23,4 +122,16 @@ const ZoomType = Object.freeze({ refW: "ReflowWidth", }); -export { ZoomType }; +export { + Border, + Cursor, + Display, + Font, + Highlight, + Position, + ScaleHow, + ScaleWhen, + Style, + Trans, + ZoomType, +}; diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 1eb0304c9a056d..af62de32031298 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -13,6 +13,7 @@ * limitations under the License. */ +import { Color } from "./color.js"; import { PDFObject } from "./pdf_object.js"; class Field extends PDFObject { @@ -41,7 +42,6 @@ class Field extends PDFObject { this.editable = data.editable; this.exportValues = data.exportValues; this.fileSelect = data.fileSelect; - this.fillColor = data.fillColor; this.hidden = data.hidden; this.highlight = data.highlight; this.lineWidth = data.lineWidth; @@ -59,10 +59,8 @@ class Field extends PDFObject { this.richText = data.richText; this.richValue = data.richValue; this.rotation = data.rotation; - this.strokeColor = data.strokeColor; this.style = data.style; this.submitName = data.submitName; - this.textColor = data.textColor; this.textFont = data.textFont; this.textSize = data.textSize; this.type = data.type; @@ -73,6 +71,40 @@ class Field extends PDFObject { // Private this._document = data.doc; this._actions = this._createActionsMap(data.actions); + + this._fillColor = data.fillColor | ["T"]; + this._strokeColor = data.strokeColor | ["G", 0]; + this._textColor = data.textColor | ["G", 0]; + } + + get fillColor() { + return this._fillColor; + } + + set fillColor(color) { + if (Color._isValidColor(color)) { + this._fillColor = color; + } + } + + get strokeColor() { + return this._strokeColor; + } + + set strokeColor(color) { + if (Color._isValidColor(color)) { + this._strokeColor = color; + } + } + + get textColor() { + return this._textColor; + } + + set textColor(color) { + if (Color._isValidColor(color)) { + this._textColor = color; + } } setAction(cTrigger, cScript) { @@ -131,7 +163,7 @@ class Field extends PDFObject { `"${error.toString()}" for event ` + `"${eventName}" in object ${this._id}.` + `\n${error.stack}`; - this._send({ id: "error", value }); + this._send({ command: "error", value }); } return true; diff --git a/src/scripting_api/fullscreen.js b/src/scripting_api/fullscreen.js new file mode 100644 index 00000000000000..87b7d7f9d3d523 --- /dev/null +++ b/src/scripting_api/fullscreen.js @@ -0,0 +1,145 @@ +/* Copyright 2020 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 { Cursor } from "./constants.js"; +import { PDFObject } from "./pdf_object.js"; + +class FullScreen extends PDFObject { + constructor(data) { + super(data); + + this._backgroundColor = []; + this._clickAdvances = true; + this._cursor = Cursor.hidden; + this._defaultTransition = ""; + this._escapeExits = true; + this._isFullScreen = true; + this._loop = false; + this._timeDelay = 3600; + this._usePageTiming = false; + this._useTimer = false; + } + + get backgroundColor() { + return this._backgroundColor; + } + + set backgroundColor(_) { + /* TODO or not */ + } + + get clickAdvances() { + return this._clickAdvances; + } + + set clickAdvances(_) { + /* TODO or not */ + } + + get cursor() { + return this._cursor; + } + + set cursor(_) { + /* TODO or not */ + } + + get defaultTransition() { + return this._defaultTransition; + } + + set defaultTransition(_) { + /* TODO or not */ + } + + get escapeExits() { + return this._escapeExits; + } + + set escapeExits(_) { + /* TODO or not */ + } + + get isFullScreen() { + return this._isFullScreen; + } + + set isFullScreen(_) { + /* TODO or not */ + } + + get loop() { + return this._loop; + } + + set loop(_) { + /* TODO or not */ + } + + get timeDelay() { + return this._timeDelay; + } + + set timeDelay(_) { + /* TODO or not */ + } + + get transitions() { + // This list of possible value for transition has been found: + // https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/5186AcroJS.pdf#page=198 + return [ + "Replace", + "WipeRight", + "WipeLeft", + "WipeDown", + "WipeUp", + "SplitHorizontalIn", + "SplitHorizontalOut", + "SplitVerticalIn", + "SplitVerticalOut", + "BlindsHorizontal", + "BlindsVertical", + "BoxIn", + "BoxOut", + "GlitterRight", + "GlitterDown", + "GlitterRightDown", + "Dissolve", + "Random", + ]; + } + + set transitions(_) { + throw new Error("fullscreen.transitions is read-only"); + } + + get usePageTiming() { + return this._usePageTiming; + } + + set usePageTiming(_) { + /* TODO or not */ + } + + get useTimer() { + return this._useTimer; + } + + set useTimer(_) { + /* TODO or not */ + } +} + +export { FullScreen }; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 255386a39b74f5..85fafd6ffd1e64 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -13,18 +13,38 @@ * limitations under the License. */ +import { + Border, + Cursor, + Display, + Font, + Highlight, + Position, + ScaleHow, + ScaleWhen, + Style, + Trans, + ZoomType, +} from "./constants.js"; import { AForm } from "./aform.js"; import { App } from "./app.js"; +import { Color } from "./color.js"; import { Console } from "./console.js"; import { Doc } from "./doc.js"; import { Field } from "./field.js"; import { ProxyHandler } from "./proxy.js"; import { Util } from "./util.js"; -import { ZoomType } from "./constants.js"; function initSandbox({ data, extra, out }) { const proxyHandler = new ProxyHandler(data.dispatchEventName); - const { send, crackURL } = extra; + const { + send, + crackURL, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + } = extra; const doc = new Doc({ send, ...data.docInfo, @@ -32,8 +52,14 @@ function initSandbox({ data, extra, out }) { const _document = { obj: doc, wrapped: new Proxy(doc, proxyHandler) }; const app = new App({ send, + setTimeout, + clearTimeout, + setInterval, + clearInterval, _document, calculationOrder: data.calculationOrder, + proxyHandler, + ...data.appInfo, }); const util = new Util({ crackURL }); const aform = new AForm(doc, app, util); @@ -50,9 +76,21 @@ function initSandbox({ data, extra, out }) { out.global = Object.create(null); out.app = new Proxy(app, proxyHandler); + out.color = new Proxy(new Color(), proxyHandler); out.console = new Proxy(new Console({ send }), proxyHandler); out.util = new Proxy(util, proxyHandler); + out.border = Border; + out.cursor = Cursor; + out.display = Display; + out.font = Font; + out.highlight = Highlight; + out.position = Position; + out.scaleHow = ScaleHow; + out.scaleWhen = ScaleWhen; + out.style = Style; + out.trans = Trans; out.zoomtype = ZoomType; + for (const name of Object.getOwnPropertyNames(AForm.prototype)) { if (name.startsWith("AF")) { out[name] = aform[name].bind(aform); diff --git a/src/scripting_api/quickjs-sandbox.js b/src/scripting_api/quickjs-sandbox.js index 59b4a5b45dbd43..cbf6e4e871a9c7 100644 --- a/src/scripting_api/quickjs-sandbox.js +++ b/src/scripting_api/quickjs-sandbox.js @@ -83,7 +83,11 @@ class Sandbox { evalForTesting(code, key) { if (this._testMode) { this._evalInSandbox( - `send({ id: "${key}", result: ${code} });`, + `try { + send({ id: "${key}", result: ${code} }); + } catch (error) { + send({ id: "${key}", result: error.message }); + }`, this._alertOnError ); } diff --git a/src/scripting_api/thermometer.js b/src/scripting_api/thermometer.js new file mode 100644 index 00000000000000..5e164779439c52 --- /dev/null +++ b/src/scripting_api/thermometer.js @@ -0,0 +1,69 @@ +/* Copyright 2020 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 { PDFObject } from "./pdf_object.js"; + +class Thermometer extends PDFObject { + constructor(data) { + super(data); + + this._cancelled = false; + this._duration = 100; + this._text = ""; + this._value = 0; + } + + get cancelled() { + return this._cancelled; + } + + set cancelled(_) { + throw new Error("thermometer.cancelled is read-only"); + } + + get duration() { + return this._duration; + } + + set duration(val) { + this._duration = val; + } + + get text() { + return this._text; + } + + set text(val) { + this._text = val; + } + + get value() { + return this._value; + } + + set value(val) { + this._value = val; + } + + begin() { + /* TODO */ + } + + end() { + /* TODO */ + } +} + +export { Thermometer }; diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 6495da2ceb3ebd..e4d6b1077c8a24 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -23,6 +23,15 @@ describe("Scripting", function () { return id; } + function myeval(code) { + const key = (test_id++).toString(); + return sandbox.eval(code, key).then(() => { + const result = send_queue.get(key).result; + send_queue.delete(key); + return result; + }); + } + beforeAll(function (done) { test_id = 0; ref = 1; @@ -92,6 +101,7 @@ describe("Scripting", function () { ], }, calculationOrder: [], + appInfo: { language: "en-US", platform: "Linux x86_64" }, dispatchEventName: "_dispatchMe", }; sandbox.createSandbox(data); @@ -115,15 +125,9 @@ describe("Scripting", function () { }); describe("Util", function () { - function myeval(code) { - const key = (test_id++).toString(); - return sandbox.eval(code, key).then(() => { - return send_queue.get(key).result; - }); - } - beforeAll(function (done) { sandbox.createSandbox({ + appInfo: { language: "en-US", platform: "Linux x86_64" }, objects: {}, calculationOrder: [], dispatchEventName: "_dispatchMe", @@ -214,7 +218,7 @@ describe("Scripting", function () { .then(() => done()); }); - it(" print a string with a percent", function (done) { + it("print a string with a percent", function (done) { myeval(`util.printf("%%s")`) .then(value => { expect(value).toEqual("%%s"); @@ -250,6 +254,7 @@ describe("Scripting", function () { }, ], }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, calculationOrder: [], dispatchEventName: "_dispatchMe", }; @@ -287,6 +292,7 @@ describe("Scripting", function () { }, ], }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, calculationOrder: [], dispatchEventName: "_dispatchMe", }; @@ -328,6 +334,7 @@ describe("Scripting", function () { }, ], }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, calculationOrder: [], dispatchEventName: "_dispatchMe", }; @@ -368,6 +375,7 @@ describe("Scripting", function () { }, ], }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, calculationOrder: [], dispatchEventName: "_dispatchMe", }; @@ -412,6 +420,7 @@ describe("Scripting", function () { }, ], }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, calculationOrder: [refId2], dispatchEventName: "_dispatchMe", }; @@ -435,4 +444,133 @@ describe("Scripting", function () { .catch(done.fail); }); }); + + describe("Color", function () { + beforeAll(function (done) { + sandbox.createSandbox({ + appInfo: { language: "en-US", platform: "Linux x86_64" }, + objects: {}, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }); + done(); + }); + + function round(color) { + return [ + color[0], + ...color.slice(1).map(x => Math.round(x * 1000) / 1000), + ]; + } + + it("should convert RGB color for different color spaces", function (done) { + Promise.all([ + myeval(`color.convert(["RGB", 0.1, 0.2, 0.3], "T")`).then(value => { + expect(round(value)).toEqual(["T"]); + }), + myeval(`color.convert(["RGB", 0.1, 0.2, 0.3], "G")`).then(value => { + expect(round(value)).toEqual(["G", 0.181]); + }), + myeval(`color.convert(["RGB", 0.1, 0.2, 0.3], "RGB")`).then(value => { + expect(round(value)).toEqual(["RGB", 0.1, 0.2, 0.3]); + }), + myeval(`color.convert(["RGB", 0.1, 0.2, 0.3], "CMYK")`).then(value => { + expect(round(value)).toEqual(["CMYK", 0.9, 0.8, 0.7, 0.7]); + }), + ]).then(() => done()); + }); + + it("should convert CMYK color for different color spaces", function (done) { + Promise.all([ + myeval(`color.convert(["CMYK", 0.1, 0.2, 0.3, 0.4], "T")`).then( + value => { + expect(round(value)).toEqual(["T"]); + } + ), + myeval(`color.convert(["CMYK", 0.1, 0.2, 0.3, 0.4], "G")`).then( + value => { + expect(round(value)).toEqual(["G", 0.371]); + } + ), + myeval(`color.convert(["CMYK", 0.1, 0.2, 0.3, 0.4], "RGB")`).then( + value => { + expect(round(value)).toEqual(["RGB", 0.5, 0.3, 0.4]); + } + ), + myeval(`color.convert(["CMYK", 0.1, 0.2, 0.3, 0.4], "CMYK")`).then( + value => { + expect(round(value)).toEqual(["CMYK", 0.1, 0.2, 0.3, 0.4]); + } + ), + ]).then(() => done()); + }); + + it("should convert Gray color for different color spaces", function (done) { + Promise.all([ + myeval(`color.convert(["G", 0.1], "T")`).then(value => { + expect(round(value)).toEqual(["T"]); + }), + myeval(`color.convert(["G", 0.1], "G")`).then(value => { + expect(round(value)).toEqual(["G", 0.1]); + }), + myeval(`color.convert(["G", 0.1], "RGB")`).then(value => { + expect(round(value)).toEqual(["RGB", 0.1, 0.1, 0.1]); + }), + myeval(`color.convert(["G", 0.1], "CMYK")`).then(value => { + expect(round(value)).toEqual(["CMYK", 0, 0, 0, 0.9]); + }), + ]).then(() => done()); + }); + + it("should convert Transparent color for different color spaces", function (done) { + Promise.all([ + myeval(`color.convert(["T"], "T")`).then(value => { + expect(round(value)).toEqual(["T"]); + }), + myeval(`color.convert(["T"], "G")`).then(value => { + expect(round(value)).toEqual(["G", 0]); + }), + myeval(`color.convert(["T"], "RGB")`).then(value => { + expect(round(value)).toEqual(["RGB", 0, 0, 0]); + }), + myeval(`color.convert(["T"], "CMYK")`).then(value => { + expect(round(value)).toEqual(["CMYK", 0, 0, 0, 1]); + }), + ]).then(() => done()); + }); + }); + + describe("App", function () { + beforeAll(function (done) { + sandbox.createSandbox({ + appInfo: { language: "en-US", platform: "Linux x86_64" }, + objects: {}, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }); + done(); + }); + + it("should test language", function (done) { + Promise.all([ + myeval(`app.language`).then(value => { + expect(value).toEqual("ENU"); + }), + myeval(`app.language = "hello"`).then(value => { + expect(value).toEqual("app.language is read-only"); + }), + ]).then(() => done()); + }); + + it("should test platform", function (done) { + Promise.all([ + myeval(`app.platform`).then(value => { + expect(value).toEqual("UNIX"); + }), + myeval(`app.platform = "hello"`).then(value => { + expect(value).toEqual("app.platform is read-only"); + }), + ]).then(() => done()); + }); + }); }); diff --git a/web/app.js b/web/app.js index 91384891b80efe..094af54d0bd300 100644 --- a/web/app.js +++ b/web/app.js @@ -1486,6 +1486,10 @@ const PDFViewerApplication = { objects, dispatchEventName, calculationOrder, + appInfo: { + platform: navigator.platform, + language: navigator.language, + }, docInfo: { ...info, baseURL: this.baseUrl,