From b633ad3e3fe555ed2cd4c6ff6e41284c5ce3cfaf 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 --- src/scripting_api/util.js | 723 ++++++++++++++++++++++++++++++++++++ test/unit/clitests.json | 1 + test/unit/jasmine-boot.js | 1 + test/unit/scripting_spec.js | 85 +++++ 4 files changed, 810 insertions(+) create mode 100644 test/unit/scripting_spec.js diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index db85f1b2315d70..c84b26f9ea0a50 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -20,11 +20,734 @@ 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", + ]; } 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]+)?([dfsx])/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 + ) { + 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, bXFAPicture = false) { + if (bXFAPicture) { + return this._printdXFAPicture(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 year = oDate.getFullYear(); + const month = oDate.getMonth(); + const day = oDate.getDate(); + const dayOfWeek = oDate.getDay(); + const hours = oDate.getHours(); + const minutes = oDate.getMinutes(); + const seconds = oDate.getSeconds(); + const data = { year, month, day, dayOfWeek, hours, minutes, seconds }; + + 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); + }); + } + + _getTZOffset(date, separator) { + // Offset can be non-integer so truncate it + let offset = Math.trunc(date.getTimezoneOffset()); + if (offset === 0) { + return "Z"; + } + + let sign = "+"; + if (offset < 0) { + sign = "-"; + offset = -offset; + } + + let hours = Math.floor(offset / 60); + let minutes = offset - 60 * hours; + + hours = hours.toString().padStart(2, "0"); + if (minutes !== 0) { + minutes = minutes.toString().padStart(2, "0"); + return `${sign}${hours}${separator}${minutes}`; + } + return `${sign}${hours}`; + } + + _printdXFAPicture(cFormat, cDate) { + const handlers = { + MMMM: data => { + return data.cDate.toLocaleDateString(undefined, { month: "long" }); + }, + MMM: data => { + return data.cDate.toLocaleDateString(undefined, { month: "short" }); + }, + MM: data => { + return (data.month + 1).toString().padStart(2, "0"); + }, + M: data => { + return (data.month + 1).toString(); + }, + EEEE: data => { + return data.cDate.toLocaleDateString(undefined, { weekday: "long" }); + }, + EEE: data => { + return data.cDate.toLocaleDateString(undefined, { weekday: "short" }); + }, + E: data => { + return (data.dayOfWeek() + 1).toString(); + }, + DD: data => { + return data.day.toString().padStart(2, "0"); + }, + D: data => { + return data.day.toString(); + }, + JJJ: data => { + const day = Math.floor( + (data.cDate - new Date(data.year, 0, 0)) / 86400000 + ); + return day.toString().padStart(3, "0"); + }, + J: data => { + return Math.floor((data.cDate - new Date(data.year, 0, 0)) / 86400000); + }, + YYYY: data => { + return data.year.toString(); + }, + YY: data => { + return (data.year % 100).toString().padStart(2, "0"); + }, + G: data => { + return data.cDate.toLocaleDateString(undefined, { era: "short" }); + }, + w: data => { + const date = new Date(data.cDate.getTime()); + // Week start on monday. + // Date for the closest wednesday + date.setDate(data.day + 2 - ((data.dayOfWeek + 6) % 7)); + + // Week 1 of a month is the week containing the 3rd + // so get the wednesday in week 1. + const first = new Date(data.year, data.month, 3); + first.setDate(first.getDate() + 2 - ((first.getDay() + 6) % 7)); + + return 1 + Math.floor((date - first) / 604800000); + }, + WW: data => { + const date = new Date(data.cDate.getTime()); + // Weeks start on monday. + // Date for the closest thursday + date.setDate(data.day + 3 - ((data.dayOfWeek + 6) % 7)); + + // Week 1 of a year is the week containing January 4 + // so get the thursday in week 1. + const first = new Date(data.year, 0, 4); + first.setDate(first.getDate() + 3 - ((first.getDay() + 6) % 7)); + + return 1 + Math.floor((date - first) / 604800000); + }, + 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(); + }, + kk: data => { + return (data.hours % 12).toString().padStart(2, "0"); + }, + k: data => { + return (data.hours % 12).toString(); + }, + KK: data => { + return (1 + ((data.hours + 23) % 24)).toString().padStart(2, "0"); + }, + K: data => { + return (1 + ((data.hours + 23) % 24)).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(); + }, + FFF: data => { + return data.milliseconds.toString().padStart(3, "0"); + }, + A: data => { + return data.hours < 12 ? "AM" : "PM"; + }, + Z: data => { + return data.cDate + .toLocaleDateString(undefined, { timeZoneName: "short" }) + .split(" ")[1]; + }, + zz: data => { + return this._getTZOffset(data.cDate, ":"); + }, + z: data => { + return this._getTZOffset(data.cDate, ""); + }, + }; + + const year = cDate.getFullYear(); + const month = cDate.getMonth(); + const day = cDate.getDate(); + const dayOfWeek = cDate.getDay(); + const hours = cDate.getHours(); + const minutes = cDate.getMinutes(); + const seconds = cDate.getSeconds(); + const milliseconds = cDate.getMilliseconds(); + const data = { + year, + month, + day, + dayOfWeek, + hours, + minutes, + seconds, + milliseconds, + cDate, + }; + + const pattern = /(MMMM|MMM|MM|M|DD|D|YYYY|YY|JJJ|J|EEEE|EEE|E|G|w|WW|hh|h|kk|k|HH|H|KK|K|mm|m|SS|S|FFF|A|Z|zz|z|\\.)/g; + return cFormat.replace(pattern, function (match, p1) { + if (p1 in handlers) { + return handlers[p1](data); + } + return p1.charCodeAt(1); + }); + } + + printx(cFormat, cSource) { + // case + const handlers = [x => x, x => x.toUpperCase(), x => x.toLowerCase()]; + + // limits + const [LA, LZ, UA, UZ, ZER, NIN] = Array.from("azAZ09").map(c => + c.charCodeAt(0) + ); + + 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 code = cSource.charCodeAt(i++); + if ( + (LA <= code && code <= LZ) || + (UA <= code && code <= UZ) || + (ZER <= code && code <= NIN) + ) { + buf.push(currCase(String.fromCharCode(code))); + break; + } + } + break; + case "A": + while (i < ii) { + const code = cSource.charCodeAt(i++); + if ((LA <= code && code <= LZ) || (UA <= code && code <= UZ)) { + buf.push(currCase(String.fromCharCode(code))); + break; + } + } + break; + case "9": + while (i < ii) { + const code = cSource.charCodeAt(i++); + if (ZER <= code && code <= NIN) { + buf.push(String.fromCharCode(code)); + 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 d4766617c17630..d8319c6676217d 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 f05126d9dafc36..85ba95fc26543d 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -75,6 +75,7 @@ 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 00000000000000..163f59ce6021e5 --- /dev/null +++ b/test/unit/scripting_spec.js @@ -0,0 +1,85 @@ +/* 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 { Util } from "../../src/scripting_api/util.js"; + +describe("Util", function () { + describe("printd", function () { + it("should print a date according to a format", function (done) { + const util = new Util({ send: null }); + 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(); + }); + + it("should print a date according to a format using XFA picture", function (done) { + const util = new Util({ send: null }); + const date = new Date("April 15, 1707 3:14:15"); + expect(util.printd("MMMM EEEE JJJ w WW", date, true)).toEqual( + "April Friday 105 3 15" + ); + done(); + }); + }); + + describe("scand", function () { + it("should parse a date according to a format", function (done) { + const util = new Util({ send: null }); + 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) { + const util = new Util({ send: null }); + 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(); + }); + }); + + describe("printx", function () { + it("should print some data according to a format", function (done) { + const util = new Util({ send: null }); + expect(util.printx("9 (999) 999-9999", "aaa14159697489zzz")).toEqual( + "1 (415) 969-7489" + ); + done(); + }); + }); +});