Skip to content

Instantly share code, notes, and snippets.

@AZagatti
Created August 6, 2020 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AZagatti/736d5be318a299e95daef2d423e4c0b8 to your computer and use it in GitHub Desktop.
Save AZagatti/736d5be318a299e95daef2d423e4c0b8 to your computer and use it in GitHub Desktop.
PDF
import PDFDocument from 'pdfkit';
import {
copyFileSync,
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
watchFile,
unwatchFile,
} from 'fs';
import { join } from 'path';
import { toDataURL } from 'qrcode';
import shortid from 'shortid';
import rimraf from 'rimraf';
import { promisify } from 'util';
import { exec } from 'child_process';
import { log, warn } from 'console';
import crypto from 'crypto';
import accents from 'remove-accents';
import { rejects } from 'assert';
import { prototype } from 'events';
const shellExec = promisify(exec);
const debug = false;
shortid.characters(
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
);
interface IFullPath {
files: string;
images: string;
outPut: string;
tempFile: string;
endFile: string;
}
interface IParams {
company: string | null;
margin: number | null;
custom: {
qrCode: {
size: number | null;
position:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| null;
};
signature: {
position:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| null;
};
logo: {
sizeMultiplier: number | null;
opacity: number | null;
visible: boolean | null;
position:
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| null;
};
};
}
interface IDocumentoPDFSerialized {
data: Date;
nomeArquivo: string;
tamanho: number;
idDocumento: string;
idPaginas: any;
caminhos: {
arquivo: string;
pasta: string;
};
sha1Hash: string;
}
interface IDocumentoPDF {
uniqueFolder: string;
filesPath: string | null;
imagesPath: string | null;
outPutPath: string | null;
companyName: string;
companyNameFileName: string;
logoTipo: Buffer;
customFont: string;
uuid1: string;
outPutName: string;
fullPath: IFullPath;
preDoc: PDFKit.PDFDocument;
finalDoc: PDFKit.PDFDocument;
currentDate: string;
retorno: IDocumentoPDFSerialized;
}
class DocumentoPDF implements IDocumentoPDF {
public static normalId(size: number = 8) {
let id = shortid.generate().substr(0, size);
const lenDiff = size - id.length;
if (lenDiff) {
for (let i = 0; i < lenDiff; i += 1) {
id += String(Math.round(Math.random() * 9));
}
}
return id;
}
public static gerarHash(chave: string, dados: string[]) {
if (!chave || !dados || !dados.length) {
throw new Error('Não foi possível gerar hash.');
}
const hash = crypto
.createHmac('sha1', chave)
.update(dados.join(''))
.digest('hex');
if (debug) {
log(chave, dados.join(''), hash);
}
return hash;
}
public uniqueFolder: string;
public filesPath: string | null;
public imagesPath: string | null;
public outPutPath: string | null;
public params: IParams | null;
public companyName: string;
public companyNameFileName: string;
public logoTipo: Buffer;
public customFont: string;
public uuid1: string;
public outPutName: string;
public fullPath: IFullPath;
public preDoc: PDFKit.PDFDocument;
public finalDoc: PDFKit.PDFDocument;
public currentDate: string;
public retorno: IDocumentoPDFSerialized;
constructor(
uniqueFolder = '',
params = null,
filesPath = null,
imagesPath = null,
outPutPath = null,
) {
if (!process.env.PDFAUTH_HASH_KEY || !process.env.API_URL) {
throw new Error(
'Não foi possível verificar dados necessários ao gerador.',
);
}
this.uniqueFolder = uniqueFolder;
if (!uniqueFolder) {
throw new Error('Pasta única obrigatória.');
}
this.filesPath =
filesPath || join(__dirname, '../../public/documentopdf/files/');
this.imagesPath =
imagesPath || join(__dirname, '../../public/documentopdf/images/');
this.outPutPath =
outPutPath || join(__dirname, '../../public/documentopdf/output/');
this.params = params;
this.companyName = this.params!.company || 'Analise Já';
this.companyNameFileName = accents(
this.companyName.toLowerCase().replace(/\s/g, ''),
);
this.logoTipo = readFileSync(
join(__dirname, '../../public/documentopdf/logo.png'),
);
this.customFont = join(__dirname, '../../public/documentopdf/Symtext.ttf');
this.uuid1 = DocumentoPDF.normalId();
this.outPutName = `${this.companyNameFileName}.${this.uuid1}.pdf`;
this.fullPath = {
files: join(this.filesPath, this.uniqueFolder),
images: join(this.imagesPath, this.uniqueFolder),
outPut: join(this.outPutPath, this.uniqueFolder),
tempFile: join(
this.outPutPath,
this.uniqueFolder,
`temp_${this.outPutName}`,
),
endFile: join(this.outPutPath, this.uniqueFolder, this.outPutName),
};
this.preDoc = new PDFDocument({
autoFirstPage: false,
compress: true,
});
this.finalDoc = new PDFDocument({
autoFirstPage: false,
// userPassword: "1234",
compress: true,
ownerPassword: DocumentoPDF.gerarHash(process.env.PDFAUTH_HASH_KEY!, [
this.uuid1!,
]),
permissions: {
printing: 'highResolution',
fillingForms: false,
modifying: false,
copying: false,
annotating: false,
contentAccessibility: false,
documentAssembly: false,
},
// layout: "portrait", // podeser 'landscape' - horizontal
info: {
Title: `${this.companyName} - ${this.uuid1}`,
Author: `${this.companyName}`, // Autor
// Subject: "", // Assunto
// Keywords: "pdf;javascript", // Palavras-chave
// CreationDate:'DD/MM/YYYY', // Data de criação (é gerada automaticamento caso n seja setada)
// ModDate: "DD/MM/YYYY" // Data de modificação
},
});
this.currentDate = new Date().toLocaleString();
this.retorno = {
data: new Date(),
nomeArquivo: '',
tamanho: 0,
idDocumento: '',
idPaginas: [{}],
caminhos: {
arquivo: '',
pasta: '',
},
sha1Hash: '',
};
}
public async gerarPastas() {
try {
// Gerando /files
if (!existsSync(this.filesPath!)) {
mkdirSync(this.filesPath!);
throw new Error(
'Gerando `/files`, pasta vazia. Preencha a pasta e reinicie o processo.',
);
}
if (!existsSync(this.fullPath.files)) {
throw new Error('Pasta única inexistente.');
return true;
}
// Gerando /images
if (!existsSync(this.imagesPath!)) {
if (debug) {
warn('Gerando /images');
}
mkdirSync(this.imagesPath!);
}
if (!existsSync(this.fullPath.images)) {
if (debug) {
warn('Gerando /images');
}
mkdirSync(this.fullPath.images);
}
// Gerando /output
if (!existsSync(this.outPutPath!)) {
if (debug) {
warn('Gerando /output');
}
mkdirSync(this.outPutPath!);
}
// Gerando /output/unique
if (!existsSync(this.fullPath.outPut)) {
if (debug) {
warn('Gerando /output');
}
mkdirSync(this.fullPath.outPut);
}
} catch (error) {
throw new Error(error);
}
}
public async converterPDFs(
files: string[],
folderWrapper: string | null = null,
) {
try {
// Converter imagens
return Promise.all(
files.map(
(file: any): Promise<any> =>
new Promise(
async (resolve, reject): Promise<boolean | any> => {
try {
if (file.endsWith('.pdf')) {
if (debug) {
log(
`Convertendo pdf: ${folderWrapper ||
this.fullPath.files}${file}`,
);
}
const { stdout, stderr } = await shellExec(
`pdftoppm ${folderWrapper ||
this.fullPath.files}${file} ${
this.fullPath.images
}${file.replace('.pdf', '')} -png -r 250`,
);
if (stderr) {
throw new Error(stderr);
}
if (debug) {
log(`stdout: ${stdout}`);
log('Arquivo convertido.');
}
} else if (
file.endsWith('.png') ||
file.endsWith('.jpg') ||
file.endsWith('.jpeg')
) {
if (debug) {
log(
`Copiando imagem existente: ${this.fullPath.files}${file}`,
);
}
copyFileSync(
`${this.fullPath.files}${file}`,
`${this.fullPath.images}${file}`,
);
}
resolve(true);
} catch (err) {
reject(err);
}
},
),
),
);
} catch (error) {
throw new Error(error);
}
}
public async desenharPDF(prop: any, images: string[], i: number) {
try {
const uuid2 = DocumentoPDF.normalId();
prop.pages.push({
uuid2,
sha1Hash: DocumentoPDF.gerarHash(process.env.PDFAUTH_HASH_KEY!, [
this.uuid1,
uuid2,
]),
});
const qrCodeImage = await toDataURL(`${this.uuid1}/${uuid2}`);
if (debug) {
log('Gerando página, id:', this.uuid1, uuid2);
}
// gerar página, adicionar imagem
this.preDoc
.addPage({
size: [prop.width, prop.height],
margin: 0,
})
.image(
`${this.fullPath.images}${images[i]}`,
prop.relX(0),
prop.relX(0),
{
fit: [prop.relX(100) - prop.relX(0), prop.relY(100) - prop.relY(0)],
align: 'center',
valign: 'center',
},
)
.save();
// debug
// this.preDoc
// .rect(0, 0, prop.width, prop.height)
// .fill('#C60')
// .restore()
// .save();
// colocar marca d´agua
const uuid1Label = Array.from(this.uuid1).join('\n');
const uuid2Label = Array.from(uuid2).join('\n');
const waterMarkLineGap = 6.4;
this.preDoc
.fillOpacity(0.1)
.lineWidth(prop.area(0.75))
.strokeOpacity(0.1)
.fillAndStroke('#000', '#FFF')
.font('Helvetica')
.fontSize(prop.area(30))
.text(uuid1Label, prop.relX(0), prop.relY(0), {
width: prop.relX(100) - prop.relX(0),
height: prop.relY(100),
lineGap: -prop.area(waterMarkLineGap),
fill: true,
stroke: true,
columns: 2,
columnGap: 0,
})
.save()
.rotate(180)
.text(uuid2Label, -prop.relX(100), -prop.relY(100), {
width: prop.relX(100) - prop.relX(0),
height: prop.relY(100),
lineGap: -prop.area(waterMarkLineGap),
fill: true,
stroke: true,
columns: 2,
columnGap: prop.area(1),
})
.restore()
.save();
if (prop.custom.signature.visible) {
// referenciar autenticação
const autenticLabel = `Documento autenticado por ${this.companyName} em ${this.currentDate}`;
this.preDoc
.font('Times-Bold')
.rotate(-90)
.fontSize(prop.area(1))
.fillOpacity(0.5)
.rect(
-prop.filtrarY(
prop.custom.signature.position,
this.preDoc.widthOfString(autenticLabel),
),
prop.filtrarX(
prop.custom.signature.position,
this.preDoc.heightOfString(autenticLabel),
),
this.preDoc.widthOfString(autenticLabel),
this.preDoc.heightOfString(autenticLabel),
)
.fill('#FFF')
.fillOpacity(1)
.fill('#333')
.text(
autenticLabel,
-prop.filtrarY(
prop.custom.signature.position,
this.preDoc.widthOfString(autenticLabel),
),
prop.filtrarX(
prop.custom.signature.position,
this.preDoc.heightOfString(autenticLabel),
),
{
align: 'left',
width: prop.relY(100) - prop.relY(0),
},
)
// .fontSize(prop.area(1.6))
// .text(`${this.uuid1} ${uuid2}`, -prop.relX(41.55), prop.relY(68), {
// align: 'right',
// width: prop.relY(28.5),
// })
.restore()
.save();
}
// assinar referencia do documento, no topo
const topRefLabel = `${this.uuid1} ${new Date().getTime()}`;
this.preDoc
.fontSize(prop.area(1))
.fillOpacity(0.5)
.rect(
prop.relX(50) - this.preDoc.widthOfString(topRefLabel) / 2,
prop.relY(0),
this.preDoc.widthOfString(topRefLabel),
this.preDoc.heightOfString(topRefLabel),
)
.fill('#FFF')
.fillOpacity(1)
.fill('#333')
.text(topRefLabel, prop.relX(0), prop.relY(0.1), {
align: 'center',
width: prop.relX(100) - prop.relX(0),
})
.save();
// assinar referencia da pagina, à esquerda
const leftRefLabel = `${uuid2} ${new Date().getTime()}`;
this.preDoc
.rotate(90)
.fontSize(prop.area(1))
.fillOpacity(0.5)
.rect(
prop.relY(50) - this.preDoc.widthOfString(leftRefLabel) / 2,
-prop.relX(0) - prop.area(1),
this.preDoc.widthOfString(leftRefLabel),
this.preDoc.heightOfString(leftRefLabel),
)
.fill('#FFF')
.fillOpacity(1)
.fill('#333')
.text(leftRefLabel, prop.relY(0), -prop.relX(0) - prop.area(0.9), {
align: 'center',
width: prop.relY(100) - prop.relY(0),
})
.restore()
.save();
// Numerar páginas, com margem para o qrCode
if (prop.custom.qrCode.visible) {
const qrCodeSize = prop.area(prop.custom.qrCode.size);
const paginationLabel = `${i + 1}/${images.length}`;
this.preDoc
.fontSize(prop.area(1))
.fillOpacity(0.5)
.rect(
prop.filtrarX(
prop.custom.qrCode.position,
this.preDoc.widthOfString(paginationLabel),
),
prop.filtrarY(
prop.custom.qrCode.position,
qrCodeSize - prop.margin,
qrCodeSize +
this.preDoc.heightOfString(paginationLabel) -
prop.margin,
),
this.preDoc.widthOfString(paginationLabel),
this.preDoc.heightOfString(paginationLabel),
)
.fill('#FFF')
.fillOpacity(1)
.fill('#333')
.text(
paginationLabel,
prop.filtrarX(
prop.custom.qrCode.position,
this.preDoc.widthOfString(paginationLabel),
),
prop.filtrarY(
prop.custom.qrCode.position,
qrCodeSize - prop.margin,
qrCodeSize +
this.preDoc.heightOfString(paginationLabel) -
prop.margin,
),
{
align: 'left',
width: qrCodeSize,
fill: true,
},
)
.save();
// colocar o qrCode
this.preDoc
.fillOpacity(1)
.image(
Buffer.from(
qrCodeImage.replace('data:image/png;base64,', ''),
'base64',
),
prop.filtrarX(
prop.custom.qrCode.position,
qrCodeSize - prop.margin,
),
prop.filtrarY(
prop.custom.qrCode.position,
0,
qrCodeSize - prop.margin,
),
{
width: qrCodeSize - prop.relX(0),
height: qrCodeSize - prop.relX(0),
},
)
.save();
}
if (prop.custom.logo.visible) {
// colocar logotipo
this.preDoc
.fillOpacity(prop.custom.logo.opacity)
.rect(
prop.filtrarX(prop.custom.logo.position, prop.custom.logo.width()),
prop.filtrarY(
prop.custom.logo.position,
0,
prop.custom.logo.height(),
),
prop.custom.logo.width(),
prop.custom.logo.height(),
)
.fill('#FFF')
.image(
this.logoTipo,
prop.filtrarX(prop.custom.logo.position, prop.custom.logo.width()),
prop.filtrarY(
prop.custom.logo.position,
0,
prop.custom.logo.height(),
),
{
width: prop.custom.logo.width(),
height: prop.custom.logo.height(),
},
)
.save();
}
// borda traçado
this.preDoc
.rect(
prop.relX(0) - prop.area(0.1),
prop.relY(0) - prop.area(0.1),
prop.relX(100) - prop.relX(0) + prop.area(0.1),
prop.relY(100) - prop.relY(0) + prop.area(0.1),
)
.lineWidth(prop.area(0.1))
.strokeColor('#000')
.strokeOpacity(0.25)
.dash(prop.area(0.4), { space: prop.area(0.3) })
.stroke()
.restore()
.save();
} catch (error) {
throw new Error(error);
}
}
public async gerarPDFAutenticado() {
try {
await this.gerarPastas();
// Ler pasta unica de arquivos
const files = readdirSync(this.fullPath.files);
if (!files.length) {
if (debug) {
warn(
'Nenhum arquivo encontrado em /files. Preencha a pasta e reinicie o processo.',
);
}
return true;
}
if (debug) {
log(
`\n\rConvertendo PDFs para imagens, pasta:\n\r${this.filesPath}\n\r`,
);
}
// Converter arquivos da pasta /files/:unique
await this.converterPDFs(files);
// Definir propriedades do documento
const prop = {
width: 595,
height: 842,
margin: this.params!.margin || 25,
custom: {
qrCode: {
size: this.params!.custom?.qrCode?.size || 10,
visible: true,
position: this.params!.custom?.qrCode?.position || 'bottom-left',
},
signature: {
visible: true,
position:
this.params!.custom?.signature?.position || 'bottom-right',
},
logo: {
sizeMultiplier: this.params!.custom?.logo?.sizeMultiplier || 0.4,
width: () => 512 * prop.custom?.logo?.sizeMultiplier,
height: () => 83 * prop.custom?.logo?.sizeMultiplier,
opacity: this.params!.custom?.logo?.opacity || 0.5,
visible: true,
position: this.params!.custom?.logo?.position || 'top-left',
},
},
withMargin: {
vertical: () => prop.width - 2 * prop.margin,
horizontal: () => prop.height - 2 * prop.margin,
},
relX: (percent: number) =>
prop.margin +
Math.round((percent * prop.withMargin.vertical()) / 100),
relY: (percent: number) =>
prop.margin +
Math.round((percent * prop.withMargin.horizontal()) / 100),
area: (percent: number) =>
Math.round(
(percent * Math.sqrt(prop.relX(100) * prop.relY(100))) / 100,
),
pages: [] as any[],
filtrarX: (
location: string,
rightOffset: number = 0,
leftOffset: number = 0,
) => {
const map: any = {
'top-right': prop.relX(100) - rightOffset,
'top-left': prop.relX(0) + leftOffset,
'bottom-left': prop.relX(0) + leftOffset,
'bottom-right': prop.relX(100) - rightOffset,
// tslint:disable-next-line: object-literal-key-quotes
default: prop.relX(0),
};
return map[location] || map.default;
},
filtrarY: (
location: string,
upOffset: number = 0,
downOffset: number = 0,
) => {
const map: any = {
'top-right': prop.relY(0) + upOffset,
'top-left': prop.relY(0) + upOffset,
'bottom-left': prop.relY(100) - downOffset,
'bottom-right': prop.relY(100) - downOffset,
// tslint:disable-next-line: object-literal-key-quotes
default: prop.relY(0),
};
return map[location] || map.default;
},
};
// Ler pasta images/:unique
const images = readdirSync(this.fullPath.images);
if (debug) {
log(
`\n\rGerando um PDF a partir das imagens da pasta temporária:\n\r${this.fullPath.images}\n\r`,
);
}
// Definindo stream 'temp'
this.preDoc.pipe(createWriteStream(this.fullPath.tempFile));
// Desenhando PDF
for (let i = 0; i < images.length; i++) {
// for (let i = 0; i < 1; i++) {
if (
images[i].endsWith('.png') ||
images[i].endsWith('.jpg') ||
images[i].endsWith('.jpeg')
) {
if (debug) {
log(`Referência: ${this.fullPath.images}${images[i]}`);
}
await this.desenharPDF(prop, images, i);
}
}
this.preDoc.end();
return new Promise((resolve, reject) => {
watchFile(this.fullPath.tempFile, async (event) => {
try {
if (event) {
unwatchFile(this.fullPath.tempFile);
await this.gerarPDFImagens(prop);
resolve(true);
}
throw new Error('Não foi possível gerar o PDF');
} catch (error) {
reject(error);
}
});
});
} catch (error) {
rimraf.sync(this.fullPath.files);
rimraf.sync(this.fullPath.images);
rimraf.sync(this.fullPath.outPut);
if (debug) {
warn('\n\rArquivo deletado:', this.fullPath.endFile);
warn('\n\rPasta deletada:', this.fullPath.files);
warn('\n\rPasta deletada:', this.fullPath.images);
warn('\n\rPasta deletada:', this.fullPath.outPut);
}
throw new Error(error);
}
}
public async gerarPDFImagens(prop: any) {
try {
rimraf.sync(this.fullPath.files);
rimraf.sync(this.fullPath.images);
if (!existsSync(this.fullPath.images)) {
if (debug) {
warn('Deletando e recriando', this.fullPath.images);
}
mkdirSync(this.fullPath.images);
}
if (debug) {
warn('\n\rPasta deletada:', this.fullPath.files);
warn('\n\rPasta recriada:', this.fullPath.images);
log('\n\rGerado em:', this.fullPath.tempFile);
}
const tempFile = readdirSync(this.fullPath.outPut);
if (!tempFile.length) {
if (debug) {
warn(
`Nenhum arquivo encontrado em ${this.fullPath.outPut}. Ocorreu algum erro.`,
);
}
return true;
}
if (debug) {
log(
`\n\rConvertendo PDFs para imagens, pasta:\n\r${this.fullPath.outPut}\n\r`,
);
}
await this.converterPDFs(tempFile, this.fullPath.outPut);
const images = readdirSync(this.fullPath.images);
if (debug) {
log(
`\n\rGerando um PDF a partir das imagens da pasta temporária:\n\r${this.fullPath.images}\n\r`,
);
}
// Definindo stream final
this.finalDoc.pipe(createWriteStream(this.fullPath.endFile));
// Desenhando PDF Final
for (const image of images) {
if (
image.endsWith('.png') ||
image.endsWith('.jpg') ||
image.endsWith('.jpeg')
) {
if (debug) {
log(`Referência: ${this.fullPath.images}${image}`);
}
this.finalDoc
.addPage({
size: [prop.width, prop.height],
margin: 0,
})
.image(`${this.fullPath.images}${image}`, 0, 0, {
fit: [prop.width, prop.height],
align: 'center',
valign: 'center',
})
.save();
}
}
this.finalDoc.end();
return new Promise((resolve, reject) => {
watchFile(this.fullPath.endFile, async (event) => {
if (event) {
try {
unwatchFile(this.fullPath.endFile);
rimraf.sync(this.fullPath.images);
if (debug) {
warn('\n\rPasta deletada:', this.fullPath.images);
log('\n\rGerado em:', this.fullPath.endFile);
}
const sha1Hash = DocumentoPDF.gerarHash(
process.env.PDFAUTH_HASH_KEY!,
[this.uuid1, ...prop.pages.map((el: any) => el.uuid2)],
);
this.retorno = {
data: new Date(),
nomeArquivo: this.outPutName,
tamanho: statSync(this.fullPath.endFile).size,
idDocumento: this.uuid1,
idPaginas: prop.pages,
caminhos: {
arquivo: this.fullPath.endFile,
pasta: this.fullPath.outPut,
},
sha1Hash,
};
resolve(true);
} catch (error) {
reject(error);
}
}
});
});
} catch (error) {
throw new Error(error);
}
}
public serialize(): IDocumentoPDFSerialized {
return this.retorno;
}
}
export { DocumentoPDF, IDocumentoPDFSerialized };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment