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