From 4def4068f7d0bb5f09ba6908dcfc2acc39c27816 Mon Sep 17 00:00:00 2001 From: zboris12 Date: Sun, 18 Aug 2024 21:32:27 +0900 Subject: [PATCH] Improved to reuse the font if it is already embedded in the PDF. --- README.md | 7 +- closure/pdflib-ext.js | 63 ++++++++++++- lib/zgaindex.js | 6 +- lib/zganode.d.ts | 7 +- lib/zganode.js | 1 + lib/zgapdfsigner.js | 210 +++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- test-ts/package.json | 2 +- test-ts/src/index.ts | 17 ++-- test.html | 4 +- test4node.js | 21 ++++- 11 files changed, 303 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 2a32093..16c76a3 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,10 @@ Just import the dependencies and this tool. ``` -When drawing text by non-standard font, importing the fontkit library is necessary. +When drawing text for signature, importing fontkit and pako library is necessary. ```html + ``` ### [Google Apps Script](https://developers.google.com/apps-script) @@ -68,8 +69,10 @@ function setTimeout(func, sleep){ var window = globalThis; // Load pdf-lib eval(UrlFetchApp.fetch("https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js").getContentText()); -// It is necessary for drawing text by non-standard font. +// It is necessary for drawing text for signature. eval(UrlFetchApp.fetch("https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.min.js").getContentText()); +// Load pako, It is necessary for drawing text for signature. +eval(UrlFetchApp.fetch("https://unpkg.com/pako@1.0.11/dist/pako_inflate.min.js").getContentText()); // Load node-forge eval(UrlFetchApp.fetch("https://unpkg.com/node-forge@1.3.1/dist/forge.min.js").getContentText()); // Load ZgaPdfSigner diff --git a/closure/pdflib-ext.js b/closure/pdflib-ext.js index 4b95961..68b4cbc 100644 --- a/closure/pdflib-ext.js +++ b/closure/pdflib-ext.js @@ -9,7 +9,17 @@ var PdfLoadOptions; /** @const */ -var fontkit = {}; +var pako = {}; +/** + * @param {Uint8Array} input + * @return {Uint8Array} + */ +pako.inflate = function(input){}; + +/** @constructor */ +var Fontkit = function(){}; +/** @const {Fontkit} */ +var fontkit; /** @const */ var PDFLib = {}; @@ -41,6 +51,11 @@ PDFLib.lineSplit = function(text){}; * @return {boolean} */ PDFLib.isNewlineChar = function(text){}; +/** + * @param {string} fntnm + * @return {boolean} + */ +PDFLib.isStandardFont = function(fntnm){}; /** @constructor */ PDFLib.PDFDocument = function(){}; @@ -105,7 +120,7 @@ PDFLib.PDFDocument.prototype.embedFont = function(font, options){}; */ PDFLib.PDFDocument.prototype.embedStandardFont = function(font, customName){}; /** - * @lends {fontkit} fkt + * @param {Fontkit} fkt */ PDFLib.PDFDocument.prototype.registerFontkit = function(fkt){}; /** @@ -316,6 +331,8 @@ PDFLib.PDFName.Annots; * @return {PDFLib.PDFName} */ PDFLib.PDFName.of = function(value){}; +/** @return {string} */ +PDFLib.PDFName.prototype.asString = function(){}; /** @type {string} */ PDFLib.PDFName.prototype.encodedName; /** @type {number} */ @@ -338,10 +355,20 @@ PDFLib.PDFArray.prototype.push = function(object){}; * @return {PDFLib.PDFObject} */ PDFLib.PDFArray.prototype.get = function(idx){}; +/** + * @return {number} + */ +PDFLib.PDFArray.prototype.size = function(){}; /** * @return {Array} */ PDFLib.PDFArray.prototype.asArray = function(){}; +/** + * @param {number} idx + * @param {*} typ + * @return {PDFLib.PDFObject} + */ +PDFLib.PDFArray.prototype.lookupMaybe = function(idx, typ){}; /** * @constructor @@ -403,8 +430,34 @@ PDFLib.PDFImage.prototype.size = function(){}; /** @type {PDFLib.PDFRef} */ PDFLib.PDFImage.prototype.ref; +/** @constructor */ +PDFLib.StandardFontEmbedder = function(){}; +/** + * @param {string} fontName + * @param {string=} customName + * @return {PDFLib.StandardFontEmbedder} + */ +PDFLib.StandardFontEmbedder.for = function(fontName, customName){}; + +/** @constructor */ +PDFLib.CustomFontEmbedder = function(){}; +/** + * @param {Fontkit} fontkit + * @param {Uint8Array} fontData + * @param {string=} customName + * @return {PDFLib.CustomFontEmbedder} + */ +PDFLib.CustomFontEmbedder.for = function(fontkit, fontData, customName){}; + /** @constructor */ PDFLib.PDFFont = function(){}; +/** + * @param {PDFLib.PDFRef} ref + * @param {PDFLib.PDFDocument} doc + * @param {PDFLib.StandardFontEmbedder|PDFLib.CustomFontEmbedder} embedder + * @return {PDFLib.PDFFont} + */ +PDFLib.PDFFont.of = function(ref, doc, embedder){}; /** @type {PDFLib.PDFRef} */ PDFLib.PDFFont.prototype.ref; /** @type {string} */ @@ -540,6 +593,12 @@ PDFLib.PDFContentStream.of = function(dict, operators, encode){}; * @extends {PDFLib.PDFStream} */ PDFLib.PDFRawStream = function(){}; +/** @type {PDFLib.PDFDict} */ +PDFLib.PDFRawStream.prototype.dict; +/** + * @return {Uint8Array} + */ +PDFLib.PDFRawStream.prototype.getContents = function(){}; /** * @constructor diff --git a/lib/zgaindex.js b/lib/zgaindex.js index 04703df..c7743fe 100644 --- a/lib/zgaindex.js +++ b/lib/zgaindex.js @@ -8,11 +8,11 @@ function genZga(){ const z = {}; /** - * @param {string} msg + * @param {...string} msg */ - z.log = function(msg){ + z.log = function(...msg){ if(z.debug){ - console.log(msg); + console.log(...msg); } }; diff --git a/lib/zganode.d.ts b/lib/zganode.d.ts index e602ccb..8f0525e 100644 --- a/lib/zganode.d.ts +++ b/lib/zganode.d.ts @@ -37,7 +37,7 @@ export type SignAreaInfo = { }; export type SignTextInfo = { text: string, - fontData?: Array | Uint8Array | ArrayBuffer | string; + fontData?: Array | Uint8Array | ArrayBuffer | PDFLib.StandardFonts; color?: string; opacity?: number; blendMode?: string; @@ -109,3 +109,8 @@ export declare class TsaFetcher { getToken(forP7?: boolean): forge.asn1.Asn1; queryTsa(data?: string): Promise; } +export declare class PdfFonts { + private constructor(); + static from(pdfdoc: PDFLib.PDFDocument): Promise; + getEmbeddedFont(fontData?: Array | Uint8Array | ArrayBuffer | PDFLib.StandardFonts): Promise; +} diff --git a/lib/zganode.js b/lib/zganode.js index d15355f..470b175 100644 --- a/lib/zganode.js +++ b/lib/zganode.js @@ -7,6 +7,7 @@ const z = require("./zgaindex.js"); z.forge = require("node-forge"); z.PDFLib = require("pdf-lib"); z.fontkit = require("@pdf-lib/fontkit"); +z.pako = require("pako"); /** * @param {string} url * @param {UrlFetchParams} params diff --git a/lib/zgapdfsigner.js b/lib/zgapdfsigner.js index 5c8386e..0b0e37d 100644 --- a/lib/zgapdfsigner.js +++ b/lib/zgapdfsigner.js @@ -15,6 +15,9 @@ if(z.PDFLib){ if(z.fontkit){ var fontkit = z.fontkit; } +if(z.pako){ + var pako = z.pako; +} //Only for nodejs End// /** @type {Object} */ @@ -243,6 +246,8 @@ z.PdfSigner = class{ this.oriU8pdf = null; /** @private @type {Array} */ this.apobjs = null; + /** @private @type {z.PdfFonts} */ + this.fonts = null; if(typeof this.opt.debug == "boolean"){ z.debug = this.opt.debug; @@ -341,21 +346,8 @@ z.PdfSigner = class{ } if(_this.opt.drawinf && _this.opt.drawinf.textInfo && !_this.opt.drawinf.font){ - /** @type {Uint8Array|ArrayBuffer|string} */ - var fontData2 = null; - if(Array.isArray(_this.opt.drawinf.textInfo.fontData)){ - fontData2 = new Uint8Array(_this.opt.drawinf.textInfo.fontData); - }else if(_this.opt.drawinf.textInfo.fontData){ - fontData2 = _this.opt.drawinf.textInfo.fontData; - }else{ - fontData2 = "Helvetica"; - } - if(typeof fontData2 == "string"){ - _this.opt.drawinf.font = pdfdoc.embedStandardFont(fontData2); - }else{ - pdfdoc.registerFontkit(fontkit); - _this.opt.drawinf.font = await pdfdoc.embedFont(fontData2); - } + _this.fonts = await z.PdfFonts.from(pdfdoc); + _this.opt.drawinf.font = await _this.fonts.getEmbeddedFont(_this.opt.drawinf.textInfo.fontData); } /** @type {forge_cert} */ @@ -1839,6 +1831,186 @@ z.SignatureCreator = class{ } }; +z.PdfFonts = class{ + /** + * @private + * @param {PDFLib.PDFDocument} pdfdoc + * @param {Array} fonts + */ + constructor(pdfdoc, fonts){ + /** @private @type {PDFLib.PDFDocument} */ + this.doc = pdfdoc; + /** @private @type {Array} */ + this.fonts = fonts; + } + + /** + * @public + * @param {PDFLib.PDFDocument} pdfdoc + * @return {Promise} + */ + static async from(pdfdoc){ + /** + * @param {PDFLib.PDFDict} dict + * @param {string} nm + * @return {string|undefined} + */ + var lookupName = function(dict, nm){ + var pnm = /** @type {PDFLib.PDFName} */(dict.lookupMaybe(PDFLib.PDFName.of(nm), PDFLib.PDFName)); + if(pnm){ + return pnm.asString(); + }else{ + return undefined; + } + }; + + /** @type Array */ + var fonts = []; + /** @type {Array} */ + var objs = pdfdoc.context.enumerateIndirectObjects(); + /** @type {number} */ + var i = 0; + while(i < objs.length){ + /** @type {PdfObjEntry} */ + var poe = objs[i]; + i++; + + if(poe[1] instanceof PDFLib.PDFDict){ + /** @type {string|undefined} */ + var typ = lookupName(poe[1], "Type"); + if(typ !== "/Font"){ + continue; + } + /** @type {string|undefined} */ + var fntnm = lookupName(poe[1], "BaseFont"); + if(fntnm){ + fntnm = fntnm.substring(1); + if(PDFLib.isStandardFont(fntnm)){ + fonts.push({ + font: PDFLib.PDFFont.of(poe[0], pdfdoc, PDFLib.StandardFontEmbedder.for(fntnm)), + }); + continue; + } + }else{ + continue; + } + + var dfnts = /** @type {PDFLib.PDFArray} */(poe[1].lookupMaybe(PDFLib.PDFName.of("DescendantFonts"), PDFLib.PDFArray)); + if(dfnts && dfnts.size()){ + var fntdict = /** @type {PDFLib.PDFDict} */(dfnts.lookupMaybe(0, PDFLib.PDFDict)); + if(fntdict){ + var fntdesc = /** @type {PDFLib.PDFDict} */(fntdict.lookupMaybe(PDFLib.PDFName.of("FontDescriptor"), PDFLib.PDFDict)); + if(fntdesc){ + var rstm = /** @type {PDFLib.PDFRawStream} */(fntdesc.lookupMaybe(PDFLib.PDFName.of("FontFile2"), PDFLib.PDFRawStream)); + if(rstm){ + /** @type {Uint8Array} */ + var fdat = rstm.getContents(); + /** @type {string|undefined} */ + var fltr = lookupName(rstm.dict, "Filter"); + if(fltr == "/FlateDecode"){ + fdat = pako.inflate(fdat); + } + /** @type {PDFLib.CustomFontEmbedder} */ + var emdr = await PDFLib.CustomFontEmbedder.for(fontkit, fdat); + fonts.push({ + font: PDFLib.PDFFont.of(poe[0], pdfdoc, emdr), + data: fdat, + }); + } + } + } + } + } + } + + return new z.PdfFonts(pdfdoc, fonts); + } + + /** + * @public + * @param {Array|Uint8Array|ArrayBuffer|string|undefined} fontData + * @return {Promise} + */ + async getEmbeddedFont(fontData){ + if(!fontData){ + if(this.fonts.length){ + z.log("Use existing default font.", this.fonts[0].font.name); + return this.fonts[0].font; + }else{ + fontData = "Helvetica"; + z.log("Use default font.", fontData); + } + } + if(typeof fontData == "string"){ + return this.getStandardFont(fontData); + }else{ + /** @type {Uint8Array} */ + var u8dat = (fontData instanceof Uint8Array) ? fontData : new Uint8Array(fontData); + return await this.getCustomFont(u8dat); + } + } + + /** + * @private + * @param {string} fontData + * @return {PDFLib.PDFFont} + */ + getStandardFont(fontData){ + /** @type {number} */ + var i = 0; + while(i < this.fonts.length){ + /** @type {FontInfo} */ + var fi = this.fonts[i]; + i++; + if(!fi.data && fi.font.name == fontData){ + z.log("Existing font found.", fi.font.name); + return fi.font; + } + } + return this.doc.embedStandardFont(fontData); + } + /** + * @private + * @param {Uint8Array} fontData + * @return {Promise} + */ + async getCustomFont(fontData){ + /** @type {number} */ + var i = 0; + while(i < this.fonts.length){ + /** @type {FontInfo} */ + var fi = this.fonts[i]; + i++; + if(fi.data && this.isSameData(fi.data, fontData)){ + z.log("Existing font found.", fi.font.name); + return fi.font; + } + } + this.doc.registerFontkit(fontkit); + return await this.doc.embedFont(fontData); + } + /** + * @private + * @param {Uint8Array} dat1 + * @param {Uint8Array} dat2 + * @return {boolean} + */ + isSameData(dat1, dat2){ + if(dat1.length != dat2.length){ + return false; + } + /** @type {number} */ + var i = 0; + while(i < dat1.length){ + if(dat1[i] != dat2[i]){ + return false; + } + i++; + } + return true; + } +}; + /** * @typedef * {{ @@ -1848,6 +2020,14 @@ z.SignatureCreator = class{ * }} */ var SplitLongWordResult; +/** + * @typedef + * {{ + * font: PDFLib.PDFFont, + * data: (Uint8Array|undefined), + * }} + */ +var FontInfo; } diff --git a/package.json b/package.json index 54010aa..c68fc17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zgapdfsigner", - "version": "2.7.0", + "version": "2.7.1", "author": "zboris12", "description": "A javascript tool to sign a pdf or set protection to a pdf in web browser, Google Apps Script and nodejs.", "homepage": "https://github.com/zboris12/zgapdfsigner", diff --git a/test-ts/package.json b/test-ts/package.json index 5485ad7..fdfe375 100644 --- a/test-ts/package.json +++ b/test-ts/package.json @@ -11,7 +11,7 @@ "test": "node test/index.js ${pfxpwd}" }, "dependencies": { - "zgapdfsigner": "^2.7.0" + "zgapdfsigner": "^2.7.1" }, "devDependencies": { "@types/node-forge": "^1.3.11", diff --git a/test-ts/src/index.ts b/test-ts/src/index.ts index e8d059f..d92979c 100644 --- a/test-ts/src/index.ts +++ b/test-ts/src/index.ts @@ -9,7 +9,7 @@ async function sign_protect(pdfPath: string, pfxPath: string, ps: string, perm: let pfx: Buffer = m_fs.readFileSync(pfxPath); let img: Buffer | undefined = undefined; let imgType: string = ""; - let font: Buffer | undefined = undefined; + let font: Buffer | Zga.PDFLib.StandardFonts | undefined = undefined; if (perm == 1) { console.log("\nTest signing pdf with full protection. (permission 1 and password encryption)"); @@ -22,7 +22,11 @@ async function sign_protect(pdfPath: string, pfxPath: string, ps: string, perm: imgType = m_path.extname(imgPath).slice(1); } if (fontPath) { - font = m_fs.readFileSync(fontPath); + if (Zga.PDFLib.isStandardFont(fontPath)) { + font = fontPath as string as Zga.PDFLib.StandardFonts; + } else { + font = m_fs.readFileSync(fontPath); + } } let sopt: Zga.SignOption = { p12cert: pfx, @@ -38,7 +42,7 @@ async function sign_protect(pdfPath: string, pfxPath: string, ps: string, perm: if (img || txt) { sopt.drawinf = { area: { - x: 25, // left + x: perm ? 25 : 200, // left y: 50, // top w: txt ? undefined : 60, h: txt ? undefined : 100, @@ -108,7 +112,7 @@ async function main1(angle: number): Promise { let pfxPath: string = m_path.join(__dirname, workpath + "_test.pfx"); let ps: string = ""; let imgPath: string = m_path.join(__dirname, workpath + "_test.png"); - let fontPath: string = m_path.join(__dirname, workpath + "_test.ttf"); + let fontPath: string = angle ? Zga.PDFLib.StandardFonts.TimesRomanBold : m_path.join(__dirname, workpath + "_test.ttf"); if (process.argv.length > 3) { pfxPath = process.argv[2]; @@ -123,8 +127,9 @@ async function main1(angle: number): Promise { } if (pfxPath) { - await sign_protect(pdfPath, pfxPath, ps, 1, imgPath, "あいうえおあいうえおか\r\n\nThis is a test of text!\n", fontPath); - pdfPath = await sign_protect(pdfPath, pfxPath, ps, 2, undefined, "ありがとうご\r\n\nThis is an another test of text!\n", fontPath); + await sign_protect(pdfPath, pfxPath, ps, 1, imgPath, "あいうえおあいうえおか\r\n\nThis is a test of text!\n"); + pdfPath = await sign_protect(pdfPath, pfxPath, ps, 2, imgPath, (angle ? "" : "ありがとうご\r\n\n") + "This is an another test of text!\n", fontPath); + pdfPath = await sign_protect(pdfPath, pfxPath, ps, 0, undefined, (angle ? "" : "たちつて得\n\n") + "This is a test for same font!\n", fontPath); await addtsa(pdfPath); } else { await addtsa(pdfPath); diff --git a/test.html b/test.html index b3335e4..237933a 100644 --- a/test.html +++ b/test.html @@ -6,6 +6,7 @@ Test for ZgaPdfSigner +