parent
6b0d906fb5
commit
1583e5f70f
|
@ -1,3 +1,5 @@
|
||||||
|
test
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|
108
README.md
108
README.md
|
@ -1,2 +1,106 @@
|
||||||
# zgapdfsigner
|
# ZgaPdfSigner
|
||||||
A javascript tool to sign a pdf in web browser.
|
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
|
||||||
|
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://unpkg.com/node-forge/dist/forge.min.js" type="text/javascript"></script>
|
||||||
|
<script src="zgapdfsigner.js" type="text/javascript"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title> Test for ZgaPdfSigner </title>
|
||||||
|
<script src="https://unpkg.com/node-forge/dist/forge.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://unpkg.com/pdf-lib/dist/pdf-lib.min.js" type="text/javascript"></script>
|
||||||
|
<script src="zgapdfsigner.js" type="text/javascript"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function test(){
|
||||||
|
var img = document.getElementById("img").files[0];
|
||||||
|
if(img){
|
||||||
|
readAsArrayBuffer(img, function(a_buff){
|
||||||
|
testSign(a_buff, getFilExt(img));
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
testSign();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getFilExt(f){
|
||||||
|
var n = f.name;
|
||||||
|
var i = n.lastIndexOf(".");
|
||||||
|
if(i >= 0){
|
||||||
|
return n.slice(i + 1);
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readAsArrayBuffer(f, func){
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(a_evt){
|
||||||
|
func(a_evt.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(f);
|
||||||
|
}
|
||||||
|
function testSign(imgdat, imgtyp){
|
||||||
|
var fil = document.getElementById("fff").files[0];
|
||||||
|
var kf = document.getElementById("kkk").files[0];
|
||||||
|
readAsArrayBuffer(fil, function(a_pdf){
|
||||||
|
readAsArrayBuffer(kf, async function(b_kfbuf){
|
||||||
|
var b_kfdat = String.fromCharCode.apply(null, new Uint8Array(b_kfbuf));
|
||||||
|
var b_opt = {
|
||||||
|
p12cert: b_kfdat,
|
||||||
|
pwd: document.getElementById("pwd").value,
|
||||||
|
};
|
||||||
|
if(imgdat){
|
||||||
|
b_opt.drawinf = {
|
||||||
|
area: {
|
||||||
|
x: 25, // left
|
||||||
|
y: 150, // top
|
||||||
|
w: 60,
|
||||||
|
h: 60,
|
||||||
|
},
|
||||||
|
imgData: imgdat,
|
||||||
|
imgType: imgtyp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var b_signer = new PdfSigner(b_opt);
|
||||||
|
var b_blob = await b_signer.sign(a_pdf);
|
||||||
|
document.getElementById("download").download = "zzzs3.pdf";
|
||||||
|
document.getElementById("download").href = window.URL.createObjectURL(b_blob);
|
||||||
|
document.getElementById("download").click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<label>pdf </label><input type="file" id="fff" /><br />
|
||||||
|
<label>certificate </label><input type="file" id="kkk" /><br />
|
||||||
|
<label>passphrase </label><input type="password" id="pwd" /><br />
|
||||||
|
<label>signature image </label><input type="file" id="img" /><br />
|
||||||
|
<a id="download" href="#" download="" style="display: none;" target="_blank">dummy</a>
|
||||||
|
<input type="button" value="test" onclick="test()" />
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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<string, *>} */
|
||||||
|
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<string, *>} */
|
||||||
|
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<raw.length; i++){
|
||||||
|
arr[i] = raw.charCodeAt(i);
|
||||||
|
}
|
||||||
|
var opt = null;
|
||||||
|
if(typ){
|
||||||
|
opt = {"type" : typ};
|
||||||
|
}
|
||||||
|
return new Blob([arr], opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VisualSignature {
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {SignDrawInfo=} drawinf
|
||||||
|
*/
|
||||||
|
constructor(drawinf){
|
||||||
|
/** @private @type {number} */
|
||||||
|
this.pgidx = 0;
|
||||||
|
/** @private @type {Array<number>} */
|
||||||
|
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<number>}
|
||||||
|
*/
|
||||||
|
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<PDFLib.PDFPage>} */
|
||||||
|
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<PDFLib.PDFOperator>} */
|
||||||
|
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<string, number>} 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<number>}
|
||||||
|
*/
|
||||||
|
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<string, *>} // { 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue