From c485960759081f5a0152dce0dcfbafe9d89d1b49 Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Tue, 22 Apr 2025 10:42:13 -0300 Subject: [PATCH] tenta fix pdfservice.js --- src/server/services/PdfService.ts | 278 ++++++++++++++---------------- 1 file changed, 129 insertions(+), 149 deletions(-) diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 2b37adc..cff6ab9 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -266,36 +266,35 @@ export class PdfService { .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"') // 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 .replace(/class="[^"]*?MsoListParagraph[^"]*?"/gi, 'data-list-para="true"') - // Normaliza blockquote e indentaçõesatributos de dados para preservar formatação - .replace(/]*>/gi, '

')list-para="true"') - .replace(/<\/blockquote>/gi, '

')s - // Trata listaskquote[^>]*>/gi, '

') + // Normaliza blockquote e indentações + .replace(/]*>/gi, '

') + .replace(/<\/blockquote>/gi, '

') + // Trata listas .replace(/]*>/gi, '

• ') .replace(/<\/li>/gi, '

'); - .replace(/]*>/gi, '

• ') // Remove todas as tags, exceto , , , , ,
,

, // e mantém os atributos data-* que acabamos de criar para formatação - return simplifiedom alinhamento para nosso formato especial - .replace(/<(b|strong|i|em|u|p|span|br)([^>]*)>/gi, (match, tag, attrs) => {>]*>/gi, '

§§ALIGN=$1§§'); + return simplified + .replace(/<(b|strong|i|em|u|p|span|br)([^>]*)>/gi, (match, tag, attrs) => { // Preserva apenas data-* atributos const dataAttrs = attrs.match(/\s+data-[^=]+="[^"]*"/gi) || []; - return `<${tag}${dataAttrs.join('')}>`;a-indent="([^"]+)"[^>]*>/gi, '

§§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, '') // Converte números seguidos de parêntese (como em listas numeradas) em formato padronizado - .replace(/(\d+\))\s+/g, '$1 '); '**') - } .replace(/<\/?(?:em|i)>/gi, '_') - .replace(/<\/?u>/gi, '_') + .replace(/(\d+\))\s+/g, '$1 '); + } + // Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc) function decodeHtmlEntities(str: string): string { - if (!str) return '';]*>/gi, '') - // Remove todas as outras tags + if (!str) return ''; // 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 .replace(/Á/g, 'Á').replace(/á/g, 'á') .replace(/É/g, 'É').replace(/é/g, 'é') @@ -303,208 +302,189 @@ export class PdfService { .replace(/Ó/g, 'Ó').replace(/ó/g, 'ó') .replace(/Ú/g, 'Ú').replace(/ú/g, 'ú') .replace(/À/g, 'À').replace(/à/g, 'à') - .replace(/È/g, 'È').replace(/è/g, 'è')ação, símbolos, º, etc) + .replace(/È/g, 'È').replace(/è/g, 'è') .replace(/Ì/g, 'Ì').replace(/ì/g, 'ì') .replace(/Ò/g, 'Ò').replace(/ò/g, 'ò') .replace(/Ù/g, 'Ù').replace(/ù/g, 'ù') - .replace(/Â/g, 'Â').replace(/â/g, 'â')e substituição + .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, 'ü')'è') - // Símbolos comuns/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, 'ü') + // Símbolos comuns + .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, '>')‘') - // Numéricossquo;/g, '’') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + // Numéricos .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); - } .replace(/”/g, '”') - .replace(/€/g, '€') - const texto = decodeHtmlEntities( .replace(/¥/g, '¥') + } + + const texto = decodeHtmlEntities( stripUnsupportedTags( this.processarTemplate(conteudo, escritorio) - ) .replace(/ /g, ' ') + ) ); -(/"/g, '"') - // Process paragraph alignment and formatting#39;/g, "'") - const paragrafos = texto.split(/\n{2,}/).map(p => {(/</g, '<') - let paragraph = p.trim();(/>/g, '>') - if (!paragraph) return '';icos - (\d+);/g, (m, code) => String.fromCharCode(code)); + + // Process paragraph alignment and formatting + const paragrafos = texto.split(/\n{2,}/).map(p => { + let paragraph = p.trim(); + if (!paragraph) return ''; + // Process paragraph alignment let align = 'justify'; // default const alignMatch = paragraph.match(/^§§ALIGN=(center|justify|right)§§/); - if (alignMatch) {um parágrafo com formatação inline (negrito, itálico, etc) - align = alignMatch[1];rágrafo, ou null se precisar de nova página + if (alignMatch) { + align = alignMatch[1]; paragraph = paragraph.substring(alignMatch[0].length); - }ragraph( + } // Process indentation let indent = 0; const indentMatch = paragraph.match(/^§§INDENT=([0-9.]+)(?:pt|px|em)§§/); if (indentMatch) { indent = parseFloat(indentMatch[1]); - paragraph = paragraph.substring(indentMatch[0].length);ontItalic: any, - } fontBoldItalic: any, if (forceNewPage || y < marginBottom + fontSize * 2) { + paragraph = paragraph.substring(indentMatch[0].length); + } // Mark paragraph with alignment and indentation 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 timbradoPageCount = 0;er, - let timbradoPages: any[] = [];: numberara encontrar tags , , , , , , , - if (timbradoExists) { number | null {const regex = /<\/?(?:b|strong|i|em)>/gi; - const timbradoBytes = await fs.readFile(timbradoPath);om + fontSize * 2) { - timbradoPdf = await PDFDocument.load(timbradoBytes);necessidade de nova página - timbradoPageCount = timbradoPdf.getPageCount();} + let timbradoPdf: PDFDocument | null = null; + let timbradoPageCount = 0; + let timbradoPages: any[] = []; + if (timbradoExists) { + const timbradoBytes = await fs.readFile(timbradoPath); + timbradoPdf = await PDFDocument.load(timbradoBytes); + timbradoPageCount = timbradoPdf.getPageCount(); timbradoPages = []; - for (let i = 0; i < timbradoPageCount; i++) {o em palavras preservando as tags = text; - const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]);: string; isBold: boolean; isItalic: boolean }[] = []; - timbradoPages.push(embedded);let currentSegment = { text: '', isBold: false, isItalic: false };// Processar tags de indentação e alinhamento + for (let i = 0; i < timbradoPageCount; i++) { + const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]); + timbradoPages.push(embedded); } - }ntrar tags , , , , , , , ign; -/gi; - let currentPar = 0;[0-9.]+(?:pt|px|em))§§/); + } + + let currentPar = 0; let pageIndex = 0; while (currentPar < paragrafos.length) { - let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);o para manipulaçãoseInt(indentValue); - if (timbradoPages.length > 0) {t;indentPt = 20; // Padrão: 20pt - const embedded = timbradoPages[pageIndex % timbradoPages.length];textToProcess = textToProcess.substring(indentMatch[0].length); - page.drawPage(embedded, {// Processar tags de indentação e alinhamento x += indentPt; + let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); + if (timbradoPages.length > 0) { + const embedded = timbradoPages[pageIndex % timbradoPages.length]; + page.drawPage(embedded, { x: 0, - y: 0,lign; + y: 0, width: PAGE_WIDTH, - height: PAGE_HEIGHT|px|em))§§/);|right)§§/); - });f (indentMatch) {f (alignMatch) { - } const indentValue = indentMatch[1]; textAlign = alignMatch[1] as 'center' | 'justify' | 'left'; + height: PAGE_HEIGHT + }); + } + let y = PAGE_HEIGHT - margens.top; - while (currentPar < paragrafos.length) { 20; // Padrão: 20pt - const paraText = paragrafos[currentPar];substring(indentMatch[0].length); + while (currentPar < paragrafos.length) { + const paraText = paragrafos[currentPar]; - // Extract alignmentaxWidth -= indentPt;/ Adiciona texto entre as tags - let align: 'left' | 'center' | 'justify' = 'left';f (match.index > lastIndex) { - const alignMatch = paraText.match(/^§§ALIGN=(center|justify|right)§§/);ss.substring(lastIndex, match.index); - 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'; - }stify' | 'left'; - s.substring(alignMatch[0].length);Case(); + } + // Extract indentation let indent = 0; - const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/); ((match = regex.exec(textToProcess)) !== null) {f (currentSegment.text) { - if (indentMatch) {egment }); + const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/); + if (indentMatch) { indent = parseFloat(indentMatch[1]); - }tring(lastIndex, match.index); + } // Clean text - let text = paraTextio - .replace(/^§§ALIGN=(center|justify|right)§§/, '')st tag = match[0].toLowerCase();f (currentSegment.text) { - .replace(/§§INDENT=[0-9.]+§§/, '')trong>') {gment }); + let text = paraText + .replace(/^§§ALIGN=(center|justify|right)§§/, '') + .replace(/§§INDENT=[0-9.]+§§/, '') .trim(); - // Process bold/italic formattingntSegment });lse; - text = text - .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/_(.*?)_/g, '$1');urrentSegment.isBold = true;f (currentSegment.text) { - === '') {ment }); let x = margens.left + indent; let parWidth = MAX_WIDTH - indent; - ntSegment });true; + let yAfter = renderInlineFormattedParagraph( page, - text,urrentSegment.isBold = false;f (currentSegment.text) { - x,= '') {ent }); - y, // Finaliza o segmento atual se não vazio currentSegment.text = ''; - font, if (currentSegment.text) { } + text, + x, + y, + font, fontBold, - fontItalic, currentSegment.text = ''; } - fontBoldItalic, } - FONT_SIZE,= true;match[0].length; - rgb(0, 0, 0),'') { + fontItalic, + fontBoldItalic, + FONT_SIZE, + rgb(0, 0, 0), align, - parWidth, if (currentSegment.text) {/ Adiciona o texto restante - false, segments.push({ ...currentSegment });if (lastIndex < textToProcess.length) { + parWidth, + false, margens.top, margens.bottom, - PAGE_HEIGHTlse; - ); }/ Adiciona o último segmento se não estiver vazio - if (yAfter === null) { if (currentSegment.text) { + PAGE_HEIGHT + ); + + if (yAfter === null) { pageIndex++; break; } - y = yAfter - FONT_SIZE * 0.5;xto restante segmento e divide em linhas - currentPar++;textToProcess.length) {egments: { text: string; isBold: boolean; isItalic: boolean; width: number; font: any }[]; totalWidth: number }[] = []; - 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 } = { - }} segments: [], + y = yAfter - FONT_SIZE * 0.5; + currentPar++; + if (y < margens.bottom + FONT_SIZE * 1.5) break; + } pageIndex++; - }er vazio - (currentSegment.text) { + } + const pdfBytes = await pdfDoc.save(); return Buffer.from(pdfBytes); } catch (error) { - console.error('Erro geral ao gerar PDF:', error);rocessa cada segmento e divide em linhasr (let i = 0; i < words.length; i++) { - throw error; boolean; width: number; font: any }[]; totalWidth: number }[] = []; - }n; width: number; font: any }[]; totalWidth: number } = { + console.error('Erro geral ao gerar PDF:', error); + throw error; + } } -ntes se não for a primeira palavra da linha + private processarTemplate(template: string, dados: any): string { return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { - const keyParts = key.trim().split('.');{ - let value = dados;split(/\s+/);ment.isItalic) { + const keyParts = key.trim().split('.'); + let value = dados; - for (const part of keyParts) {gth; i++) {{ - if (value && typeof value === 'object' && part in value) {rd = words[i];tFont = fontBold; - value = value[part];talic) { - } else {segmentFont = fontItalic; - value = undefined;// Adiciona espaço antes se não for a primeira palavra da linha} else { - break;; - }// Seleciona a fonte correta com base na formatação} + for (const part of keyParts) { + if (value && typeof value === 'object' && part in value) { + value = value[part]; + } else { + value = undefined; + break; + } } - ntSize); - 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) { } + return value !== undefined ? value : match; + }); + } }