pdf: fix feature
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo.sousa.577@gmail.com>
This commit is contained in:
1 parent
f4afe83db0
commit
db7b832044
11 files changed
+1009
-104
No files matched your search
@@ -1,3 +1,4 @@
|
||||
/// <reference types="react" />
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import axios from 'axios';
|
||||
@@ -20,6 +21,7 @@ interface OperationResult {
|
||||
operacaoId: string;
|
||||
outputPaths?: string[];
|
||||
outputPath?: string;
|
||||
zipPath?: string;
|
||||
processingTime?: number;
|
||||
reduction?: number;
|
||||
filesCreated?: number;
|
||||
@@ -45,7 +47,8 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
const [history, setHistory] = useState<any[]>([]);
|
||||
|
||||
// Configurações para cada operação
|
||||
const [compressQuality, setCompressQuality] = useState(95);
|
||||
const [compressQuality, setCompressQuality] = useState(50);
|
||||
const [enableOCR, setEnableOCR] = useState(false);
|
||||
const [splitType, setSplitType] = useState<'pages' | 'range' | 'chunks'>('pages');
|
||||
const [splitPages, setSplitPages] = useState<string>('1,2,3');
|
||||
const [splitRanges, setSplitRanges] = useState<string>('1-5,6-10');
|
||||
@@ -97,6 +100,7 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
timeout: 600000, // 10 minutos para operações longas (compressão, OCR, etc)
|
||||
};
|
||||
|
||||
let response;
|
||||
@@ -104,6 +108,7 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
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;
|
||||
@@ -127,6 +132,7 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
formData.append('enableOCR', enableOCR.toString());
|
||||
response = await axios.post('/api/pdf/merge', formData, config);
|
||||
toast.success('PDFs mesclados com sucesso!');
|
||||
break;
|
||||
@@ -140,6 +146,7 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
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!');
|
||||
@@ -186,13 +193,34 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
// 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', `arquivo-${fileIndex}.pdf`);
|
||||
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) {
|
||||
@@ -201,6 +229,64 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}, []);
|
||||
@@ -367,20 +453,38 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
<h2 className="text-xl font-semibold mb-4">Configurações</h2>
|
||||
|
||||
{selectedOperation === 'compress' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Qualidade ({compressQuality}%)
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="100"
|
||||
value={compressQuality}
|
||||
onChange={(e) => setCompressQuality(parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
95% recomendado para qualidade sem perda perceptível
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Qualidade ({compressQuality}%)
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="30"
|
||||
max="100"
|
||||
value={compressQuality}
|
||||
onChange={(e) => setCompressQuality(parseInt(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
30-50%: Máxima compressão (>60% redução) | 70-100%: Alta qualidade
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enableOCR"
|
||||
checked={enableOCR}
|
||||
onChange={(e) => setEnableOCR(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 rounded"
|
||||
/>
|
||||
<label htmlFor="enableOCR" className="text-sm font-medium">
|
||||
Adicionar camada de OCR (tornar PDF pesquisável)
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
⚠️ OCR aumenta o tempo de processamento significativamente
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -510,6 +614,54 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedOperation === 'merge' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Selecione 2 ou mais PDFs para mesclar em um único arquivo.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enableOCRMerge"
|
||||
checked={enableOCR}
|
||||
onChange={(e) => setEnableOCR(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 rounded"
|
||||
/>
|
||||
<label htmlFor="enableOCRMerge" className="text-sm font-medium">
|
||||
Adicionar camada de OCR ao PDF mesclado
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
⚠️ OCR aumenta significativamente o tempo de processamento
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedOperation === 'imagesToPDF' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Converte múltiplas imagens em um único arquivo PDF.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="enableOCRImages"
|
||||
checked={enableOCR}
|
||||
onChange={(e) => setEnableOCR(e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 rounded"
|
||||
/>
|
||||
<label htmlFor="enableOCRImages" className="text-sm font-medium">
|
||||
Adicionar camada de OCR (tornar PDF pesquisável)
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
⚠️ OCR aumenta significativamente o tempo de processamento
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={processFiles}
|
||||
disabled={processing || files.length === 0}
|
||||
@@ -565,22 +717,52 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(result.outputPaths || result.outputPath) && (
|
||||
{(result.outputPaths || result.outputPath || result.zipPath) && (
|
||||
<div className="mt-4">
|
||||
<p className="font-medium mb-2">Arquivos gerados:</p>
|
||||
{result.outputPaths?.map((path, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => downloadFile(result.operacaoId, index)}
|
||||
className="flex items-center space-x-2 w-full p-2 hover:bg-gray-50 rounded"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-5 w-5 text-blue-600" />
|
||||
<span className="text-sm">Arquivo {index + 1}</span>
|
||||
</button>
|
||||
))}
|
||||
{result.outputPath && (
|
||||
|
||||
{/* Botão de download ZIP (se disponível) */}
|
||||
{result.zipPath && (
|
||||
<button
|
||||
onClick={() => downloadFile(result.operacaoId, 0)}
|
||||
className="flex items-center space-x-2 w-full p-3 mb-3 bg-blue-50 hover:bg-blue-100 rounded border-2 border-blue-300"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-6 w-6 text-blue-600" />
|
||||
<div className="text-left">
|
||||
<span className="text-sm font-semibold text-blue-900">
|
||||
📦 Download ZIP (Todos os arquivos)
|
||||
</span>
|
||||
<p className="text-xs text-blue-700">
|
||||
Recomendado para múltiplos arquivos
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 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 (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => downloadFile(result.operacaoId, result.zipPath ? index + 1 : index)}
|
||||
className="flex items-center space-x-2 w-full p-2 hover:bg-gray-50 rounded"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-5 w-5 text-blue-600" />
|
||||
<span className="text-sm">{fileName}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{result.outputPath && !result.outputPaths && (
|
||||
<button
|
||||
onClick={() => downloadFile(result.operacaoId, result.zipPath ? 1 : 0)}
|
||||
className="flex items-center space-x-2 w-full p-2 hover:bg-gray-50 rounded"
|
||||
>
|
||||
<ArrowDownTrayIcon className="h-5 w-5 text-blue-600" />
|
||||
@@ -610,7 +792,27 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
{/* Histórico */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-white rounded-lg shadow p-6 sticky top-8">
|
||||
<h2 className="text-xl font-semibold mb-4">Histórico</h2>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">Histórico</h2>
|
||||
{history.length > 0 && (
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={cleanupOldFiles}
|
||||
className="text-xs px-2 py-1 bg-yellow-100 hover:bg-yellow-200 text-yellow-800 rounded transition-colors"
|
||||
title="Remover operações com mais de 24 horas"
|
||||
>
|
||||
🕐 Limpar Antigos
|
||||
</button>
|
||||
<button
|
||||
onClick={clearAllHistory}
|
||||
className="text-xs px-2 py-1 bg-red-100 hover:bg-red-200 text-red-800 rounded transition-colors"
|
||||
title="Apagar todo o histórico e arquivos"
|
||||
>
|
||||
🗑️ Limpar Tudo
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{history.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">Nenhuma operação ainda</p>
|
||||
@@ -622,17 +824,28 @@ export const PDFManipulationPage: React.FC = () => {
|
||||
<span className="text-sm font-medium">
|
||||
{op.tipoOperacao.replace('_', ' ')}
|
||||
</span>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
op.status === 'CONCLUIDO'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: op.status === 'ERRO'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}
|
||||
>
|
||||
{op.status}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded ${
|
||||
op.status === 'CONCLUIDO'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: op.status === 'ERRO'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}
|
||||
>
|
||||
{op.status}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => deleteOperation(op.id)}
|
||||
className="text-red-600 hover:text-red-800 hover:bg-red-50 p-1 rounded transition-colors"
|
||||
title="Deletar esta operação"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-gray-500 mt-1">
|
||||
<ClockIcon className="h-3 w-3 mr-1" />
|
||||
|
||||
Reference in new issue
Block a user