From f69e848b1c2efe9d2cc47d7c658e480ef15b94ab Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 26 Oct 2020 10:14:57 +0100 Subject: [PATCH] JS -- Add 'util' object This patch provides an implementation of the util object as described: * https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf#page=716 --- gulpfile.js | 2 +- src/scripting_api/util.js | 551 ++++++++++++++++++++++++++++++++++++ test/unit/clitests.json | 1 + test/unit/jasmine-boot.js | 1 + test/unit/scripting_spec.js | 98 +++++++ 5 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 test/unit/scripting_spec.js diff --git a/gulpfile.js b/gulpfile.js index a8344793c..1c5d4c415 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1273,7 +1273,7 @@ function buildLib(defines, dir) { return merge([ gulp.src( [ - "src/{core,display,shared}/*.js", + "src/{core,display,scripting_api,shared}/*.js", "!src/shared/{cffStandardStrings,fonts_utils}.js", "src/{pdf,pdf.worker}.js", ], diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index db85f1b23..14f91e26a 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -20,11 +20,562 @@ class Util extends PDFObject { super(data); this._crackURL = data.crackURL; + this._scandCache = new Map(); + this._months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + this._days = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + this.MILLISECONDS_IN_DAY = 86400000; + this.MILLISECONDS_IN_WEEK = 604800000; } crackURL(cURL) { return this._crackURL(cURL); } + + printf(...args) { + if (args.length === 0) { + throw new Error("Invalid number of params in printf"); + } + + if (typeof args[0] !== "string") { + throw new TypeError("First argument of printf must be a string"); + } + + const pattern = /%(,[0-4])?([\+ 0#]+)?([0-9]+)?(\.[0-9]+)?(.)/g; + const PLUS = 1; + const SPACE = 2; + const ZERO = 4; + const HASH = 8; + let i = 0; + return args[0].replace(pattern, function ( + match, + nDecSep, + cFlags, + nWidth, + nPrecision, + cConvChar + ) { + // cConvChar must be one of d, f, s, x + if ( + cConvChar !== "d" && + cConvChar !== "f" && + cConvChar !== "s" && + cConvChar !== "x" + ) { + const buf = ["%"]; + for (const str of [nDecSep, cFlags, nWidth, nPrecision, cConvChar]) { + if (str) { + buf.push(str); + } + } + return buf.join(""); + } + + i++; + if (i === args.length) { + throw new Error("Not enough arguments in printf"); + } + const arg = args[i]; + + if (cConvChar === "s") { + return arg.toString(); + } + + let flags = 0; + if (cFlags) { + for (const flag of cFlags) { + switch (flag) { + case "+": + flags |= PLUS; + break; + case " ": + flags |= SPACE; + break; + case "0": + flags |= ZERO; + break; + case "#": + flags |= HASH; + break; + } + } + } + cFlags = flags; + + if (nWidth) { + nWidth = parseInt(nWidth); + } + + let intPart = Math.trunc(arg); + + if (cConvChar === "x") { + let hex = Math.abs(intPart).toString(16).toUpperCase(); + if (nWidth !== undefined) { + hex = hex.padStart(nWidth, cFlags & ZERO ? "0" : " "); + } + if (cFlags & HASH) { + hex = `0x${hex}`; + } + return hex; + } + + if (nPrecision) { + nPrecision = parseInt(nPrecision.substring(1)); + } + + nDecSep = nDecSep ? nDecSep.substring(1) : "0"; + const separators = { + 0: [",", "."], + 1: ["", "."], + 2: [".", ","], + 3: ["", ","], + 4: ["'", "."], + }; + const [thousandSep, decimalSep] = separators[nDecSep]; + + let decPart = ""; + if (cConvChar === "f") { + if (nPrecision !== undefined) { + decPart = (arg - intPart).toFixed(nPrecision); + } else { + decPart = (arg - intPart).toString(); + } + if (decPart.length > 2) { + decPart = `${decimalSep}${decPart.substring(2)}`; + } else if (cFlags & HASH) { + decPart = "."; + } else { + decPart = ""; + } + } + + let sign = ""; + if (intPart < 0) { + sign = "-"; + intPart = -intPart; + } else if (cFlags & PLUS) { + sign = "+"; + } else if (cFlags & SPACE) { + sign = " "; + } + + if (thousandSep && intPart >= 1000) { + const buf = []; + while (true) { + buf.push((intPart % 1000).toString().padStart(3, "0")); + intPart = Math.trunc(intPart / 1000); + if (intPart < 1000) { + buf.push(intPart.toString()); + break; + } + } + intPart = buf.reverse().join(thousandSep); + } else { + intPart = intPart.toString(); + } + + let n = `${intPart}${decPart}`; + if (nWidth !== undefined) { + n = n.padStart(nWidth - sign.length, cFlags & ZERO ? "0" : " "); + } + + return `${sign}${n}`; + }); + } + + iconStreamFromIcon() { + /* Not implemented */ + } + + printd(cFormat, oDate) { + switch (cFormat) { + case 0: + return this.printd("D:yyyymmddHHMMss", oDate); + case 1: + return this.printd("yyyy.mm.dd HH:MM:ss", oDate); + case 2: + return this.printd("m/d/yy h:MM:ss tt", oDate); + } + + const handlers = { + mmmm: data => { + return this._months[data.month]; + }, + mmm: data => { + return this._months[data.month].substring(0, 3); + }, + mm: data => { + return (data.month + 1).toString().padStart(2, "0"); + }, + m: data => { + return (data.month + 1).toString(); + }, + dddd: data => { + return this._days[data.dayOfWeek]; + }, + ddd: data => { + return this._days[data.dayOfWeek].substring(0, 3); + }, + dd: data => { + return data.day.toString().padStart(2, "0"); + }, + d: data => { + return data.day.toString(); + }, + yyyy: data => { + return data.year.toString(); + }, + yy: data => { + return (data.year % 100).toString().padStart(2, "0"); + }, + HH: data => { + return data.hours.toString().padStart(2, "0"); + }, + H: data => { + return data.hours.toString(); + }, + hh: data => { + return (1 + ((data.hours + 11) % 12)).toString().padStart(2, "0"); + }, + h: data => { + return (1 + ((data.hours + 11) % 12)).toString(); + }, + MM: data => { + return data.minutes.toString().padStart(2, "0"); + }, + M: data => { + return data.minutes.toString(); + }, + ss: data => { + return data.seconds.toString().padStart(2, "0"); + }, + s: data => { + return data.seconds.toString(); + }, + tt: data => { + return data.hours < 12 ? "am" : "pm"; + }, + t: data => { + return data.hours < 12 ? "a" : "p"; + }, + }; + + const data = { + year: oDate.getFullYear(), + month: oDate.getMonth(), + day: oDate.getDate(), + dayOfWeek: oDate.getDay(), + hours: oDate.getHours(), + minutes: oDate.getMinutes(), + seconds: oDate.getSeconds(), + }; + + const patterns = /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t|\\.)/g; + return cFormat.replace(patterns, function (match, pattern) { + if (pattern in handlers) { + return handlers[pattern](data); + } + return pattern.charCodeAt(1); + }); + } + + printx(cFormat, cSource) { + // case + const handlers = [x => x, x => x.toUpperCase(), x => x.toLowerCase()]; + const buf = []; + let i = 0; + const ii = cSource.length; + let currCase = handlers[0]; + let escaped = false; + + for (const command of cFormat) { + if (escaped) { + buf.push(command); + escaped = false; + continue; + } + if (i >= ii) { + break; + } + switch (command) { + case "?": + buf.push(currCase(cSource.charAt(i++))); + break; + case "X": + while (i < ii) { + const char = cSource.charAt(i++); + if ( + ("a" <= char && char <= "z") || + ("A" <= char && char <= "Z") || + ("0" <= char && char <= "9") + ) { + buf.push(currCase(char)); + break; + } + } + break; + case "A": + while (i < ii) { + const char = cSource.charAt(i++); + if (("a" <= char && char <= "z") || ("A" <= char && char <= "Z")) { + buf.push(currCase(char)); + break; + } + } + break; + case "9": + while (i < ii) { + const char = cSource.charAt(i++); + if ("0" <= char && char <= "9") { + buf.push(char); + break; + } + } + break; + case "*": + while (i < ii) { + buf.push(currCase(cSource.charAt(i++))); + } + break; + case "\\": + escaped = true; + break; + case ">": + currCase = handlers[1]; + break; + case "<": + currCase = handlers[2]; + break; + case "=": + currCase = handlers[0]; + break; + default: + buf.push(command); + } + } + + return buf.join(""); + } + + scand(cFormat, cDate) { + switch (cFormat) { + case 0: + return this.scand("D:yyyymmddHHMMss", cDate); + case 1: + return this.scand("yyyy.mm.dd HH:MM:ss", cDate); + case 2: + return this.scand("m/d/yy h:MM:ss tt", cDate); + } + + if (!this._scandCache.has(cFormat)) { + const months = this._months; + const days = this._days; + + const handlers = { + mmmm: { + pattern: `(${months.join("|")})`, + action: (value, data) => { + data.month = months.indexOf(value); + }, + }, + mmm: { + pattern: `(${months.map(month => month.substring(0, 3)).join("|")})`, + action: (value, data) => { + data.month = months.findIndex( + month => month.substring(0, 3) === value + ); + }, + }, + mm: { + pattern: `([0-9]{2})`, + action: (value, data) => { + data.month = parseInt(value) - 1; + }, + }, + m: { + pattern: `([0-9]{1,2})`, + action: (value, data) => { + data.month = parseInt(value) - 1; + }, + }, + dddd: { + pattern: `(${days.join("|")})`, + action: (value, data) => { + data.day = days.indexOf(value); + }, + }, + ddd: { + pattern: `(${days.map(day => day.substring(0, 3)).join("|")})`, + action: (value, data) => { + data.day = days.findIndex(day => day.substring(0, 3) === value); + }, + }, + dd: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.day = parseInt(value); + }, + }, + d: { + pattern: "([0-9]{1,2})", + action: (value, data) => { + data.day = parseInt(value); + }, + }, + yyyy: { + pattern: "([0-9]{4})", + action: (value, data) => { + data.year = parseInt(value); + }, + }, + yy: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.year = 2000 + parseInt(value); + }, + }, + HH: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.hours = parseInt(value); + }, + }, + H: { + pattern: "([0-9]{1,2})", + action: (value, data) => { + data.hours = parseInt(value); + }, + }, + hh: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.hours = parseInt(value); + }, + }, + h: { + pattern: "([0-9]{1,2})", + action: (value, data) => { + data.hours = parseInt(value); + }, + }, + MM: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.minutes = parseInt(value); + }, + }, + M: { + pattern: "([0-9]{1,2})", + action: (value, data) => { + data.minutes = parseInt(value); + }, + }, + ss: { + pattern: "([0-9]{2})", + action: (value, data) => { + data.seconds = parseInt(value); + }, + }, + s: { + pattern: "([0-9]{1,2})", + action: (value, data) => { + data.seconds = parseInt(value); + }, + }, + tt: { + pattern: "([aApP][mM])", + action: (value, data) => { + const char = value.charAt(0); + data.am = char === "a" || char === "A"; + }, + }, + t: { + pattern: "([aApP])", + action: (value, data) => { + data.am = value === "a" || value === "A"; + }, + }, + }; + + // escape the string + const escapedFormat = cFormat.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); + const patterns = /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t)/g; + const actions = []; + + const re = escapedFormat.replace(patterns, function ( + match, + patternElement + ) { + const { pattern, action } = handlers[patternElement]; + actions.push(action); + return pattern; + }); + + this._scandCache.set(cFormat, [new RegExp(re, "g"), actions]); + } + + const [regexForFormat, actions] = this._scandCache.get(cFormat); + + const matches = regexForFormat.exec(cDate); + if (matches.length !== actions.length + 1) { + throw new Error("Invalid date in util.scand"); + } + + const data = { + year: 0, + month: 0, + day: 0, + hours: 0, + minutes: 0, + seconds: 0, + am: null, + }; + actions.forEach((action, i) => action(matches[i + 1], data)); + if (data.am !== null) { + data.hours = (data.hours % 12) + (data.am ? 0 : 12); + } + + return new Date( + data.year, + data.month, + data.day, + data.hours, + data.minutes, + data.seconds + ); + } + + spansToXML() { + /* Not implemented */ + } + + stringFromStream() { + /* Not implemented */ + } + + xmlToSpans() { + /* Not implemented */ + } } export { Util }; diff --git a/test/unit/clitests.json b/test/unit/clitests.json index d4766617c..d8319c667 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -32,6 +32,7 @@ "pdf_find_utils_spec.js", "pdf_history_spec.js", "primitives_spec.js", + "scripting_spec.js", "stream_spec.js", "type1_parser_spec.js", "ui_utils_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 13e358eed..f12469d15 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -77,6 +77,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/pdf_find_utils_spec.js", "pdfjs-test/unit/pdf_history_spec.js", "pdfjs-test/unit/primitives_spec.js", + "pdfjs-test/unit/scripting_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/type1_parser_spec.js", "pdfjs-test/unit/ui_utils_spec.js", diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js new file mode 100644 index 000000000..2e97b3360 --- /dev/null +++ b/test/unit/scripting_spec.js @@ -0,0 +1,98 @@ +/* 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 { initSandbox } from "../../src/scripting_api/initialization.js"; + +describe("Util", function () { + let sandbox, util; + + beforeAll(function (done) { + sandbox = Object.create(null); + const extra = { send: null, crackURL: null }; + const data = { objects: {}, calculationOrder: [] }; + initSandbox(data, extra, sandbox); + util = sandbox.util; + done(); + }); + + afterAll(function () { + sandbox = util = null; + }); + + describe("printd", function () { + it("should print a date according to a format", function (done) { + const date = new Date("April 15, 1707 3:14:15"); + expect(util.printd(0, date)).toEqual("D:17070415031415"); + expect(util.printd(1, date)).toEqual("1707.04.15 03:14:15"); + expect(util.printd(2, date)).toEqual("4/15/07 3:14:15 am"); + expect(util.printd("mmmm mmm mm m", date)).toEqual("April Apr 04 4"); + expect(util.printd("dddd ddd dd d", date)).toEqual("Friday Fri 15 15"); + done(); + }); + }); + + describe("scand", function () { + it("should parse a date according to a format", function (done) { + const date = new Date("April 15, 1707 3:14:15"); + expect(util.scand(0, "D:17070415031415")).toEqual(date); + expect(util.scand(1, "1707.04.15 03:14:15")).toEqual(date); + expect(util.scand(2, "4/15/07 3:14:15 am")).toEqual( + new Date("April 15, 2007 3:14:15") + ); + done(); + }); + }); + + describe("printf", function () { + it("should print some data according to a format", function (done) { + expect(util.printf("Integer numbers: %d, %d,...", 1.234, 56.789)).toEqual( + "Integer numbers: 1, 56,..." + ); + expect(util.printf("Hex numbers: %x, %x,...", 1234, 56789)).toEqual( + "Hex numbers: 4D2, DDD5,..." + ); + expect( + util.printf("Hex numbers with 0x: %#x, %#x,...", 1234, 56789) + ).toEqual("Hex numbers with 0x: 0x4D2, 0xDDD5,..."); + expect(util.printf("Decimal number: %,0+.3f", 1234567.89123)).toEqual( + "Decimal number: +1,234,567.891" + ); + expect(util.printf("Decimal number: %,0+8.3f", 1.234567)).toEqual( + "Decimal number: + 1.235" + ); + done(); + }); + + it("should print a string with no argument", function (done) { + expect(util.printf("hello world")).toEqual("hello world"); + done(); + }); + + it("should print a string with a percent", function (done) { + expect(util.printf("%%s")).toEqual("%%s"); + expect(util.printf("%%s", "hello")).toEqual("%%s"); + done(); + }); + }); + + describe("printx", function () { + it("should print some data according to a format", function (done) { + expect(util.printx("9 (999) 999-9999", "aaa14159697489zzz")).toEqual( + "1 (415) 969-7489" + ); + done(); + }); + }); +});