Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active January 19, 2025 23:17
PDF to MARKDOWN JS
<!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