initial commit

This commit is contained in:
nxglabs
2023-10-22 01:37:25 +05:30
parent 80327652c3
commit 97fd28697a
182 changed files with 55033 additions and 122 deletions
@@ -0,0 +1,18 @@
/*
PDFAbstractReference by Devon Govett used below.
The class is part of pdfkit. See https://github.com/foliojs/pdfkit
LICENSE: MIT. Included in this folder.
Modifications may have been applied for the purposes of node-signpdf.
*/
/*
PDFAbstractReference - abstract class for PDF reference
*/
class PDFAbstractReference {
toString() {
throw new Error('Must be implemented by subclasses');
}
}
export default PDFAbstractReference;
@@ -0,0 +1,17 @@
import PDFAbstractReference from './PDFAbstractReference.js';
class PDFKitReferenceMock extends PDFAbstractReference {
constructor(index, additionalData = undefined) {
super();
this.index = index;
if (typeof additionalData !== 'undefined') {
Object.assign(this, additionalData);
}
}
toString() {
return `${this.index} 0 R`;
}
}
export default PDFKitReferenceMock;
@@ -0,0 +1,19 @@
export const ERROR_TYPE_UNKNOWN = 1;
export const ERROR_TYPE_INPUT = 2;
export const ERROR_TYPE_PARSE = 3;
export const ERROR_VERIFY_SIGNATURE = 4;
class SignPdfError extends Error {
constructor(msg, type = ERROR_TYPE_UNKNOWN) {
super(msg);
this.type = type;
}
}
// Shorthand
SignPdfError.TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN;
SignPdfError.TYPE_INPUT = ERROR_TYPE_INPUT;
SignPdfError.TYPE_PARSE = ERROR_TYPE_PARSE;
SignPdfError.VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE;
export default SignPdfError;
@@ -0,0 +1,6 @@
export const DEFAULT_SIGNATURE_LENGTH = 8192;
export const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********';
export const SUBFILTER_ADOBE_PKCS7_DETACHED = 'adbe.pkcs7.detached';
export const SUBFILTER_ADOBE_PKCS7_SHA1 = 'adbe.pkcs7.sha1';
export const SUBFILTER_ADOBE_X509_SHA1 = 'adbe.x509.rsa.sha1';
export const SUBFILTER_ETSI_CADES_DETACHED = 'ETSI.CAdES.detached';
@@ -0,0 +1,40 @@
import findObject from './findObject.js';
import getIndexFromRef from './getIndexFromRef.js';
const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => {
const pagesDictionary = findObject(pdf, info.xref, pagesRef).toString();
// Extend page dictionary with newly created annotations
let annotsStart; let annotsEnd; let
annots;
annotsStart = pagesDictionary.indexOf('/Annots');
if (annotsStart > -1) {
annotsEnd = pagesDictionary.indexOf(']', annotsStart);
annots = pagesDictionary.substr(annotsStart, annotsEnd + 1 - annotsStart);
annots = annots.substr(0, annots.length - 1); // remove the trailing ]
} else {
annotsStart = pagesDictionary.length;
annotsEnd = pagesDictionary.length;
annots = '/Annots [';
}
const pagesDictionaryIndex = getIndexFromRef(info.xref, pagesRef);
const widgetValue = widget.toString();
annots = `${annots} ${widgetValue}]`; // add the trailing ] back
const preAnnots = pagesDictionary.substr(0, annotsStart);
let postAnnots = '';
if (pagesDictionary.length > annotsEnd) {
postAnnots = pagesDictionary.substr(annotsEnd + 1);
}
return Buffer.concat([
Buffer.from(`${pagesDictionaryIndex} 0 obj\n`),
Buffer.from('<<\n'),
Buffer.from(`${preAnnots + annots + postAnnots}\n`),
Buffer.from('\n>>\nendobj\n'),
]);
};
export default createBufferPageWithAnnotation;
@@ -0,0 +1,15 @@
import getIndexFromRef from './getIndexFromRef.js';
const createBufferRootWithAcroform = (pdf, info, form) => {
const rootIndex = getIndexFromRef(info.xref, info.rootRef);
return Buffer.concat([
Buffer.from(`${rootIndex} 0 obj\n`),
Buffer.from('<<\n'),
Buffer.from(`${info.root}\n`),
Buffer.from(`/AcroForm ${form}`),
Buffer.from('\n>>\nendobj\n'),
]);
};
export default createBufferRootWithAcroform;
@@ -0,0 +1,28 @@
const createBufferTrailer = (pdf, info, addedReferences) => {
let rows = [];
rows[0] = '0000000000 65535 f '; // info.xref.tableRows[0];
addedReferences.forEach((offset, index) => {
const paddedOffset = (`0000000000${offset}`).slice(-10);
rows[index + 1] = `${index} 1\n${paddedOffset} 00000 n `;
});
rows = rows.filter((row) => row !== undefined);
return Buffer.concat([
Buffer.from('xref\n'),
Buffer.from(`${info.xref.startingIndex} 1\n`),
Buffer.from(rows.join('\n')),
Buffer.from('\ntrailer\n'),
Buffer.from('<<\n'),
Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`),
Buffer.from(`/Root ${info.rootRef}\n`),
Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ''),
Buffer.from(`/Prev ${info.xRefPosition}\n`),
Buffer.from('>>\n'),
Buffer.from('startxref\n'),
Buffer.from(`${pdf.length}\n`),
Buffer.from('%%EOF'),
]);
};
export default createBufferTrailer;
@@ -0,0 +1,21 @@
import getIndexFromRef from './getIndexFromRef.js';
/**
* @param {Buffer} pdf
* @param {Map} refTable
* @returns {object}
*/
const findObject = (pdf, refTable, ref) => {
const index = getIndexFromRef(refTable, ref);
const offset = refTable.offsets.get(index);
let slice = pdf.slice(offset);
slice = slice.slice(0, slice.indexOf('endobj', 'utf8'));
// FIXME: What if it is a stream?
slice = slice.slice(slice.indexOf('<<', 'utf8') + 2);
slice = slice.slice(0, slice.lastIndexOf('>>', 'utf8'));
return slice;
};
export default findObject;
@@ -0,0 +1,21 @@
import SignPdfError from './SignPdfError.js';
/**
* @param {object} refTable
* @param {string} ref
* @returns {number}
*/
const getIndexFromRef = (refTable, ref) => {
let [index] = ref.split(' ');
index = parseInt(index);
if (!refTable.offsets.has(index)) {
throw new SignPdfError(
`Failed to locate object "${ref}".`,
SignPdfError.TYPE_PARSE,
);
}
return index;
};
export default getIndexFromRef;
@@ -0,0 +1,41 @@
import getPagesDictionaryRef from "./getPagesDictionaryRef.js";
import findObject from "./findObject.js";
/**
* Finds the reference to a page.
*
* @param {Buffer} pdfBuffer
* @param {Object} info As extracted from readRef()
* @param {Number} Page is pageNo on which we want widget/sign
*/
export default function getPageRef(pdfBuffer, info, Page) {
const pagesRef = getPagesDictionaryRef(info);
const pagesDictionary = findObject(pdfBuffer, info.xref, pagesRef);
const kidsPosition = pagesDictionary.indexOf("/Kids");
const kidsStart = pagesDictionary.indexOf("[", kidsPosition) + 1;
const kidsEnd = pagesDictionary.indexOf("]", kidsPosition);
const pages = pagesDictionary.slice(kidsStart, kidsEnd).toString();
// console.log("pages ", pages);
// const split = pages.trim().split(" ", 3);
// return `${split[0]} ${split[1]} ${split[2]}`;
// below code is used to get pageIndex (10 0 R) from pageNo to add sign widget on it
let indices = [];
for (var i = 0; i < pages.length; i++) {
if (pages[i] === "R") indices.push(i);
}
let pageIndex = {};
let startPosition = 0;
indices.forEach((x, index) => {
pageIndex = {
...pageIndex,
[index + 1]: pages.substring(startPosition + 1, x + 1),
};
startPosition = x + 1;
});
// console.log("pageIndex ", pageIndex);
const pageStr = Page ? pageIndex[Page] : pageIndex[1];
// console.log("pageStr ", pageStr);
return pageStr;
}
@@ -0,0 +1,18 @@
import SignPdfError from './SignPdfError.js';
/**
* @param {Object} info As extracted from readRef()
*/
export default function getPagesDictionaryRef(info) {
const pagesRefRegex = /\/Pages\s+(\d+\s+\d+\s+R)/g;
const match = pagesRefRegex.exec(info.root);
if (match === null) {
throw new SignPdfError(
'Failed to find the pages descriptor. This is probably a problem in node-signpdf.',
SignPdfError.TYPE_PARSE,
);
}
return match[1];
}
@@ -0,0 +1,131 @@
/*
PDFObject by Devon Govett used below.
The class is part of pdfkit. See https://github.com/foliojs/pdfkit
LICENSE: MIT. Included in this folder.
Modifications may have been applied for the purposes of node-signpdf.
*/
import PDFAbstractReference from './PDFAbstractReference.js';
/*
PDFObject - converts JavaScript types into their corresponding PDF types.
By Devon Govett
*/
const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length);
const escapableRe = /[\n\r\t\b\f()\\]/g;
const escapable = {
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\b': '\\b',
'\f': '\\f',
'\\': '\\\\',
'(': '\\(',
')': '\\)',
};
// Convert little endian UTF-16 to big endian
const swapBytes = (buff) => buff.swap16();
export default class PDFObject {
static convert(object, encryptFn = null) {
// String literals are converted to the PDF name type
if (typeof object === 'string') {
return `/${object}`;
// String objects are converted to PDF strings (UTF-16)
} if (object instanceof String) {
let string = object;
// Detect if this is a unicode string
let isUnicode = false;
for (let i = 0, end = string.length; i < end; i += 1) {
if (string.charCodeAt(i) > 0x7f) {
isUnicode = true;
break;
}
}
// If so, encode it as big endian UTF-16
let stringBuffer;
if (isUnicode) {
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le'));
} else {
stringBuffer = Buffer.from(string, 'ascii');
}
// Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(stringBuffer).toString('binary');
} else {
string = stringBuffer.toString('binary');
}
// Escape characters as required by the spec
string = string.replace(escapableRe, (c) => escapable[c]);
return `(${string})`;
// Buffers are converted to PDF hex strings
} if (Buffer.isBuffer(object)) {
return `<${object.toString('hex')}>`;
} if (object instanceof PDFAbstractReference) {
return object.toString();
} if (object instanceof Date) {
let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(object.getUTCDate(), 2)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(object.getUTCSeconds(), 2)}Z`;
// Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(Buffer.from(string, 'ascii')).toString('binary');
// Escape characters as required by the spec
string = string.replace(escapableRe, (c) => escapable[c]);
}
return `(${string})`;
} if (Array.isArray(object)) {
const items = object.map((e) => PDFObject.convert(e, encryptFn)).join(' ');
return `[${items}]`;
}
if ({}.toString.call(object) === '[object Object]') {
const out = ['<<'];
let streamData;
// @todo this can probably be refactored into a reduce
Object.entries(object).forEach(([key, val]) => {
let checkedValue = '';
if (val && val.toString().indexOf('<<') !== -1) {
checkedValue = val;
} else {
checkedValue = PDFObject.convert(val, encryptFn);
}
// // if (key === 'stream') {
// // streamData = `${key}\n${val}\nendstream`;
// // }
// else {
out.push(`/${key} ${checkedValue}`);
// }
});
out.push('>>');
// if (streamData) {
// out.push(streamData);
// }
return out.join('\n');
}
if (typeof object === 'number') {
return PDFObject.number(object);
}
return `${object}`;
}
static number(n) {
if (n > -1e21 && n < 1e21) {
return Math.round(n * 1e6) / 1e6;
}
throw new Error(`unsupported number: ${n}`);
}
}
@@ -0,0 +1,449 @@
import {
DEFAULT_BYTE_RANGE_PLACEHOLDER,
DEFAULT_SIGNATURE_LENGTH,
SUBFILTER_ADOBE_PKCS7_DETACHED,
} from "./const.js";
// eslint-disable-next-line import/no-unresolved
import PDFKitReferenceMock from "./PDFKitReferenceMock.js";
/**
* Adds the objects that are needed for Adobe.PPKLite to read the signature.
* Also includes a placeholder for the actual signature.
* Returns an Object with all the added PDFReferences.
* @param {PDFDocument} pdf
* @param {string} reason
* @returns {object}
*/
// testing
import PNG from "png-js";
import zlib from "zlib";
import fs from "node:fs";
const specialCharacters = [
"á",
"Á",
"é",
"É",
"í",
"Í",
"ó",
"Ó",
"ö",
"Ö",
"ő",
"Ő",
"ú",
"Ú",
"ű",
"Ű",
];
const MARKERS = [
0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc5, 0xffc6, 0xffc7, 0xffc8, 0xffc9,
0xffca, 0xffcb, 0xffcc, 0xffcd, 0xffce, 0xffcf,
];
const COLOR_SPACE_MAP = {
1: "DeviceGray",
3: "DeviceRGB",
4: "DeviceCMYK",
};
// testing
function getImage(imagePath, pdf) {
let img;
const data = imagePath; //fs.readFileSync(imagePath);
if (data[0] === 0xff && data[1] === 0xd8) {
img = getJpgImage(pdf, data);
} else if (data[0] === 0x89 && data.toString("ascii", 1, 4) === "PNG") {
img = getPngImage(pdf, data);
} else {
throw new Error("Unknown image format.");
}
return img;
}
function getAnnotationApparance(pdf, IMG, APFONT) {
return pdf.ref(
{
CropBox: [0, 0, 197, 70],
Type: "XObject",
FormType: 1,
BBox: [0, 0, 197.0, 70.0], //[-10, 10, 197.0, 70.0],
Resources: `<</XObject <<\n/Img${IMG.index} ${IMG.index} 0 R\n>>\n/Font <<\n/f1 ${APFONT.index} 0 R\n>>\n>>`,
MediaBox: [0, 0, 197, 70],
Subtype: "Form",
},
null,
getStream(IMG.index)
);
}
const getStream = (imgIndex) => {
// (Aláírta: ${userInformation.commonName}) Tj
// (Aláírta: testing name) Tj
// (${new Date().toISOString().slice(0, 10)}) Tj
return getConvertedText(`
1.0 1.0 1.0 rg
0.0 0.0 0.0 RG
q
q
200 0 0 50 0 10 cm
/Img${imgIndex} Do
Q
0 0 0 rg
BT
0 Tr
/f1 10.0 Tf
1.4 0 0 1 20 45.97412 Tm
ET
BT
0 Tr
/f1 10.0 Tf
1.4 0 0 1 20 33.56006 Tm
ET
Q`);
};
const getFont = (pdf, baseFont) => {
return pdf.ref({
Type: "Font",
BaseFont: baseFont,
Encoding: "WinAnsiEncoding",
Subtype: "Type1",
});
};
const getJpgImage = (pdf, data) => {
if (data.readUInt16BE(0) !== 0xffd8) {
throw "SOI not found in JPEG";
}
let pos = 2;
let marker;
while (pos < data.length) {
marker = data.readUInt16BE(pos);
pos += 2;
if (MARKERS.includes(marker)) {
break;
}
pos += data.readUInt16BE(pos);
}
if (!MARKERS.includes(marker)) {
throw "Invalid JPEG.";
}
pos += 2;
const bits = data[pos++];
const height = data.readUInt16BE(pos);
pos += 2;
const width = data.readUInt16BE(pos);
pos += 2;
const channels = data[pos++];
const colorSpace = COLOR_SPACE_MAP[channels];
const baseJpgData = {
Type: "XObject",
Subtype: "Image",
BitsPerComponent: bits,
Width: width,
Height: height,
ColorSpace: colorSpace,
Filter: "DCTDecode",
};
if (colorSpace === "DeviceCMYK") {
baseJpgData["Decode"] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0];
}
const image = pdf.ref(baseJpgData, null, data);
return image;
};
const getPngImage = (pdf, data) => {
const image = new PNG(data);
const hasAlphaChannel = image.hasAlphaChannel;
const pngBaseData = {
Type: "XObject",
Subtype: "Image",
BitsPerComponent: hasAlphaChannel ? 8 : image.bits,
Width: image.width,
Height: image.height,
Filter: "FlateDecode",
};
if (!hasAlphaChannel) {
const params = pdf.ref({
Predictor: 15,
Colors: image.colors,
BitsPerComponent: image.bits,
Columns: image.width,
});
pngBaseData["DecodeParms"] = params;
}
if (image.palette.length === 0) {
pngBaseData["ColorSpace"] = image.colorSpace;
} else {
const palette = pdf.ref({
stream: new Buffer(image.palette),
});
pngBaseData["ColorSpace"] = [
"Indexed",
"DeviceRGB",
image.palette.length / 3 - 1,
palette,
];
}
if (image.transparency.grayscale != null) {
const val = image.transparency.grayscale;
pngBaseData["Mask"] = [val, val];
} else if (image.transparency.rgb) {
const { rgb } = image.transparency;
const mask = [];
for (let x of rgb) {
mask.push(x, x);
}
pngBaseData["Mask"] = mask;
} else if (image.transparency.indexed) {
loadIndexedAlphaChannel(image);
} else if (hasAlphaChannel) {
splitAlphaChannel(image);
const sMask = getSmask(pdf, image);
pngBaseData["Mask"] = sMask;
}
const pngImage = pdf.ref(pngBaseData, null, image.imgData);
return pngImage;
};
const loadIndexedAlphaChannel = (image) => {
const transparency = image.transparency.indexed;
return image.decodePixels((pixels) => {
const alphaChannel = new Buffer(image.width * image.height);
let i = 0;
for (let j = 0, end = pixels.length; j < end; j++) {
alphaChannel[i++] = transparency[pixels[j]];
}
image.alphaChannel = zlib.deflateSync(alphaChannel);
});
};
const splitAlphaChannel = (image) => {
image.decodePixels((pixels) => {
let a, p;
const colorCount = image.colors;
const pixelCount = image.width * image.height;
const imgData = new Buffer(pixelCount * colorCount);
const alphaChannel = new Buffer(pixelCount);
let i = (p = a = 0);
const len = pixels.length;
const skipByteCount = image.bits === 16 ? 1 : 0;
while (i < len) {
for (let colorIndex = 0; colorIndex < colorCount; colorIndex++) {
imgData[p++] = pixels[i++];
i += skipByteCount;
}
alphaChannel[a++] = pixels[i++];
i += skipByteCount;
}
image.imgData = zlib.deflateSync(imgData);
image.alphaChannel = zlib.deflateSync(alphaChannel);
});
};
const getSmask = (pdf, image) => {
let sMask;
console.log("image.hasAlphaChannel ", image.hasAlphaChannel);
if (image.hasAlphaChannel) {
console.log("image.alphaChannel ", image.alphaChannel);
sMask = pdf.ref({
Type: "XObject",
Subtype: "Image",
Height: image.height,
Width: image.width,
BitsPerComponent: 8,
Filter: "FlateDecode",
ColorSpace: "DeviceGray",
Decode: [0, 1],
stream: image.alphaChannel,
});
}
return sMask;
};
const getConvertedText = (text) => {
return text
.split("")
.map((character) => {
return specialCharacters.includes(character)
? getOctalCodeFromCharacter(character)
: character;
})
.join("");
};
const getOctalCodeFromCharacter = (character) => {
return "\\" + character.charCodeAt(0).toString(8);
};
//
const pdfkitAddPlaceholder = ({
pdf,
pdfBuffer,
reason,
contactInfo = "emailfromp1289@gmail.com",
name = "Name from p12",
location = "Location from p12",
signatureLength = DEFAULT_SIGNATURE_LENGTH,
byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER,
subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED,
sign,
}) => {
// testing
const FONT = getFont(pdf, "Helvetica");
const ZAF = getFont(pdf, "ZapfDingbats");
const APFONT = getFont(pdf, "Helvetica");
// const imagePath = "exports/img.jpg";
const signBase64 = sign.Base64;
const imagePath = Buffer.from(signBase64, "base64");
const IMG = getImage(imagePath, pdf);
const AP = getAnnotationApparance(pdf, IMG, APFONT);
//
/* eslint-disable no-underscore-dangle,no-param-reassign */
// Generate the signature placeholder
const signature = pdf.ref({
Type: "Sig",
Filter: "Adobe.PPKLite",
SubFilter: subFilter,
ByteRange: [
0,
byteRangePlaceholder,
byteRangePlaceholder,
byteRangePlaceholder,
],
Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)),
Reason: new String(reason), // eslint-disable-line no-new-wrappers
M: new Date(),
ContactInfo: new String(contactInfo), // eslint-disable-line no-new-wrappers
Name: new String(name), // eslint-disable-line no-new-wrappers
Location: new String(location), // eslint-disable-line no-new-wrappers
});
// Check if pdf already contains acroform field
const acroFormPosition = pdfBuffer.lastIndexOf("/Type /AcroForm");
const isAcroFormExists = acroFormPosition !== -1;
let fieldIds = [];
let acroFormId;
if (isAcroFormExists) {
let acroFormStart = acroFormPosition;
// 10 is the distance between "/Type /AcroForm" and AcroFrom ID
const charsUntilIdEnd = 10;
const acroFormIdEnd = acroFormPosition - charsUntilIdEnd;
// Let's find AcroForm ID by trying to find the "\n" before the ID
// 12 is a enough space to find the "\n"
// (generally it's 2 or 3, but I'm giving a big space though)
const maxAcroFormIdLength = 12;
let foundAcroFormId = "";
let index = charsUntilIdEnd + 1;
for (index; index < charsUntilIdEnd + maxAcroFormIdLength; index += 1) {
const acroFormIdString = pdfBuffer
.slice(acroFormPosition - index, acroFormIdEnd)
.toString();
if (acroFormIdString[0] === "\n") {
break;
}
foundAcroFormId = acroFormIdString;
acroFormStart = acroFormPosition - index;
}
const pdfSlice = pdfBuffer.slice(acroFormStart);
const acroForm = pdfSlice.slice(0, pdfSlice.indexOf("endobj")).toString();
acroFormId = parseInt(foundAcroFormId);
const acroFormFields = acroForm.slice(
acroForm.indexOf("/Fields [") + 9,
acroForm.indexOf("]")
);
fieldIds = acroFormFields
.split(" ")
.filter((element, i) => i % 3 === 0)
.map((fieldId) => new PDFKitReferenceMock(fieldId));
}
const signatureName = "Signature";
const signatureLeftOffset = fieldIds.length * 125;
const signatureBottomOffset = 5;
const left = sign.Left; //461.50001525878906 //477.3333282470703//409.5000305175781; // first block 44.33332824707031;
const bottom = sign.Bottom; // 730.1979598999023 - 39 + 5.072906494140625 //720.1979598999023 - 30 + 15.072906494140625; // first block 652.1979598999023 - 60 + 19.072906494140625;
const RightX = left + sign.Width; // 98;
const RightY = bottom + sign.Height; //39;
// Generate signature annotation widget
const widget = pdf.ref({
Type: "Annot",
Subtype: "Widget",
FT: "Sig",
Rect: [left, bottom, RightX, RightY], // [signatureLeftOffset, 0, signatureLeftOffset + 90, signatureBottomOffset + 60, ], // [50, 50, 100, 100], //[25, 25, 100, 300], //
V: signature,
T: new String(signatureName + (fieldIds.length + 1)), // eslint-disable-line no-new-wrappers
F: 4,
P: pdf.page.dictionary, // eslint-disable-line no-underscore-dangle
AP: `<</N ${AP.index} 0 R>>`, // testing
DA: new String("/Helvetica 0 Tf 0 g"), // eslint-disable-line no-new-wrappers // testing
});
pdf.page.dictionary.data.Annots = [widget];
// Include the widget in a page
let form;
if (!isAcroFormExists) {
// Create a form (with the widget) and link in the _root
form = pdf.ref({
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget],
});
} else {
// Use existing acroform and extend the fields with newly created widgets
form = pdf.ref(
{
Type: "AcroForm",
SigFlags: 3,
Fields: [...fieldIds, widget],
DR: `<</Font\n<</Helvetica ${FONT.index} 0 R/ZapfDingbats ${ZAF.index} 0 R>>\n>>`, /// testing
},
acroFormId
);
}
pdf._root.data.AcroForm = form;
return {
signature,
form,
widget,
};
/* eslint-enable no-underscore-dangle,no-param-reassign */
};
export default pdfkitAddPlaceholder;
@@ -0,0 +1,138 @@
import PDFObject from "./pdfObject.js";
import PDFKitReferenceMock from "./PDFKitReferenceMock.js";
import removeTrailingNewLine from "./removeTrailingNewLine.js";
import {
DEFAULT_SIGNATURE_LENGTH,
SUBFILTER_ADOBE_PKCS7_DETACHED,
} from "./const.js";
import pdfkitAddPlaceholder from "./pdfkitAddPlaceholder.js";
import getIndexFromRef from "./getIndexFromRef.js";
import readPdf from "./readPdf.js";
import getPageRef from "./getPageRef.js";
import createBufferRootWithAcroform from "./createBufferRootWithAcroform.js";
import createBufferPageWithAnnotation from "./createBufferPageWithAnnotation.js";
import createBufferTrailer from "./createBufferTrailer.js";
const isContainBufferRootWithAcroform = (pdf) => {
const bufferRootWithAcroformRefRegex = /\/AcroForm\s+(\d+\s\d+\sR)/g;
const match = bufferRootWithAcroformRefRegex.exec(pdf.toString());
return match != null && match[1] != null && match[1] !== "";
};
// testing
const getAssembledPdf = (pdf, index, input, stream) => {
let finalPdf = pdf;
finalPdf = Buffer.concat([
finalPdf,
Buffer.from("\n"),
Buffer.from(`${index} 0 obj\n`),
Buffer.from(PDFObject.convert(input)),
]);
if (stream) {
finalPdf = Buffer.concat([
finalPdf,
Buffer.from("\nstream\n"),
Buffer.from(stream),
Buffer.from("\nendstream"),
]);
}
finalPdf = Buffer.concat([finalPdf, Buffer.from("\nendobj\n")]);
return finalPdf;
};
//
/**
* Adds a signature placeholder to a PDF Buffer.
*
* This contrasts with the default pdfkit-based implementation.
* Parsing is done using simple string operations.
* Adding is done with `Buffer.concat`.
* This allows node-signpdf to be used on any PDF and
* not only on a freshly created through PDFKit one.
*/
const plainAddPlaceholder = ({
pdfBuffer,
reason,
contactInfo = "emailfromp1289@gmail.com",
name = "Name from p12",
location = "Location from p12",
signatureLength = DEFAULT_SIGNATURE_LENGTH,
subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED,
sign,
}) => {
let pdf = removeTrailingNewLine(pdfBuffer);
const info = readPdf(pdf);
const pageRef = getPageRef(pdf, info, sign.Page);
const pageIndex = getIndexFromRef(info.xref, pageRef);
const addedReferences = new Map();
const pdfKitMock = {
ref: (input, additionalIndex, stream) => {
info.xref.maxIndex += 1;
const index =
additionalIndex != null ? additionalIndex : info.xref.maxIndex;
addedReferences.set(index, pdf.length + 1); // + 1 new line
pdf = getAssembledPdf(pdf, index, input, stream);
return new PDFKitReferenceMock(info.xref.maxIndex);
},
page: {
dictionary: new PDFKitReferenceMock(pageIndex, {
data: {
Annots: [],
},
}),
},
_root: {
data: {},
},
};
const { form, widget } = pdfkitAddPlaceholder({
pdf: pdfKitMock,
pdfBuffer,
reason,
contactInfo,
name,
location,
signatureLength,
subFilter,
sign,
});
if (!isContainBufferRootWithAcroform(pdf)) {
const rootIndex = getIndexFromRef(info.xref, info.rootRef);
addedReferences.set(rootIndex, pdf.length + 1);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
createBufferRootWithAcroform(pdf, info, form),
]);
}
addedReferences.set(pageIndex, pdf.length + 1);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
createBufferPageWithAnnotation(pdf, info, pageRef, widget),
]);
pdf = Buffer.concat([
pdf,
Buffer.from("\n"),
createBufferTrailer(pdf, info, addedReferences),
]);
return pdf;
};
export default plainAddPlaceholder;
@@ -0,0 +1,54 @@
import readRefTable from './readRefTable.js';
import findObject from './findObject.js';
const getValue = (trailer, key) => {
let index = trailer.indexOf(key);
if (index === -1) {
return undefined;
}
const slice = trailer.slice(index);
index = slice.indexOf('/', 1);
if (index === -1) {
index = slice.indexOf('>', 1);
}
return slice.slice(key.length + 1, index).toString().trim(); // key + at least one space
};
/**
* Simplified parsing of a PDF Buffer.
* Extracts reference table, root info and trailer start.
*
* See section 7.5.5 (File Trailer) of the PDF specs.
*
* @param {Buffer} pdfBuffer
*/
const readPdf = (pdfBuffer) => {
// Extract the trailer dictionary.
const trailerStart = pdfBuffer.lastIndexOf('trailer');
// The trailer is followed by xref. Then an EOF. EOF's length is 6 characters.
const trailer = pdfBuffer.slice(trailerStart, pdfBuffer.length - 6);
let xRefPosition = trailer.slice(trailer.lastIndexOf('startxref') + 10).toString();
xRefPosition = parseInt(xRefPosition);
const refTable = readRefTable(pdfBuffer);
const rootRef = getValue(trailer, '/Root');
const root = findObject(pdfBuffer, refTable, rootRef).toString();
const infoRef = getValue(trailer, '/Info');
return {
xref: refTable,
rootRef,
root,
infoRef,
trailerStart,
previousXrefs: [],
xRefPosition,
};
};
export default readPdf;
@@ -0,0 +1,118 @@
import SignPdfError from './SignPdfError.js';
import xrefToRefMap from './xrefToRefMap.js';
export const getLastTrailerPosition = (pdf) => {
const trailerStart = pdf.lastIndexOf(Buffer.from('trailer', 'utf8'));
const trailer = pdf.slice(trailerStart, pdf.length - 6);
const xRefPosition = trailer
.slice(trailer.lastIndexOf(Buffer.from('startxref', 'utf8')) + 10)
.toString();
return parseInt(xRefPosition);
};
export const getXref = (pdf, position) => {
let refTable = pdf.slice(position); // slice starting from where xref starts
const realPosition = refTable.indexOf(Buffer.from('xref', 'utf8'));
if (realPosition === -1) {
throw new SignPdfError(
`Could not find xref anywhere at or after ${position}.`,
SignPdfError.TYPE_PARSE,
);
}
if (realPosition > 0) {
const prefix = refTable.slice(0, realPosition);
if (prefix.toString().replace(/\s*/g, '') !== '') {
throw new SignPdfError(
`Expected xref at ${position} but found other content.`,
SignPdfError.TYPE_PARSE,
);
}
}
const nextEofPosition = refTable.indexOf(Buffer.from('%%EOF', 'utf8'));
if (nextEofPosition === -1) {
throw new SignPdfError(
'Expected EOF after xref and trailer but could not find one.',
SignPdfError.TYPE_PARSE,
);
}
refTable = refTable.slice(0, nextEofPosition);
refTable = refTable.slice(realPosition + 4); // move ahead with the "xref"
refTable = refTable.slice(refTable.indexOf('\n') + 1); // move after the next new line
// extract the size
let size = refTable.toString().split('/Size')[1];
if (!size) {
throw new SignPdfError(
'Size not found in xref table.',
SignPdfError.TYPE_PARSE,
);
}
size = (/^\s*(\d+)/).exec(size);
if (size === null) {
throw new SignPdfError(
'Failed to parse size of xref table.',
SignPdfError.TYPE_PARSE,
);
}
size = parseInt(size[1]);
const [objects, infos] = refTable.toString().split('trailer');
const isContainingPrev = infos.split('/Prev')[1] != null;
let prev;
if (isContainingPrev) {
const pagesRefRegex = /Prev (\d+)/g;
const match = pagesRefRegex.exec(infos);
const [, prevPosition] = match;
prev = prevPosition;
}
const xRefContent = xrefToRefMap(objects);
return {
size,
prev,
xRefContent,
};
};
export const getFullXrefTable = (pdf) => {
const lastTrailerPosition = getLastTrailerPosition(pdf);
const lastXrefTable = getXref(pdf, lastTrailerPosition);
if (lastXrefTable.prev === undefined) {
return lastXrefTable.xRefContent;
}
const pdfWithoutLastTrailer = pdf.slice(0, lastTrailerPosition);
const partOfXrefTable = getFullXrefTable(pdfWithoutLastTrailer);
const mergedXrefTable = new Map([
...partOfXrefTable,
...lastXrefTable.xRefContent,
]);
return mergedXrefTable;
};
/**
* @param {Buffer} pdfBuffer
* @returns {object}
*/
const readRefTable = (pdf) => {
const fullXrefTable = getFullXrefTable(pdf);
const startingIndex = 0;
const maxIndex = Math.max(...fullXrefTable.keys());
return {
startingIndex,
maxIndex,
offsets: fullXrefTable,
};
};
export default readRefTable;
@@ -0,0 +1,42 @@
import SignPdfError from './SignPdfError.js';
const sliceLastChar = (pdf, character) => {
const lastChar = pdf.slice(pdf.length - 1).toString();
if (lastChar === character) {
return pdf.slice(0, pdf.length - 1);
}
return pdf;
};
/**
* Removes a trailing new line if there is such.
*
* Also makes sure the file ends with an EOF line as per spec.
* @param {Buffer} pdf
* @returns {Buffer}
*/
const removeTrailingNewLine = (pdf) => {
if (!(pdf instanceof Buffer)) {
throw new SignPdfError(
'PDF expected as Buffer.',
SignPdfError.TYPE_INPUT,
);
}
let output = pdf;
output = sliceLastChar(output, '\n');
output = sliceLastChar(output, '\r');
const lastLine = output.slice(output.length - 6).toString();
if (lastLine !== '\n%%EOF') {
throw new SignPdfError(
'A PDF file must end with an EOF line.',
SignPdfError.TYPE_PARSE,
);
}
return output;
};
export default removeTrailingNewLine;
@@ -0,0 +1,48 @@
import SignPdfError from './SignPdfError.js';
const xrefToRefMap = (xrefString) => {
const lines = xrefString.split('\n').filter((l) => l !== '');
let index = 0;
let expectedLines = 0;
const xref = new Map();
lines.forEach((line) => {
const split = line.split(' ');
if (split.length === 2) {
index = parseInt(split[0]);
expectedLines = parseInt(split[1]);
return;
}
if (expectedLines <= 0) {
throw new SignPdfError(
'Too many lines in xref table.',
SignPdfError.TYPE_PARSE,
);
}
expectedLines -= 1;
const [offset, , inUse] = split;
if (inUse.trim() === 'f') {
index += 1;
return;
}
if (inUse.trim() !== 'n') {
throw new SignPdfError(
`Unknown in-use flag "${inUse}". Expected "n" or "f".`,
SignPdfError.TYPE_PARSE,
);
}
if (!/^\d+$/.test(offset.trim())) {
throw new SignPdfError(
`Expected integer offset. Got "${offset}".`,
SignPdfError.TYPE_PARSE,
);
}
const storeOffset = parseInt(offset.trim());
xref.set(index, storeOffset);
index += 1;
});
return xref;
};
export default xrefToRefMap;