Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active January 19, 2025 23:17

Revisions

  1. celsowm revised this gist Jan 19, 2025. 1 changed file with 70 additions and 13 deletions.
    83 changes: 70 additions & 13 deletions index.htm
    Original 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" (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.
    // 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);
    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}***;
    mdText = `***${rawText}***`;
    } else if (isBold) {
    mdText = **${rawText}**;
    mdText = `**${rawText}**`;
    } else if (isItalic) {
    mdText = *${rawText}*;
    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 += lineBuffer.join(' ') + '\n\n';
    finalMarkdown += convertLineToMarkdown(lineBuffer) + '\n\n';
    lineBuffer = [mdText];
    } else if (diffY > LINE_THRESHOLD) {
    // apenas uma nova linha
    finalMarkdown += lineBuffer.join(' ') + '\n';
    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 += lineBuffer.join(' ') + '\n';
    finalMarkdown += convertLineToMarkdown(lineBuffer) + '\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)
    // (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>
  2. celsowm created this gist Jan 19, 2025.
    216 changes: 216 additions & 0 deletions index.htm
    Original 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>