tenta fix pdfservice.js

This commit is contained in:
rodrigo committed 2025-04-22 10:42:13 -03:00
1 parent 79db5cc065
commit c485960759
1 file changed
+130 -150
+130 -150
View File
@@ -266,36 +266,35 @@ export class PdfService {
.replace(/style="[^"]*?text-indent:\s*(\d+(?:\.\d+)?(?:pt|px|em))[^"]*?"/gi, 'data-indent="$1"') .replace(/style="[^"]*?text-indent:\s*(\d+(?:\.\d+)?(?:pt|px|em))[^"]*?"/gi, 'data-indent="$1"')
.replace(/style="[^"]*?text-align:\s*(center|justify|right)[^"]*?"/gi, 'data-align="$1"') .replace(/style="[^"]*?text-align:\s*(center|justify|right)[^"]*?"/gi, 'data-align="$1"')
// Remove todos os outros atributos de estilo // Remove todos os outros atributos de estilo
.replace(/\s+style="[^"]*?"/gi, '')\s*(\d+(?:\.\d+)?(?:pt|px|em))[^"]*?"/gi, 'data-indent="$1"') .replace(/\s+style="[^"]*?"/gi, '')
// Substitui classes específicas por atributos de dados para preservar formatação // Substitui classes específicas por atributos de dados para preservar formatação
.replace(/class="[^"]*?MsoListParagraph[^"]*?"/gi, 'data-list-para="true"') .replace(/class="[^"]*?MsoListParagraph[^"]*?"/gi, 'data-list-para="true"')
// Normaliza blockquote e indentaçõesatributos de dados para preservar formatação // Normaliza blockquote e indentações
.replace(/<blockquote[^>]*>/gi, '<p data-indent="40pt">')list-para="true"') .replace(/<blockquote[^>]*>/gi, '<p data-indent="40pt">')
.replace(/<\/blockquote>/gi, '</p>')s .replace(/<\/blockquote>/gi, '</p>')
// Trata listaskquote[^>]*>/gi, '<p data-indent="40pt">') // Trata listas
.replace(/<li[^>]*>/gi, '<p data-list-item="true">• ') .replace(/<li[^>]*>/gi, '<p data-list-item="true">• ')
.replace(/<\/li>/gi, '</p>'); .replace(/<\/li>/gi, '</p>');
.replace(/<li[^>]*>/gi, '<p data-list-item="true">• ')
// Remove todas as tags, exceto <b>, <strong>, <i>, <em>, <u>, <br>, <p>, <span> // Remove todas as tags, exceto <b>, <strong>, <i>, <em>, <u>, <br>, <p>, <span>
// e mantém os atributos data-* que acabamos de criar para formatação // e mantém os atributos data-* que acabamos de criar para formatação
return simplifiedom alinhamento para nosso formato especial return simplified
.replace(/<(b|strong|i|em|u|p|span|br)([^>]*)>/gi, (match, tag, attrs) => {>]*>/gi, '<p>§§ALIGN=$1§§'); .replace(/<(b|strong|i|em|u|p|span|br)([^>]*)>/gi, (match, tag, attrs) => {
// Preserva apenas data-* atributos // Preserva apenas data-* atributos
const dataAttrs = attrs.match(/\s+data-[^=]+="[^"]*"/gi) || []; const dataAttrs = attrs.match(/\s+data-[^=]+="[^"]*"/gi) || [];
return `<${tag}${dataAttrs.join('')}>`;a-indent="([^"]+)"[^>]*>/gi, '<p>§§INDENT=$1§§'); return `<${tag}${dataAttrs.join('')}>`;
}) })
// Remove todas as outras tags não suportadasto interno // Remove todas as outras tags não suportadas
.replace(/<(?!\/?(?:b|strong|i|em|u|br|p|span))[^>]+>/gi, '') .replace(/<(?!\/?(?:b|strong|i|em|u|br|p|span))[^>]+>/gi, '')
// Converte números seguidos de parêntese (como em listas numeradas) em formato padronizado // Converte números seguidos de parêntese (como em listas numeradas) em formato padronizado
.replace(/(\d+\))\s+/g, '$1 '); '**') .replace(/(\d+\))\s+/g, '$1 ');
} .replace(/<\/?(?:em|i)>/gi, '_') }
.replace(/<\/?u>/gi, '_')
// Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc) // Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc)
function decodeHtmlEntities(str: string): string { function decodeHtmlEntities(str: string): string {
if (!str) return '';]*>/gi, '') if (!str) return '';
// Remove todas as outras tags
// Como estamos em Node.js, usamos apenas o método de substituição // Como estamos em Node.js, usamos apenas o método de substituição
return str(/\r\n/g, '\n') return str
.replace(/\r\n/g, '\n')
// Acentos minúsculos e maiúsculos // Acentos minúsculos e maiúsculos
.replace(/&Aacute;/g, 'Á').replace(/&aacute;/g, 'á') .replace(/&Aacute;/g, 'Á').replace(/&aacute;/g, 'á')
.replace(/&Eacute;/g, 'É').replace(/&eacute;/g, 'é') .replace(/&Eacute;/g, 'É').replace(/&eacute;/g, 'é')
@@ -303,208 +302,189 @@ export class PdfService {
.replace(/&Oacute;/g, 'Ó').replace(/&oacute;/g, 'ó') .replace(/&Oacute;/g, 'Ó').replace(/&oacute;/g, 'ó')
.replace(/&Uacute;/g, 'Ú').replace(/&uacute;/g, 'ú') .replace(/&Uacute;/g, 'Ú').replace(/&uacute;/g, 'ú')
.replace(/&Agrave;/g, 'À').replace(/&agrave;/g, 'à') .replace(/&Agrave;/g, 'À').replace(/&agrave;/g, 'à')
.replace(/&Egrave;/g, 'È').replace(/&egrave;/g, 'è')ação, símbolos, º, etc) .replace(/&Egrave;/g, 'È').replace(/&egrave;/g, 'è')
.replace(/&Igrave;/g, 'Ì').replace(/&igrave;/g, 'ì') .replace(/&Igrave;/g, 'Ì').replace(/&igrave;/g, 'ì')
.replace(/&Ograve;/g, 'Ò').replace(/&ograve;/g, 'ò') .replace(/&Ograve;/g, 'Ò').replace(/&ograve;/g, 'ò')
.replace(/&Ugrave;/g, 'Ù').replace(/&ugrave;/g, 'ù') .replace(/&Ugrave;/g, 'Ù').replace(/&ugrave;/g, 'ù')
.replace(/&Acirc;/g, 'Â').replace(/&acirc;/g, 'â')e substituição .replace(/&Acirc;/g, 'Â').replace(/&acirc;/g, 'â')
.replace(/&Ecirc;/g, 'Ê').replace(/&ecirc;/g, 'ê') .replace(/&Ecirc;/g, 'Ê').replace(/&ecirc;/g, 'ê')
.replace(/&Icirc;/g, 'Î').replace(/&icirc;/g, 'î') .replace(/&Icirc;/g, 'Î').replace(/&icirc;/g, 'î')
.replace(/&Ocirc;/g, 'Ô').replace(/&ocirc;/g, 'ô')') .replace(/&Ocirc;/g, 'Ô').replace(/&ocirc;/g, 'ô')
.replace(/&Ucirc;/g, 'Û').replace(/&ucirc;/g, 'û')') .replace(/&Ucirc;/g, 'Û').replace(/&ucirc;/g, 'û')
.replace(/&Atilde;/g, 'Ã').replace(/&atilde;/g, 'ã') .replace(/&Atilde;/g, 'Ã').replace(/&atilde;/g, 'ã')
.replace(/&Otilde;/g, 'Õ').replace(/&otilde;/g, 'õ') .replace(/&Otilde;/g, 'Õ').replace(/&otilde;/g, 'õ')
.replace(/&Ccedil;/g, 'Ç').replace(/&ccedil;/g, 'ç') .replace(/&Ccedil;/g, 'Ç').replace(/&ccedil;/g, 'ç')
.replace(/&Ntilde;/g, 'Ñ').replace(/&ntilde;/g, 'ñ') .replace(/&Ntilde;/g, 'Ñ').replace(/&ntilde;/g, 'ñ')
.replace(/&Uuml;/g, 'Ü').replace(/&uuml;/g, 'ü')'è') .replace(/&Uuml;/g, 'Ü').replace(/&uuml;/g, 'ü')
// Símbolos comuns/g, 'Ì').replace(/&igrave;/g, 'ì') // Símbolos comuns
.replace(/&ordm;/g, 'º')').replace(/&ograve;/g, 'ò') .replace(/&ordm;/g, 'º')
.replace(/&ordf;/g, 'ª')').replace(/&ugrave;/g, 'ù') .replace(/&ordf;/g, 'ª')
.replace(/&sect;/g, '§')).replace(/&acirc;/g, 'â') .replace(/&sect;/g, '§')
.replace(/&deg;/g, '°')').replace(/&ecirc;/g, 'ê') .replace(/&deg;/g, '°')
.replace(/&copy;/g, '©')).replace(/&icirc;/g, 'î') .replace(/&copy;/g, '©')
.replace(/&reg;/g, '®')').replace(/&ocirc;/g, 'ô') .replace(/&reg;/g, '®')
.replace(/&para;/g, '')).replace(/&ucirc;/g, 'û') .replace(/&para;/g, '¶')
.replace(/&bull;/g, '•')').replace(/&atilde;/g, 'ã') .replace(/&bull;/g, '•')
.replace(/&ndash;/g, '–')).replace(/&otilde;/g, 'õ') .replace(/&ndash;/g, '–')
.replace(/&mdash;/g, '—')).replace(/&ccedil;/g, 'ç') .replace(/&mdash;/g, '—')
.replace(/&laquo;/g, '«')).replace(/&ntilde;/g, 'ñ') .replace(/&laquo;/g, '«')
.replace(/&raquo;/g, '»')replace(/&uuml;/g, 'ü') .replace(/&raquo;/g, '»')
.replace(/&lsquo;/g, '‘') .replace(/&lsquo;/g, '‘')
.replace(/&rsquo;/g, '’') .replace(/&rsquo;/g, '’')
.replace(/&ldquo;/g, '“') .replace(/&ldquo;/g, '“')
.replace(/&rdquo;/g, '”') .replace(/&rdquo;/g, '”')
.replace(/&euro;/g, '€') .replace(/&euro;/g, '€')
.replace(/&yen;/g, '¥')) .replace(/&yen;/g, '¥')
.replace(/&pound;/g, '£') .replace(/&pound;/g, '£')
.replace(/&cent;/g, '¢') .replace(/&cent;/g, '¢')
.replace(/&nbsp;/g, ' ') .replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')') .replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')) .replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")') .replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')»') .replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')‘') .replace(/&gt;/g, '>')
// Numéricossquo;/g, '’') // Numéricos
.replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code));
} .replace(/&rdquo;/g, '”') }
.replace(/&euro;/g, '€')
const texto = decodeHtmlEntities( .replace(/&yen;/g, '¥') const texto = decodeHtmlEntities(
stripUnsupportedTags( stripUnsupportedTags(
this.processarTemplate(conteudo, escritorio) this.processarTemplate(conteudo, escritorio)
) .replace(/&nbsp;/g, ' ') )
); );
(/&quot;/g, '"')
// Process paragraph alignment and formatting#39;/g, "'") // Process paragraph alignment and formatting
const paragrafos = texto.split(/\n{2,}/).map(p => {(/&lt;/g, '<') const paragrafos = texto.split(/\n{2,}/).map(p => {
let paragraph = p.trim();(/&gt;/g, '>') let paragraph = p.trim();
if (!paragraph) return '';icos if (!paragraph) return '';
(\d+);/g, (m, code) => String.fromCharCode(code));
// Process paragraph alignment // Process paragraph alignment
let align = 'justify'; // default let align = 'justify'; // default
const alignMatch = paragraph.match(/^§§ALIGN=(center|justify|right)§§/); const alignMatch = paragraph.match(/^§§ALIGN=(center|justify|right)§§/);
if (alignMatch) {um parágrafo com formatação inline (negrito, itálico, etc) if (alignMatch) {
align = alignMatch[1];rágrafo, ou null se precisar de nova página align = alignMatch[1];
paragraph = paragraph.substring(alignMatch[0].length); paragraph = paragraph.substring(alignMatch[0].length);
}ragraph( }
// Process indentation // Process indentation
let indent = 0; let indent = 0;
const indentMatch = paragraph.match(/^§§INDENT=([0-9.]+)(?:pt|px|em)§§/); const indentMatch = paragraph.match(/^§§INDENT=([0-9.]+)(?:pt|px|em)§§/);
if (indentMatch) { if (indentMatch) {
indent = parseFloat(indentMatch[1]); indent = parseFloat(indentMatch[1]);
paragraph = paragraph.substring(indentMatch[0].length);ontItalic: any, paragraph = paragraph.substring(indentMatch[0].length);
} fontBoldItalic: any, if (forceNewPage || y < marginBottom + fontSize * 2) { }
// Mark paragraph with alignment and indentation // Mark paragraph with alignment and indentation
return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph}`; return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph}`;
}).filter(Boolean); maxWidth: number, // Dividir o texto em palavras preservando as tags }).filter(Boolean);
let timbradoPdf: PDFDocument | null = null;}; let timbradoPdf: PDFDocument | null = null;
let timbradoPageCount = 0;er, let timbradoPageCount = 0;
let timbradoPages: any[] = [];: numberara encontrar tags <b>, </b>, <i>, </i>, <strong>, </strong>, <em>, </em> let timbradoPages: any[] = [];
if (timbradoExists) { number | null {const regex = /<\/?(?:b|strong|i|em)>/gi; if (timbradoExists) {
const timbradoBytes = await fs.readFile(timbradoPath);om + fontSize * 2) { const timbradoBytes = await fs.readFile(timbradoPath);
timbradoPdf = await PDFDocument.load(timbradoBytes);necessidade de nova página timbradoPdf = await PDFDocument.load(timbradoBytes);
timbradoPageCount = timbradoPdf.getPageCount();} timbradoPageCount = timbradoPdf.getPageCount();
timbradoPages = []; timbradoPages = [];
for (let i = 0; i < timbradoPageCount; i++) {o em palavras preservando as tags = text; for (let i = 0; i < timbradoPageCount; i++) {
const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]);: string; isBold: boolean; isItalic: boolean }[] = []; const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]);
timbradoPages.push(embedded);let currentSegment = { text: '', isBold: false, isItalic: false };// Processar tags de indentação e alinhamento timbradoPages.push(embedded);
} }
}ntrar tags <b>, </b>, <i>, </i>, <strong>, </strong>, <em>, </em>ign; }
/gi;
let currentPar = 0;[0-9.]+(?:pt|px|em))§§/); let currentPar = 0;
let pageIndex = 0; let pageIndex = 0;
while (currentPar < paragrafos.length) { while (currentPar < paragrafos.length) {
let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);o para manipulaçãoseInt(indentValue); let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
if (timbradoPages.length > 0) {t;indentPt = 20; // Padrão: 20pt if (timbradoPages.length > 0) {
const embedded = timbradoPages[pageIndex % timbradoPages.length];textToProcess = textToProcess.substring(indentMatch[0].length); const embedded = timbradoPages[pageIndex % timbradoPages.length];
page.drawPage(embedded, {// Processar tags de indentação e alinhamento x += indentPt; page.drawPage(embedded, {
x: 0, x: 0,
y: 0,lign; y: 0,
width: PAGE_WIDTH, width: PAGE_WIDTH,
height: PAGE_HEIGHT|px|em))§§/);|right)§§/); height: PAGE_HEIGHT
});f (indentMatch) {f (alignMatch) { });
} const indentValue = indentMatch[1]; textAlign = alignMatch[1] as 'center' | 'justify' | 'left'; }
let y = PAGE_HEIGHT - margens.top;
while (currentPar < paragrafos.length) { 20; // Padrão: 20pt
const paraText = paragrafos[currentPar];substring(indentMatch[0].length);
// Extract alignmentaxWidth -= indentPt;/ Adiciona texto entre as tags let y = PAGE_HEIGHT - margens.top;
let align: 'left' | 'center' | 'justify' = 'left';f (match.index > lastIndex) { while (currentPar < paragrafos.length) {
const alignMatch = paraText.match(/^§§ALIGN=(center|justify|right)§§/);ss.substring(lastIndex, match.index); const paraText = paragrafos[currentPar];
if (alignMatch) {h(/^§§ALIGN=(center|justify|right)§§/);
// Extract alignment
let align: 'left' | 'center' | 'justify' = 'left';
const alignMatch = paraText.match(/^§§ALIGN=(center|justify|right)§§/);
if (alignMatch) {
align = alignMatch[1] as 'center' | 'justify' | 'left'; align = alignMatch[1] as 'center' | 'justify' | 'left';
}stify' | 'left'; }
s.substring(alignMatch[0].length);Case();
// Extract indentation // Extract indentation
let indent = 0; let indent = 0;
const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/); ((match = regex.exec(textToProcess)) !== null) {f (currentSegment.text) { const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/);
if (indentMatch) {egment }); if (indentMatch) {
indent = parseFloat(indentMatch[1]); indent = parseFloat(indentMatch[1]);
}tring(lastIndex, match.index); }
// Clean text // Clean text
let text = paraTextio let text = paraText
.replace(/^§§ALIGN=(center|justify|right)§§/, '')st tag = match[0].toLowerCase();f (currentSegment.text) { .replace(/^§§ALIGN=(center|justify|right)§§/, '')
.replace(/§§INDENT=[0-9.]+§§/, '')trong>') {gment }); .replace(/§§INDENT=[0-9.]+§§/, '')
.trim(); .trim();
// Process bold/italic formattingntSegment });lse;
text = text
.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>')
.replace(/_(.*?)_/g, '<i>$1</i>');urrentSegment.isBold = true;f (currentSegment.text) {
=== '</strong>') {ment });
let x = margens.left + indent; let x = margens.left + indent;
let parWidth = MAX_WIDTH - indent; let parWidth = MAX_WIDTH - indent;
ntSegment });true;
let yAfter = renderInlineFormattedParagraph( let yAfter = renderInlineFormattedParagraph(
page, page,
text,urrentSegment.isBold = false;f (currentSegment.text) { text,
x,= '<em>') {ent }); x,
y, // Finaliza o segmento atual se não vazio currentSegment.text = ''; y,
font, if (currentSegment.text) { } font,
fontBold, fontBold,
fontItalic, currentSegment.text = ''; } fontItalic,
fontBoldItalic, } fontBoldItalic,
FONT_SIZE,= true;match[0].length; FONT_SIZE,
rgb(0, 0, 0),'</em>') { rgb(0, 0, 0),
align, align,
parWidth, if (currentSegment.text) {/ Adiciona o texto restante parWidth,
false, segments.push({ ...currentSegment });if (lastIndex < textToProcess.length) { false,
margens.top, margens.top,
margens.bottom, margens.bottom,
PAGE_HEIGHTlse; PAGE_HEIGHT
); }/ Adiciona o último segmento se não estiver vazio );
if (yAfter === null) { if (currentSegment.text) {
if (yAfter === null) {
pageIndex++; pageIndex++;
break; break;
} }
y = yAfter - FONT_SIZE * 0.5;xto restante segmento e divide em linhas y = yAfter - FONT_SIZE * 0.5;
currentPar++;textToProcess.length) {egments: { text: string; isBold: boolean; isItalic: boolean; width: number; font: any }[]; totalWidth: number }[] = []; currentPar++;
if (y < margens.bottom + FONT_SIZE * 1.5) break;currentSegment.text += textToProcess.substring(lastIndex);t currentLine: { segments: { text: string; isBold: boolean; isItalic: boolean; width: number; font: any }[]; totalWidth: number } = { if (y < margens.bottom + FONT_SIZE * 1.5) break;
}} segments: [], }
pageIndex++; pageIndex++;
}er vazio }
(currentSegment.text) {
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes); return Buffer.from(pdfBytes);
} catch (error) { } catch (error) {
console.error('Erro geral ao gerar PDF:', error);rocessa cada segmento e divide em linhasr (let i = 0; i < words.length; i++) { console.error('Erro geral ao gerar PDF:', error);
throw error; boolean; width: number; font: any }[]; totalWidth: number }[] = []; throw error;
}n; width: number; font: any }[]; totalWidth: number } = { }
} }
ntes se não for a primeira palavra da linha
private processarTemplate(template: string, dados: any): string { private processarTemplate(template: string, dados: any): string {
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const keyParts = key.trim().split('.');{ const keyParts = key.trim().split('.');
let value = dados;split(/\s+/);ment.isItalic) { let value = dados;
for (const part of keyParts) {gth; i++) {{ for (const part of keyParts) {
if (value && typeof value === 'object' && part in value) {rd = words[i];tFont = fontBold; if (value && typeof value === 'object' && part in value) {
value = value[part];talic) { value = value[part];
} else {segmentFont = fontItalic; } else {
value = undefined;// Adiciona espaço antes se não for a primeira palavra da linha} else { value = undefined;
break;; break;
}// Seleciona a fonte correta com base na formatação} }
} }
ntSize); return value !== undefined ? value : match;
return value !== undefined ? value : match;c; });
});d) {linha atual }
}wordWidth <= maxWidth) {
}
} } }); return value !== undefined ? value : match; } } break; value = undefined; } else { value = value[part]; if (value && typeof value === 'object' && part in value) { }
} }