diff --git a/.gitignore b/.gitignore
index 6704566..4ad1f15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+test
+
# Logs
logs
*.log
diff --git a/README.md b/README.md
index ff50ef9..9109c37 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,106 @@
-# zgapdfsigner
-A javascript tool to sign a pdf in web browser.
+# ZgaPdfSigner
+A javascript tool to sign a pdf in web browser.
+
+PS: __ZGA__ is the abbreviation of my father's name.
+And I use this name to hope the merits from this application will be dedicated to my parents.
+
+## The Dependencies
+
+* [pdf-lib](https://pdf-lib.js.org/)
+* [node-forge](https://github.com/digitalbazaar/forge)
+
+## How to use this tool
+
+Just import the dependencies and this tool.
+```html
+
+
+
+```
+
+## Let's sign
+
+Sign with an invisible signature.
+
+```js
+/**
+ * @param {ArrayBuffer} pdf
+ * @param {ArrayBuffer} cert
+ * @param {string} pwd
+ * @return {Blob}
+ */
+async function sign1(pdf, cert, pwd){
+ /** @type {SignOption} */
+ var sopt = {
+ p12cert: cert,
+ pwd: pwd,
+ };
+ var signer = new PdfSigner(sopt);
+ return await signer.sign(pdf);
+}
+```
+
+Sign with a visible signature of a picture.
+
+```js
+/**
+ * @param {ArrayBuffer} pdf
+ * @param {ArrayBuffer} cert
+ * @param {string} pwd
+ * @param {ArrayBuffer} imgdat
+ * @param {string} imgtyp
+ * @return {Blob}
+ */
+async function sign2(pdf, cert, pwd, imgdat, imgtyp){
+ /** @type {SignOption} */
+ var sopt = {
+ p12cert: cert,
+ pwd: pwd,
+ drawinf: {
+ area: {
+ x: 25, // left
+ y: 150, // top
+ w: 60, // width
+ h: 60, // height
+ },
+ imgData: imgdat,
+ imgType: imgtyp,
+ },
+ };
+ var signer = new PdfSigner(sopt);
+ return await signer.sign(pdf);
+}
+```
+
+Sign with a visible signature of drawing a text.
+
+```js
+//TODO
+```
+
+## Detail of SignOption
+
+* __p12cert__: string :point_right: A raw data of the certificate
+* __pwd__: string :point_right: The passphrase of the certificate
+* __reason__: string :point_right: (Optional) The reason for signing
+* __location__: string :point_right: (Optional) Your location
+* __contact__: string :point_right: (Optional) Your contact information
+* __signdate__: Date :point_right: (Optional) The date and time of signing
+* __signame__: string :point_right: (Optional) The name of the signature
+* __drawinf__: SignDrawInfo :point_right: (Optional) Visible signature's information
+ * __area__: SignAreaInfo :point_right: The signature's drawing area
+ * __x__: number :point_right: Distance from left
+ * __y__: number :point_right: Distance from top
+ * __w__: number :point_right: Width
+ * __h__: number :point_right: Height
+ * __pageidx__: number :point_right: (Optional) The page index for drawing the signature
+ * __imgData__: Uint8Array|ArrayBuffer|string :point_right: (Optional) The image's data
+ * __imgType__: string :point_right: (Optional) The image's type, __only support jpg and png__
+ * __text__: string :point_right: (Optional) A text drawing on signature, __not implemented yet__
+ * __fontData__: PDFLib.StandardFonts|Uint8Array|ArrayBuffer|string :point_right: (Optional) The font's data for drawing text, __not implemented yet__
+
+
+## License
+
+This tool is available under the
+[MIT license](https://opensource.org/licenses/MIT).
diff --git a/test.html b/test.html
new file mode 100644
index 0000000..aaecd2b
--- /dev/null
+++ b/test.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+ Test for ZgaPdfSigner
+
+
+
+
+
+
+
+
+
+
+dummy
+
+
+
diff --git a/zgapdfsigner.js b/zgapdfsigner.js
new file mode 100644
index 0000000..50c6272
--- /dev/null
+++ b/zgapdfsigner.js
@@ -0,0 +1,507 @@
+/**
+ * the base point of x, y is top left corner.
+ * @typedef
+ * {{
+ * x: number,
+ * y: number,
+ * w: number,
+ * h: number,
+ * }}
+ */
+var SignAreaInfo;
+/**
+ * @typedef
+ * {{
+ * area: SignAreaInfo,
+ * pageidx: (number|undefined),
+ * imgData: (Uint8Array|ArrayBuffer|string|undefined),
+ * imgType: (string|undefined),
+ * text: (string|undefined),
+ * fontData: (PDFLib.StandardFonts|Uint8Array|ArrayBuffer|string|undefined),
+ * img: (PDFLib.PDFImage|undefined),
+ * font: (PDFLib.PDFFont|undefined),
+ * }}
+ */
+var SignDrawInfo;
+/**
+ * @typedef
+ * {{
+ * p12cert: string,
+ * pwd: string,
+ * reason: (string|undefined),
+ * location: (string|undefined),
+ * contact: (string|undefined),
+ * signdate: (Date|undefined),
+ * signame: (string|undefined),
+ * drawinf: (SignDrawInfo|undefined),
+ * }}
+ */
+var SignOption;
+
+class PdfSigner {
+ /**
+ * @constructor
+ * @param {SignOption} signopt
+ */
+ constructor(signopt){
+ /** @private @type {SignOption} */
+ this.opt = signopt;
+ }
+
+ /**
+ * @public
+ * @param {PDFLib.PDFDocument|Uint8Array|ArrayBuffer|string} pdf
+ * @return {Blob}
+ */
+ async sign(pdf){
+ /** @type {PDFLib.PDFDocument} */
+ var pdfdoc = null;
+ if(pdf.addPage){
+ pdfdoc = pdf;
+ }else{
+ pdfdoc = await PDFLib.PDFDocument.load(pdf);
+ }
+
+ if(this.opt.drawinf && this.opt.drawinf.imgData && !this.opt.drawinf.img){
+ if(this.opt.drawinf.imgType == "png"){
+ this.opt.drawinf.img = await pdfdoc.embedPng(this.opt.drawinf.imgData);
+ }else if(this.opt.drawinf.imgType == "jpg"){
+ this.opt.drawinf.img = await pdfdoc.embedJpg(this.opt.drawinf.imgData);
+ }else{
+ throw new Error("Unkown image type. " + this.opt.drawinf.imgType);
+ }
+ }
+
+ this.addSignHolder(pdfdoc);
+ var uarr = await pdfdoc.save({"useObjectStreams": false});
+ // return new Blob([uarr], {"type" : "application/pdf"});
+ var pdfstr = String.fromCharCode.apply(null, uarr);
+
+ return this.signPdf(pdfstr);
+ }
+
+ /**
+ * @private
+ * @param {PDFLib.PDFDocument} pdfdoc
+ */
+ addSignHolder(pdfdoc){
+ /** @const {string} */
+ const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
+ /** @const {number} */
+ const SIGNATURE_LENGTH = 3322;
+
+ /** @const {VisualSignature} */
+ const visign = new VisualSignature(this.opt.drawinf);
+ /** @const {PDFLib.PDFRef} */
+ const strmRef = visign.createStream(pdfdoc, this.opt.signame);
+ /** @const {PDFLib.PDFPage} */
+ const page = pdfdoc.getPages()[visign.getPageIndex()];
+
+ if(!this.opt.signdate){
+ this.opt.signdate = new Date();
+ }
+
+ /** @type {PDFLib.PDFArray} */
+ var bytrng = new PDFLib.PDFArray(pdfdoc.context);
+ bytrng.push(PDFLib.PDFNumber.of(0));
+ bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER));
+ bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER));
+ bytrng.push(PDFLib.PDFName.of(DEFAULT_BYTE_RANGE_PLACEHOLDER));
+
+ /** @type {Object} */
+ var signObj = {
+ "Type": "Sig",
+ "Filter": "Adobe.PPKLite",
+ "SubFilter": "adbe.pkcs7.detached",
+ "ByteRange": bytrng,
+ "Contents": PDFLib.PDFHexString.of("0".repeat(SIGNATURE_LENGTH)),
+ "M": PDFLib.PDFString.fromDate(this.opt.signdate),
+ "Prop_Build": pdfdoc.context.obj({
+ "App": pdfdoc.context.obj({
+ "Name": "ZbPdfSinger",
+ }),
+ }),
+ };
+ if(this.opt.reason){
+ signObj["Reason"] = PDFLib.PDFString.of(this.opt.reason);
+ }
+ if(this.opt.location){
+ signObj["Location"] = PDFLib.PDFString.of(this.opt.location);
+ }
+ if(this.opt.contact){
+ signObj["ContactInfo"] = PDFLib.PDFString.of(this.opt.contact);
+ }
+ var signatureDictRef = pdfdoc.context.register(pdfdoc.context.obj(signObj));
+
+ /** @type {Object} */
+ var widgetObj = {
+ "Type": "Annot",
+ "Subtype": "Widget",
+ "FT": "Sig",
+ "Rect": visign.getSignRect(),
+ "V": signatureDictRef,
+ "T": PDFLib.PDFString.of(this.opt.signame ? this.opt.signame : "Signature1"),
+ "F": 132,
+ "P": page.ref,
+ };
+ if(strmRef){
+ widgetObj["AP"] = pdfdoc.context.obj({
+ "N": strmRef,
+ });
+ }
+ var widgetDictRef = pdfdoc.context.register(pdfdoc.context.obj(widgetObj));
+
+ // Add our signature widget to the page
+ page.node.set(PDFLib.PDFName.of("Annots"), pdfdoc.context.obj([widgetDictRef]));
+
+ // Create an AcroForm object containing our signature widget
+ pdfdoc.catalog.set(
+ PDFLib.PDFName.of("AcroForm"),
+ pdfdoc.context.obj({
+ "SigFlags": 3,
+ "Fields": [widgetDictRef],
+ }),
+ );
+ }
+
+ /**
+ * @private
+ * @param {string} pdfstr
+ * @return {Blob}
+ */
+ signPdf(pdfstr){
+ if(!this.opt.signdate){
+ this.opt.signdate = new Date();
+ }
+
+ // Finds ByteRange information within a given PDF Buffer if one exists
+ var byteRangeStrings = pdfstr.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g);
+ var byteRangePlaceholder = byteRangeStrings.find(function(a_str){
+ return a_str.includes("/**********");
+ });
+ if(!byteRangePlaceholder){
+ throw new Error("no signature placeholder");
+ }
+ var byteRangePos = pdfstr.indexOf(byteRangePlaceholder);
+ var byteRangeEnd = byteRangePos + byteRangePlaceholder.length;
+ var contentsTagPos = pdfstr.indexOf('/Contents ', byteRangeEnd);
+ var placeholderPos = pdfstr.indexOf('<', contentsTagPos);
+ var placeholderEnd = pdfstr.indexOf('>', placeholderPos);
+ var placeholderLengthWithBrackets = placeholderEnd + 1 - placeholderPos;
+ var placeholderLength = placeholderLengthWithBrackets - 2;
+ var byteRange = [0, 0, 0, 0];
+ byteRange[1] = placeholderPos;
+ byteRange[2] = byteRange[1] + placeholderLengthWithBrackets;
+ byteRange[3] = pdfstr.length - byteRange[2];
+ var actualByteRange = "/ByteRange [" + byteRange.join(" ") +"]";
+ actualByteRange += ' '.repeat(byteRangePlaceholder.length - actualByteRange.length);
+ // Replace the /ByteRange placeholder with the actual ByteRange
+ pdfstr = pdfstr.slice(0, byteRangePos) + actualByteRange + pdfstr.slice(byteRangeEnd);
+ // Remove the placeholder signature
+ pdfstr = pdfstr.slice(0, byteRange[1]) + pdfstr.slice(byteRange[2], byteRange[2] + byteRange[3]);
+
+ // Convert Buffer P12 to a forge implementation.
+ var p12Asn1 = forge.asn1.fromDer(this.opt.p12cert);
+ var p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, true, this.opt.pwd);
+
+ // Extract safe bags by type.
+ // We will need all the certificates and the private key.
+ var certBags = p12.getBags({
+ "bagType": forge.pki.oids.certBag,
+ })[forge.pki.oids.certBag];
+ var keyBags = p12.getBags({
+ "bagType": forge.pki.oids.pkcs8ShroudedKeyBag,
+ })[forge.pki.oids.pkcs8ShroudedKeyBag];
+
+ var privateKey = keyBags[0].key;
+ // Here comes the actual PKCS#7 signing.
+ var p7 = forge.pkcs7.createSignedData();
+ // Start off by setting the content.
+ p7.content = forge.util.createBuffer(pdfstr);
+
+ // Then add all the certificates (-cacerts & -clcerts)
+ // Keep track of the last found client certificate.
+ // This will be the public key that will be bundled in the signature.
+ var cert = null;
+ Object.keys(certBags).forEach(function(a_ele){
+ var a_cert = certBags[a_ele].cert;
+
+ p7.addCertificate(a_cert);
+
+ // Try to find the certificate that matches the private key.
+ if(privateKey.n.compareTo(a_cert.publicKey.n) === 0
+ && privateKey.e.compareTo(a_cert.publicKey.e) === 0){
+ cert = a_cert;
+ }
+ });
+ if(!cert){
+ throw new Error("Failed to find a certificate.");
+ }
+
+ // Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects.
+ p7.addSigner({
+ key: privateKey,
+ certificate: cert,
+ digestAlgorithm: forge.pki.oids.sha256,
+ authenticatedAttributes: [
+ {
+ "type": forge.pki.oids.contentType,
+ "value": forge.pki.oids.data,
+ }, {
+ "type": forge.pki.oids.messageDigest,
+ }, {
+ "type": forge.pki.oids.signingTime,
+ "value": this.opt.signdate,
+ },
+ ],
+ });
+
+ // Sign in detached mode.
+ p7.sign({"detached": true});
+ // Check if the PDF has a good enough placeholder to fit the signature.
+ var sighex = forge.asn1.toDer(p7.toAsn1()).toHex();
+ // placeholderLength represents the length of the HEXified symbols but we're
+ // checking the actual lengths.
+ if(sighex.length > placeholderLength){
+ throw new Error("Signature is too big.");
+ }else{
+ // Pad the signature with zeroes so the it is the same length as the placeholder
+ sighex += "0".repeat(placeholderLength - sighex.length);
+ }
+ // Place it in the document.
+ pdfstr = pdfstr.slice(0, byteRange[1]) + "<" + sighex + ">" + pdfstr.slice(byteRange[1]);
+
+ return this.rawToBlob(pdfstr, "application/pdf");
+ }
+
+ /**
+ * @private
+ * @param {string} raw
+ * @param {string=} typ
+ * @return {Blob}
+ */
+ rawToBlob(raw, typ){
+ var arr = new Uint8Array(raw.length);
+ for(var i=0; i} */
+ this.rect = [0, 0, 0, 0];
+ /** @private @type {SignDrawInfo} */
+ this.drawinf = null;
+
+ if(drawinf){
+ this.drawinf = drawinf;
+ if(this.drawinf.pageidx){
+ this.pgidx = this.drawinf.pageidx;
+ }
+ }
+ }
+
+ /**
+ * @public
+ * @return {number}
+ */
+ getPageIndex(){
+ return this.pgidx;
+ }
+
+ /**
+ * @public
+ * @return {Array}
+ */
+ getSignRect(){
+ return this.rect;
+ }
+
+ /**
+ * @public
+ * @param {PDFLib.PDFDocument} pdfdoc
+ * @param {string=} signame
+ * @return {PDFLib.PDFRef} The unique reference assigned to the signature stream
+ */
+ createStream(pdfdoc, signame){
+ if(!this.drawinf){
+ return null;
+ }else if(!(this.drawinf.img || (this.drawinf.font && this.drawinf.font))){
+ return null;
+ }
+
+ /** @type {Array} */
+ var pages = pdfdoc.getPages();
+ /** @type {PDFLib.PDFPage} */
+ var page = null;
+ if(this.pgidx < pages.length){
+ page = pages[this.pgidx];
+ }else{
+ throw new Error("Page index is overflow to pdf pages.");
+ }
+ var pgrot = page.getRotation();
+ pgrot.angle = PDFLib.toDegrees(pgrot) % 360;
+ pgrot.type = PDFLib.RotationTypes.Degrees;
+ var pgsz = page.getSize();
+ var areainf = this.calcAreaInf(pgsz, pgrot.angle, this.drawinf.area);
+
+ // resources object
+ var rscObj = {};
+ /** @type {Array} */
+ var sigOprs = [];
+ var imgName = signame ? signame.concat("Img") : "SigImg";
+ var fontName = signame ? signame.concat("Font") : "SigFont";
+ if(this.drawinf.img){
+ // Get scaled image size
+ var imgsz = this.drawinf.img.size();
+ var tmp = areainf.w * imgsz.height / imgsz.width;
+ if(tmp <= areainf.h){
+ areainf.h = tmp;
+ }else{
+ areainf.w = areainf.h * imgsz.width / imgsz.height;
+ }
+
+ rscObj["XObject"] = {
+ [imgName]: this.drawinf.img.ref,
+ };
+ sigOprs = sigOprs.concat(PDFLib.drawImage(imgName, this.calcDrawImgInf(pgrot, areainf)));
+ }
+ if(this.drawinf.font){
+ rscObj["Font"] = {
+ [fontName]: this.drawinf.font.ref,
+ };
+ }
+
+ this.rect = this.calcRect(pgrot.angle, areainf);
+
+ var frmDict = pdfdoc.context.obj({
+ "Type": "XObject",
+ "Subtype": "Form",
+ "FormType": 1,
+ "BBox": [0, 0, areainf.w, areainf.h],
+ "Resources": rscObj,
+ });
+ var strm = PDFLib.PDFContentStream.of(frmDict, sigOprs, false);
+ return pdfdoc.context.register(strm);
+ }
+
+ /**
+ * Calculate area informations for drawing signature after rotate
+ *
+ * @private
+ * @param {Object} pgsz // { width, height }
+ * @param {number} angle
+ * @param {SignAreaInfo} visinf
+ * @return {SignAreaInfo}
+ */
+ calcAreaInf(pgsz, angle, visinf){
+ var ret = Object.assign({}, visinf);
+ // Calculate position after rotate
+ switch(angle){
+ case 90:
+ ret.w = visinf.h;
+ ret.h = visinf.w;
+ ret.x = visinf.y;
+ ret.y = visinf.x;
+ break;
+ case 180:
+ case -180:
+ ret.x = pgsz.width - visinf.x;
+ break;
+ case 270:
+ case -90:
+ ret.w = visinf.h;
+ ret.h = visinf.w;
+ ret.x = pgsz.width - visinf.y;
+ ret.y = pgsz.height - visinf.x;
+ break;
+ default:
+ ret.y = pgsz.height - visinf.y;
+ }
+ return ret;
+ }
+
+ /**
+ * @private
+ * @param {number} angle
+ * @param {SignAreaInfo} areainf // { x, y, w, h }
+ * @return {Array}
+ */
+ calcRect(angle, areainf){
+ var rect = [0, 0, 0, 0];
+ rect[0] = areainf.x;
+ rect[1] = areainf.y;
+ switch(angle){
+ case 90:
+ rect[2] = areainf.x - areainf.h;
+ rect[3] = areainf.y + areainf.w;
+ break;
+ case 180:
+ case -180:
+ rect[2] = areainf.x - areainf.w;
+ rect[3] = areainf.y - areainf.h;
+ break;
+ case 270:
+ case -90:
+ rect[2] = areainf.x + areainf.h;
+ rect[3] = areainf.y - areainf.w;
+ break;
+ default:
+ rect[2] = areainf.x + areainf.w;
+ rect[3] = areainf.y + areainf.h;
+ }
+ return rect;
+ }
+
+ /**
+ * Calculate informations for drawing image after rotate
+ *
+ * @private
+ * @param {PDFLib.Rotation} rot
+ * @param {SignAreaInfo} areainf // { x, y, w, h }
+ * @return {Object} // { x, y, width, height, rotate, xSkew, ySkew }
+ */
+ calcDrawImgInf(rot, areainf){
+ var ret = {
+ "x": 0,
+ "y": 0,
+ "width": areainf.w,
+ "height": areainf.h,
+ "rotate": rot,
+ "xSkew": PDFLib.degrees(0),
+ "ySkew": PDFLib.degrees(0),
+ };
+ switch(rot.angle){
+ case 90:
+ ret["x"] = areainf.w;
+ ret["width"] = areainf.h;
+ ret["height"] = areainf.w;
+ break;
+ case 180:
+ case -180:
+ ret["x"] = areainf.w;
+ ret["y"] = areainf.h;
+ break;
+ case 270:
+ case -90:
+ ret["y"] = areainf.h;
+ ret["width"] = areainf.h;
+ ret["height"] = areainf.w;
+ break;
+ }
+ return ret;
+ }
+}
\ No newline at end of file