/// import React, { useState, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import axios from 'axios'; import { toast } from 'react-toastify'; import { useAuth } from '../../contexts/AuthContext'; import { DocumentIcon, CloudArrowUpIcon, ArrowsPointingInIcon, ScissorsIcon, ArrowsUpDownIcon, PhotoIcon, DocumentTextIcon, InformationCircleIcon, ArrowDownTrayIcon, ClockIcon, } from '@heroicons/react/24/outline'; interface OperationResult { operacaoId: string; outputPaths?: string[]; outputPath?: string; zipPath?: string; processingTime?: number; reduction?: number; filesCreated?: number; text?: string; confidence?: number; } type OperationType = | 'compress' | 'split' | 'merge' | 'pdfToImages' | 'imagesToPDF' | 'ocr' | 'info'; export const PDFManipulationPage: React.FC = () => { const { getToken } = useAuth(); const [selectedOperation, setSelectedOperation] = useState('compress'); const [files, setFiles] = useState([]); const [processing, setProcessing] = useState(false); const [result, setResult] = useState(null); const [history, setHistory] = useState([]); // Configurações para cada operação const [compressQuality, setCompressQuality] = useState(85); const [enableOCR, setEnableOCR] = useState(false); const [splitType, setSplitType] = useState<'pages' | 'range' | 'chunks'>('pages'); const [splitPages, setSplitPages] = useState('1,2,3'); const [splitRanges, setSplitRanges] = useState('1-5,6-10'); const [chunkSize, setChunkSize] = useState(5); const [imageFormat, setImageFormat] = useState<'png' | 'jpg' | 'webp'>('png'); const [imageQuality, setImageQuality] = useState(95); const [imageDensity, setImageDensity] = useState(300); const [ocrLanguage, setOcrLanguage] = useState('por'); const onDrop = useCallback((acceptedFiles: File[]) => { setFiles((prev) => [...prev, ...acceptedFiles]); }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { 'application/pdf': ['.pdf'], 'image/*': ['.png', '.jpg', '.jpeg', '.webp'], }, }); const removeFile = (index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }; const clearFiles = () => { setFiles([]); setResult(null); }; const processFiles = async () => { if (files.length === 0) { toast.error('Selecione pelo menos um arquivo'); return; } setProcessing(true); setResult(null); try { const formData = new FormData(); files.forEach((file) => { formData.append('files', file); }); const token = getToken(); const config = { headers: { 'Content-Type': 'multipart/form-data', Authorization: `Bearer ${token}`, }, timeout: 600000, // 10 minutos para operações longas (compressão, OCR, etc) }; let response; switch (selectedOperation) { case 'compress': formData.append('quality', compressQuality.toString()); formData.append('enableOCR', enableOCR.toString()); response = await axios.post('/api/pdf/compress', formData, config); toast.success('PDFs comprimidos com sucesso!'); break; case 'split': formData.append('splitType', splitType); if (splitType === 'pages') { const pages = splitPages.split(',').map((p) => parseInt(p.trim())); formData.append('pages', JSON.stringify(pages)); } else if (splitType === 'range') { const ranges = splitRanges.split(',').map((r) => { const [start, end] = r.trim().split('-').map(Number); return { start, end }; }); formData.append('ranges', JSON.stringify(ranges)); } else { formData.append('chunkSize', chunkSize.toString()); } response = await axios.post('/api/pdf/split', formData, config); toast.success('PDF dividido com sucesso!'); break; case 'merge': formData.append('enableOCR', enableOCR.toString()); response = await axios.post('/api/pdf/merge', formData, config); toast.success('PDFs mesclados com sucesso!'); break; case 'pdfToImages': formData.append('format', imageFormat); formData.append('quality', imageQuality.toString()); formData.append('density', imageDensity.toString()); response = await axios.post('/api/pdf/pdf-to-images', formData, config); toast.success('PDF convertido em imagens!'); break; case 'imagesToPDF': formData.append('enableOCR', enableOCR.toString()); formData.append('fitToPage', 'true'); response = await axios.post('/api/pdf/images-to-pdf', formData, config); toast.success('Imagens convertidas em PDF!'); break; case 'ocr': formData.append('language', ocrLanguage); response = await axios.post('/api/pdf/ocr', formData, config); toast.success('OCR realizado com sucesso!'); break; case 'info': response = await axios.post('/api/pdf/info', formData, config); break; } setResult(response.data); loadHistory(); } catch (error: any) { console.error('Erro ao processar arquivos:', error); toast.error(error.response?.data?.message || 'Erro ao processar arquivos'); } finally { setProcessing(false); } }; const loadHistory = async () => { try { const token = getToken(); const response = await axios.get('/api/pdf/history', { headers: { Authorization: `Bearer ${token}` }, }); setHistory(response.data.operacoes || []); } catch (error) { console.error('Erro ao carregar histórico:', error); } }; const downloadFile = async (operacaoId: string, fileIndex: number) => { try { const token = getToken(); const response = await axios.get(`/api/pdf/download/${operacaoId}/${fileIndex}`, { headers: { Authorization: `Bearer ${token}` }, responseType: 'blob', }); // Extrai o nome do arquivo do header Content-Disposition const contentDisposition = response.headers['content-disposition']; let fileName = `arquivo-${fileIndex}`; if (contentDisposition) { const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition); if (matches != null && matches[1]) { fileName = matches[1].replace(/['"]/g, ''); } } // Usa o Content-Type correto do servidor const contentType = response.headers['content-type'] || 'application/octet-stream'; console.log(`Download frontend: ${fileName} (${contentType})`); // Cria o blob com o tipo correto const blob = new Blob([response.data], { type: contentType }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); link.remove(); // Libera a memória do blob URL setTimeout(() => window.URL.revokeObjectURL(url), 100); toast.success('Download iniciado!'); } catch (error) { console.error('Erro ao fazer download:', error); toast.error('Erro ao fazer download'); } }; const deleteOperation = async (operacaoId: string) => { if (!window.confirm('Deseja realmente deletar esta operação e seus arquivos?')) { return; } try { const token = getToken(); await axios.delete(`/api/pdf/operation/${operacaoId}`, { headers: { Authorization: `Bearer ${token}` }, }); toast.success('Operação deletada com sucesso!'); loadHistory(); } catch (error) { console.error('Erro ao deletar operação:', error); toast.error('Erro ao deletar operação'); } }; const clearAllHistory = async () => { if (!window.confirm('⚠️ ATENÇÃO: Isso irá deletar TODO o histórico e TODOS os arquivos armazenados. Esta ação não pode ser desfeita. Deseja continuar?')) { return; } try { const token = getToken(); const response = await axios.delete('/api/pdf/history/clear', { headers: { Authorization: `Bearer ${token}` }, }); toast.success(`Histórico limpo! ${response.data.operacoesRemovidas} operações removidas.`); setHistory([]); setResult(null); } catch (error) { console.error('Erro ao limpar histórico:', error); toast.error('Erro ao limpar histórico'); } }; const cleanupOldFiles = async () => { if (!window.confirm('Deseja remover operações com mais de 24 horas?')) { return; } try { const token = getToken(); const response = await axios.delete('/api/pdf/history/cleanup?hours=24', { headers: { Authorization: `Bearer ${token}` }, }); toast.success(`${response.data.operacoesRemovidas} operações antigas removidas!`); loadHistory(); } catch (error) { console.error('Erro ao limpar arquivos antigos:', error); toast.error('Erro ao limpar arquivos antigos'); } }; React.useEffect(() => { loadHistory(); }, []); const operations = [ { id: 'compress' as OperationType, name: 'Comprimir PDF', icon: ArrowsPointingInIcon, description: 'Reduza o tamanho mantendo a qualidade', }, { id: 'split' as OperationType, name: 'Dividir PDF', icon: ScissorsIcon, description: 'Separe páginas específicas', }, { id: 'merge' as OperationType, name: 'Mesclar PDFs', icon: ArrowsUpDownIcon, description: 'Combine múltiplos PDFs', }, { id: 'pdfToImages' as OperationType, name: 'PDF para Imagens', icon: PhotoIcon, description: 'Converta páginas em imagens', }, { id: 'imagesToPDF' as OperationType, name: 'Imagens para PDF', icon: DocumentIcon, description: 'Crie PDF a partir de imagens', }, { id: 'ocr' as OperationType, name: 'OCR', icon: DocumentTextIcon, description: 'Extraia texto de PDFs', }, { id: 'info' as OperationType, name: 'Informações', icon: InformationCircleIcon, description: 'Obtenha detalhes do PDF', }, ]; const formatBytes = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; }; return (

Manipulação de PDFs

Ferramentas completas para trabalhar com arquivos PDF

{/* Seleção de operação */}
{operations.map((op) => ( ))}
{/* Área principal */}
{/* Upload de arquivos */}

Arquivos

{isDragActive ? (

Solte os arquivos aqui...

) : (

Arraste e solte arquivos aqui, ou clique para selecionar

PDFs e imagens (PNG, JPG, WEBP)

)}
{/* Lista de arquivos */} {files.length > 0 && (

Arquivos selecionados ({files.length})

{files.map((file, index) => (

{file.name}

{formatBytes(file.size)}

))}
)}
{/* Configurações específicas */}

Configurações

{selectedOperation === 'compress' && (
setCompressQuality(parseInt(e.target.value))} className="w-full" />

50-70%: Máxima compressão | 80-90%: Equilíbrio ideal | 90-100%: Alta fidelidade

setEnableOCR(e.target.checked)} className="h-4 w-4 text-blue-600 rounded" />

OCR torna o PDF pesquisável. É instantâneo para PDFs digitais.

)} {selectedOperation === 'split' && (
{splitType === 'pages' && (
setSplitPages(e.target.value)} placeholder="1,2,3,5,8" className="w-full border border-gray-300 rounded-lg px-3 py-2" />
)} {splitType === 'range' && (
setSplitRanges(e.target.value)} placeholder="1-5,6-10,11-15" className="w-full border border-gray-300 rounded-lg px-3 py-2" />
)} {splitType === 'chunks' && (
setChunkSize(parseInt(e.target.value))} min="1" className="w-full border border-gray-300 rounded-lg px-3 py-2" />
)}
)} {selectedOperation === 'pdfToImages' && (
setImageQuality(parseInt(e.target.value))} className="w-full" />
setImageDensity(parseInt(e.target.value))} className="w-full" />
)} {selectedOperation === 'ocr' && (
)} {selectedOperation === 'merge' && (

Selecione 2 ou mais PDFs para mesclar em um único arquivo.

setEnableOCR(e.target.checked)} className="h-4 w-4 text-blue-600 rounded" />

⚠️ OCR aumenta significativamente o tempo de processamento

)} {selectedOperation === 'imagesToPDF' && (

Converte múltiplas imagens em um único arquivo PDF.

setEnableOCR(e.target.checked)} className="h-4 w-4 text-blue-600 rounded" />

⚠️ OCR aumenta significativamente o tempo de processamento

)}
{/* Resultado */} {result && (

Resultado

{selectedOperation === 'compress' && result.reduction !== undefined && (

Redução de {result.reduction.toFixed(2)}%

Tempo de processamento: {result.processingTime}ms

)} {(result.outputPaths || result.outputPath || result.zipPath) && (

Arquivos gerados:

{/* Botão de download ZIP (se disponível) */} {result.zipPath && ( )} {/* Arquivos individuais */} {result.outputPaths?.map((path, index) => { // Determina o nome do arquivo baseado na operação let fileName = `Arquivo ${index + 1}`; if (selectedOperation === 'pdfToImages') { fileName = `Imagem ${index + 1}`; } else if (selectedOperation === 'split') { fileName = `Página ${index + 1}`; } return ( ); })} {result.outputPath && !result.outputPaths && ( )}
)} {selectedOperation === 'ocr' && result.text && (

Texto extraído:

{result.text}
{result.confidence && (

Confiança: {result.confidence.toFixed(2)}%

)}
)}
)}
{/* Histórico */}

Histórico

{history.length > 0 && (
)}
{history.length === 0 ? (

Nenhuma operação ainda

) : (
{history.map((op) => (
{op.tipoOperacao.replace('_', ' ')}
{op.status}
{new Date(op.criadoEm).toLocaleString('pt-BR')}
{op.tempoProcessamento && (

{op.tempoProcessamento}ms

)} {op.status === 'CONCLUIDO' && op.arquivosResultado && op.arquivosResultado.length > 0 && (
{op.arquivosResultado.map((arquivo: string, index: number) => ( ))}
)}
))}
)}
); };