UNPKG

@digital-blueprint/annotpdf

Version:

Javascript library for creating annotations in PDF documents

394 lines 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const util_1 = require("./util"); const object_util_1 = require("./object-util"); const document_history_1 = require("./document-history"); const crypto_1 = require("./crypto"); class Annotation { constructor(data = new Uint8Array([]), cryptoInterface = new CryptoInterface()) { this.data = data; this.cryptoInterface = cryptoInterface; this.page = -1; // the target page of the annotation this.type = ""; // the sub type of the array (Text, Highlight, ...) this.object_id = undefined; // an unused object id this.id = ""; // unique annation identifier this.rect = []; // the location of the annotation this.author = ""; // the author of the annotation this.pageReference = undefined; // The reference to the page object to which the annotation is added this.updateDate = ""; // The date when the annotation was created or updated this.border = undefined; // define the border this.color = undefined; // the color set this.fill = undefined; // the fill color set this.data = data; } /** * Extract the annotation object (partially) * */ extract(xref, page, objectLookupTable) { let annot_obj = object_util_1.ObjectUtil.extractObject(this.data, xref, objectLookupTable); this.object_id = annot_obj.id; annot_obj = annot_obj.value; this.type = annot_obj["/Subtype"]; this.rect = annot_obj["/Rect"]; this.pageReference = page; if (annot_obj["/M"]) this.updateDate = util_1.Util.convertUnicodeToString(this.cryptoInterface.decrypt(annot_obj["/M"], this.object_id)); if (annot_obj["/Border"]) this.border = annot_obj["/Border"]; if (annot_obj["/C"]) this.color = annot_obj["/C"]; if (annot_obj["/T"]) this.author = util_1.Util.convertUnicodeToString(this.cryptoInterface.decrypt(annot_obj["/T"], this.object_id)); if (annot_obj["/NM"]) this.id = util_1.Util.convertUnicodeToString(this.cryptoInterface.decrypt(annot_obj["/NM"], this.object_id)); if (annot_obj["/Contents"]) this.contents = util_1.Util.convertUnicodeToString(this.cryptoInterface.decrypt(annot_obj["/Contents"], this.object_id)); if (annot_obj["/QuadPoints"]) this.quadPoints = annot_obj["/QuadPoints"]; if (annot_obj["/Inklist"]) this.inkList = annot_obj["/Inklist"]; } } exports.Annotation = Annotation; /** * Represents the Catalog object of the PDF document * */ class CatalogObject { /** * Extracts the data representing the object. * */ constructor(data, xref, objectLookupTable) { this.data = data; this.xref = xref; this.objectLookupTable = objectLookupTable; this.pagesObjectId = { obj: -1, generation: -1 }; this.data = data; let page_obj = object_util_1.ObjectUtil.extractObject(this.data, xref, objectLookupTable).value; if (page_obj["/Type"] !== "/Catalog") throw Error(`Invalid catalog object at position ${xref.pointer}`); this.pagesObjectId = page_obj["/Pages"]; } getPagesObjectId() { return this.pagesObjectId; } } exports.CatalogObject = CatalogObject; /** * Represents the PageTree object of the PDF document * This is the object with /Type /Pages * */ class PageTree { constructor(data, objectLookupTable) { this.data = data; this.objectLookupTable = objectLookupTable; this.pageCount = -1; this.pageReferences = []; this.visitedPages = []; this.data = data; } /** * Extracts the kids references recursively. * For every kid it checks if the referenced object type is: * - a /Pages object then it recursively lookups its children * - a /Page object then it adds the references * */ extractPageReferences(references) { for (let reference of references) { if (this.visitedPages.some(el => el.obj === reference.obj && el.generation === reference.generation)) { continue; } let xref = this.objectLookupTable[reference.obj]; let kid_page_obj = object_util_1.ObjectUtil.extractObject(this.data, xref, this.objectLookupTable).value; if (kid_page_obj["/Type"] === "/Page") { this.pageReferences.push(reference); } else if (kid_page_obj["/Type"] === "/Pages") { this.visitedPages.push(reference); this.extractPageReferences(kid_page_obj["/Kids"]); } else { throw Error(`Invalid object type ${kid_page_obj["/Type"]}`); } } } /** * Extract the object data at the given pointer * */ extract(xref, objectLookupTable) { let page_tree_obj = object_util_1.ObjectUtil.extractObject(this.data, xref, objectLookupTable).value; if (!page_tree_obj["/Kids"]) throw Error(`Could not find index of /Kids in /Pages object`); let refs = page_tree_obj["/Kids"]; this.pageReferences = []; this.extractPageReferences(refs); this.pageCount = this.pageReferences.length; } /** * Returns the number of pages the page tree comprises * */ getPageCount() { return this.pageCount; } /** * Returns the reference to the page objects * */ getPageReferences() { return this.pageReferences; } } exports.PageTree = PageTree; /** * Represents a page object in the PDF document * */ class Page { constructor(data, documentHistory) { this.data = data; this.documentHistory = documentHistory; this.annots = []; this.hasAnnotsField = false; this.data = data; } /** * Extracts the references in the linked annotations array * */ extractAnnotationArray() { let obj_table = this.documentHistory.createObjectLookupTable(); if (!this.annotsPointer) throw Error("Annotations pointer not set"); let ref_annot_table = obj_table[this.annotsPointer.obj]; let annotations_obj = object_util_1.ObjectUtil.extractObject(this.data, ref_annot_table, obj_table); this.annots = annotations_obj.value; } /** * Extracts the page object starting at position ptr * */ extract(xref, objectLookupTable) { let page_obj = object_util_1.ObjectUtil.extractObject(this.data, xref, objectLookupTable); this.object_id = page_obj.id; let annots = page_obj.value["/Annots"]; if (annots) { this.hasAnnotsField = true; if (Array.isArray(annots)) { this.annots = annots.filter((x) => x !== 'null'); } else { this.annotsPointer = annots; this.extractAnnotationArray(); } } } } exports.Page = Page; /** * Provides a configured interface to handle the encryption and decryption of PDFs * */ class CryptoInterface { constructor(data, documentHistory, ref_ptr, user_pwd, owner_pwd) { this.data = data; this.documentHistory = documentHistory; this.ref_ptr = ref_ptr; this.cryptoConfiguration = { version: undefined, revision: undefined, filter: undefined, user_pwd: "", owner_pwd: "", length: undefined, permissions: undefined, owner_pwd_c: undefined, user_pwd_c: undefined }; this.cryptoEngine = new crypto_1.IdentityEngine(); this.data = data; this.documentHistory = documentHistory; this.cryptoConfiguration.user_pwd = user_pwd ? user_pwd : ""; this.cryptoConfiguration.owner_pwd = owner_pwd ? owner_pwd : ""; if (this.ref_ptr && this.documentHistory) { this.extractEncryptionDictionary(this.ref_ptr); // setup crypto-engine if (this.cryptoConfiguration.version === 1) { this.cryptoEngine = new crypto_1.RC4CryptoEngine(this.cryptoConfiguration, this.documentHistory.getRecentUpdate().id, crypto_1.RC4_40_BIT); } else if (this.cryptoConfiguration.version === 2) { this.cryptoEngine = new crypto_1.RC4CryptoEngine(this.cryptoConfiguration, this.documentHistory.getRecentUpdate().id); } else if (this.cryptoConfiguration.version === 4) { console.log("Some fancy AES encryption"); } else { throw Error(`Unsupported Encryption ${this.cryptoConfiguration.version}`); } } } /** * Returns the reference pointer * */ getEncryptionDictReference() { if (!this.ref_ptr) return undefined; return { obj: this.ref_ptr.id, generation: this.ref_ptr.generation }; } encrypt(data, reference) { return this.cryptoEngine.encrypt(data, reference); } decrypt(data, reference) { return this.cryptoEngine.decrypt(data, reference); } isUserPasswordCorrect() { if (!this.cryptoEngine) { throw Error("Crypto engine not configured"); } return this.cryptoEngine.isUserPasswordCorrect(); } isOwnerPasswordCorrect() { if (!this.cryptoEngine) { throw Error("Crypto engine not configured"); } return this.cryptoEngine.isOwnerPasswordCorrect(); } /** * Extracts the enrcyption dictionary * */ extractEncryptionDictionary(ptr) { if (!this.documentHistory) { throw Error("Documenthistory not configured"); } if (!this.data) { throw Error("Data not configured"); } let obj_table = this.documentHistory.createObjectLookupTable(); let page_obj = object_util_1.ObjectUtil.extractObject(this.data, ptr, obj_table); this.cryptoConfiguration.version = page_obj.value["/V"]; this.cryptoConfiguration.revision = page_obj.value["/R"]; this.cryptoConfiguration.filter = page_obj.value["/Filter"]; this.cryptoConfiguration.user_pwd_c = page_obj.value["/U"]; this.cryptoConfiguration.owner_pwd_c = page_obj.value["/O"]; this.cryptoConfiguration.length = page_obj.value["/Length"]; this.cryptoConfiguration.permissions = page_obj.value["/P"]; } } exports.CryptoInterface = CryptoInterface; /** * Parses the relevant parts of the PDF document and provides functionality to extract the necessary information for * adding annotations * */ class PDFDocumentParser { constructor(data, userpwd = "", ownerpwd = "") { this.data = data; this.version = undefined; this.documentHistory = new document_history_1.DocumentHistory(new Uint8Array([])); this.catalogObject = undefined; this.pageTree = undefined; this.cryptoInterface = new CryptoInterface(); this.data = new Uint8Array(data); this.documentHistory = new document_history_1.DocumentHistory(this.data); this.documentHistory.extractDocumentHistory(); if (this.documentHistory.isEncrypted()) { // extract encryption dictionary let obj_table = this.documentHistory.createObjectLookupTable(); let enc_obj = this.documentHistory.getRecentUpdate().encrypt; if (!enc_obj) throw Error("Invalid encryption indication"); let enc_obj_ptr = obj_table[enc_obj.obj]; this.cryptoInterface = new CryptoInterface(this.data, this.documentHistory, enc_obj_ptr, userpwd, ownerpwd); // verify keys if (!this.cryptoInterface.isUserPasswordCorrect()) { if (!this.cryptoInterface.isOwnerPasswordCorrect()) { throw Error("No valid user credentials"); } } } } /** * Returns the crypto interface * */ getCryptoInterface() { return this.cryptoInterface; } /** * Returns the major and minor version of the pdf document * */ getPDFVersion() { if (this.version) return this.version; this.version = util_1.Util.extractVersion(this.data, 0); return this.version; } /** * Returns a free object id. It first checks wether there can be an freed object id reused. If that is not the case * it creates a new one * */ getFreeObjectId() { return this.documentHistory.getFreeObjectId(); } /** * Returns the catalog object of the PDF file * */ getCatalog() { let recent_update = this.documentHistory.getRecentUpdate(); if (recent_update.root) { let root_obj = recent_update.root; let obj_table = this.documentHistory.createObjectLookupTable(); return new CatalogObject(this.data, obj_table[root_obj.obj], obj_table); } else { // If we do not know the catalogue object we need to look it up // In cross reference stream objects no /ROOT field is required, however often it is provided anyway // otherwise run this routine, but buffer the catalog object if (this.catalogObject) return this.catalogObject; throw Error("Does not work for compressed data"); //let obj_table = this.documentHistory.createObjectLookupTable() //for (let i = 1; i < recent_update.size; ++i) { // let _type = Util.extractField(this.data, Util._TYPE, obj_table[i].pointer) // if (Util.areArraysEqual(_type, Util.CATALOG)) { // this.catalogObject = new CatalogObject(this.data, obj_table[i]) // if (this.catalogObject) // return this.catalogObject // } //} } throw Error("Could not identify catalog object"); } /** * Returns the latest version of the page tree object of the document * */ getPageTree() { if (this.pageTree) return this.pageTree; let obj_table = this.documentHistory.createObjectLookupTable(); let catalog_object = this.getCatalog(); let pages_id = catalog_object.getPagesObjectId(); let pages_ref = obj_table[pages_id.obj]; let pageTree = new PageTree(this.data, obj_table); pageTree.extract(pages_ref, obj_table); this.pageTree = pageTree; return pageTree; } /** * Returns the latest version of the page with the given pageNumber * */ getPage(pageNumber) { let pageTree = this.getPageTree(); let pageId = pageTree.getPageReferences()[pageNumber]; let obj_table = this.documentHistory.createObjectLookupTable(); let obj_ptr = obj_table[pageId.obj]; let page = new Page(this.data, this.documentHistory); page.extract(obj_ptr, obj_table); return page; } /** * Returns the annotations that exist in the document * */ extractAnnotations() { let annots = []; let pt = this.getPageTree(); let obj_table = this.documentHistory.createObjectLookupTable(); let pageCount = pt.getPageCount(); for (let i = 0; i < pageCount; ++i) { let page = this.getPage(i); let annotationReferences = page.annots; let pageAnnots = []; for (let refPtr of annotationReferences) { let a = new Annotation(this.data, this.cryptoInterface); a.extract(obj_table[refPtr.obj], page, obj_table); a.page = i; pageAnnots.push(a); } annots.push(pageAnnots); } return annots; } } exports.PDFDocumentParser = PDFDocumentParser; //# sourceMappingURL=parser.js.map