From 1fc7961ed9493e25d42f07cff023d92906caf6e9 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 31 Aug 2026 15:02:22 +0000 Subject: [PATCH] Garante proporcao exata e sem deformacao das imagens nas exportacoes de PDF de quadrinhos (Retrato e Paisagem) --- public/app.js | 118 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 42 deletions(-) diff --git a/public/app.js b/public/app.js index 7b94de4..4c0093d 100644 --- a/public/app.js +++ b/public/app.js @@ -4899,24 +4899,36 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` } }); - // Helper para converter URL de imagem local em base64 - const getBase64Image = async (imgUrl) => { + // Helper para converter URL de imagem local em base64 e obter dimensões reais sem deformar + const getBase64ImageDetails = async (imgUrl) => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => { + const width = img.naturalWidth || img.width || 1280; + const height = img.naturalHeight || img.height || 720; const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; + canvas.width = width; + canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); - resolve(canvas.toDataURL('image/jpeg')); + resolve({ + dataUrl: canvas.toDataURL('image/jpeg'), + width: width, + height: height, + aspectRatio: width / height + }); }; img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl)); img.src = imgUrl; }); }; + const getBase64Image = async (imgUrl) => { + const details = await getBase64ImageDetails(imgUrl); + return details.dataUrl; + }; + // Helper para execução concorrente controlada const mapConcurrent = async (items, limit, fn) => { const results = new Array(items.length); @@ -5444,51 +5456,62 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.setTextColor(30, 41, 59); - doc.text(title, 297, 50, { align: 'center' }); + doc.text(title, 297, 45, { align: 'center' }); - // Imagem - const base64Img = await getBase64Image(panel.imageUrl); - const imgWidth = 515; - const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386; - doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight); + const imgDetails = await getBase64ImageDetails(panel.imageUrl); + const boxWidth = 515; + const boxMaxHeight = 350; + const textHeight = 150; - // Altura esticada para a caixa de diálogo (160pt de altura - aprox dobro) - const textHeight = 160; - const textY = 80 + imgHeight; + // Cálculo exato mantendo a proporção real da imagem (sem esticar) + let renderW = boxWidth; + let renderH = renderW / imgDetails.aspectRatio; + if (renderH > boxMaxHeight) { + renderH = boxMaxHeight; + renderW = renderH * imgDetails.aspectRatio; + } - // Caixa de diálogo com fundo cinza claro e borda integrada + const imgX = 40 + (boxWidth - renderW) / 2; + const imgY = 70 + (boxMaxHeight - renderH) / 2; + const cardY = 70; + const textY = cardY + boxMaxHeight; + + // Desenha a imagem centralizada e proporcional + doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH); + + // Fundo da caixa de texto doc.setFillColor(248, 250, 252); - doc.rect(40, textY, imgWidth, textHeight, 'F'); + doc.rect(40, textY, boxWidth, textHeight, 'F'); - // Moldura unificada ao redor de toda a imagem + texto + // Moldura unificada ao redor de todo o quadro doc.setDrawColor(30, 41, 59); doc.setLineWidth(2); - doc.rect(40, 80, imgWidth, imgHeight + textHeight, 'D'); + doc.rect(40, cardY, boxWidth, boxMaxHeight + textHeight, 'D'); - // Linha divisória entre a imagem e a caixa de texto + // Linha divisória entre imagem e texto doc.line(40, textY, 555, textY); - // Texto maior e mais legível (dobro do tamanho relativo - 15pt) + // Texto legível if (panel.dialogue) { doc.setFont('helvetica', 'normal'); doc.setFontSize(15); doc.setTextColor(15, 23, 42); - const splitText = doc.splitTextToSize(panel.dialogue, imgWidth - 40); + const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 40); doc.text(splitText, 60, textY + 35); } - // Círculo discreto com o número do quadrinho no canto superior esquerdo da imagem + // Numeração do quadrinho doc.setFillColor(254, 240, 138); - doc.circle(65, 105, 12, 'F'); + doc.circle(65, 95, 12, 'F'); doc.setDrawColor(30, 41, 59); doc.setLineWidth(1.5); - doc.circle(65, 105, 12, 'D'); + doc.circle(65, 95, 12, 'D'); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(30, 41, 59); - doc.text(String(panel.panel_number), 65, 109, { align: 'center' }); + doc.text(String(panel.panel_number), 65, 99, { align: 'center' }); // Rodapé doc.setFont('helvetica', 'italic'); @@ -5526,7 +5549,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` for (let page = 0; page < totalPages; page++) { if (page > 0) doc.addPage(); - // Título centralizado no topo da página paisagem + // Título centralizado no topo doc.setFont('helvetica', 'bold'); doc.setFontSize(18); doc.setTextColor(30, 41, 59); @@ -5539,40 +5562,51 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` const panel = generatedPanelsData[index]; const startX = col === 0 ? 40 : 426; - const cardWidth = 376; - - // Proporções exatas mantidas - const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282; - const textHeight = 140; // Dobro de tamanho (antes era 65) + const boxWidth = 376; + const boxMaxHeight = 230; + const textHeight = 140; - // Imagem do quadrinho - const base64Img = await getBase64Image(panel.imageUrl); - doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight); + const imgDetails = await getBase64ImageDetails(panel.imageUrl); + + // Cálculo rigoroso da dimensão sem deformar (preserva aspect ratio real) + let renderW = boxWidth; + let renderH = renderW / imgDetails.aspectRatio; + if (renderH > boxMaxHeight) { + renderH = boxMaxHeight; + renderW = renderH * imgDetails.aspectRatio; + } + + const imgX = startX + (boxWidth - renderW) / 2; + const imgY = 70 + (boxMaxHeight - renderH) / 2; + const cardY = 70; + const textY = cardY + boxMaxHeight; + + // Imagem do quadrinho centralizada e mantendo proporções exatas + doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH); // Fundo da caixa de texto - const textY = 70 + imgHeight; doc.setFillColor(248, 250, 252); - doc.rect(startX, textY, cardWidth, textHeight, 'F'); + doc.rect(startX, textY, boxWidth, textHeight, 'F'); // Moldura unificada ao redor do quadro (imagem + texto) doc.setDrawColor(30, 41, 59); doc.setLineWidth(2); - doc.rect(startX, 70, cardWidth, imgHeight + textHeight, 'D'); + doc.rect(startX, cardY, boxWidth, boxMaxHeight + textHeight, 'D'); - // Linha separando a imagem da caixa de texto - doc.line(startX, textY, startX + cardWidth, textY); + // Linha separadora entre imagem e caixa de texto + doc.line(startX, textY, startX + boxWidth, textY); - // Legenda formatada em tamanho maior (13pt) + // Legenda formatada if (panel.dialogue) { doc.setFont('helvetica', 'normal'); doc.setFontSize(13); doc.setTextColor(15, 23, 42); - const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 30); + const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 30); doc.text(splitText, startX + 15, textY + 30); } - // Círculo discreto com o numeral do quadro no canto superior esquerdo da imagem + // Numeral do quadro doc.setFillColor(254, 240, 138); doc.circle(startX + 20, 90, 11, 'F'); doc.setDrawColor(30, 41, 59);