mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-24 16:42:33 +02:00
initial commit
This commit is contained in:
@@ -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';
|
||||
+40
@@ -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;
|
||||
+15
@@ -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;
|
||||
Reference in New Issue
Block a user