Last active
January 19, 2025 23:17
PDF to MARKDOWN JS
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 characters
<!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(); | |
}); | |
/** | |
* 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...'; | |
// 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". | |
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 += 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 | |
lineBuffer.push(mdText); | |
} | |
lastY = y; | |
} | |
}); | |
// 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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment