diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 121fb12..1f384a4 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -386,336 +386,337 @@ export class PdfService { // Simplificar texto antes de processar para evitar loops infinitos // Remove tags não suportadas como , , etc. text = text - .replace(/|<\/u>/gi, '') // Remover tags de sublinhado + .replace(/|<\/u>/gi, '') // Remover tags de sublinhados de alinhamento que não foram processados .replace(/]*>|<\/span>/gi, '') // Remover tags span .replace(/<[^>]*style=[^>]*>/gi, '') // Remover atributos style .replace(/<(?!\/?(?:b|i|strong|em)(?!\w)[^>]*>)[^>]*>/gi, ''); // Manter apenas b, i, strong, em - + .replace(/<(?!\/?(?:b|i|strong|em)(?!\w)[^>]*>)[^>]*>/gi, ''); // Manter apenas b, i, strong, em // Primeiro, extrair partes do texto com suas formatações - let i = 0; + let i = 0;o, extrair partes do texto com suas formatações while (i < text.length) { // Limite de segurança para evitar loops infinitos - if (textParts.length > 1000) { + if (textParts.length > 1000) {itar loops infinitos console.error('Limite de processamento de texto excedido'); - break; + break;e.error('Limite de processamento de texto excedido'); + } break; } - if (text.substring(i, i+3) === '' || text.substring(i, i+7) === '') { - boldActive = true; + boldActive = true;, i+3) === '' || text.substring(i, i+7) === '') { currentFont = italicActive ? boldItalicFont : boldFont; - i += text.substring(i, i+3) === '' ? 3 : 7; - } + i += text.substring(i, i+3) === '' ? 3 : 7;boldFont; + } i += text.substring(i, i+3) === '' ? 3 : 7; else if (text.substring(i, i+4) === '' || text.substring(i, i+8) === '') { - boldActive = false; + boldActive = false;ng(i, i+4) === '' || text.substring(i, i+8) === '') { currentFont = italicActive ? italicFont : normalFont; - i += text.substring(i, i+4) === '' ? 4 : 8; - } + i += text.substring(i, i+4) === '' ? 4 : 8;lFont; + } i += text.substring(i, i+4) === '' ? 4 : 8; else if (text.substring(i, i+3) === '' || text.substring(i, i+4) === '') { - italicActive = true; + italicActive = true;g(i, i+3) === '' || text.substring(i, i+4) === '') { currentFont = boldActive ? boldItalicFont : italicFont; - i += text.substring(i, i+3) === '' ? 3 : 4; - } + i += text.substring(i, i+3) === '' ? 3 : 4;alicFont; + } i += text.substring(i, i+3) === '' ? 3 : 4; else if (text.substring(i, i+4) === '' || text.substring(i, i+5) === '') { - italicActive = false; + italicActive = false;(i, i+4) === '' || text.substring(i, i+5) === '') { currentFont = boldActive ? boldFont : normalFont; - i += text.substring(i, i+4) === '' ? 4 : 5; - } + i += text.substring(i, i+4) === '' ? 4 : 5;t; + } i += text.substring(i, i+4) === '' ? 4 : 5; else { // Encontrar o próximo início de tag let nextTagPos = text.indexOf('<', i); - + let nextTagPos = text.indexOf('<', i); if (nextTagPos === -1) { // Não há mais tags, pegar o restante do texto - textParts.push({ + textParts.push({ags, pegar o restante do texto text: text.substring(i), - font: currentFont - }); + font: currentFonting(i), + });ont: currentFont break; } else { // Pegar texto até a próxima tag - textParts.push({ + textParts.push({té a próxima tag text: text.substring(i, nextTagPos), - font: currentFont - }); + font: currentFonting(i, nextTagPos), + });ont: currentFont i = nextTagPos; - } - } + } i = nextTagPos; + } } + } } } - // Processa partes de texto em palavras para quebra de linha - for (const part of textParts) { + for (const part of textParts) {palavras para quebra de linha + const partWords = part.text.split(/\s+/); const partWords = part.text.split(/\s+/); - for (let i = 0; i < partWords.length; i++) { + if (partWords[i].length === 0) continue; { if (partWords[i].length === 0) continue; - const wordText = i < partWords.length - 1 ? partWords[i] + ' ' : partWords[i]; + const wordWidth = part.font.widthOfTextAtSize(wordText, fontSize);artWords[i]; const wordWidth = part.font.widthOfTextAtSize(wordText, fontSize); - words.push({ text: wordText, font: part.font, width: wordWidth - }); - } + });idth: wordWidth + } }); + } } } - // Processa palavras em linhas for (let i = 0; i < words.length; i++) { + const word = words[i];s.length; i++) { const word = words[i]; - if (currentLineWidth + word.width <= maxWidth) { - // A palavra cabe na linha atual - currentLine.push(word); + // A palavra cabe na linha atual<= maxWidth) { + currentLine.push(word);nha atual currentLineWidth += word.width; - } else { + } else {tLineWidth += word.width; // A palavra não cabe, desenha a linha atual e começa nova linha - // Primeiro verifica se há espaço vertical suficiente - if (y - lineHeight < marginBottom) { + // Primeiro verifica se há espaço vertical suficiente nova linha + if (y - lineHeight < marginBottom) {rtical suficiente return null; // Não há espaço suficiente na página + } return null; // Não há espaço suficiente na página } - // Desenha a linha com o alinhamento apropriado y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); - + y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); // Começa uma nova linha com a palavra atual - currentLine = [word]; + currentLine = [word];nha com a palavra atual currentLineWidth = word.width; - } + } currentLineWidth = word.width; + } } } - // Desenha a última linha se houver palavras restantes - if (currentLine.length > 0) { + if (currentLine.length > 0) {houver palavras restantes if (y - lineHeight < marginBottom) { return null; // Não há espaço suficiente na página + } return null; // Não há espaço suficiente na página } - y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); + } y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); } - return y; + } return y; } - /** * Desenha uma linha formatada com alinhamento específico - */ + */Desenha uma linha formatada com alinhamento específico function drawFormattedLine( - page: any, + page: any, FormattedLine( line: Array<{text: string, font: any, width: number}>, - x: number, + x: number,y<{text: string, font: any, width: number}>, y: number, maxWidth: number, align: 'left' | 'center' | 'right' | 'justify', - fontSize: number, - color: any - ): number { + fontSize: number,center' | 'right' | 'justify', + color: anynumber, + ): number {y const lineHeight = fontSize * 1.5; - let currentX = x; + let currentX = x;= fontSize * 1.5; + let totalWidth = line.reduce((sum, word) => sum + word.width, 0); let totalWidth = line.reduce((sum, word) => sum + word.width, 0); - // Calcula o espaço adicional entre palavras para justificação - let extraSpacing = 0; + let extraSpacing = 0;dicional entre palavras para justificação if (align === 'justify' && line.length > 1 && totalWidth < maxWidth * 0.8) { - extraSpacing = (maxWidth - totalWidth) / (line.length - 1); + extraSpacing = (maxWidth - totalWidth) / (line.length - 1);xWidth * 0.8) { + } extraSpacing = (maxWidth - totalWidth) / (line.length - 1); } - // Ajusta a posição x inicial baseada no alinhamento - if (align === 'center') { + if (align === 'center') {cial baseada no alinhamento currentX = x + (maxWidth - totalWidth) / 2; - } else if (align === 'right') { + } else if (align === 'right') {talWidth) / 2; currentX = x + maxWidth - totalWidth; + } currentX = x + maxWidth - totalWidth; } - // Desenha cada palavra for (let i = 0; i < line.length; i++) { - const word = line[i]; + const word = line[i];e.length; i++) { page.drawText(word.text, { - x: currentX, - y: y, + x: currentX,word.text, { + y: y,rrentX, font: word.font, - size: fontSize, - color: color + size: fontSize,, + color: colorze, + });olor: color }); - // Avança X para a próxima palavra + currentX += word.width;ima palavra currentX += word.width; - // Adiciona espaço extra para justificação, exceto na última palavra - if (align === 'justify' && i < line.length - 1) { - currentX += extraSpacing; - } + if (align === 'justify' && i < line.length - 1) {o na última palavra + currentX += extraSpacing;i < line.length - 1) { + } currentX += extraSpacing; + } } } - // Retorna a nova posição Y após desenhar a linha - return y - lineHeight; + return y - lineHeight;ção Y após desenhar a linha + } return y - lineHeight; } - // Processe o texto com as novas funções melhoradas - const textoLimpo = stripUnsupportedTags( + const textoLimpo = stripUnsupportedTags( melhoradas this.processarTemplate(conteudo, escritorio) - ); + );this.processarTemplate(conteudo, escritorio) + const texto = decodeHtmlEntities(textoLimpo); const texto = decodeHtmlEntities(textoLimpo); - console.log('Texto processado para renderização:', texto.substring(0, 100) + '...'); - + console.log('Texto processado para renderização:', texto.substring(0, 100) + '...'); // Corrigir o processamento de parágrafos para lidar melhor com formatações - const paragrafos = texto.split(/\n{2,}/).map(p => { - let paragraph = p.trim(); + const paragrafos = texto.split(/\n{2,}/).map(p => {r melhor com formatações + let paragraph = p.trim();lit(/\n{2,}/).map(p => { if (!paragraph) return ''; - - // Extrair informações de formatação - let align = 'left'; // padrão + if (!paragraph) return ''; + // Extrair informações de formataçãoo a todos os parágrafos normais + let align = 'left'; // padrãorão é justificadomatação const alignMatch = paragraph.match(/§§ALIGN=(center|right|justify)§§/); - if (alignMatch) { - align = alignMatch[1]; + if (alignMatch) { parece um título (começa com # ou tem menos de 50 caracteres e termina com :)= paragraph.match(/§§ALIGN=(center|right|justify)§§/); + align = alignMatch[1];('#') || paragraph = paragraph.replace(/§§ALIGN=(center|right|justify)§§/, ''); - } - - let indent = 0; + } (paragraph.toUpperCase() === paragraph && paragraph.length < 100)) { paragraph = paragraph.replace(/§§ALIGN=(center|right|justify)§§/, ''); + align = 'center'; // centraliza títulos} + let indent = 0;aragraph.replace(/^#+\s*/, ''); // Remove marcadores de título # const indentMatch = paragraph.match(/§§INDENT=(\d+)(pt|px|em)§§/); - if (indentMatch) { - indent = parseInt(indentMatch[1]); + if (indentMatch) {/§§INDENT=(\d+)(pt|px|em)§§/); + indent = parseInt(indentMatch[1]);nhamento se estiverem presentes explicitamente if (indentMatch[2] === 'em') indent *= 12; // Converter em para pontos aproximadamente paragraph = paragraph.replace(/§§INDENT=\d+(pt|px|em)§§/, ''); - } - - return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph.trim()}`; - }).filter(Boolean); - - let currentPar = 0; - let pageIndex = 0; + } align = alignMatch[1]; paragraph = paragraph.replace(/§§INDENT=\d+(pt|px|em)§§/, ''); + paragraph = paragraph.replace(/§§ALIGN=(center|right|justify|left)§§/, '');} + return { text: paragraph.trim(), align, indent }; + }).filter(Boolean);=${indent}§§${paragraph.trim()}`; + let indent = 0;}).filter(Boolean); + let currentPar = 0; = paragraph.match(/§§INDENT=(\d+)(pt|px|em)?§§/); + let pageIndex = 0; {; console.log(`Gerando PDF com ${paragrafos.length} parágrafos`); - - // Define um contador máximo para evitar loops infinitos - const MAX_PAGES = 100; + if (indentMatch[2] === 'em') indent *= 12; // Converter em para pontos aproximadamenteconsole.log(`Gerando PDF com ${paragrafos.length} parágrafos`); + // Define um contador máximo para evitar loops infinitos)?§§/, ''); + const MAX_PAGES = 100;loops infinitos while (currentPar < paragrafos.length && pageIndex < MAX_PAGES) { console.log(`Criando página ${pageIndex + 1}, processando parágrafo ${currentPar + 1} de ${paragrafos.length}`); - let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); - + let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);+ 1} de ${paragrafos.length}`); + align: align as 'left' | 'center' | 'right' | 'justify',let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); // Adicionar timbrado à página if (timbradoPages.length > 0) { console.log(`Adicionando timbrado na página ${pageIndex + 1}`); page.drawPage(timbradoPages[0], { - x: 0, - y: 0, - width: PAGE_WIDTH, + x: 0,Par = 0;awPage(timbradoPages[0], { + y: 0,ex = 0; + width: PAGE_WIDTH, com ${paragrafos.length} parágrafos`); height: PAGE_HEIGHT - }); - } - - let y = PAGE_HEIGHT - margens.top; + });ne um contador máximo para evitar loops infinitoseight: PAGE_HEIGHT + }st MAX_PAGES = 100; }); + ile (currentPar < paragrafos.length && pageIndex < MAX_PAGES) {} + let y = PAGE_HEIGHT - margens.top;Index + 1}, processando parágrafo ${currentPar + 1} de ${paragrafos.length}`); + let processedAnyParagraph = false; // Flag para verificar se processamos algum parágrafo let processedAnyParagraph = false; // Flag para verificar se processamos algum parágrafo - // Tenta processar parágrafos na página atual while (currentPar < paragrafos.length) { - const paraText = paragrafos[currentPar]; + const paraObj = paragrafos[currentPar];ina ${pageIndex + 1}`); + page.drawPage(timbradoPages[0], {const paraText = paragrafos[currentPar]; + // Extrair propriedades do parágrafo + const text = paraObj.text; + const align = paraObj.align || 'justify'; // Por padrão, justifica o texto + const indent = paraObj.indent || 0;IGHT= paraText.match(/^§§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'; - } - - // Extract indentation - let indent = 0; - const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/); - if (indentMatch) { - indent = parseFloat(indentMatch[1]); - } - - // Clean text - let text = paraText - .replace(/^§§ALIGN=(center|justify|right)§§/, '') - .replace(/§§INDENT=[0-9.]+§§/, '') - .trim(); - let x = margens.left + indent; + let x = margens.left + indent;lign = alignMatch[1] as 'center' | 'justify' | 'left'; let parWidth = MAX_WIDTH - indent; - - // Se o texto for muito curto, avance para o próximo parágrafo + rgens.top; + // Se o texto for muito curto, avance para o próximo parágrafoaragraph = false; // Flag para verificar se processamos algum parágrafontation if (text.length < 2) { - currentPar++; + currentPar++;arágrafos na página atual= paraText.match(/§§INDENT=([0-9.]+)§§/); processedAnyParagraph = true; - continue; - } - - console.log(`Tentando renderizar parágrafo ${currentPar + 1} em y=${y}`); - let yAfter = renderInlineFormattedParagraph( + continue;onst paraText = paragrafos[currentPar]; indent = parseFloat(indentMatch[1]); + }} + ignment + console.log(`Tentando renderizar parágrafo ${currentPar + 1} em y=${y} com alinhamento ${align}`); 'center' | 'justify' = 'left'; + let yAfter = renderInlineFormattedParagraph(|justify|right)§§/); page, - text, + text,alignMatch[1] as 'center' | 'justify' | 'left';(/§§INDENT=[0-9.]+§§/, '') x, y, - font, + font,// Extract indentationlet parWidth = MAX_WIDTH - indent; fontBold, - fontItalic, - fontBoldItalic, - FONT_SIZE, - rgb(0, 0, 0), - align, - parWidth, + fontItalic,raText.match(/§§INDENT=([0-9.]+)§§/);o curto, avance para o próximo parágrafo + fontBoldItalic,) { < 2) { + FONT_SIZE,ch[1]); + rgb(0, 0, 0),ph = true; + align,continue; + parWidth,// Clean text} false, - margens.top, - margens.bottom, - PAGE_HEIGHT - ); - + margens.top,§/, ''){currentPar + 1} em y=${y}`); + margens.bottom,ace(/§§INDENT=[0-9.]+§§/, '')ter = renderInlineFormattedParagraph( + PAGE_HEIGHT(); + );x = margens.left + indent;xt, + parWidth = MAX_WIDTH - indent; if (yAfter === null) { - // Se não conseguimos renderizar este parágrafo, vamos para a próxima página - // Mas só se já tivermos processado algum parágrafo nesta página + // Se não conseguimos renderizar este parágrafo, vamos para a próxima páginato for muito curto, avance para o próximo parágrafo + // Mas só se já tivermos processado algum parágrafo nesta páginath < 2) { if (processedAnyParagraph) { - console.log(`Página ${pageIndex + 1} está cheia, criando nova página`); + console.log(`Página ${pageIndex + 1} está cheia, criando nova página`);nyParagraph = true;alic, break; } else { // Se não conseguimos nem processar um parágrafo, algo está errado - // Vamos avançar para o próximo parágrafo para evitar um loop infinito - console.log(`Não foi possível processar o parágrafo ${currentPar + 1}, avançando`); + // Vamos avançar para o próximo parágrafo para evitar um loop infinitolog(`Tentando renderizar parágrafo ${currentPar + 1} em y=${y}`);th, + console.log(`Não foi possível processar o parágrafo ${currentPar + 1}, avançando`);enderInlineFormattedParagraph( currentPar++; processedAnyParagraph = true; - } - } else { + }x,PAGE_HEIGHT + } else { y,); // Parágrafo foi renderizado com sucesso y = yAfter - FONT_SIZE * 0.5; currentPar++; - processedAnyParagraph = true; + processedAnyParagraph = true; parágrafo nesta página console.log(`Parágrafo ${currentPar} processado com sucesso, nova posição y=${y}`); - + , 0),e.log(`Página ${pageIndex + 1} está cheia, criando nova página`); // Verifica se ainda temos espaço para pelo menos uma linha if (y < margens.bottom + FONT_SIZE * 2) { console.log(`Espaço insuficiente para próximo parágrafo, criando nova página`); break; - } + }Não foi possível processar o parágrafo ${currentPar + 1}, avançando`); } - } + }processedAnyParagraph = true; pageIndex++; - } - + }zar este parágrafo, vamos para a próxima páginacom sucesso + já tivermos processado algum parágrafo nesta páginaFONT_SIZE * 0.5; if (pageIndex >= MAX_PAGES) { console.warn(`AVISO: Número máximo de páginas (${MAX_PAGES}) atingido. Geração de PDF interrompida.`); - } + } break;console.log(`Parágrafo ${currentPar} processado com sucesso, nova posição y=${y}`); - console.log(`PDF gerado com ${pageIndex} páginas`); + console.log(`PDF gerado com ${pageIndex} páginas`);parágrafo, algo está erradolo menos uma linha const pdfBytes = await pdfDoc.save(); - return Buffer.from(pdfBytes); - } catch (error) { - console.error('Erro geral ao gerar PDF:', error); - throw error; - } - } - - private processarTemplate(template: string, dados: any): string { - return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { + return Buffer.from(pdfBytes);e.log(`Não foi possível processar o parágrafo ${currentPar + 1}, avançando`);e.log(`Espaço insuficiente para próximo parágrafo, criando nova página`); + } catch (error) { currentPar++; break; + console.error('Erro geral ao gerar PDF:', error); processedAnyParagraph = true; } + throw error; } } + } } else {} + }rafo foi renderizado com sucesso + y = yAfter - FONT_SIZE * 0.5; pageIndex++; + private processarTemplate(template: string, dados: any): string { currentPar++;} + return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { true; const keyParts = key.trim().split('.'); - let value = dados; - for (const part of keyParts) { + let value = dados; console.warn(`AVISO: Número máximo de páginas (${MAX_PAGES}) atingido. Geração de PDF interrompida.`); + for (const part of keyParts) { // Verifica se ainda temos espaço para pelo menos uma linha } if (value && typeof value === 'object' && part in value) { - value = value[part]; + value = value[part];nte para próximo parágrafo, criando nova página`);ex} páginas`); } else { - value = undefined; + value = undefined;ytes); break; - } - } - return value !== undefined ? value : match; - }); + }eral ao gerar PDF:', error); + } throw error; + return value !== undefined ? value : match; pageIndex++; } + }); } } } } + + + + + + + + + + + + + + +} } }); return value !== undefined ? value : match; } } break; value = undefined; } else { value = value[part]; if (value && typeof value === 'object' && part in value) { for (const part of keyParts) { let value = dados; \ No newline at end of file