From 0505fd1e0efa1d2a6a7dd33f059e50ed36b082af Mon Sep 17 00:00:00 2001 From: zboris12 Date: Fri, 9 Aug 2024 21:57:20 +0900 Subject: [PATCH] Working on text signature and placing on multiple pages. --- README.md | 73 ++++++- closure/pdflib-ext.js | 46 +++- closure/zb-externs.js | 60 +++++- lib/zgapdfsigner.js | 486 ++++++++++++++++++++++++++++++++---------- test4node.js | 61 ++++-- 5 files changed, 583 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 3037d8c..16ba4ed 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ And I use this name to hope the merits from this application will be dedicated t ## Main features * Sign a pdf with an invisible pkcs#7 signature. -* Sign a pdf with a visible pkcs#7 signature by drawing an image. +* Sign a pdf with a visible pkcs#7 signature by drawing an image or a text or both. +* A visible signature can be placed on multiple pages. (In the same position) * Sign a pdf and set [DocMDP](https://github.com/zboris12/zgapdfsigner/wiki/API#note). * Add a new signature to a pdf if it has been signed already. (An incremental update) * Add a document timestamp from [TSA](https://github.com/zboris12/zgapdfsigner/wiki/API#note). ( :no_entry_sign:__Not__ available in web browser) @@ -133,8 +134,10 @@ async function sign2(pdf, cert, pwd, imgdat, imgtyp){ w: 60, // width h: 60, // height }, - imgData: imgdat, - imgType: imgtyp, + imgInfo: { + imgData: imgdat, + imgType: imgtyp, + }, }, }; var signer = new Zga.PdfSigner(sopt); @@ -143,10 +146,41 @@ async function sign2(pdf, cert, pwd, imgdat, imgtyp){ } ``` -Sign with a visible signature of drawing a text. +Sign with a visible signature by drawing a text. ```js -//TODO +/** + * @param {ArrayBuffer} pdf + * @param {ArrayBuffer} cert + * @param {string} pwd + * @param {string} txt + * @param {ArrayBuffer} fontdat + * @return {Promise} + */ +async function sign3(pdf, cert, pwd, txt, fontdat){ + /** @type {SignOption} */ + var sopt = { + p12cert: cert, + pwd: pwd, + drawinf: { + area: { + x: 25, // left + y: 150, // top + w: 60, // width + h: 60, // height + }, + textInfo: { + text: txt, + fontData: fontdat, + color: "#00f0f1", + size: 16, + }, + }, + }; + var signer = new Zga.PdfSigner(sopt); + var u8arr = await signer.sign(pdf); + return new Blob([u8arr], {"type" : "application/pdf"}); +} ``` Use it in [Google Apps Script](https://developers.google.com/apps-script) @@ -198,6 +232,10 @@ async function main(){ var ps = ""; /** @type {string} */ var imgPath = m_path.join(__dirname, "_test.png"); + /** @type {string} */ + var txt = "I am a test string!"; + /** @type {string} */ + var fontPath = m_path.join(__dirname, "_test.ttf"); if(process.argv.length > 3){ pfxPath = process.argv[2]; @@ -226,6 +264,11 @@ async function main(){ img = m_fs.readFileSync(imgPath); imgType = m_path.extname(imgPath).slice(1); } + /** @type {Buffer} */ + var font = null; + if(fontPath){ + font = m_fs.readFileSync(fontPath); + } /** @type {SignOption} */ var sopt = { @@ -239,16 +282,24 @@ async function main(){ ltv: 1, debug: true, }; - if(img){ + if(img || txt){ sopt.drawinf = { area: { x: 25, // left - y: 150, // top - w: 60, - h: 60, + y: 50, // top + w: txt ? undefined : 60, + h: txt ? undefined : 100, }, - imgData: img, - imgType: imgType, + pageidx: "2-3", // Placed the signature on the 3rd page and the 4th page. (Indexes of pages start from 0) + imgInfo: img ? { + imgData: img, + imgType: imgType, + } : undefined, + textInfo: txt ? { + text: txt, + fontData: font, + size: 16, + } : undefined, }; } diff --git a/closure/pdflib-ext.js b/closure/pdflib-ext.js index 269f880..4b95961 100644 --- a/closure/pdflib-ext.js +++ b/closure/pdflib-ext.js @@ -8,6 +8,9 @@ */ var PdfLoadOptions; +/** @const */ +var fontkit = {}; + /** @const */ var PDFLib = {}; @@ -23,6 +26,21 @@ PDFLib.copyStringIntoBuffer = function(str, buffer, offset){}; * @return {Uint8Array} */ PDFLib.toUint8Array = function(input){}; +/** + * @param {string} text + * @return {string} + */ +PDFLib.cleanText = function(text){}; +/** + * @param {string} text + * @return {Array} + */ +PDFLib.lineSplit = function(text){}; +/** + * @param {string} text + * @return {boolean} + */ +PDFLib.isNewlineChar = function(text){}; /** @constructor */ PDFLib.PDFDocument = function(){}; @@ -50,6 +68,10 @@ PDFLib.PDFDocument.prototype.save = function(options){}; * @returns {Array} */ PDFLib.PDFDocument.prototype.getPages = function(){}; +/** + * @returns {number} + */ +PDFLib.PDFDocument.prototype.getPageCount = function(){}; /** * @param {ArrayBuffer|Uint8Array|string} png * @returns {Promise} @@ -82,6 +104,10 @@ PDFLib.PDFDocument.prototype.embedFont = function(font, options){}; * @returns {PDFLib.PDFFont} */ PDFLib.PDFDocument.prototype.embedStandardFont = function(font, customName){}; +/** + * @lends {fontkit} fkt + */ +PDFLib.PDFDocument.prototype.registerFontkit = function(fkt){}; /** * @returns {Promise} */ @@ -383,8 +409,23 @@ PDFLib.PDFFont = function(){}; PDFLib.PDFFont.prototype.ref; /** @type {string} */ PDFLib.PDFFont.prototype.name; -/** @constructor */ -PDFLib.StandardFonts = function(){}; +/** + * @param {string} text + * @return {PDFLib.PDFHexString } + */ +PDFLib.PDFFont.prototype.encodeText = function(text){}; +/** + * @param {number} size + * @param {Object=} options + * @return {number} + */ +PDFLib.PDFFont.prototype.heightAtSize = function(size, options){}; +/** + * @param {string} text + * @param {number} size + * @return {number} + */ +PDFLib.PDFFont.prototype.widthOfTextAtSize = function(text, size){}; PDFLib.RotationTypes = {}; /** @type {string} */ @@ -416,6 +457,7 @@ PDFLib.PDFOperator = function(){}; * rotate: (PDFLib.Rotation|undefined), * xSkew: (PDFLib.Rotation|undefined), * ySkew: (PDFLib.Rotation|undefined), + * graphicsState: (string|undefined), * }} */ var PdfDrawimgOption; diff --git a/closure/zb-externs.js b/closure/zb-externs.js index 7c91203..ae9cce3 100644 --- a/closure/zb-externs.js +++ b/closure/zb-externs.js @@ -9,24 +9,74 @@ var TsaServiceInfo; /** * the base point of x, y is top left corner. + * wDraw, hDraw: Only for internal process. * @typedef * {{ * x: number, * y: number, - * w: number, - * h: number, + * w: (number|undefined), + * h: (number|undefined), + * wDraw: (number|undefined), + * hDraw: (number|undefined), * }} */ var SignAreaInfo; /** + * fontData: default: StandardFonts.Helvetica + * color: A Hex string of color. default: #000 + * opacity: valid value is from 0 to 1. default: 1 // Not implemented + * blendMode: https://pdf-lib.js.org/docs/api/enums/blendmode // Not implemented + * lineHeight: default is the height of the font at the given size + * xOffset: An offset from SignAreaInfo's x + * yOffset: An offset from SignAreaInfo's y + * align: Text alignment: 0 left, 1 center, 2 right. default: 0 + * noBreaks: A regular expression string that indicates which characters should not be used to break a word. default: [A-Za-z0-9] + * + * @typedef + * {{ + * text: string, + * fontData: (Array|Uint8Array|ArrayBuffer|string|undefined), + * color: (string|undefined), + * opacity: (number|undefined), + * blendMode: (string|undefined), + * lineHeight: (number|undefined), + * size: number, + * xOffset: (number|undefined), + * yOffset: (number|undefined), + * wMax: (number|undefined), + * align: (number|undefined), + * noBreaks: (string|undefined), + * }} + */ +var SignTextInfo; +/** + * opacity: valid value is from 0 to 1. default: 1 // Not implemented + * blendMode: https://pdf-lib.js.org/docs/api/enums/blendmode // Not implemented + * + * @typedef + * {{ + * imgData: (Array|Uint8Array|ArrayBuffer|string|undefined), + * imgType: (string|undefined), + * opacity: (number|undefined), + * blendMode: (string|undefined), + * }} + */ +var SignImageInfo; +/** + * The signature can be placed in the same position on multiple pages, but all pages must have the same size and rotation angle. + * pageidx: Can be a string to indicate placing the signature on multiple pages. + * For example: A pdf contains 17 pages and specify "-3,5-7,9,12,15-" means [0,1,2,3,5,6,7,9,12,15,16] + * imgData, imgType: Deprecated, use imgInfo instead. + * img, font: Only for internal process. + * * @typedef * {{ * area: SignAreaInfo, - * pageidx: (number|undefined), + * pageidx: (number|string|undefined), * imgData: (Array|Uint8Array|ArrayBuffer|string|undefined), * imgType: (string|undefined), - * text: (string|undefined), - * fontData: (Array|Uint8Array|ArrayBuffer|string|undefined), + * imgInfo: (SignImageInfo|undefined), + * textInfo: (SignTextInfo|undefined), * img: (PDFLib.PDFImage|undefined), * font: (PDFLib.PDFFont|undefined), * }} diff --git a/lib/zgapdfsigner.js b/lib/zgapdfsigner.js index bd0eafb..001344b 100644 --- a/lib/zgapdfsigner.js +++ b/lib/zgapdfsigner.js @@ -315,30 +315,40 @@ z.PdfSigner = class{ pdfdoc = await PDFLib.PDFDocument.load(_this.oriU8pdf); } - if(_this.opt.drawinf && _this.opt.drawinf.imgData && !_this.opt.drawinf.img){ + // For backward compatibility + if(_this.opt.drawinf && _this.opt.drawinf.imgData && !_this.opt.drawinf.imgInfo){ + _this.opt.drawinf.imgInfo = { + imgData: _this.opt.drawinf.imgData, + imgType: _this.opt.drawinf.imgType, + }; + } + + if(_this.opt.drawinf && _this.opt.drawinf.imgInfo && !_this.opt.drawinf.img){ /** @type {Uint8Array|ArrayBuffer|string} */ var imgData2 = null; - if(Array.isArray(_this.opt.drawinf.imgData)){ - imgData2 = new Uint8Array(_this.opt.drawinf.imgData); + if(Array.isArray(_this.opt.drawinf.imgInfo.imgData)){ + imgData2 = new Uint8Array(_this.opt.drawinf.imgInfo.imgData); }else{ - imgData2 = _this.opt.drawinf.imgData; + imgData2 = _this.opt.drawinf.imgInfo.imgData; } - if(_this.opt.drawinf.imgType == "png"){ + if(_this.opt.drawinf.imgInfo.imgType == "png"){ _this.opt.drawinf.img = await pdfdoc.embedPng(imgData2); - }else if(_this.opt.drawinf.imgType == "jpg"){ + }else if(_this.opt.drawinf.imgInfo.imgType == "jpg"){ _this.opt.drawinf.img = await pdfdoc.embedJpg(imgData2); }else{ - throw new Error("Unkown image type. " + _this.opt.drawinf.imgType); + throw new Error("Unkown image type. " + _this.opt.drawinf.imgInfo.imgType); } } - if(_this.opt.drawinf && _this.opt.drawinf.text && _this.opt.drawinf.fontData && !_this.opt.drawinf.font){ + 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.fontData)){ - fontData2 = new Uint8Array(_this.opt.drawinf.fontData); + 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 = _this.opt.drawinf.fontData; + fontData2 = "Helvetica"; } if(typeof fontData2 == "string"){ _this.opt.drawinf.font = pdfdoc.embedStandardFont(fontData2); @@ -719,14 +729,18 @@ z.PdfSigner = class{ /** @const {PDFLib.PDFContext} */ const pdfcont = pdfdoc.context; /** @const {z.SignatureCreator} */ - const signcrt = new z.SignatureCreator(_this.opt.drawinf); + const signcrt = new z.SignatureCreator(_this.opt.drawinf, pdfdoc.getPageCount()); + /** @type {Array} */ + var pgidxs = signcrt.getPageIndexes(); /** @const {PDFLib.PDFPage} */ - const page = pdfdoc.getPages()[signcrt.getPageIndex()]; + const page = pdfdoc.getPages()[pgidxs[0]]; /** @type {PDFLib.PDFRef} */ var strmRef = signcrt.createStream(pdfdoc, _this.opt.signame); if(docMdp && !strmRef){ strmRef = signcrt.createEmptyField(pdfcont); + // For invisible signature, only place on one page. + pgidxs = [pgidxs[0]]; } /** @type {Array} */ @@ -834,18 +848,22 @@ z.PdfSigner = class{ /** @type {PDFLib.PDFRef} */ var widgetDictRef = pdfcont.register(pdfcont.obj(widgetObj)); - // Add our signature widget to the page - /** @type {PDFLib.PDFArray} */ - var ans = page.node.Annots(); - if(!ans){ - ans = new PDFLib.PDFArray(pdfcont); - // if(docMdp){ - page.node.set(PDFLib.PDFName.Annots, ans); - // }else{ - // page.node.set(PDFLib.PDFName.Annots, pdfcont.register(ans)); - // } - } - ans.push(widgetDictRef); + // Add our signature widget to the pages + pgidxs.forEach(function(pi){ + /** @const {PDFLib.PDFPage} */ + var p = pdfdoc.getPages()[pi]; + /** @type {PDFLib.PDFArray} */ + var ans = p.node.Annots(); + if(!ans){ + ans = new PDFLib.PDFArray(pdfcont); + // if(docMdp){ + p.node.set(PDFLib.PDFName.Annots, ans); + // }else{ + // p.node.set(PDFLib.PDFName.Annots, pdfcont.register(ans)); + // } + } + ans.push(widgetDictRef); + }); if(!afrm.dict.lookup(PDFLib.PDFName.of("SigFlags"))){ afrm.dict.set(PDFLib.PDFName.of("SigFlags"), PDFLib.PDFNumber.of(3)); @@ -1233,10 +1251,11 @@ z.PdfSigner = class{ z.SignatureCreator = class{ /** * @param {SignDrawInfo=} drawinf + * @param {number=} pgcnt */ - constructor(drawinf){ - /** @private @type {number} */ - this.pgidx = 0; + constructor(drawinf, pgcnt){ + /** @private @type {Array} */ + this.pgidxs = []; /** @private @type {Array} */ this.rect = [0, 0, 0, 0]; /** @private @type {?SignDrawInfo} */ @@ -1244,18 +1263,40 @@ z.SignatureCreator = class{ if(drawinf){ this.drawinf = drawinf; - if(this.drawinf.pageidx){ - this.pgidx = this.drawinf.pageidx; + if(typeof this.drawinf.pageidx == "string"){ + /** @type {Array} */ + var sarr = this.drawinf.pageidx.split(","); + /** @type {number} */ + var i = 0; + for(i=0; i} */ + var sarr2 = sarr[i].split("-"); + /** @type {number} */ + var j = sarr2[0] ? parseInt(sarr2[0], 10) : 0; + /** @type {number} */ + var ed = sarr2[sarr2.length - 1] ? parseInt(sarr2[sarr2.length - 1], 10) : (pgcnt ? pgcnt -1 : j); + while(j <= ed){ + this.pgidxs.push(j); + j++; + } + } + } + }else if(this.drawinf.pageidx){ + this.pgidxs = [/** @type {number} */(this.drawinf.pageidx)]; } } + if(this.pgidxs.length == 0){ + this.pgidxs = [0]; + } } /** * @public - * @return {number} + * @return {Array} */ - getPageIndex(){ - return this.pgidx; + getPageIndexes(){ + return this.pgidxs; } /** @@ -1289,7 +1330,7 @@ z.SignatureCreator = class{ createStream(pdfdoc, signame){ if(!this.drawinf){ return null; - }else if(!(this.drawinf.img || this.drawinf.text)){ + }else if(!(this.drawinf.img || this.drawinf.textInfo)){ return null; } @@ -1297,8 +1338,8 @@ z.SignatureCreator = class{ var pages = pdfdoc.getPages(); /** @type {PDFLib.PDFPage} */ var page = null; - if(this.pgidx < pages.length){ - page = pages[this.pgidx]; + if(this.pgidxs[0] < pages.length){ + page = pages[this.pgidxs[0]]; }else{ throw new Error("Page index is overflow to pdf pages."); } @@ -1309,59 +1350,46 @@ z.SignatureCreator = class{ /** @type {PdfSize} */ var pgsz = page.getSize(); /** @type {SignAreaInfo} */ - var areainf = this.calcAreaInf(pgsz, pgrot.angle, this.drawinf.area); + var areainf = this.drawinf.area; // resources object /** @type {Object} */ var rscObj = {}; - /** @type {Array} */ - var sigOprs = []; /** @type {string} */ var imgName = signame ? signame.concat("Img") : "SigImg"; /** @type {string} */ var fontName = signame ? signame.concat("Font") : "SigFont"; - if(this.drawinf.img){ - // Get scaled image size - /** @type {PdfSize} */ - var imgsz = this.drawinf.img.size(); - /** @type {number} */ - var tmp = areainf.w * imgsz.height / imgsz.width; - if(tmp <= areainf.h){ - areainf.h = tmp; - }else{ - areainf.w = areainf.h * imgsz.width / imgsz.height; - } + /** @type {Array} */ + var txtOprs = []; + if(this.drawinf.textInfo){ + rscObj["Font"] = { + [fontName]: this.drawinf.font.ref, + }; + txtOprs = this.createDrawTextOper(pdfdoc, pgrot, fontName, areainf); + } + + /** @type {Array} */ + var imgOprs = []; + if(this.drawinf.img){ rscObj["XObject"] = { [imgName]: this.drawinf.img.ref, }; - sigOprs = sigOprs.concat(PDFLib.drawImage(imgName, this.calcDrawImgInf(pgrot, areainf))); - } - if(this.drawinf.text){ - /** @type {PDFLib.PDFHexString|undefined} */ - var txt = undefined; - if(this.drawinf.font){ - rscObj["Font"] = { - [fontName]: this.drawinf.font.ref, - }; - txt = this.drawinf.font.encodeText(this.drawinf.text); - }else{ - txt = PDFLib.PDFHexString.fromText(this.drawinf.text); - } - sigOprs = sigOprs.concat(PDFLib.drawLinesOfText([txt], this.calcDrawTextInf(pgrot, areainf, this.drawinf.font ? fontName : "Courier8"))); + imgOprs = PDFLib.drawImage(imgName, this.calcDrawImgInf(pgrot, this.drawinf.img.size(), areainf, txtOprs.length == 0)); } + areainf = this.calcAreaInf(pgsz, pgrot.angle, areainf); this.rect = this.calcRect(pgrot.angle, areainf); var frmDict = /** @type {PDFLib.PDFDict} */(pdfdoc.context.obj({ "Type": "XObject", "Subtype": "Form", "FormType": 1, - "BBox": [0, 0, areainf.w, areainf.h], + "BBox": [0, 0, areainf.wDraw, areainf.hDraw], "Resources": rscObj, })); /** @type {PDFLib.PDFContentStream} */ - var strm = PDFLib.PDFContentStream.of(frmDict, sigOprs, true); + var strm = PDFLib.PDFContentStream.of(frmDict, imgOprs.concat(txtOprs), true); return pdfdoc.context.register(strm); } @@ -1379,25 +1407,25 @@ z.SignatureCreator = class{ // Calculate position after rotate switch(angle){ case 90: - ret.w = visinf.h; - ret.h = visinf.w; - ret.x = visinf.y + visinf.h; + ret.wDraw = visinf.hDraw; + ret.hDraw = visinf.wDraw; + ret.x = visinf.y + visinf.hDraw; ret.y = visinf.x; break; case 180: case -180: ret.x = pgsz.width - visinf.x; - ret.y = visinf.y + visinf.h; + ret.y = visinf.y + visinf.hDraw; break; case 270: case -90: - ret.w = visinf.h; - ret.h = visinf.w; - ret.x = pgsz.width - visinf.y - visinf.h; + ret.wDraw = visinf.hDraw; + ret.hDraw = visinf.wDraw; + ret.x = pgsz.width - visinf.y - visinf.hDraw; ret.y = pgsz.height - visinf.x; break; default: - ret.y = pgsz.height - visinf.y - visinf.h; + ret.y = pgsz.height - visinf.y - visinf.hDraw; } return ret; } @@ -1405,7 +1433,7 @@ z.SignatureCreator = class{ /** * @private * @param {number} angle - * @param {SignAreaInfo} areainf // { x, y, w, h } + * @param {SignAreaInfo} areainf * @return {Array} */ calcRect(angle, areainf){ @@ -1415,22 +1443,22 @@ z.SignatureCreator = class{ rect[1] = areainf.y; switch(angle){ case 90: - rect[2] = areainf.x - areainf.h; - rect[3] = areainf.y + areainf.w; + rect[2] = areainf.x - areainf.wDraw; + rect[3] = areainf.y + areainf.hDraw; break; case 180: case -180: - rect[2] = areainf.x - areainf.w; - rect[3] = areainf.y - areainf.h; + rect[2] = areainf.x - areainf.wDraw; + rect[3] = areainf.y - areainf.hDraw; break; case 270: case -90: - rect[2] = areainf.x + areainf.h; - rect[3] = areainf.y - areainf.w; + rect[2] = areainf.x + areainf.wDraw; + rect[3] = areainf.y - areainf.hDraw; break; default: - rect[2] = areainf.x + areainf.w; - rect[3] = areainf.y + areainf.h; + rect[2] = areainf.x + areainf.wDraw; + rect[3] = areainf.y + areainf.hDraw; } return rect; } @@ -1440,78 +1468,310 @@ z.SignatureCreator = class{ * * @private * @param {PDFLib.Rotation} rot - * @param {SignAreaInfo} areainf // { x, y, w, h } + * @param {PdfSize} imgsz + * @param {SignAreaInfo} areainf + * @param {boolean} canResize * @return {PdfDrawimgOption} */ - calcDrawImgInf(rot, areainf){ + calcDrawImgInf(rot, imgsz, areainf, canResize){ + if(!areainf.wDraw){ + if(areainf.w){ + areainf.wDraw = areainf.w; + }else{ + areainf.wDraw = imgsz.width; + } + } + if(!areainf.hDraw){ + if(areainf.h){ + areainf.hDraw = areainf.h; + }else{ + areainf.hDraw = imgsz.height; + } + } + /** @type {number} */ + var wImg = areainf.wDraw; + /** @type {number} */ + var hImg = areainf.hDraw; + if(wImg != imgsz.width && hImg != imgsz.height){ + /** @type {number} */ + var tmp = wImg * imgsz.height / imgsz.width; + if(tmp <= hImg){ + hImg = tmp; + }else{ + wImg = hImg * imgsz.width / imgsz.height; + } + } + if(canResize){ + areainf.wDraw = wImg; + areainf.hDraw = hImg; + } + /** @type {PdfDrawimgOption} */ var ret = { "x": 0, "y": 0, - "width": areainf.w, - "height": areainf.h, + "width": wImg, + "height": hImg, "rotate": rot, "xSkew": PDFLib.degrees(0), "ySkew": PDFLib.degrees(0), + // "graphicsState": "", }; switch(rot.angle){ + case 0: + ret["y"] = areainf.hDraw - hImg - ret["y"]; + break; case 90: - ret["x"] = areainf.w; - ret["width"] = areainf.h; - ret["height"] = areainf.w; + ret["x"] += hImg; break; case 180: case -180: - ret["x"] = areainf.w; - ret["y"] = areainf.h; + ret["x"] = areainf.wDraw - ret["x"]; + ret["y"] += hImg; break; case 270: case -90: - ret["y"] = areainf.h; - ret["width"] = areainf.h; - ret["height"] = areainf.w; + ret["x"] = areainf.hDraw - hImg - ret["x"]; + ret["y"] = areainf.wDraw - ret["y"]; break; } return ret; } /** - * Calculate informations for drawing text after rotate + * Create operations for drawing text after rotate * * @private + * @param {PDFLib.PDFDocument} pdfdoc * @param {PDFLib.Rotation} rot - * @param {SignAreaInfo} areainf // { x, y, w, h } - * @param {string=} font - * @return {DrawLinesOfTextOptions} + * @param {string} fontName + * @param {SignAreaInfo} areainf + * @return {Array} */ - calcDrawTextInf(rot, areainf, font){ + createDrawTextOper(pdfdoc, rot, fontName, areainf){ + /** @const {z.SignatureCreator} */ + const _this = this; + var txtInf = /** @type {SignTextInfo} */(_this.drawinf.textInfo); + var font = /** @type {!PDFLib.PDFFont} */(_this.drawinf.font); /** @type {DrawLinesOfTextOptions} */ - var ret = { - "x": 0, - "y": 10, - "color": PDFLib.rgb(0, 0, 0), - "font": font, - "lineHeight": 35, - "size": 35, + var opts = { + "x": txtInf.xOffset || 0, + "y": txtInf.yOffset || 0, + "color": _this.hexToColor(txtInf.color), + "font": fontName, + "lineHeight": txtInf.lineHeight || font.heightAtSize(txtInf.size, {descender: true}), + "size": txtInf.size, "rotate": rot, "xSkew": PDFLib.degrees(0), "ySkew": PDFLib.degrees(0), + // "graphicsState": "", }; - switch(rot.angle){ + /** + * @param {string} t + * @return {number} + */ + var calcTextWidth = function(t){ + return font.widthOfTextAtSize(t, txtInf.size); + }; + /** @type {Array} */ + var txts = []; + /** @type {boolean} */ + var needW = false; + /** @type {number} */ + var w = txtInf.wMax || areainf.w || 0; + if(w){ + txts = _this.breakTextIntoLines(txtInf.text, w, calcTextWidth, txtInf.noBreaks); + }else{ + txts = PDFLib.lineSplit(PDFLib.cleanText(txtInf.text)); + needW = true; + } + + /** @type {Array} */ + var wids = []; + /** @type {Array} */ + var enctxts = txts.map(function(t2){ + /** @type {number} */ + var cw = 0; + t2 = t2.trim(); + if(needW || txtInf.align){ + cw = calcTextWidth(t2); + wids.push(cw); + } + if(needW){ + w = Math.max(w, cw); + } + return font.encodeText(t2); + }); + if(areainf.w){ + areainf.wDraw = areainf.w; + }else{ + areainf.wDraw = w + opts["x"]; + } + if(areainf.h){ + areainf.hDraw = areainf.h; + }else{ + areainf.hDraw = txts.length * opts["lineHeight"] + opts["y"]; + } + + /** @type {Array} */ + var ret = []; + /** @type {Array} */ + var pos = null; + if(txtInf.align){ + wids.forEach(function(w1, i1){ + /** @type {number} */ + var x = opts["x"]; + if(txtInf.align == 1){ + // center alignment + x = (w - w1) / 2 + opts["x"]; + }else{ + // right alignment + x = (w - w1) + opts["x"]; + } + ret = ret.concat(PDFLib.drawLinesOfText([enctxts[i1]], _this.calcTextPos(opts, areainf.wDraw, areainf.hDraw, i1, x))); + }); + }else{ + ret = PDFLib.drawLinesOfText(enctxts, _this.calcTextPos(opts, areainf.wDraw, areainf.hDraw)); + } + + return ret; + } + + /** + * Convert hex string to Color + * + * @private + * @param {string=} hex + * @return {PDFLib.Color} + */ + hexToColor(hex){ + /** @type {Array} */ + var rgb = [0,0,0]; + if(hex){ + if(hex.charAt(0) == "#"){ + hex = hex.substring(1); + } + if(hex.length == 3){ + rgb[0] = parseInt(hex.charAt(0)+hex.charAt(0), 16); + rgb[1] = parseInt(hex.charAt(1)+hex.charAt(1), 16); + rgb[2] = parseInt(hex.charAt(2)+hex.charAt(2), 16); + }else if(hex.length == 6){ + rgb[0] = parseInt(hex.substring(0, 2), 16); + rgb[1] = parseInt(hex.substring(2, 4), 16); + rgb[2] = parseInt(hex.substring(4, 6), 16); + }else{ + throw new Error("The hex string is not a valid color."); + } + } + return PDFLib.rgb(rgb[0]/255, rgb[1]/255, rgb[2]/255); + } + + /** + * @private + * @param {string} text + * @param {number} maxWidth + * @param {function(string):number} computeWidthOfText + * @param {string=} noBreakRx + * @return {Array} + */ + breakTextIntoLines(text, maxWidth, computeWidthOfText, noBreakRx){ + /** @type {string} */ + var ctxt = PDFLib.cleanText(text); + /** @type {string} */ + var currLine = ""; + /** @type {number} */ + var currWidth = 0; + /** @type {Array} */ + var lines = []; + + var nwRegexp = new RegExp(noBreakRx || "[A-Za-z0-9]"); + /** @type {Array} */ + var words = []; + /** @type {number} */ + var idx = 0; + /** @type {number} */ + var len = ctxt.length; + while(idx < len){ + /** @type {string} */ + var c = ctxt.charAt(idx); + if(nwRegexp.test(c)){ + currLine += c; + }else{ + if(currLine)words.push(currLine); + currLine = ""; + words.push(c); + } + if(c == "\r" && idx + 1 < len && ctxt.charAt(idx + 1) == "\n"){ + idx++; + } + idx++; + } + if(currLine)words.push(currLine); + + currLine = ""; + idx = 0; + len = words.length; + while(idx < len){ + /** @type {string} */ + var word = words[idx]; + if(PDFLib.isNewlineChar(word)){ + lines.push(currLine); + currLine = ""; + currWidth = 0; + }else{ + /** @type {number} */ + var width = computeWidthOfText(word); + if(currWidth + width > maxWidth){ + lines.push(currLine); + currLine = ""; + currWidth = 0; + } + currLine += word; + currWidth += width; + } + idx++; + } + if(currLine)lines.push(currLine); + + return lines; + } + + /** + * @private + * @param {DrawLinesOfTextOptions} opts + * @param {number=} w // It must not be undefined, but need to suppress warning of mismatch + * @param {number=} h // It must not be undefined, but need to suppress warning of mismatch + * @param {number=} idx // line index + * @param {number=} aX // x of alignment + * @return {DrawLinesOfTextOptions} // A copy of opts, and x, y are calculated. + */ + calcTextPos(opts, w, h, idx, aX){ + var newopts = /** @type {DrawLinesOfTextOptions} */(Object.assign({}, opts)); + /** @type {number} */ + var i = idx || 0; + /** @type {number} */ + var x = aX || opts["x"]; + switch(opts["rotate"].angle){ + case 0: + newopts["x"] = x; + newopts["y"] = h - opts["lineHeight"] - opts["y"] - (opts["lineHeight"] * i); + break; case 90: - ret["x"] = areainf.w; + newopts["x"] = opts["lineHeight"] + opts["y"] + (opts["lineHeight"] * i); + newopts["y"] = x; break; case 180: case -180: - ret["x"] = areainf.w; - ret["y"] = areainf.h; + newopts["x"] = w - x; + newopts["y"] = opts["lineHeight"] + opts["y"] + (opts["lineHeight"] * i); break; case 270: case -90: - ret["y"] = areainf.h; + newopts["x"] = h - opts["lineHeight"] - opts["y"] - (opts["lineHeight"] * i); + newopts["y"] = w - x; break; } - return ret; + return newopts; } }; diff --git a/test4node.js b/test4node.js index a5ba99c..c9336fa 100644 --- a/test4node.js +++ b/test4node.js @@ -10,9 +10,11 @@ const workpath = "test/"; * @param {string} ps * @param {number} perm * @param {string=} imgPath + * @param {string=} txt + * @param {string=} fontPath * @return {Promise} output path */ -async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath){ +async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath, txt, fontPath){ /** @type {Buffer} */ var pdf = m_fs.readFileSync(pdfPath); /** @type {Buffer} */ @@ -21,6 +23,8 @@ async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath){ var img = null; /** @type {string} */ var imgType = ""; + /** @type {Buffer} */ + var font = null; if(perm == 1){ console.log("\nTest signing pdf with full protection. (permission 1 and password encryption)"); @@ -32,6 +36,9 @@ async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath){ img = m_fs.readFileSync(imgPath); imgType = m_path.extname(imgPath).slice(1); } + if(fontPath){ + font = m_fs.readFileSync(fontPath); + } /** @type {SignOption} */ var sopt = { p12cert: pfx, @@ -44,16 +51,31 @@ async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath){ ltv: 1, debug: true, }; - if(img){ + if(img || txt){ sopt.drawinf = { area: { x: 25, // left - y: 150, // top - w: 60, - h: 60, + y: 50, // top + w: txt ? undefined : 60, + h: txt ? undefined : 100, }, - imgData: img, - imgType: imgType, + pageidx: "-", + imgInfo: img ? { + imgData: img, + imgType: imgType, + } : undefined, + textInfo: txt ? { + text: txt, + fontData: font, + color: "00f0f1", + lineHeight: 20, + size: 16, + align: 1, + wMax: 80, + yOffset: 10, + xOffset: 20, + noBreaks: "[あいうえおA-Za-z0-9]", + } : undefined, }; } @@ -73,7 +95,7 @@ async function sign_protect(pdfPath, pfxPath, ps, perm, imgPath){ var u8dat = await ser.sign(pdf, eopt); if(u8dat){ /** @type {string} */ - var outPath = m_path.join(__dirname, workpath+"test_perm"+perm+".pdf"); + var outPath = m_path.join(__dirname, workpath+"test_perm"+perm+m_path.basename(pdfPath)); m_fs.writeFileSync(outPath, u8dat); console.log("Output file: " + outPath); } @@ -109,15 +131,20 @@ async function addtsa(pdfPath){ return outPath; } -async function main(){ +/** + * @param {number} angle + */ +async function main1(angle){ /** @type {string} */ - var pdfPath = m_path.join(__dirname, workpath+"_test.pdf"); + var pdfPath = m_path.join(__dirname, workpath+"_test"+(angle ? "_"+angle : "")+".pdf"); /** @type {string} */ var pfxPath = m_path.join(__dirname, workpath+"_test.pfx"); /** @type {string} */ var ps = ""; /** @type {string} */ var imgPath = m_path.join(__dirname, workpath+"_test.png"); + /** @type {string} */ + var fontPath = m_path.join(__dirname, workpath+"_test.ttf"); if(process.argv.length > 3){ pfxPath = process.argv[2]; @@ -132,8 +159,8 @@ async function main(){ } if(pfxPath){ - await sign_protect(pdfPath, pfxPath, ps, 1, imgPath); - pdfPath = await sign_protect(pdfPath, pfxPath, ps, 2, imgPath); + 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 addtsa(pdfPath); }else{ await addtsa(pdfPath); @@ -142,4 +169,14 @@ async function main(){ console.log("Done"); } +async function main(){ + /** @type {Array} */ + var arr = [0, 90, 180, 270]; + /** @type {number} */ + for(var i=0; i