tenta fix pdfservice.js
This commit is contained in:
1 parent
e91294df22
commit
f3ff4f8939
1 file changed
+134
-153
+134
-153
@@ -247,115 +247,6 @@ export class PdfService {
|
|||||||
const LINE_HEIGHT = FONT_SIZE * 1.5;
|
const LINE_HEIGHT = FONT_SIZE * 1.5;
|
||||||
const MAX_WIDTH = PAGE_WIDTH - margens.left - margens.right;
|
const MAX_WIDTH = PAGE_WIDTH - margens.left - margens.right;
|
||||||
|
|
||||||
// Função para decodificar entidades HTML comuns (acentuação e outros)
|
|
||||||
function decodeHtmlEntities(str: string): string {
|
|
||||||
if (!str) return '';
|
|
||||||
return str
|
|
||||||
// Acentos minúsculos e maiúsculos
|
|
||||||
.replace(/Á/g, 'Á').replace(/á/g, 'á')
|
|
||||||
.replace(/É/g, 'É').replace(/é/g, 'é')
|
|
||||||
.replace(/Í/g, 'Í').replace(/í/g, 'í')
|
|
||||||
.replace(/Ó/g, 'Ó').replace(/ó/g, 'ó')
|
|
||||||
.replace(/Ú/g, 'Ú').replace(/ú/g, 'ú')
|
|
||||||
.replace(/À/g, 'À').replace(/à/g, 'à')
|
|
||||||
.replace(/È/g, 'È').replace(/è/g, 'è')
|
|
||||||
.replace(/Ì/g, 'Ì').replace(/ì/g, 'ì')
|
|
||||||
.replace(/Ò/g, 'Ò').replace(/ò/g, 'ò')
|
|
||||||
.replace(/Ù/g, 'Ù').replace(/ù/g, 'ù')
|
|
||||||
.replace(/Â/g, 'Â').replace(/â/g, 'â')
|
|
||||||
.replace(/Ê/g, 'Ê').replace(/ê/g, 'ê')
|
|
||||||
.replace(/Î/g, 'Î').replace(/î/g, 'î')
|
|
||||||
.replace(/Ô/g, 'Ô').replace(/ô/g, 'ô')
|
|
||||||
.replace(/Û/g, 'Û').replace(/û/g, 'û')
|
|
||||||
.replace(/Ã/g, 'Ã').replace(/ã/g, 'ã')
|
|
||||||
.replace(/Õ/g, 'Õ').replace(/õ/g, 'õ')
|
|
||||||
.replace(/Ç/g, 'Ç').replace(/ç/g, 'ç')
|
|
||||||
.replace(/Ñ/g, 'Ñ').replace(/ñ/g, 'ñ')
|
|
||||||
.replace(/Ü/g, 'Ü').replace(/ü/g, 'ü')
|
|
||||||
// Outras entidades comuns
|
|
||||||
.replace(/ /gi, ' ')
|
|
||||||
.replace(/&/gi, '&')
|
|
||||||
.replace(/"/gi, '"')
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/</gi, '<')
|
|
||||||
.replace(/>/gi, '>')
|
|
||||||
.replace(/–/gi, '–')
|
|
||||||
.replace(/—/gi, '—')
|
|
||||||
.replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Função para converter HTML do TinyMCE em uma árvore de blocos com estilos
|
|
||||||
function parseHtmlToBlocks(html: string): any[] {
|
|
||||||
// Usa DOMParser se disponível, senão fallback simples
|
|
||||||
let doc: Document;
|
|
||||||
try {
|
|
||||||
const { JSDOM } = require('jsdom');
|
|
||||||
doc = new JSDOM(`<body>${html}</body>`).window.document;
|
|
||||||
} catch {
|
|
||||||
// fallback: ignora tags e retorna texto puro
|
|
||||||
return [{ text: html, tag: 'p', styles: {} }];
|
|
||||||
}
|
|
||||||
const blocks: any[] = [];
|
|
||||||
function parseNode(node: Node, parentStyles: any = {}) {
|
|
||||||
if (node.nodeType === 3) { // Text
|
|
||||||
let text = decodeHtmlEntities(node.textContent || '');
|
|
||||||
if (text.trim() !== '' || parentStyles['pre']) {
|
|
||||||
blocks.push({ text, tag: parentStyles.tag || 'span', styles: { ...parentStyles } });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.nodeType !== 1) return; // Element
|
|
||||||
const el = node as HTMLElement;
|
|
||||||
let styles = { ...parentStyles };
|
|
||||||
let tag = el.tagName.toLowerCase();
|
|
||||||
|
|
||||||
// TinyMCE: <span style="font-weight:bold">, <span style="font-style:italic">, <span style="text-decoration:underline">
|
|
||||||
if (el.style) {
|
|
||||||
if (el.style.fontWeight === 'bold' || tag === 'b' || tag === 'strong') styles.bold = true;
|
|
||||||
if (el.style.fontStyle === 'italic' || tag === 'i' || tag === 'em') styles.italic = true;
|
|
||||||
if (el.style.textDecoration?.includes('underline') || tag === 'u') styles.underline = true;
|
|
||||||
if (el.style.textAlign === 'center' || tag === 'center') styles.align = 'center';
|
|
||||||
if (el.style.textAlign === 'right') styles.align = 'right';
|
|
||||||
if (el.style.textIndent) styles.indent = true;
|
|
||||||
if (el.style.fontFamily) styles.fontFamily = el.style.fontFamily;
|
|
||||||
}
|
|
||||||
if (tag === 'b' || tag === 'strong') styles.bold = true;
|
|
||||||
if (tag === 'i' || tag === 'em') styles.italic = true;
|
|
||||||
if (tag === 'u') styles.underline = true;
|
|
||||||
if (tag === 'center') styles.align = 'center';
|
|
||||||
if (tag === 'pre') styles.pre = true;
|
|
||||||
if (tag === 'p' && el.style.textAlign === 'center') styles.align = 'center';
|
|
||||||
if (tag === 'p' && el.style.textAlign === 'right') styles.align = 'right';
|
|
||||||
if (tag === 'p' && el.style.textIndent) styles.indent = true;
|
|
||||||
if (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'h4' || tag === 'h5' || tag === 'h6') styles.heading = tag;
|
|
||||||
if (tag === 'ul' || tag === 'ol') styles.list = tag;
|
|
||||||
if (tag === 'li') styles.listItem = true;
|
|
||||||
|
|
||||||
// Blocos de texto
|
|
||||||
if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'pre', 'div', 'center'].includes(tag)) {
|
|
||||||
let blockText = '';
|
|
||||||
el.childNodes.forEach(child => {
|
|
||||||
if (child.nodeType === 3) blockText += child.textContent;
|
|
||||||
});
|
|
||||||
if (blockText.trim() !== '' || styles['pre']) {
|
|
||||||
blocks.push({ text: blockText, tag, styles: { ...styles } });
|
|
||||||
}
|
|
||||||
el.childNodes.forEach(child => parseNode(child, styles));
|
|
||||||
if (tag === 'li') blocks.push({ text: '', tag: 'br', styles: {} });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Quebra de linha
|
|
||||||
if (tag === 'br') {
|
|
||||||
blocks.push({ text: '', tag: 'br', styles: { ...styles } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Span ou inline
|
|
||||||
el.childNodes.forEach(child => parseNode(child, styles));
|
|
||||||
}
|
|
||||||
doc.body.childNodes.forEach(node => parseNode(node));
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carregar o timbrado (se houver) e criar o PDF final
|
// Carregar o timbrado (se houver) e criar o PDF final
|
||||||
let timbradoPdf: PDFDocument | null = null;
|
let timbradoPdf: PDFDocument | null = null;
|
||||||
let timbradoPageCount = 0;
|
let timbradoPageCount = 0;
|
||||||
@@ -366,7 +257,6 @@ export class PdfService {
|
|||||||
const timbradoBytes = await fs.readFile(timbradoPath);
|
const timbradoBytes = await fs.readFile(timbradoPath);
|
||||||
timbradoPdf = await PDFDocument.load(timbradoBytes);
|
timbradoPdf = await PDFDocument.load(timbradoBytes);
|
||||||
timbradoPageCount = timbradoPdf.getPageCount();
|
timbradoPageCount = timbradoPdf.getPageCount();
|
||||||
// Copiar as páginas do timbrado como EMBEDDED PAGE (PDFEmbeddedPage)
|
|
||||||
timbradoPages = [];
|
timbradoPages = [];
|
||||||
for (let i = 0; i < timbradoPageCount; i++) {
|
for (let i = 0; i < timbradoPageCount; i++) {
|
||||||
const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]);
|
const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]);
|
||||||
@@ -376,7 +266,6 @@ export class PdfService {
|
|||||||
|
|
||||||
// Fonte Fira Sans (se disponível), senão Helvetica
|
// Fonte Fira Sans (se disponível), senão Helvetica
|
||||||
let font = null, fontBold = null, fontItalic = null, fontBoldItalic = null;
|
let font = null, fontBold = null, fontItalic = null, fontBoldItalic = null;
|
||||||
let useCustomFont = false;
|
|
||||||
try {
|
try {
|
||||||
const firaPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Regular.ttf');
|
const firaPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Regular.ttf');
|
||||||
const firaBoldPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Bold.ttf');
|
const firaBoldPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Bold.ttf');
|
||||||
@@ -388,7 +277,7 @@ export class PdfService {
|
|||||||
fs.readFile(firaItalicPath).catch(() => null),
|
fs.readFile(firaItalicPath).catch(() => null),
|
||||||
fs.readFile(firaBoldItalicPath).catch(() => null),
|
fs.readFile(firaBoldItalicPath).catch(() => null),
|
||||||
]);
|
]);
|
||||||
if (fira) { font = await pdfDoc.embedFont(fira); useCustomFont = true; }
|
if (fira) font = await pdfDoc.embedFont(fira);
|
||||||
if (firaBoldBuf) fontBold = await pdfDoc.embedFont(firaBoldBuf);
|
if (firaBoldBuf) fontBold = await pdfDoc.embedFont(firaBoldBuf);
|
||||||
if (firaItalicBuf) fontItalic = await pdfDoc.embedFont(firaItalicBuf);
|
if (firaItalicBuf) fontItalic = await pdfDoc.embedFont(firaItalicBuf);
|
||||||
if (firaBoldItalicBuf) fontBoldItalic = await pdfDoc.embedFont(firaBoldItalicBuf);
|
if (firaBoldItalicBuf) fontBoldItalic = await pdfDoc.embedFont(firaBoldItalicBuf);
|
||||||
@@ -398,8 +287,96 @@ export class PdfService {
|
|||||||
if (!fontItalic) fontItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
|
if (!fontItalic) fontItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
|
||||||
if (!fontBoldItalic) fontBoldItalic = fontBold;
|
if (!fontBoldItalic) fontBoldItalic = fontBold;
|
||||||
|
|
||||||
// Parse TinyMCE HTML para blocos estilizados
|
// --- NOVO PARSER HTML TinyMCE PARA BLOCOS ---
|
||||||
const blocks = parseHtmlToBlocks(conteudo);
|
// Usa jsdom para manter estrutura de blocos e estilos
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
const dom = new JSDOM(`<body>${conteudo}</body>`);
|
||||||
|
const body = dom.window.document.body;
|
||||||
|
|
||||||
|
// Função auxiliar para obter estilos relevantes
|
||||||
|
function getBlockStyles(el: HTMLElement) {
|
||||||
|
const styles: any = {};
|
||||||
|
const computed = el.getAttribute('style') || '';
|
||||||
|
if (/font-weight:\s*bold/i.test(computed) || el.tagName === 'B' || el.tagName === 'STRONG') styles.bold = true;
|
||||||
|
if (/font-style:\s*italic/i.test(computed) || el.tagName === 'I' || el.tagName === 'EM') styles.italic = true;
|
||||||
|
if (/text-decoration:\s*underline/i.test(computed) || el.tagName === 'U') styles.underline = true;
|
||||||
|
if (/text-align:\s*center/i.test(computed) || el.align === 'center' || el.tagName === 'CENTER') styles.align = 'center';
|
||||||
|
if (/text-align:\s*right/i.test(computed) || el.align === 'right') styles.align = 'right';
|
||||||
|
if (/text-indent:\s*2em/i.test(computed)) styles.indent = true;
|
||||||
|
if (/font-family:\s*['"]?Fira Sans/i.test(computed)) styles.fontFamily = 'Fira Sans';
|
||||||
|
if (el.tagName.match(/^H[1-6]$/)) styles.heading = el.tagName.toLowerCase();
|
||||||
|
return styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para quebrar texto em linhas respeitando largura máxima
|
||||||
|
function breakLines(text: string, fontObj: any, fontSize: number, maxWidth: number) {
|
||||||
|
const words = text.split(' ');
|
||||||
|
const lines: string[] = [];
|
||||||
|
let currentLine = '';
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine = currentLine ? currentLine + ' ' + word : word;
|
||||||
|
const width = fontObj.widthOfTextAtSize(testLine, fontSize);
|
||||||
|
if (width > maxWidth && currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = word;
|
||||||
|
} else {
|
||||||
|
currentLine = testLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine) lines.push(currentLine);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para processar blocos do HTML
|
||||||
|
function parseBlocks(node: HTMLElement, parentStyles: any = {}) {
|
||||||
|
const blocks: any[] = [];
|
||||||
|
for (const el of Array.from(node.childNodes)) {
|
||||||
|
if (el.nodeType === 3) { // texto puro
|
||||||
|
let text = el.textContent || '';
|
||||||
|
if (text.trim() !== '') {
|
||||||
|
blocks.push({ text, tag: parentStyles.tag || 'span', styles: { ...parentStyles } });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (el.nodeType !== 1) continue;
|
||||||
|
const elem = el as HTMLElement;
|
||||||
|
let styles = { ...parentStyles, ...getBlockStyles(elem) };
|
||||||
|
let tag = elem.tagName.toLowerCase();
|
||||||
|
|
||||||
|
if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'center'].includes(tag)) {
|
||||||
|
let blockText = '';
|
||||||
|
elem.childNodes.forEach(child => {
|
||||||
|
if (child.nodeType === 3) blockText += child.textContent;
|
||||||
|
});
|
||||||
|
blocks.push({ text: blockText, tag, styles: { ...styles } });
|
||||||
|
blocks.push(...parseBlocks(elem, styles));
|
||||||
|
blocks.push({ tag: 'br', text: '', styles: {} });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (tag === 'br') {
|
||||||
|
blocks.push({ tag: 'br', text: '', styles: { ...styles } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (['b', 'strong', 'i', 'em', 'u', 'span'].includes(tag)) {
|
||||||
|
blocks.push(...parseBlocks(elem, styles));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Listas
|
||||||
|
if (tag === 'ul' || tag === 'ol') {
|
||||||
|
elem.childNodes.forEach(li => {
|
||||||
|
if (li.nodeType === 1 && (li as HTMLElement).tagName === 'LI') {
|
||||||
|
blocks.push({ text: '• ' + (li.textContent || ''), tag: 'li', styles: { ...styles } });
|
||||||
|
blocks.push({ tag: 'br', text: '', styles: {} });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gera os blocos do HTML do TinyMCE
|
||||||
|
const blocks = parseBlocks(body);
|
||||||
|
|
||||||
// Renderização dos blocos no PDF
|
// Renderização dos blocos no PDF
|
||||||
let pageIndex = 0;
|
let pageIndex = 0;
|
||||||
@@ -433,53 +410,57 @@ export class PdfService {
|
|||||||
// Remove quebras de linha do texto antes de medir e desenhar
|
// Remove quebras de linha do texto antes de medir e desenhar
|
||||||
const cleanText = (block.text || '').replace(/\r?\n/g, ' ');
|
const cleanText = (block.text || '').replace(/\r?\n/g, ' ');
|
||||||
|
|
||||||
let x = margens.left + (block.styles.indent ? FONT_SIZE * 2 : 0);
|
// Quebra em múltiplas linhas se necessário
|
||||||
let width = usedFont.widthOfTextAtSize(cleanText, blockFontSize);
|
const lines = breakLines(cleanText, usedFont, blockFontSize, MAX_WIDTH);
|
||||||
|
|
||||||
// Centralização
|
for (const line of lines) {
|
||||||
if (block.styles.align === 'center' || block.styles.heading === 'h1') {
|
let x = margens.left + (block.styles.indent ? FONT_SIZE * 2 : 0);
|
||||||
x = margens.left + ((MAX_WIDTH - width) / 2);
|
let width = usedFont.widthOfTextAtSize(line, blockFontSize);
|
||||||
}
|
|
||||||
if (block.styles.align === 'right') {
|
|
||||||
x = margens.left + (MAX_WIDTH - width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quebra de página se necessário
|
// Centralização
|
||||||
if (y < margens.bottom + blockFontSize * 1.2) {
|
if (block.styles.align === 'center' || block.styles.heading === 'h1') {
|
||||||
pageIndex++;
|
x = margens.left + ((MAX_WIDTH - width) / 2);
|
||||||
page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
}
|
||||||
if (timbradoPages && timbradoPages.length > 0) {
|
if (block.styles.align === 'right') {
|
||||||
const embedded = timbradoPages[pageIndex % timbradoPages.length];
|
x = margens.left + (MAX_WIDTH - width);
|
||||||
page.drawPage(embedded, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: PAGE_WIDTH,
|
|
||||||
height: PAGE_HEIGHT
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
y = PAGE_HEIGHT - margens.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renderiza o texto (sem \n)
|
// Quebra de página se necessário
|
||||||
page.drawText(cleanText, {
|
if (y < margens.bottom + blockFontSize * 1.2) {
|
||||||
x,
|
pageIndex++;
|
||||||
y,
|
page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
||||||
size: blockFontSize,
|
if (timbradoPages && timbradoPages.length > 0) {
|
||||||
font: usedFont,
|
const embedded = timbradoPages[pageIndex % timbradoPages.length];
|
||||||
color: rgb(0, 0, 0)
|
page.drawPage(embedded, {
|
||||||
});
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: PAGE_WIDTH,
|
||||||
|
height: PAGE_HEIGHT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
y = PAGE_HEIGHT - margens.top;
|
||||||
|
}
|
||||||
|
|
||||||
// Sublinhado
|
page.drawText(line, {
|
||||||
if (block.styles.underline) {
|
x,
|
||||||
page.drawLine({
|
y,
|
||||||
start: { x, y: y - 2 },
|
size: blockFontSize,
|
||||||
end: { x: x + width, y: y - 2 },
|
font: usedFont,
|
||||||
thickness: 0.7,
|
|
||||||
color: rgb(0, 0, 0)
|
color: rgb(0, 0, 0)
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
y -= blockFontSize * 1.5;
|
// Sublinhado
|
||||||
|
if (block.styles.underline) {
|
||||||
|
page.drawLine({
|
||||||
|
start: { x, y: y - 2 },
|
||||||
|
end: { x: x + width, y: y - 2 },
|
||||||
|
thickness: 0.7,
|
||||||
|
color: rgb(0, 0, 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
y -= blockFontSize * 1.5;
|
||||||
|
}
|
||||||
// Espaço extra entre parágrafos
|
// Espaço extra entre parágrafos
|
||||||
if (block.tag === 'p' || block.styles.heading) {
|
if (block.tag === 'p' || block.styles.heading) {
|
||||||
y -= blockFontSize * 0.3;
|
y -= blockFontSize * 0.3;
|
||||||
|
|||||||
Reference in new issue
Block a user