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 = `
🗑️ Excluir e Reordenar Páginas
`;
break;
case 'merge':
content = `
🔗 Mesclar e Reordenar PDFs
`;
break;
case 'compress':
content = `
🗜️ Comprimir PDF
`;
break;
case 'split':
content = `
✂️ Particionar PDF
Nota: Se uma única página for maior que o limite, ela não poderá ser dividida.
`;
break;
case 'convert':
content = `
🔄 Conversões
`;
break;
case 'security':
content = `
🛡️ Segurança AES
`;
break;
case 'compare':
content = `
👁️ Comparar PDFs
`;
break;
case 'ocr':
content = `
🔍 Reconhecimento de Texto (OCR)
`;
break;
case 'validate_signature':
content = `
✒️ Validar Assinaturas
Verifique a integridade de assinaturas digitais padrão ICP-Brasil ou Gov.br contidas no documento PDF.
`;
break;
case 'repair':
content = `
🩹 Reparar PDF
Reconstrua PDFs corrompidos para tentar recuperar o acesso ao arquivo original.
`;
break;
case 'ai_assistant':
content = `
🤖 Inteligência Artificial
Gere resumos ou traduza documentos inteiros de forma automática.
Resumo Curto
Resumo Médio
Resumo Longo
Inglês
Espanhol
Português
Traduzir Texto
A IA está processando o documento, isso pode levar alguns minutos...
Pré-visualização
📥 Baixar PDF
`;
break;
case 'editor':
content = `
📝 Editar Conteúdo
Extraia e edite o conteúdo textual do seu PDF e exporte novamente.
`;
// 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 => `${f.name}
`).join('');
document.getElementById('fileName').innerHTML = `${currentFiles.length} arquivo(s) selecionado(s): ${fileNamesHtml}
`;
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 = `
Processamento concluído com sucesso!
${extraText ? '
'+extraText+'
' : ''}
📥 Baixar Arquivo
`;
}
// 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 => `${f.name}
`).join('');
document.getElementById('fileName').innerHTML = `${currentFiles.length} arquivo(s) selecionado(s): ${fileNamesHtml}
`;
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 += `✕ `;
}
htmlContent += `
${page.filename}
Pág ${page.page_index + 1}
`;
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 = `
Nenhuma Assinatura Encontrada
O documento não possui assinaturas digitais embutidas detectáveis (apenas marcas de carimbo visuais não contam como assinaturas criptográficas válidas).
`;
} else {
html = `${data.total_signatures} Assinatura(s) Detectada(s) `;
html += ``;
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 += `
Documento Íntegro `;
} else {
badgeHtml += `
Adulterado após assinado `;
}
if (sig.trust_type === "icp") {
badgeHtml += `
[${sig.trust_class}] ${sig.trust_name} `;
} else if (sig.trust_type === "govbr") {
badgeHtml += `
[${sig.trust_class}] ${sig.trust_name} `;
} else if (sig.trust_type === "suite") {
badgeHtml += `
[${sig.trust_class}] ${sig.trust_name} `;
} else {
badgeHtml += `
Emissor Comum/Desconhecido `;
}
html += `
${index + 1}. ${sig.signer_name}
${badgeHtml}
Data: ${sig.date}
Emissor/Autoridade: ${sig.issuer}
${isError ? `
${sig.validation_message}
` : ''}
`;
});
html += `
`;
}
document.getElementById('resultArea').innerHTML = html;
} catch (err) {
document.getElementById('resultArea').innerHTML = `
Erro ao validar:
${err.message}
`;
} 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 = '';
html += '
Partições Geradas ';
html += '
';
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 += '';
html += '' + file.name + ' (' + file.size + ' MB) ';
html += 'Baixar Parte ';
html += ' ';
});
html += ' ';
html += '
📥 Baixar Todas as Partições (.zip) ';
html += '
';
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 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';
html += '';
html += '
Original - Página ' + (i+1) + '
';
if(data.images_a[i]) html += '
';
html += '
';
html += '';
html += '
Modificado - Página ' + (i+1) + '
';
if(data.images_b[i]) html += '
';
html += '
';
html += '';
}
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');
}