UNPKG

@digital-blueprint/annotpdf

Version:

Javascript library for creating annotations in PDF documents

571 lines 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * This class provides methods to navigate through the byte array representing the PDF document * */ class Util { /** * Extracts the version information of a PDF file * */ static extractVersion(data, index = 0) { let ptr_version_start = Util.locateSequence(Util.VERSION, data, index) + Util.VERSION.length; let ptr_delimiter = Util.locateSequence([Util.DOT], data, ptr_version_start); let major_version = Util.extractNumber(data, ptr_version_start, ptr_delimiter).result; let ptr_end = Util.locateDelimiter(data, ptr_delimiter); let minor_version = Util.extractNumber(data, ptr_delimiter + 1, ptr_end).result; return { major: major_version, minor: minor_version }; } /** * Consumes comments that is * '%.................. EOL' * * Also handles mulitple line comments * */ static consumeComment(data, index) { while (data[index] !== 13 && data[index] != 10 && index < data.length) ++index; index = Util.skipSpaces(data, index); if (data[index] == 37) //% return Util.consumeComment(data, index); return index; } /** * Returns the next word. These are bytes that are not separated by a delimiter and a ptr to the position where the word ends * It ignores/skips comments. * */ static readNextWord(data, index = 0) { if (index >= data.length) { return { result: undefined, start_index: index, end_index: data.length - 1 }; } index = Util.skipSpaces(data, index); if (data[index] === 37) { //% index = Util.consumeComment(data, index); } let start_index = index; index++; while (!Util.isDelimiter(data[index]) && index < data.length) ++index; if (index <= data.length) return { result: data.slice(start_index, index), start_index: start_index, end_index: index - 1 }; return { result: undefined, start_index: start_index, end_index: index - 1 }; } /** * Assumes that at position index is a delimiter and than runs as long forward until it finds * another delimiter or reaches the end of the document * */ static skipDelimiter(data, index = 0) { while (index < data.length && this.isDelimiter(data[index])) ++index; return index; } /** * Skips only spaces * */ static skipSpaces(data, index = 0) { while (index < data.length && (data[index] === 10 || data[index] === 9 || data[index] === 13 || data[index] === 32)) ++index; return index; } /** * Skips only spaces * */ static skipSpacesReverse(data, index = 0) { while (index > 0 && (data[index] === 10 || data[index] === 9 || data[index] === 13 || data[index] === 32)) --index; return index; } /** * Assumes that at position index is a delimiter and than runs as long backwards until it finds * another delimiter or reaches the end of the document * */ static skipDelimiterReverse(data, index = 0) { while (index > 0 && this.isDelimiter(data[index])) --index; return index; } /** * Transforms a string into an array of the corresponding ascii values * */ static convertStringToAscii(toConvert) { let asciis = []; for (let i = 0; i < toConvert.length; ++i) { asciis.push(+toConvert.charCodeAt(i)); } return asciis; } static isSpace(value) { return value === Util.LF || value === Util.CR || value === Util.SPACE; } static isDelimiter(value) { return Util.isSpace(value) || value === 47 || // / value === 37 || // % value === 60 || // < value === 62 || // > value === 91 || // [ value === 93 || // ] value === 40 || // ( value === 41; // ) } static isNumber(value) { return value >= 48 && value <= 57; // 0 <= value <= 9 } /** * Search the sequence in data starting at the offset * * Set the 'closed' flag to check if the suceeding char after the sequence is a line feed (10), a carriage return (13), the end * of the whole sequence or a space (32) * */ static locateSequence(sequence, data, offset = 0, closed = false) { let i = offset; for (let j = 0; i < data.length; ++i) { if (data[i] == sequence[j]) { if (j == sequence.length - 1) { if (!closed || data.length == i + 1 || this.isDelimiter(data[i + 1])) { return i - (sequence.length - 1); } else { j = -1; } } ++j; } else { j = 0; } } return -1; } /** * Search the sequence in data starting at the offset in reverse direction * * Set the 'closed' flag to check if the preceding char before the sequence is a line feed (10), a carriage return (13), the start * of the whole data sequence or a space (32) * */ static locateSequenceReversed(sequence, data, offset = data.length - 1, closed = false) { let i = offset; for (let j = sequence.length - 1; i >= 0; --i) { if (data[i] == sequence[j]) { if (j == 0) { if (!closed || i - 1 < 0 || this.isDelimiter(data[i - 1])) { return i; } else { j = sequence.length; } } --j; } else { j = sequence.length - 1; } } return -1; } /** * Locates the index before the next delimiter. Delimiters can be a line feed (10), a carriage return (13), the end of the whole sequence * or a space (32) * */ static locateDelimiter(data, offset = 0) { while (offset < data.length && !this.isDelimiter(data[offset])) ++offset; return offset - 1; } /** * Locates the index after the last delimiter. Delimiters can be a line feed (10), a carriage return (13), the end of the whole sequence * or a space (32) * */ static locateDelimiterReversed(data, offset = data.length - 1) { while (offset > 0 && !this.isDelimiter(data[offset])) --offset; if (offset <= 0) return offset; return offset - 1; } /** * Extract an object identifier * <ID> <GEN> obj * */ static extractObjectId(data, index) { index = Util.skipDelimiter(data, index); let end_obj_ptr = Util.locateDelimiter(data, index + 1); let obj = Util.extractNumber(data, index, end_obj_ptr).result; let start_gen_ptr = Util.skipDelimiter(data, end_obj_ptr + 1); let end_gen_ptr = Util.locateDelimiter(data, start_gen_ptr + 1); let gen = Util.extractNumber(data, start_gen_ptr, end_gen_ptr).result; return { obj: obj, generation: gen }; } /** * Extract the reference starting at position 'index' * */ static extractReference(data, index) { index = Util.skipDelimiter(data, index); let r_index = this.locateSequence(this.convertStringToAscii(" R"), data, index, true); return data.slice(index, r_index); } /** * Returns a reference as typed object * */ static extractReferenceTyped(data, index) { let ref_data = this.extractReference(data, index); let del_index = this.locateDelimiter(ref_data, 0); let id = this.extractNumber(ref_data, 0, del_index).result; let gen = this.extractNumber(ref_data, del_index + 2).result; return { result: { obj: id, generation: gen }, start_index: index, end_index: index + ref_data.length + 1 }; // + _R } /** * Extracts a string in Hex notation <...> * */ static extractHexString(data, index) { let string_start = Util.locateSequence(Util.HEX_STRING_START, data, index); let string_end = Util.locateSequence(Util.HEX_STRING_END, data, index); data = data.slice(string_start + 1, string_end); return { result: new Uint8Array(Util.convertHexStringToByteArray(data)), start_index: string_start, end_index: string_end }; } /** * Extratcs a string (...) * */ static extractString(data, index) { let string_start = Util.locateSequence(Util.LITERAL_STRING_START, data, index); let string_end = Util.locateSequence(Util.LITERAL_STRING_END, data, index); while (data[string_end - 1] === 92) { // "\" escape string_end = Util.locateSequence(Util.LITERAL_STRING_END, data, string_end + 1); } data = data.slice(string_start + 1, string_end); return { result: Util.unescapeString(data), start_index: string_start, end_index: string_end }; } /** * Returns the value of an option * /<option> * * so for instance for /Highlight it would return 'Highlight' * * The index must point to the "/" * */ static extractOptionValue(data, index = 0) { if (data[index] !== 47) throw Error("misplaced option value pointer"); let end = Util.locateDelimiter(data, index + 1); return { result: Util.convertAsciiToString(Array.from(data.slice(index + 1, end + 1))), start_index: index, end_index: end }; } /** * Parses the ascii encoded number of the PDF file * */ static extractNumber(data, start, end = -1) { start = Util.skipDelimiter(data, start); if (-1 == end) { end = Util.locateDelimiter(data, start); } if (end < start) { throw Error(`Could not identify number bounds: [${start},${end}]`); } let str_id = ""; for (let i = start; i <= end; ++i) { str_id += String.fromCharCode(data[i]); } if ("" === str_id) { throw Error(`Could not parse number at position ${start}`); } return { result: +str_id, start_index: start, end_index: end }; } /** * Converts the given date into PDF formatting * */ static convertDateToPDFDate(date) { let date_str = "D:"; date_str += date.getFullYear(); let month = String(date.getMonth() + 1); date_str += (month.length == 1 ? "0" : "") + month; let day = String(date.getDate()); date_str += (day.length == 1 ? "0" : "") + day; let hours = String(date.getHours()); date_str += (hours.length == 1 ? "0" : "") + hours; let minutes = String(date.getMinutes()); date_str += (minutes.length == 1 ? "0" : "") + minutes; let seconds = String(date.getSeconds()); date_str += (seconds.length == 1 ? "0" : "") + seconds; return date_str; } /** * Converts a unicode sequence into a string * */ static convertUnicodeToString(val) { if (val instanceof Uint8Array) val = new Uint8Array(val); if (val[0] === 254 && val[1] === 255) { val = val.slice(2, val.length); let uintToString = (uintArray) => { let ret = ""; for (let i = 0; i < uintArray.length - 1; i += 2) { ret += String.fromCharCode((uintArray[i] << 8) | uintArray[i + 1] & 0xFF); } return ret; }; let ret = uintToString(val); return ret; } // handle utf-8 compression let Utf8ArrayToStr = (array) => { let ret = ""; let i = 0; while (i < array.length) { let char1 = array[i++]; let char2; switch (char1 >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: // one byte ret += String.fromCharCode(char1); break; case 12: case 13: // two byte sequence char2 = array[i++]; ret += String.fromCharCode(((char1 & 0x1F) << 6) | (char2 & 0x3F)); break; case 14: // three byte sequence char2 = array[i++]; let char3 = array[i++]; ret += String.fromCharCode(((char1 & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0)); break; } } return ret; }; let ret = Utf8ArrayToStr(Array.from(val)); return ret; } static convertAsciiToString(val) { let ret = ""; for (let i = 0; i < val.length; ++i) { ret += String.fromCharCode(val[i]); } return ret; } /** * takes a number and returns an array of its char representations * */ static convertNumberToCharArray(nbr) { return Util.convertStringToAscii(String(nbr)); } /** * Converts a hex string into a byte array * That means two consecutive hex values are merged into one byte that is appended to the array * */ static convertHexStringToByteArray(hex_string) { let ret_val = []; if (typeof hex_string !== "string") { hex_string = Util.convertAsciiToString(hex_string); } for (let i = 0; i < hex_string.length - 1; i += 2) { ret_val.push((parseInt(hex_string.charAt(i), 16) << 4) + parseInt(hex_string.charAt(i + 1), 16)); } if (hex_string.length % 2 !== 0) { ret_val.push(parseInt(hex_string.charAt(hex_string.length - 1), 16)); } return ret_val; } /** * Converts an array of byte values into a hex string * */ static convertByteArrayToHexString(values) { let ret_val = ""; let HEX_VALUES = "0123456789ABCDEF"; for (let i = 0; i < values.length; ++i) { ret_val += HEX_VALUES.charAt(values[i] >> 4); ret_val += HEX_VALUES.charAt(values[i] & 15); } // remove leading zeros let i = 0; while ('0' === ret_val.charAt(i) && i < ret_val.length) ++i; return ret_val.slice(i, ret_val.length); } /** * takes two arrays and checks their equality * */ static areArraysEqual(array_one, array_two) { if (array_one.length !== array_two.length) return false; for (let i = 0; i < array_one.length; ++i) { if (array_one[i] !== array_two[i]) return false; } return true; } /** * Prints the array with leading indexes 10 bytes in a row * Delimiter are substituted by '.' * */ static debug_printIndexed(array) { let outp = ""; for (let i = 0; i < array.length; ++i) { if (i % 10 === 0) { outp += "\n" + i + ":"; } if (Util.isSpace(array[i])) outp += " ."; else outp += " " + String.fromCharCode(array[i]); } console.log(outp); } /** * Converts a list of 8 bit integers into a list of 32 bit integers * */ static convertUint8ArrayToInt32Array(a) { let ret_val = new Int32Array(Math.ceil(a.length / 4)); let i = 0; let index = 0; while (i < a.length) { if (a[i] > 255 || a[i + 1] > 255 || a[i + 2] >> 255 || a[i + 3] > 255) throw Error("Invalid byte size"); ret_val[index++] = (a[i++] << 24) + (a[i++] << 16) + (a[i++] << 8) + (a[i++] << 0); } return ret_val; } /** * Converts a list of 32 bit integers into a list of 8 bit UNSIGNED integers * */ static convertInt32ArrayToUint8Array(a) { let ret_val = new Uint8Array(a.length * 4); for (let i = 0; i < a.length; ++i) { for (let j = 0; j < 4; ++j) { ret_val[i * 4 + j] = (a[i] >> 8 * (4 - j - 1)) & 0xFF; } } return ret_val; } /** * Adds escape symbols to specific elements of the provided string * * Symbols that needs to be escaped are: \ ) ( * */ static escapeString(array) { let ret_val = []; for (let i = 0; i < array.length; ++i) { switch (array[i]) { case Util.LITERAL_STRING_START[0]: case Util.LITERAL_STRING_END[0]: case 92: // 92 = '\' ret_val.push(92); ret_val.push(array[i]); break; case 9: // 9 = TAB ret_val.push(92); ret_val.push(116); // \t break; case 10: // 10 = LINE FEED ret_val.push(92); ret_val.push(110); // \n break; case 13: // 13 = CARRIAGE RETURN ret_val.push(92); ret_val.push(114); // \r break; case 8: // 8 = BACKSPACE ret_val.push(92); ret_val.push(98); // \b break; case 12: // 255 = FORM FEED ret_val.push(92); ret_val.push(102); // \f break; default: ret_val.push(array[i]); } } return new Uint8Array(ret_val); } /** * Removes escape symbols from the given string * * Symbols that needs to be escaped are: \ ) ( * */ static unescapeString(array) { let ret_val = []; let in_escape = false; for (let i = 0; i < array.length; ++i) { if (in_escape) { in_escape = false; switch (array[i]) { case 92: // \ ret_val.push(92); break; case Util.LITERAL_STRING_START[0]: ret_val.push(Util.LITERAL_STRING_START[0]); break; case Util.LITERAL_STRING_END[0]: ret_val.push(Util.LITERAL_STRING_END[0]); break; case 116: // \t ret_val.push(9); break; case 110: // \n ret_val.push(10); break; case 114: // \r ret_val.push(13); break; case 98: // \b ret_val.push(8); break; case 102: // \f ret_val.push(12); break; default: ret_val.push(array[i]); } } else { if (array[i] === 92) { in_escape = true; } else { ret_val.push(array[i]); } } } return new Uint8Array(ret_val); } } exports.Util = Util; Util.VERSION = [37, 80, 68, 70, 45]; // %PDF- Util.NULL = [110, 117, 108, 108]; // null Util.DOT = 46; Util.CR = 13; Util.LF = 10; Util.TYPE = "/Type "; Util.SPACE = 32; Util.OBJ = [111, 98, 106]; // 'obj' Util.ENDOBJ = [101, 110, 100, 111, 98, 106]; // 'endobj' Util.ARRAY_START = [91]; // '[' Util.ARRAY_END = [93]; // ']' Util.LITERAL_STRING_START = [40]; // '(' Util.HEX_STRING_START = [60]; // '<' Util.LITERAL_STRING_END = [41]; // ')' Util.HEX_STRING_END = [62]; // '>' Util.R = [82]; // 'R' Util.ANNOTS = [47, 65, 110, 110, 111, 116, 115]; // '/Annot' Util.DICT_START = [60, 60]; // '<<' Util.DICT_END = [62, 62]; // '>>' Util.PAGE = [47, 80, 97, 103, 101]; Util.SIZE = [47, 83, 105, 122, 101]; // /Size Util.ROOT = [47, 82, 111, 111, 116]; // /Root Util.PREV = [47, 80, 114, 101, 118]; // /Prev Util.STARTXREF = [115, 116, 97, 114, 116, 120, 114, 101, 102]; // = 'startxref' Util.XREF = [120, 114, 101, 102]; // = 'xref' Util.STREAM = [115, 116, 114, 101, 97, 109]; // = 'stream' Util.TRAILER = [116, 114, 97, 105, 108, 101, 114]; // = 'trailer' //# sourceMappingURL=util.js.map