Last active
January 19, 2025 23:17
Revisions
-
celsowm revised this gist
Jan 19, 2025 . 1 changed file with 70 additions and 13 deletions.There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -110,6 +110,42 @@ <h3>Resultado em Markdown</h3> input.click(); }); /** * Verifica se uma linha de texto corresponde a um item de lista não ordenada (ex.: "-", "*", "•") * ou a um item de lista ordenada (ex.: "1.", "2)", "3."). * Retorna um objeto com o tipo de lista (ordered/unordered) e o texto sem o marcador * caso seja realmente uma linha de lista. Se não for, retorna null. */ function detectListItem(line) { // Remove espaços no início (mas vamos manter a original para caso precise). const trimmed = line.trim(); // Padrão simples para detectar marcadores de lista não ordenada: -, *, • const unorderedRegex = /^[-*•]\s+(.*)$/; // Padrão simples para detectar marcadores de lista ordenada: número + . ou ) const orderedRegex = /^(\d+)[\.\)]\s+(.*)$/; let match = trimmed.match(unorderedRegex); if (match) { return { type: 'unordered', content: match[1] // texto após o marcador }; } match = trimmed.match(orderedRegex); if (match) { return { type: 'ordered', // Em match[1] estaria o número, e match[2] o texto após o número number: match[1], content: match[2] }; } return null; } async function processPDF(file) { markdownOutput.textContent = 'Processando...'; @@ -136,9 +172,7 @@ <h3>Resultado em Markdown</h3> let lastY = null; // Limiar para considerar "nova linha" (ajuste conforme necessário) const LINE_THRESHOLD = 5; // Limiar maior para considerar "novo parágrafo". const PARAGRAPH_THRESHOLD = 15; textContent.items.forEach((item) => { @@ -159,7 +193,7 @@ <h3>Resultado em Markdown</h3> realFontName = fontObj.name; // ex: "CAAAAA+LiberationSerif-Bold" } } catch (err) { console.warn(`Não foi possível obter fonte para ${fontName}:`, err); } const isBold = /bold|black/i.test(realFontName); @@ -168,11 +202,11 @@ <h3>Resultado em Markdown</h3> // Monta o texto já com a marcação let mdText = rawText; if (isBold && isItalic) { mdText = `***${rawText}***`; } else if (isBold) { mdText = `**${rawText}**`; } else if (isItalic) { mdText = `*${rawText}*`; } // Agrupamento em linhas @@ -184,11 +218,11 @@ <h3>Resultado em Markdown</h3> const diffY = Math.abs(y - lastY); if (diffY > PARAGRAPH_THRESHOLD) { // pula um parágrafo (duas quebras, por ex.) finalMarkdown += convertLineToMarkdown(lineBuffer) + '\n\n'; lineBuffer = [mdText]; } else if (diffY > LINE_THRESHOLD) { // apenas uma nova linha finalMarkdown += convertLineToMarkdown(lineBuffer) + '\n'; lineBuffer = [mdText]; } else { // continua na mesma linha @@ -200,17 +234,40 @@ <h3>Resultado em Markdown</h3> // Ao final da página, se ainda tiver texto em buffer, joga como linha if (lineBuffer.length > 0) { finalMarkdown += convertLineToMarkdown(lineBuffer) + '\n'; } // (Opcional) Quebra adicional entre páginas finalMarkdown += '\n'; } // Remove espaços extras no final markdownOutput.textContent = finalMarkdown.trim() || 'Nenhum texto extraído.'; } /** * Recebe um array de "pedacinhos" (chunks) que formam uma linha * e faz a junção, além de verificar se é linha de lista. */ function convertLineToMarkdown(chunks) { // Junta os pedaços em uma string única const joinedLine = chunks.join(' ').trim(); // Detecta se é lista const listCheck = detectListItem(joinedLine); if (listCheck) { if (listCheck.type === 'unordered') { // Retorna com o marcador de lista não ordenada padrão do Markdown return `- ${listCheck.content}`; } else if (listCheck.type === 'ordered') { // Mantém o número original e adiciona um ponto return `${listCheck.number}. ${listCheck.content}`; } } // Se não for lista, retorna a linha normal return joinedLine; } </script> </body> </html> -
celsowm created this gist
Jan 19, 2025 .There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,216 @@ <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="UTF-8" /> <title>PDF to Markdown (replicando quebras do PDF)</title> <!-- PDF.js via CDN --> <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/build/pdf.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.6.172/build/pdf.worker.min.js"></script> <style> body { margin: 0; font-family: sans-serif; display: flex; height: 100vh; overflow: hidden; } #left-panel { width: 50%; padding: 20px; box-sizing: border-box; border-right: 2px solid #ccc; display: flex; flex-direction: column; align-items: center; justify-content: center; } #dropzone { width: 80%; height: 300px; border: 2px dashed #999; border-radius: 5px; display: flex; align-items: center; justify-content: center; text-align: center; color: #666; cursor: pointer; transition: border-color 0.3s; } #dropzone.hover { border-color: #666; } #right-panel { width: 50%; padding: 20px; box-sizing: border-box; overflow: auto; background: #f8f8f8; } #markdown-output { white-space: pre-wrap; font-family: monospace, "Courier New", Courier; } </style> </head> <body> <div id="left-panel"> <div id="dropzone">Arraste e solte seu PDF aqui ou clique para selecionar</div> </div> <div id="right-panel"> <h3>Resultado em Markdown</h3> <div id="markdown-output"></div> </div> <script> const pdfjsLib = window['pdfjs-dist/build/pdf']; const dropzone = document.getElementById('dropzone'); const markdownOutput = document.getElementById('markdown-output'); // Evita comportamento padrão de arrastar no window window.addEventListener('dragover', (ev) => ev.preventDefault()); window.addEventListener('drop', (ev) => ev.preventDefault()); dropzone.addEventListener('dragover', (ev) => { ev.preventDefault(); dropzone.classList.add('hover'); }); dropzone.addEventListener('dragleave', () => { dropzone.classList.remove('hover'); }); dropzone.addEventListener('drop', (ev) => { ev.preventDefault(); dropzone.classList.remove('hover'); const files = ev.dataTransfer.files; if (files && files.length > 0) { const file = files[0]; if (file.type === 'application/pdf') { processPDF(file); } else { alert('Por favor, arraste um arquivo PDF válido.'); } } }); dropzone.addEventListener('click', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/pdf'; input.onchange = (e) => { const file = e.target.files[0]; if (file) { processPDF(file); } }; input.click(); }); async function processPDF(file) { markdownOutput.textContent = 'Processando...'; // Lê o arquivo como ArrayBuffer const arrayBuffer = await file.arrayBuffer(); // Carrega o PDF const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const totalPages = pdfDoc.numPages; let finalMarkdown = ''; for (let pageNum = 1; pageNum <= totalPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); // 1) Força carregar todos os operadores (que inclui info de fontes) await page.getOperatorList(); // 2) Extrai o conteúdo de texto (posições e fontName) const textContent = await page.getTextContent(); // Vamos agrupar texto em linhas com base na variação de Y: let lineBuffer = []; // armazena chunks que pertencem à mesma linha let lastY = null; // Limiar para considerar "nova linha" (ajuste conforme necessário) const LINE_THRESHOLD = 5; // Limiar maior para considerar "novo parágrafo" (opcional). // Se quiser forçar parágrafos quando há espaço grande, configure algo como 15 ou 20. // Se não quiser pular parágrafos, pode ignorar ou deixar maior que qualquer variação normal. const PARAGRAPH_THRESHOLD = 15; textContent.items.forEach((item) => { const rawText = item.str; // Se estiver vazio ou só espaços, ignoramos. if (!rawText.trim()) return; // Posição (transform é [scaleX, skewX, skewY, scaleY, offsetX, offsetY]) const transform = item.transform; const y = transform[5]; // Captura info de fonte real, para heurísticas de bold/italic. const fontName = item.fontName; let realFontName = ''; try { const fontObj = page.commonObjs.get(fontName); if (fontObj && fontObj.name) { realFontName = fontObj.name; // ex: "CAAAAA+LiberationSerif-Bold" } } catch (err) { console.warn(Não foi possível obter fonte para ${fontName}:, err); } const isBold = /bold|black/i.test(realFontName); const isItalic = /italic|oblique/i.test(realFontName); // Monta o texto já com a marcação let mdText = rawText; if (isBold && isItalic) { mdText = ***${rawText}***; } else if (isBold) { mdText = **${rawText}**; } else if (isItalic) { mdText = *${rawText}*; } // Agrupamento em linhas if (lastY === null) { // Primeira linha detectada nessa página lineBuffer.push(mdText); lastY = y; } else { const diffY = Math.abs(y - lastY); if (diffY > PARAGRAPH_THRESHOLD) { // pula um parágrafo (duas quebras, por ex.) finalMarkdown += lineBuffer.join(' ') + '\n\n'; lineBuffer = [mdText]; } else if (diffY > LINE_THRESHOLD) { // apenas uma nova linha finalMarkdown += lineBuffer.join(' ') + '\n'; lineBuffer = [mdText]; } else { // continua na mesma linha lineBuffer.push(mdText); } lastY = y; } }); // Ao final da página, se ainda tiver texto em buffer, joga como linha if (lineBuffer.length > 0) { finalMarkdown += lineBuffer.join(' ') + '\n'; } // Se quiser separar páginas com mais de uma linha em branco, use: // finalMarkdown += '\n\n'; // (ou apenas uma quebra simples; ajuste a gosto) finalMarkdown += '\n'; } markdownOutput.textContent = finalMarkdown.trim() || 'Nenhum texto extraído.'; } </script> </body> </html>