sopdf-editor / frontend /script.js
hergo-7's picture
Upload 3 files
2d628d5 verified
Raw History Blame Contribute Delete
55.8 kB
const API_URL = "/api"; let currentFiles = []; let pageOrder = []; let currentToolMode = ''; function updateThemeButtonLabel(isDark) { const btn = document.getElementById('themeToggleBtn'); if (btn) { btn.innerHTML = isDark ? '☀️ Modo Claro' : '🌙 Modo Escuro'; } } function toggleTheme() { const isDark = document.body.classList.toggle('dark-mode'); localStorage.setItem('sopdf_theme', isDark ? 'dark' : 'light'); updateThemeButtonLabel(isDark); } (function initTheme() { const savedTheme = localStorage.getItem('sopdf_theme'); if (savedTheme === 'dark') { document.body.classList.add('dark-mode'); updateThemeButtonLabel(true); } })(); function scrollToTools() { document.getElementById('ferramentas').scrollIntoView({ behavior: 'smooth' }); } function openTool(toolType) { const homeView = document.getElementById('home-view'); const toolView = document.getElementById('tool-view'); const body = document.getElementById('tool-content'); homeView.style.display = 'none'; toolView.style.display = 'block'; let content = ''; currentFiles = []; pageOrder = []; switch(toolType) { case 'delete': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🗑️ Excluir e Reordenar Páginas</h2> <form id="toolForm" onsubmit="handleReconstruct(event, 'modificado.pdf')"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="loadPreviews(this, 'delete')" required> </div> <div id="loadingPreview" style="display: none; text-align: center; color: var(--ceara-yellow); margin-top: 10px;">Carregando visualização...</div> <div id="previewContainer" class="page-grid"></div> <button type="submit" class="btn-primary" style="width: 100%; margin-top: 1rem; display:none;" id="submitBtn">Salvar Alterações</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'merge': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-green-1);">🔗 Mesclar e Reordenar PDFs</h2> <form id="toolForm" onsubmit="handleReconstruct(event, 'mesclado.pdf')"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione Múltiplos PDFs</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" multiple onchange="loadPreviews(this, 'merge')"> </div> <div id="loadingPreview" style="display: none; text-align: center; color: var(--ceara-yellow); margin-top: 10px;">Carregando visualização...</div> <div id="previewContainer" class="page-grid"></div> <button type="submit" class="btn-primary" style="width: 100%; margin-top: 1rem; display:none;" id="submitBtn">Salvar PDF Mesclado</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'compress': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-green-1);">🗜️ Comprimir PDF</h2> <form id="toolForm" onsubmit="handleCompress(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <div class="input-group"> <label>Nível de Compressão</label> <select id="compressLevel"> <option value="recomendada">Recomendada (Boa qualidade)</option> <option value="extrema">Extrema (Menor tamanho possível)</option> <option value="baixa">Baixa (Alta qualidade)</option> </select> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Comprimir Arquivo</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando... Aguarde.</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'split': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">✂️ Particionar PDF</h2> <p style="color: var(--text-secondary); font-size:0.8rem; margin-bottom: 1rem;">Nota: Se uma única página for maior que o limite, ela não poderá ser dividida.</p> <form id="toolForm" onsubmit="handleSplit(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="loadPreviews(this, 'split')" required> </div> <div id="loadingPreview" style="display: none; text-align: center; color: var(--ceara-yellow); margin-top: 10px;">Carregando visualização...</div> <div id="previewContainer" class="page-grid"></div> <div class="input-group"> <label>Modo de Partição</label> <select id="splitMode" onchange="toggleSplitMode()"> <option value="size">Por Tamanho Máximo</option> <option value="interval">Por Intervalos de Páginas</option> </select> </div> <div id="sizeModeParams"> <div class="input-group"> <label>Tamanho máximo por arquivo (MB)</label> <input type="number" id="maxMb" value="2.0" step="0.1" min="0.1"> </div> </div> <div id="intervalModeParams" style="display: none;"> <div class="input-group"> <label>Intervalos para extrair (ex: 1-5, 8, 11-13)</label> <input type="text" id="splitIntervals" placeholder="ex: 1-5, 8-10"> </div> <div class="input-group" style="flex-direction: row; align-items: center; gap: 10px;"> <input type="checkbox" id="splitMerge" style="width: auto;"> <label for="splitMerge" style="margin: 0; font-weight: normal; cursor: pointer;">Mesclar todos os intervalos em um arquivo PDF</label> </div> </div> <div class="input-group"> <label>Compressão Base</label> <select id="splitCompress"> <option value="nenhuma">Nenhuma (Apenas particionar)</option> <option value="recomendada">Recomendada</option> <option value="extrema">Extrema</option> </select> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Processar Partição</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando partições... Aguarde.</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'convert': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🔄 Conversões</h2> <form id="toolForm" onsubmit="handleConvert(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o Arquivo</p> <input type="file" id="fileInput" accept=".pdf,.png,.jpg,.jpeg" style="display: none;" multiple onchange="updateFileNameMultiple(this)"> </div> <div class="input-group"> <label>Tipo de Conversão</label> <select id="convertType"> <option value="word">PDF para Word (.docx)</option> <option value="excel">PDF para Excel (.xlsx)</option> <option value="pptx">PDF para PowerPoint (.pptx)</option> <option value="images">PDF para Imagens (.zip)</option> <option value="pdf-from-images">Imagens para PDF</option> </select> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Converter</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Convertendo...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'security': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-green-1);">🛡️ Segurança AES</h2> <form id="toolForm" onsubmit="handleSecurity(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <div class="input-group"> <label>Ação</label> <select id="secAction"> <option value="protect">Proteger (Adicionar Senha)</option> <option value="unlock">Desbloquear (Remover Senha)</option> </select> </div> <div class="input-group"> <label>Senha</label> <input type="password" id="secPassword" required> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Aplicar</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'compare': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">👁️ Comparar PDFs</h2> <form id="toolForm" onsubmit="handleCompare(event)"> <div style="display:flex; gap:10px;"> <div class="input-group" style="flex:1;"> <label>PDF A (Original)</label> <input type="file" id="fileA" accept=".pdf" required style="color: var(--text-primary); width:100%;"> </div> <div class="input-group" style="flex:1;"> <label>PDF B (Modificado)</label> <input type="file" id="fileB" accept=".pdf" required style="color: var(--text-primary); width:100%;"> </div> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Comparar Visualmente</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Analisando diferenças...</div> <div id="resultArea" style="margin-top: 1rem; display: flex; gap: 10px; flex-direction: column;"></div> </form> `; break; case 'ocr': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🔍 Reconhecimento de Texto (OCR)</h2> <form id="toolForm" onsubmit="handleOCRSubmit(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <div class="input-group"> <label>Idioma do Documento</label> <select id="ocr-lang"> <option value="por">Português</option> <option value="eng">Inglês</option> <option value="por+eng">Português + Inglês</option> </select> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Tornar Pesquisável</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Lendo caracteres (isso pode demorar minutos)...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'validate_signature': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">✒️ Validar Assinaturas</h2> <p style="margin-bottom: 1rem; color: var(--text-secondary);">Verifique a integridade de assinaturas digitais padrão ICP-Brasil ou Gov.br contidas no documento PDF.</p> <form id="toolForm" onsubmit="handleValidateSignatureSubmit(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF Assinado</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Analisar Assinaturas</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Lendo chaves e cadeias do certificado...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'repair': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🩹 Reparar PDF</h2> <p style="margin-bottom: 1rem; color: var(--text-secondary);">Reconstrua PDFs corrompidos para tentar recuperar o acesso ao arquivo original.</p> <form id="toolForm" onsubmit="handleRepairSubmit(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF Corrompido</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Tentar Reparar</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Analisando estrutura e reparando XREF...</div> <div id="resultArea" style="margin-top: 1rem;"></div> </form> `; break; case 'ai_assistant': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🤖 Inteligência Artificial</h2> <p style="margin-bottom: 1rem; color: var(--text-secondary);">Gere resumos ou traduza documentos inteiros de forma automática.</p> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF Base</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <div style="display: flex; gap: 10px; margin-top: 1rem; background: var(--bg-subtle); border: 1px solid var(--bg-glass-border); padding: 10px; border-radius: 8px; flex-wrap: wrap;"> <button class="btn-outline" onclick="triggerAIActions('summarize', 'curto')">Resumo Curto</button> <button class="btn-outline" onclick="triggerAIActions('summarize', 'medio')">Resumo Médio</button> <button class="btn-outline" onclick="triggerAIActions('summarize', 'longo')">Resumo Longo</button> <div style="width: 1px; background: var(--input-border); margin: 0 5px;"></div> <select id="translate-lang" style="padding: 5px 10px; border-radius: 6px; background: var(--input-bg); color: var(--text-primary); border: 1px solid var(--input-border);"> <option value="en">Inglês</option> <option value="es">Espanhol</option> <option value="pt" selected>Português</option> </select> <button class="btn-outline" onclick="triggerAIActions('translate')">Traduzir Texto</button> </div> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">A IA está processando o documento, isso pode levar alguns minutos...</div> <div id="resultContainer" style="display: none; margin-top: 2rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <h3 style="color: var(--ceara-orange-1);">Pré-visualização</h3> <button class="btn-primary" onclick="downloadPreviewPDF('Documento_IA.pdf')">📥 Baixar PDF</button> </div> <div id="previewArea" style="background: var(--bg-dark-card); color: var(--text-primary); border: 1px solid var(--bg-glass-border); border-radius: 8px; padding: 20px; max-height: 50vh; overflow-y: auto; font-family: 'Inter', sans-serif;"></div> </div> `; break; case 'editor': content = ` <h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">📝 Editar Conteúdo</h2> <p style="margin-bottom: 1rem; color: var(--text-secondary);">Extraia e edite o conteúdo textual do seu PDF e exporte novamente.</p> <form id="toolForm" onsubmit="handleEditorLoad(event)"> <div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()"> <span style="font-size: 2rem;">📄</span> <p style="margin-top: 1rem;" id="fileName">Selecione o PDF Base</p> <input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required> </div> <button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Carregar Texto no Editor</button> <div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Extraindo conteúdo (aguarde)...</div> </form> <!-- Editor Container (Oculto inicialmente) --> <div id="editor-container" style="display: none; margin-top: 2rem;"> <div style="display: flex; gap: 10px; margin-bottom: 10px; background: var(--bg-subtle); border: 1px solid var(--bg-glass-border); padding: 10px; border-radius: 8px; flex-wrap: wrap;"> <button class="btn-primary" onclick="downloadEditorPDF()">📥 Salvar Edição em PDF</button> </div> <div style="background: white; color: black; border-radius: 4px;"> <div id="quill-editor" style="height: 60vh; font-family: 'Inter', sans-serif;"></div> </div> </div> `; // Initialize Quill only when the DOM is ready. We will initialize it inside handleEditorLoad or slightly after. break; } body.innerHTML = content; setupDragAndDrop(); window.scrollTo({ top: 0, behavior: 'smooth' }); } function setupDragAndDrop() { const uploadBox = document.getElementById('uploadBox'); if (!uploadBox) return; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { uploadBox.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } ['dragenter', 'dragover'].forEach(eventName => { uploadBox.addEventListener(eventName, () => { uploadBox.style.borderColor = 'var(--ceara-orange-1)'; uploadBox.style.background = 'var(--badge-orange-bg)'; }, false); }); ['dragleave', 'drop'].forEach(eventName => { uploadBox.addEventListener(eventName, () => { uploadBox.style.borderColor = ''; uploadBox.style.background = ''; }, false); }); uploadBox.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files && files.length > 0) { const fileInput = document.getElementById('fileInput'); if (fileInput) { fileInput.files = files; const event = new Event('change', { bubbles: true }); fileInput.dispatchEvent(event); } } }, false); } function closeTool() { document.getElementById('tool-view').style.display = 'none'; document.getElementById('home-view').style.display = 'block'; window.scrollTo({ top: 0, behavior: 'smooth' }); } function updateFileName(input) { if (input.files.length > 0) { document.getElementById('fileName').innerText = "Selecionado: " + input.files[0].name; document.getElementById('uploadBox').style.borderColor = "var(--ceara-green-1)"; document.getElementById('uploadBox').style.background = "var(--upload-hover-bg)"; } } function updateFileNameMultiple(input) { const newFiles = Array.from(input.files); if (newFiles.length === 0) return; currentFiles = currentFiles.concat(newFiles); const fileNamesHtml = currentFiles.map(f => `<div style="background: var(--badge-green-bg); color: var(--badge-green-text); padding: 4px 10px; border-radius: 6px; display: inline-block; margin: 3px; font-size: 0.85rem; font-weight: 600; border: 1px solid var(--badge-green-border);">${f.name}</div>`).join(''); document.getElementById('fileName').innerHTML = `<b style="display:block; margin-bottom: 8px; color: var(--text-primary);">${currentFiles.length} arquivo(s) selecionado(s):</b><div style="max-height: 120px; overflow-y: auto; padding: 4px;">${fileNamesHtml}</div>`; document.getElementById('uploadBox').style.borderColor = "var(--ceara-green-1)"; document.getElementById('uploadBox').style.background = "var(--upload-hover-bg)"; input.value = ''; // clear so same file can be selected again } // Reusable function to render a download button function renderDownloadBtn(blob, filename, extraText="") { const url = window.URL.createObjectURL(blob); const resultArea = document.getElementById('resultArea'); resultArea.innerHTML = ` <div style="padding: 1.2rem; background: var(--upload-hover-bg); border: 1px solid var(--badge-green-border); border-radius: 10px; text-align: center;"> <p style="color: var(--text-primary); font-weight: 600; margin-bottom: 1rem;">Processamento concluído com sucesso!</p> ${extraText ? '<p style="color: var(--ceara-orange-1); margin-bottom: 1rem; font-weight: bold;">'+extraText+'</p>' : ''} <a href="${url}" download="${filename}" class="btn-primary" style="text-decoration: none; display: inline-block;">📥 Baixar Arquivo</a> </div> `; } // Previews async function loadPreviews(input, mode) { currentToolMode = mode; const newFiles = Array.from(input.files); if(newFiles.length === 0) return; let fileOffset = 0; if(mode === 'delete' || mode === 'split') { currentFiles = newFiles; updateFileName(input); } else { fileOffset = currentFiles.length; currentFiles = currentFiles.concat(newFiles); const fileNamesHtml = currentFiles.map(f => `<div style="background: var(--badge-green-bg); color: var(--badge-green-text); padding: 4px 10px; border-radius: 6px; display: inline-block; margin: 3px; font-size: 0.85rem; font-weight: 600; border: 1px solid var(--badge-green-border);">${f.name}</div>`).join(''); document.getElementById('fileName').innerHTML = `<b style="display:block; margin-bottom: 8px; color: var(--text-primary);">${currentFiles.length} arquivo(s) selecionado(s):</b><div style="max-height: 120px; overflow-y: auto; padding: 4px;">${fileNamesHtml}</div>`; document.getElementById('uploadBox').style.borderColor = "var(--ceara-green-1)"; document.getElementById('uploadBox').style.background = "var(--upload-hover-bg)"; } const fd = new FormData(); newFiles.forEach(f => fd.append("files", f)); document.getElementById('loadingPreview').style.display = 'block'; if(mode === 'delete' || mode === 'split') { document.getElementById('previewContainer').innerHTML = ''; pageOrder = []; } try { const response = await fetch(API_URL + '/preview', { method: 'POST', body: fd }); if(response.ok) { const data = await response.json(); data.files.forEach(file => { file.pages.forEach((b64, idx) => { pageOrder.push({ id: Math.random().toString(36).substr(2, 9), file_index: file.file_index + fileOffset, page_index: idx, b64: b64, deleted: false, filename: file.filename }); }); }); renderGrid(); document.getElementById('submitBtn').style.display = 'block'; } else { alert("Erro ao carregar pré-visualização."); } } catch (e) { alert("Erro de conexão."); } finally { document.getElementById('loadingPreview').style.display = 'none'; if(mode === 'merge') input.value = ''; } } function renderGrid() { const container = document.getElementById('previewContainer'); container.innerHTML = ''; pageOrder.forEach((page, index) => { const card = document.createElement('div'); card.className = 'page-card' + (page.deleted ? ' deleted' : ''); card.dataset.id = page.id; let htmlContent = ''; if (currentToolMode !== 'split') { htmlContent += `<button type="button" class="delete-btn" onclick="toggleDelete('${page.id}')" title="Remover/Restaurar">✕</button>`; } htmlContent += ` <img src="data:image/jpeg;base64,${page.b64}" onclick="toggleZoom(this.parentNode)"> <div class="page-label"> <div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;" title="${page.filename}">${page.filename}</div> <div>Pág ${page.page_index + 1}</div> </div> `; card.innerHTML = htmlContent; container.appendChild(card); }); // SortableJS (apenas se não for modo split) if (currentToolMode !== 'split') { new Sortable(container, { animation: 150, ghostClass: 'sortable-ghost', onEnd: function (evt) { const itemEl = evt.item; const movedItem = pageOrder.splice(evt.oldIndex, 1)[0]; pageOrder.splice(evt.newIndex, 0, movedItem); }, }); } } window.toggleZoom = function(cardElement) { if (cardElement.classList.contains('zoomed')) { cardElement.classList.remove('zoomed'); } else { document.querySelectorAll('.page-card.zoomed').forEach(el => el.classList.remove('zoomed')); cardElement.classList.add('zoomed'); } }; window.toggleDelete = function(id) { const page = pageOrder.find(p => p.id === id); if(page) { page.deleted = !page.deleted; renderGrid(); } }; // ========================================== // OCR Logic // ========================================== async function handleSimpleSubmit(e, path, typeField, extraFields, isFile) { e.preventDefault(); const fileInput = document.getElementById('fileInput'); if (fileInput.files.length === 0) return alert("Selecione um arquivo."); const formData = new FormData(); formData.append(isFile ? 'file' : 'files', fileInput.files[0]); if(typeField) formData.append('type', document.getElementById(typeField).value); await genericPost(path, formData, 'processado.pdf'); } async function handleValidateSignatureSubmit(e) { e.preventDefault(); const fileInput = document.getElementById('fileInput'); if (!fileInput.files || fileInput.files.length === 0) return; const formData = new FormData(); formData.append("file", fileInput.files[0]); document.getElementById('loading').style.display = 'block'; document.getElementById('submitBtn').disabled = true; document.getElementById('resultArea').innerHTML = ''; try { const response = await fetch(`${API_URL}/verify-signatures`, { method: 'POST', body: formData }); if (!response.ok) { const errText = await response.text(); throw new Error(errText); } const data = await response.json(); let html = ''; if (data.total_signatures === 0) { html = ` <div style="padding: 1.5rem; background: var(--badge-orange-bg); border: 1px solid var(--badge-orange-border); border-radius: 8px; text-align: center;"> <h3 style="color: var(--badge-orange-text); margin-bottom: 0.5rem;">Nenhuma Assinatura Encontrada</h3> <p style="color: var(--text-secondary);">O documento não possui assinaturas digitais embutidas detectáveis (apenas marcas de carimbo visuais não contam como assinaturas criptográficas válidas).</p> </div> `; } else { html = `<h3 style="color: var(--text-primary); margin-bottom: 1rem; border-bottom: 1px solid var(--bg-glass-border); padding-bottom: 0.5rem;">${data.total_signatures} Assinatura(s) Detectada(s)</h3>`; html += `<div style="display: flex; flex-direction: column; gap: 1rem;">`; data.signatures.forEach((sig, index) => { const isTrust = sig.trust_type && sig.trust_type !== "unknown"; const isIntact = sig.is_intact; const isError = sig.signer_name === "Assinatura Desconhecida"; let borderColor = isIntact && isTrust ? "var(--ceara-green-1)" : "var(--ceara-orange-1)"; if (isError || !isIntact) borderColor = "#dc2626"; if (isIntact && sig.trust_type === "govbr") borderColor = "#0284c7"; if (isIntact && sig.trust_type === "suite") borderColor = "#65a30d"; let badgeHtml = ''; if (isIntact) { badgeHtml += `<span style="background: var(--badge-green-bg); color: var(--badge-green-text); border: 1px solid var(--badge-green-border); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;">Documento Íntegro</span> `; } else { badgeHtml += `<span style="background: rgba(220, 38, 38, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;">Adulterado após assinado</span> `; } if (sig.trust_type === "icp") { badgeHtml += `<span style="background: var(--badge-green-bg); color: var(--badge-green-text); border: 1px solid var(--badge-green-border); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;">[${sig.trust_class}] ${sig.trust_name}</span>`; } else if (sig.trust_type === "govbr") { badgeHtml += `<span style="background: rgba(2, 132, 199, 0.15); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.3); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;">[${sig.trust_class}] ${sig.trust_name}</span>`; } else if (sig.trust_type === "suite") { badgeHtml += `<span style="background: rgba(101, 163, 13, 0.15); color: #84cc16; border: 1px solid rgba(132, 204, 22, 0.3); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold;">[${sig.trust_class}] ${sig.trust_name}</span>`; } else { badgeHtml += `<span style="background: var(--bg-subtle); color: var(--text-secondary); border: 1px solid var(--input-border); padding: 2px 8px; border-radius: 4px; font-size: 0.8rem;">Emissor Comum/Desconhecido</span>`; } html += ` <div style="padding: 1rem; background: var(--bg-subtle); border: 1px solid var(--bg-glass-border); border-left: 4px solid ${borderColor}; border-radius: 6px;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;"> <h4 style="color: var(--text-primary); margin: 0; font-size: 1.1rem;">${index + 1}. ${sig.signer_name}</h4> </div> <div style="margin-bottom: 0.8rem;">${badgeHtml}</div> <p style="color: var(--text-secondary); margin: 0 0 0.3rem 0; font-size: 0.9rem;"><strong>Data:</strong> ${sig.date}</p> <p style="color: var(--text-secondary); margin: 0; font-size: 0.9rem; word-break: break-all;"><strong>Emissor/Autoridade:</strong> ${sig.issuer}</p> ${isError ? `<p style="color: #ef4444; margin-top: 0.5rem; font-size: 0.9rem;">${sig.validation_message}</p>` : ''} </div> `; }); html += `</div>`; } document.getElementById('resultArea').innerHTML = html; } catch (err) { document.getElementById('resultArea').innerHTML = ` <div style="padding: 1rem; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px;"> <p style="color: #991b1b; font-weight: bold;">Erro ao validar:</p> <p style="color: #dc2626;">${err.message}</p> </div> `; } finally { document.getElementById('loading').style.display = 'none'; document.getElementById('submitBtn').disabled = false; } } async function handleOCRSubmit(e) { e.preventDefault(); const fileInput = document.getElementById('fileInput'); if (fileInput.files.length === 0) return alert("Selecione um arquivo."); const lang = document.getElementById('ocr-lang').value; document.getElementById('submitBtn').disabled = true; document.getElementById('loading').style.display = 'block'; document.getElementById('resultArea').innerHTML = ''; try { const formData = new FormData(); formData.append('file', fileInput.files[0]); formData.append('lang', lang); const response = await fetch(`${API_URL}/ocr`, { method: 'POST', body: formData }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || 'Erro ao processar OCR'); } const blob = await response.blob(); const originalName = fileInput.files[0].name.replace('.pdf', ''); renderDownloadBtn(blob, `${originalName}_pesquisavel.pdf`); } catch (error) { alert('Erro no OCR: ' + error.message); } finally { document.getElementById('submitBtn').disabled = false; document.getElementById('loading').style.display = 'none'; } } async function handleReconstruct(e, filename) { e.preventDefault(); const activePages = pageOrder.filter(p => !p.deleted); if(activePages.length === 0) { alert("Você deve deixar pelo menos 1 página ativa."); return; } const fd = new FormData(); currentFiles.forEach(f => fd.append("files", f)); const orderData = activePages.map(p => ({file_index: p.file_index, page_index: p.page_index})); fd.append("order", JSON.stringify(orderData)); await genericPost('/reconstruct', fd, filename); } async function genericPost(url, formData, defaultFilename) { document.getElementById('submitBtn').style.display = 'none'; document.getElementById('loading').style.display = 'block'; document.getElementById('resultArea').innerHTML = ''; try { const response = await fetch(API_URL + url, { method: 'POST', body: formData }); if (response.ok) { const blob = await response.blob(); const disposition = response.headers.get('Content-Disposition'); let filename = defaultFilename; if (disposition && disposition.indexOf('filename=') !== -1) { filename = disposition.split('filename=')[1].replace(/"/g, ''); } renderDownloadBtn(blob, filename); } else { const err = await response.json(); alert("Erro: " + (err.detail || "Falha no processamento.")); } } catch (error) { console.error(error); alert("Erro de conexão com o servidor."); } finally { if(document.getElementById('submitBtn')) document.getElementById('submitBtn').style.display = 'block'; if(document.getElementById('loading')) document.getElementById('loading').style.display = 'none'; } } async function handleCompress(e) { e.preventDefault(); const fd = new FormData(); fd.append("file", document.getElementById('fileInput').files[0]); fd.append("level", document.getElementById('compressLevel').value); document.getElementById('submitBtn').style.display = 'none'; document.getElementById('loading').style.display = 'block'; document.getElementById('resultArea').innerHTML = ''; try { const response = await fetch(API_URL + '/compress', { method: 'POST', body: fd }); if (response.ok) { const blob = await response.blob(); const sizeMB = (blob.size / (1024 * 1024)).toFixed(2); let filename = "comprimido.pdf"; const disposition = response.headers.get('Content-Disposition'); if (disposition && disposition.indexOf('filename=') !== -1) { filename = disposition.split('filename=')[1].replace(/["']/g, ''); } renderDownloadBtn(blob, filename, "Tamanho final: " + sizeMB + " MB"); } else { alert("Erro ao processar."); } } catch (e) { alert("Erro de conexão."); } finally { if(document.getElementById('submitBtn')) document.getElementById('submitBtn').style.display = 'block'; if(document.getElementById('loading')) document.getElementById('loading').style.display = 'none'; } } async function handleSplit(e) { e.preventDefault(); const fd = new FormData(); fd.append("file", document.getElementById('fileInput').files[0]); fd.append("level", document.getElementById('splitCompress').value); const mode = document.getElementById('splitMode').value; fd.append("mode", mode); if (mode === 'size') { fd.append("max_size_mb", document.getElementById('maxMb').value); } else { const intervals = document.getElementById('splitIntervals').value.trim(); if (!intervals) { alert("Por favor, digite os intervalos desejados."); return; } fd.append("intervals", intervals); fd.append("merge", document.getElementById('splitMerge').checked); } document.getElementById('submitBtn').style.display = 'none'; document.getElementById('loading').style.display = 'block'; const resultArea = document.getElementById('resultArea'); resultArea.innerHTML = ''; try { const response = await fetch(API_URL + '/split', { method: 'POST', body: fd }); if (response.ok) { const blob = await response.blob(); let zipFilename = "particoes.zip"; const disposition = response.headers.get('Content-Disposition'); if (disposition && disposition.indexOf('filename=') !== -1) { zipFilename = disposition.split('filename=')[1].replace(/["']/g, ''); } // Read ZIP using JSZip const jszip = new JSZip(); const zip = await jszip.loadAsync(blob); let html = '<div style="padding: 1.2rem; background: var(--upload-hover-bg); border: 1px solid var(--badge-green-border); border-radius: 10px;">'; html += '<h4 style="color: var(--text-primary); margin-bottom:10px;">Partições Geradas</h4>'; html += '<ul style="list-style:none; padding:0; margin-bottom: 15px;">'; const zipUrl = window.URL.createObjectURL(blob); // Extract entries and sort them by name (e.g. particao_1, particao_2) // Need a custom sort to handle numerical sorting of "particao_N" let entries = []; zip.forEach(function (relativePath, zipEntry) { entries.push(zipEntry); }); entries.sort((a, b) => { const numA = parseInt(a.name.match(/\d+/)) || 0; const numB = parseInt(b.name.match(/\d+/)) || 0; return numA - numB; }); let filesPromises = entries.map(zipEntry => { return zipEntry.async("blob").then(function (fileData) { const sizeMB = (fileData.size / (1024 * 1024)).toFixed(2); const fileUrl = window.URL.createObjectURL(fileData); return { name: zipEntry.name, size: sizeMB, url: fileUrl }; }); }); const resolvedFiles = await Promise.all(filesPromises); resolvedFiles.forEach(file => { html += '<li style="display:flex; justify-content:space-between; align-items:center; padding: 6px 0; border-bottom: 1px solid var(--input-border);">'; html += '<span style="color:var(--text-primary);">' + file.name + ' <strong style="color:var(--ceara-orange-1)">(' + file.size + ' MB)</strong></span>'; html += '<a href="' + file.url + '" download="' + file.name + '" class="btn-outline" style="padding: 5px 10px; font-size:0.8rem; text-decoration:none;">Baixar Parte</a>'; html += '</li>'; }); html += '</ul>'; html += '<a href="' + zipUrl + '" download="' + zipFilename + '" class="btn-primary" style="text-decoration:none; display:inline-block; width:100%; text-align:center;">📥 Baixar Todas as Partições (.zip)</a>'; html += '</div>'; resultArea.innerHTML = html; } else { alert("Erro ao particionar."); } } catch (e) { console.error(e); alert("Erro de conexão."); } finally { if(document.getElementById('submitBtn')) document.getElementById('submitBtn').style.display = 'block'; if(document.getElementById('loading')) document.getElementById('loading').style.display = 'none'; } } async function handleConvert(e) { e.preventDefault(); const fd = new FormData(); const type = document.getElementById('convertType').value; if(type === 'pdf-from-images') { if(currentFiles.length === 0) return alert("Selecione pelo menos um arquivo."); for(let i=0; i<currentFiles.length; i++){ fd.append("files", currentFiles[i]); } await genericPost('/convert/pdf-from-images', fd, "imagens.pdf"); } else { const fileInput = document.getElementById('fileInput'); let singleFile = currentFiles.length > 0 ? currentFiles[0] : (fileInput.files.length > 0 ? fileInput.files[0] : null); if(!singleFile) return alert("Selecione um arquivo."); fd.append("file", singleFile); let ext = type === 'word' ? '.docx' : type === 'excel' ? '.xlsx' : type === 'pptx' ? '.pptx' : '.zip'; await genericPost('/convert/' + type, fd, 'convertido' + ext); } } async function handleSecurity(e) { e.preventDefault(); const fd = new FormData(); fd.append("file", document.getElementById('fileInput').files[0]); fd.append("password", document.getElementById('secPassword').value); const action = document.getElementById('secAction').value; await genericPost('/security/' + action, fd, 'seguranca.pdf'); } async function handleCompare(e) { e.preventDefault(); const fd = new FormData(); fd.append("file_a", document.getElementById('fileA').files[0]); fd.append("file_b", document.getElementById('fileB').files[0]); document.getElementById('submitBtn').style.display = 'none'; document.getElementById('loading').style.display = 'block'; const resultArea = document.getElementById('resultArea'); resultArea.innerHTML = ''; try { const response = await fetch(API_URL + '/security/compare', { method: 'POST', body: fd }); if (response.ok) { const data = await response.json(); let html = ''; for(let i=0; i<data.images_a.length; i++) { html += '<div style="display:flex; width:100%; margin-bottom:20px; gap:10px;">'; html += '<div style="flex:1; border: 1px solid #444; padding: 5px; background: white; text-align:center;">'; html += '<p style="color:black; font-weight:bold; margin-bottom:5px;">Original - Página ' + (i+1) + '</p>'; if(data.images_a[i]) html += '<img src="data:image/png;base64,' + data.images_a[i] + '" style="max-width: 100%; height: auto;">'; html += '</div>'; html += '<div style="flex:1; border: 1px solid #444; padding: 5px; background: white; text-align:center;">'; html += '<p style="color:black; font-weight:bold; margin-bottom:5px;">Modificado - Página ' + (i+1) + '</p>'; if(data.images_b[i]) html += '<img src="data:image/png;base64,' + data.images_b[i] + '" style="max-width: 100%; height: auto;">'; html += '</div>'; html += '</div>'; } resultArea.innerHTML = html; } else { alert("Erro ao comparar PDFs."); } } catch (error) { console.error(error); alert("Erro de conexão."); } finally { if(document.getElementById('submitBtn')) document.getElementById('submitBtn').style.display = 'block'; if(document.getElementById('loading')) document.getElementById('loading').style.display = 'none'; } } function toggleSplitMode() { const mode = document.getElementById('splitMode').value; if (mode === 'size') { document.getElementById('sizeModeParams').style.display = 'block'; document.getElementById('intervalModeParams').style.display = 'none'; } else { document.getElementById('sizeModeParams').style.display = 'none'; document.getElementById('intervalModeParams').style.display = 'block'; } } async function handleRepairSubmit(e) { e.preventDefault(); const fd = new FormData(); fd.append("file", document.getElementById('fileInput').files[0]); await genericPost('/repair', fd, 'reparado.pdf'); } let quillEditor = null; let currentEditorFile = null; async function handleEditorLoad(e) { e.preventDefault(); const file = document.getElementById('fileInput').files[0]; if (!file) return; currentEditorFile = file; const fd = new FormData(); fd.append("file", file); document.getElementById('submitBtn').style.display = 'none'; document.getElementById('loading').style.display = 'block'; try { const response = await fetch(API_URL + '/extract-html', { method: 'POST', body: fd }); if (response.ok) { const data = await response.json(); document.getElementById('editor-container').style.display = 'block'; if (!quillEditor) { quillEditor = new Quill('#quill-editor', { theme: 'snow', modules: { toolbar: [ [{ 'header': [1, 2, 3, false] }], ['bold', 'italic', 'underline', 'strike'], [{ 'color': [] }, { 'background': [] }], [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'align': [] }], ['clean'] ] } }); } quillEditor.clipboard.dangerouslyPasteHTML(data.html || ""); } else { alert("Erro ao extrair conteúdo."); } } catch (err) { alert("Erro de conexão."); } finally { document.getElementById('submitBtn').style.display = 'block'; document.getElementById('loading').style.display = 'none'; } } async function triggerAIActions(action, param) { const file = document.getElementById('fileInput').files[0]; if (!file) { alert("Selecione um arquivo PDF primeiro!"); return; } const fd = new FormData(); fd.append("file", file); if (action === 'summarize') { fd.append("length", param); } else if (action === 'translate') { fd.append("lang", document.getElementById('translate-lang').value); } document.getElementById('loading').style.display = 'block'; document.getElementById('resultContainer').style.display = 'none'; try { const response = await fetch(API_URL + '/' + action, { method: 'POST', body: fd }); if (response.ok) { const data = await response.json(); document.getElementById('resultContainer').style.display = 'block'; document.getElementById('previewArea').innerHTML = data.html || ""; } else { let errMsg = "Erro na inteligência artificial."; try { const errData = await response.json(); errMsg += " Detalhes: " + (errData.detail || errData.error || ""); } catch(e) {} alert(errMsg); } } catch (err) { alert("Erro de conexão."); } finally { document.getElementById('loading').style.display = 'none'; } } async function downloadBackendPDF(html, defaultName) { const fd = new FormData(); fd.append("html_content", html); // Mostra um aviso enquanto baixa const btn = document.activeElement; const oldText = btn ? btn.innerText : ""; if (btn) btn.innerText = "⏳ Gerando..."; try { const response = await fetch(API_URL + '/export-pdf', { method: 'POST', body: fd }); if (response.ok) { const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = defaultName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); a.remove(); } else { alert("Erro ao exportar o PDF pelo servidor."); } } catch(err) { alert("Erro de conexão ao exportar PDF."); } finally { if (btn) btn.innerText = oldText; } } function downloadPreviewPDF(defaultName) { const content = document.getElementById('previewArea'); if (!content) return; downloadBackendPDF(content.innerHTML, defaultName); } function downloadEditorPDF() { if (!quillEditor) return; const content = document.querySelector('.ql-editor').innerHTML; downloadBackendPDF(content, 'SOPDF_Editado.pdf'); }
const API_URL = "/api";
let currentFiles = [];
let pageOrder = [];
let currentToolMode = '';
function updateThemeButtonLabel(isDark) {
const btn = document.getElementById('themeToggleBtn');
if (btn) {
btn.innerHTML = isDark ? '☀️ Modo Claro' : '🌙 Modo Escuro';
}
}
function toggleTheme() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('sopdf_theme', isDark ? 'dark' : 'light');
updateThemeButtonLabel(isDark);
}
(function initTheme() {
const savedTheme = localStorage.getItem('sopdf_theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
updateThemeButtonLabel(true);
}
})();
function scrollToTools() {
document.getElementById('ferramentas').scrollIntoView({ behavior: 'smooth' });
}
function openTool(toolType) {
const homeView = document.getElementById('home-view');
const toolView = document.getElementById('tool-view');
const body = document.getElementById('tool-content');
homeView.style.display = 'none';
toolView.style.display = 'block';
let content = '';
currentFiles = [];
pageOrder = [];
switch(toolType) {
case 'delete':
content = `
<h2 style="margin-bottom: 1rem; color: var(--ceara-orange-1);">🗑️ Excluir e Reordenar Páginas</h2>
<form id="toolForm" onsubmit="handleReconstruct(event, 'modificado.pdf')">
<div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()">
<span style="font-size: 2rem;">📄</span>
<p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p>
<input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="loadPreviews(this, 'delete')" required>
</div>
<div id="loadingPreview" style="display: none; text-align: center; color: var(--ceara-yellow); margin-top: 10px;">Carregando visualização...</div>
<div id="previewContainer" class="page-grid"></div>
<button type="submit" class="btn-primary" style="width: 100%; margin-top: 1rem; display:none;" id="submitBtn">Salvar Alterações</button>
<div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando...</div>
<div id="resultArea" style="margin-top: 1rem;"></div>
</form>
`;
break;
case 'merge':
content = `
<h2 style="margin-bottom: 1rem; color: var(--ceara-green-1);">🔗 Mesclar e Reordenar PDFs</h2>
<form id="toolForm" onsubmit="handleReconstruct(event, 'mesclado.pdf')">
<div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()">
<span style="font-size: 2rem;">📄</span>
<p style="margin-top: 1rem;" id="fileName">Selecione Múltiplos PDFs</p>
<input type="file" id="fileInput" accept=".pdf" style="display: none;" multiple onchange="loadPreviews(this, 'merge')">
</div>
<div id="loadingPreview" style="display: none; text-align: center; color: var(--ceara-yellow); margin-top: 10px;">Carregando visualização...</div>
<div id="previewContainer" class="page-grid"></div>
<button type="submit" class="btn-primary" style="width: 100%; margin-top: 1rem; display:none;" id="submitBtn">Salvar PDF Mesclado</button>
<div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando...</div>
<div id="resultArea" style="margin-top: 1rem;"></div>
</form>
`;
break;
case 'compress':
content = `
<h2 style="margin-bottom: 1rem; color: var(--ceara-green-1);">🗜️ Comprimir PDF</h2>
<form id="toolForm" onsubmit="handleCompress(event)">
<div class="upload-area" id="uploadBox" onclick="document.getElementById('fileInput').click()">
<span style="font-size: 2rem;">📄</span>
<p style="margin-top: 1rem;" id="fileName">Selecione o PDF</p>
<input type="file" id="fileInput" accept=".pdf" style="display: none;" onchange="updateFileName(this)" required>
</div>
<div class="input-group">
<label>Nível de Compressão</label>
<select id="compressLevel">
<option value="recomendada">Recomendada (Boa qualidade)</option>
<option value="extrema">Extrema (Menor tamanho possível)</option>
<option value="baixa">Baixa (Alta qualidade)</option>
</select>
</div>
<button type="submit" class="btn-primary" style="width: 100%;" id="submitBtn">Comprimir Arquivo</button>
<div id="loading" style="display: none; text-align: center; margin-top: 1rem; color: var(--ceara-yellow);">Processando... Aguarde.</div>
<div id="resultArea" style="margin-top: 1rem;"></div>
</form>
`;
break;