merge conflict

This commit is contained in:
marshal
2026-06-29 12:45:40 +03:00
202 changed files with 16895 additions and 3799 deletions

View File

@@ -80,8 +80,15 @@ export class ContractPdfService {
this.logger.error(
`Puppeteer PDF failed (executable=${executablePath ?? 'default'}): ${err}`,
);
const fallback = this.htmlToBasicPdfBuffer(preparedHtml);
if (this.isValidPdf(fallback)) {
this.logger.warn(
`Using basic PDF fallback (${fallback.length} bytes). Install Chromium or set PUPPETEER_EXECUTABLE_PATH for full layout rendering.`,
);
return fallback;
}
throw new InternalServerErrorException(
'Contract PDF could not be generated. Ensure Chromium is installed or set PUPPETEER_EXECUTABLE_PATH.',
'PDF could not be generated. Ensure Chromium is installed or set PUPPETEER_EXECUTABLE_PATH.',
);
}
}
@@ -113,4 +120,85 @@ export class ContractPdfService {
buffer.subarray(0, 5).toString('ascii') === '%PDF-'
);
}
private htmlToBasicPdfBuffer(html: string): Buffer {
const text = this.htmlToPlainText(html);
const lines = this.wrapLines(text, 92).slice(0, 72);
const body = lines
.map((line, index) => {
const prefix = index === 0 ? '50 790 Td' : '0 -12 Td';
return `${prefix} (${this.escapePdfText(line)}) Tj`;
})
.join('\n');
const stream = `BT\n/F1 10 Tf\n12 TL\n${body}\nET`;
const objects = [
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
`<< /Length ${Buffer.byteLength(stream, 'latin1')} >>\nstream\n${stream}\nendstream`,
];
let pdf = '%PDF-1.4\n';
const offsets: number[] = [0];
objects.forEach((object, index) => {
offsets.push(Buffer.byteLength(pdf, 'latin1'));
pdf += `${index + 1} 0 obj\n${object}\nendobj\n`;
});
while (Buffer.byteLength(pdf, 'latin1') < MIN_VALID_PDF_BYTES) {
pdf += '% fallback padding\n';
}
const xrefOffset = Buffer.byteLength(pdf, 'latin1');
pdf += `xref\n0 ${objects.length + 1}\n`;
pdf += '0000000000 65535 f \n';
for (const offset of offsets.slice(1)) {
pdf += `${String(offset).padStart(10, '0')} 00000 n \n`;
}
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
return Buffer.from(pdf, 'latin1');
}
private htmlToPlainText(html: string): string {
return html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<\/(h1|h2|h3|p|div|tr|table|section|header|footer)>/gi, '\n')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;/g, "'")
.replace(/[^\x09\x0a\x0d\x20-\x7e]/g, '-')
.split('\n')
.map((line) => line.replace(/\s+/g, ' ').trim())
.filter(Boolean)
.join('\n');
}
private wrapLines(text: string, width: number): string[] {
const wrapped: string[] = [];
for (const rawLine of text.split('\n')) {
const words = rawLine.split(' ');
let line = '';
for (const word of words) {
const next = line ? `${line} ${word}` : word;
if (next.length > width && line) {
wrapped.push(line);
line = word;
} else {
line = next;
}
}
if (line) wrapped.push(line);
}
return wrapped.length ? wrapped : ['Document'];
}
private escapePdfText(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
}