pdflinux 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +165 -0
  2. package/bin/pdflinux +31 -0
  3. package/index.html +13 -0
  4. package/install.sh +254 -0
  5. package/package.json +57 -0
  6. package/public/favicon.svg +1 -0
  7. package/public/icons.svg +24 -0
  8. package/src/App.tsx +68 -0
  9. package/src/assets/hero.png +0 -0
  10. package/src/assets/react.svg +1 -0
  11. package/src/assets/vite.svg +1 -0
  12. package/src/components/Dropzone.tsx +90 -0
  13. package/src/components/FileItem.tsx +27 -0
  14. package/src/components/ProgressBar.tsx +18 -0
  15. package/src/components/Sidebar.tsx +92 -0
  16. package/src/hooks/useTheme.ts +23 -0
  17. package/src/index.css +675 -0
  18. package/src/main.tsx +10 -0
  19. package/src/pages/Dashboard.tsx +71 -0
  20. package/src/pages/ImageToPdf.tsx +100 -0
  21. package/src/pages/PdfCompress.tsx +175 -0
  22. package/src/pages/PdfCrop.tsx +112 -0
  23. package/src/pages/PdfDeletePages.tsx +151 -0
  24. package/src/pages/PdfGrayscale.tsx +100 -0
  25. package/src/pages/PdfInfo.tsx +71 -0
  26. package/src/pages/PdfMerge.tsx +135 -0
  27. package/src/pages/PdfOcr.tsx +136 -0
  28. package/src/pages/PdfPageNumbers.tsx +155 -0
  29. package/src/pages/PdfProtect.tsx +171 -0
  30. package/src/pages/PdfReorder.tsx +145 -0
  31. package/src/pages/PdfRotate.tsx +89 -0
  32. package/src/pages/PdfSplit.tsx +147 -0
  33. package/src/pages/PdfToImage.tsx +134 -0
  34. package/src/pages/PdfToText.tsx +107 -0
  35. package/src/pages/PdfUnlock.tsx +79 -0
  36. package/src/pages/PdfWatermark.tsx +77 -0
  37. package/src-tauri/Cargo.lock +5347 -0
  38. package/src-tauri/Cargo.toml +32 -0
  39. package/src-tauri/build.rs +3 -0
  40. package/src-tauri/capabilities/default.json +14 -0
  41. package/src-tauri/icons/128x128.png +0 -0
  42. package/src-tauri/icons/128x128@2x.png +0 -0
  43. package/src-tauri/icons/32x32.png +0 -0
  44. package/src-tauri/icons/Square107x107Logo.png +0 -0
  45. package/src-tauri/icons/Square142x142Logo.png +0 -0
  46. package/src-tauri/icons/Square150x150Logo.png +0 -0
  47. package/src-tauri/icons/Square284x284Logo.png +0 -0
  48. package/src-tauri/icons/Square30x30Logo.png +0 -0
  49. package/src-tauri/icons/Square310x310Logo.png +0 -0
  50. package/src-tauri/icons/Square44x44Logo.png +0 -0
  51. package/src-tauri/icons/Square71x71Logo.png +0 -0
  52. package/src-tauri/icons/Square89x89Logo.png +0 -0
  53. package/src-tauri/icons/StoreLogo.png +0 -0
  54. package/src-tauri/icons/icon.icns +0 -0
  55. package/src-tauri/icons/icon.ico +0 -0
  56. package/src-tauri/icons/icon.png +0 -0
  57. package/src-tauri/src/lib.rs +48 -0
  58. package/src-tauri/src/main.rs +6 -0
  59. package/src-tauri/src/pdf_engine.rs +1022 -0
  60. package/src-tauri/tauri.conf.json +48 -0
  61. package/tsconfig.app.json +28 -0
  62. package/tsconfig.json +7 -0
  63. package/tsconfig.node.json +26 -0
  64. package/uninstall.sh +65 -0
  65. package/vite.config.ts +7 -0
@@ -0,0 +1,107 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { invoke } from '@tauri-apps/api/core';
4
+ import { save } from '@tauri-apps/plugin-dialog';
5
+ import Dropzone, { type PdfFile } from '../components/Dropzone';
6
+ import FileItem from '../components/FileItem';
7
+ import ProgressBar from '../components/ProgressBar';
8
+
9
+ interface TextResult {
10
+ output_path: string;
11
+ output_size: number;
12
+ char_count: number;
13
+ }
14
+
15
+ export default function PdfToText() {
16
+ const navigate = useNavigate();
17
+ const [file, setFile] = useState<PdfFile | null>(null);
18
+ const [processing, setProcessing] = useState(false);
19
+ const [progress, setProgress] = useState(0);
20
+ const [result, setResult] = useState<TextResult | null>(null);
21
+ const [error, setError] = useState('');
22
+
23
+ const handleFiles = (files: PdfFile[]) => { setFile(files[0]); setResult(null); setError(''); };
24
+ const removeFile = () => { setFile(null); setResult(null); setError(''); };
25
+
26
+ const handleExtract = async () => {
27
+ if (!file) return;
28
+ setProcessing(true);
29
+ setProgress(30);
30
+ setError('');
31
+ setResult(null);
32
+ try {
33
+ const tempDir = await invoke<string>('get_temp_dir');
34
+ const outputPath = `${tempDir}/${file.name.replace(/\.pdf$/i, '')}.txt`;
35
+ setProgress(60);
36
+ const res = await invoke<TextResult>('pdf_to_text', {
37
+ inputPath: file.path,
38
+ outputPath,
39
+ });
40
+ setProgress(100);
41
+ setResult(res);
42
+ } catch (err: any) {
43
+ setError(typeof err === 'string' ? err : err.message || 'Terjadi kesalahan.');
44
+ } finally {
45
+ setProcessing(false);
46
+ }
47
+ };
48
+
49
+ const handleSave = async () => {
50
+ if (!result) return;
51
+ const baseName = file?.name.replace(/\.pdf$/i, '') || 'output';
52
+ const savePath = await save({
53
+ defaultPath: `${baseName}.txt`,
54
+ filters: [{ name: 'Text Files', extensions: ['txt'] }],
55
+ });
56
+ if (savePath) await invoke('save_file_to', { source: result.output_path, destination: savePath });
57
+ };
58
+
59
+ const formatSize = (b: number) =>
60
+ b < 1048576 ? (b / 1024).toFixed(1) + ' KB' : (b / 1048576).toFixed(2) + ' MB';
61
+
62
+ return (
63
+ <>
64
+ <div className="page-header">
65
+ <button className="btn-back" onClick={() => navigate('/')}>← Kembali</button>
66
+ <h2>T PDF → Teks</h2>
67
+ <p>Ekstrak semua teks dari PDF menjadi file .txt yang bisa diedit.</p>
68
+ </div>
69
+ <div className="page-body">
70
+ <div className="tool-page">
71
+ {!file && <Dropzone onFilesSelected={handleFiles} />}
72
+ {file && <div className="file-list"><FileItem file={file} onRemove={removeFile} /></div>}
73
+
74
+ {processing && <ProgressBar progress={progress} status="Mengekstrak teks dengan pdftotext..." />}
75
+
76
+ {error && (
77
+ <div className="result-card animate-in" style={{ borderColor: 'rgba(225,112,85,0.3)' }}>
78
+ <h4>✕ Error</h4>
79
+ <p style={{ color: 'var(--danger)', fontSize: '14px' }}>{error}</p>
80
+ </div>
81
+ )}
82
+
83
+ {result && (
84
+ <div className="result-card success animate-in">
85
+ <h4>✓ Ekstraksi Berhasil!</h4>
86
+ <div className="result-stats">
87
+ <div className="stat-item">
88
+ <div className="stat-value">{result.char_count.toLocaleString()}</div>
89
+ <div className="stat-label">Karakter</div>
90
+ </div>
91
+ <div className="stat-item">
92
+ <div className="stat-value">{formatSize(result.output_size)}</div>
93
+ <div className="stat-label">Ukuran File</div>
94
+ </div>
95
+ </div>
96
+ <button className="btn-primary" onClick={handleSave}>Simpan File .txt</button>
97
+ </div>
98
+ )}
99
+
100
+ {file && !processing && !result && !error && (
101
+ <button className="btn-primary" onClick={handleExtract}>Ekstrak Teks</button>
102
+ )}
103
+ </div>
104
+ </div>
105
+ </>
106
+ );
107
+ }
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { invoke } from '@tauri-apps/api/core';
4
+ import { save } from '@tauri-apps/plugin-dialog';
5
+ import Dropzone, { type PdfFile } from '../components/Dropzone';
6
+ import FileItem from '../components/FileItem';
7
+ import ProgressBar from '../components/ProgressBar';
8
+
9
+ interface UnlockResult { output_path: string; output_size: number; }
10
+
11
+ export default function PdfUnlock() {
12
+ const navigate = useNavigate();
13
+ const [file, setFile] = useState<PdfFile | null>(null);
14
+ const [password, setPassword] = useState('');
15
+ const [processing, setProcessing] = useState(false);
16
+ const [progress, setProgress] = useState(0);
17
+ const [result, setResult] = useState<UnlockResult | null>(null);
18
+ const [error, setError] = useState('');
19
+
20
+ const handleFiles = (files: PdfFile[]) => { setFile(files[0]); setResult(null); setError(''); };
21
+ const removeFile = () => { setFile(null); setResult(null); setPassword(''); setError(''); };
22
+
23
+ const handleUnlock = async () => {
24
+ if (!file || !password) return;
25
+ setProcessing(true); setProgress(20); setError(''); setResult(null);
26
+ try {
27
+ const tempDir = await invoke<string>('get_temp_dir');
28
+ setProgress(50);
29
+ const res = await invoke<UnlockResult>('unlock_pdf', {
30
+ inputPath: file.path, outputPath: `${tempDir}/unlocked_${file.name}`, password,
31
+ });
32
+ setProgress(100); setResult(res);
33
+ } catch (err: any) { setError(typeof err === 'string' ? err : err.message || 'Error'); }
34
+ finally { setProcessing(false); }
35
+ };
36
+
37
+ const handleSave = async () => {
38
+ if (!result) return;
39
+ const savePath = await save({ defaultPath: `unlocked_${file?.name}`, filters: [{ name: 'PDF', extensions: ['pdf'] }] });
40
+ if (savePath) await invoke('save_file_to', { source: result.output_path, destination: savePath });
41
+ };
42
+
43
+ const formatSize = (b: number) => b < 1048576 ? (b / 1024).toFixed(1) + ' KB' : (b / 1048576).toFixed(2) + ' MB';
44
+
45
+ return (
46
+ <>
47
+ <div className="page-header">
48
+ <button className="btn-back" onClick={() => navigate('/')}>← Kembali</button>
49
+ <h2>⊙ Buka Kunci PDF</h2>
50
+ <p>Hapus password dari file PDF yang terkunci.</p>
51
+ </div>
52
+ <div className="page-body">
53
+ <div className="tool-page">
54
+ {!file && <Dropzone onFilesSelected={handleFiles} />}
55
+ {file && <div className="file-list"><FileItem file={file} onRemove={removeFile} /></div>}
56
+ {file && !result && !error && (
57
+ <div className="options-panel animate-in">
58
+ <h4>⊙ Masukkan Password</h4>
59
+ <div className="input-group">
60
+ <label>Password file PDF</label>
61
+ <input type="password" placeholder="Masukkan password..." value={password} onChange={(e) => setPassword(e.target.value)} />
62
+ </div>
63
+ </div>
64
+ )}
65
+ {processing && <ProgressBar progress={progress} status="Mendekripsi file..." />}
66
+ {error && <div className="result-card animate-in" style={{ borderColor: 'rgba(239,68,68,0.3)' }}><h4>✕ Error</h4><p style={{ color: 'var(--danger)', fontSize: '14px' }}>{error}</p></div>}
67
+ {result && (
68
+ <div className="result-card success animate-in">
69
+ <h4>✓ Berhasil Dibuka!</h4>
70
+ <p style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '14px' }}>Password berhasil dihapus ({formatSize(result.output_size)}).</p>
71
+ <button className="btn-primary" onClick={handleSave}>Simpan File</button>
72
+ </div>
73
+ )}
74
+ {file && !processing && !result && !error && <button className="btn-primary" onClick={handleUnlock} disabled={!password}>Buka Kunci</button>}
75
+ </div>
76
+ </div>
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,77 @@
1
+ import { useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { invoke } from '@tauri-apps/api/core';
4
+ import { save } from '@tauri-apps/plugin-dialog';
5
+ import Dropzone, { type PdfFile } from '../components/Dropzone';
6
+ import FileItem from '../components/FileItem';
7
+ import ProgressBar from '../components/ProgressBar';
8
+
9
+ interface WatermarkResult { output_path: string; output_size: number; }
10
+
11
+ export default function PdfWatermark() {
12
+ const navigate = useNavigate();
13
+ const [file, setFile] = useState<PdfFile | null>(null);
14
+ const [text, setText] = useState('');
15
+ const [processing, setProcessing] = useState(false);
16
+ const [progress, setProgress] = useState(0);
17
+ const [result, setResult] = useState<WatermarkResult | null>(null);
18
+ const [error, setError] = useState('');
19
+
20
+ const handleFiles = (files: PdfFile[]) => { setFile(files[0]); setResult(null); setError(''); };
21
+ const removeFile = () => { setFile(null); setResult(null); setText(''); setError(''); };
22
+
23
+ const handleWatermark = async () => {
24
+ if (!file || !text.trim()) return;
25
+ setProcessing(true); setProgress(20); setError(''); setResult(null);
26
+ try {
27
+ const tempDir = await invoke<string>('get_temp_dir');
28
+ setProgress(40);
29
+ const res = await invoke<WatermarkResult>('watermark_pdf', {
30
+ inputPath: file.path, outputPath: `${tempDir}/watermarked_${file.name}`, text: text.trim(),
31
+ });
32
+ setProgress(100); setResult(res);
33
+ } catch (err: any) { setError(typeof err === 'string' ? err : err.message || 'Error'); }
34
+ finally { setProcessing(false); }
35
+ };
36
+
37
+ const handleSave = async () => {
38
+ if (!result) return;
39
+ const savePath = await save({ defaultPath: `watermarked_${file?.name}`, filters: [{ name: 'PDF', extensions: ['pdf'] }] });
40
+ if (savePath) await invoke('save_file_to', { source: result.output_path, destination: savePath });
41
+ };
42
+
43
+ return (
44
+ <>
45
+ <div className="page-header">
46
+ <button className="btn-back" onClick={() => navigate('/')}>← Kembali</button>
47
+ <h2>◈ Watermark PDF</h2>
48
+ <p>Tambahkan teks watermark diagonal ke seluruh halaman PDF.</p>
49
+ </div>
50
+ <div className="page-body">
51
+ <div className="tool-page">
52
+ {!file && <Dropzone onFilesSelected={handleFiles} />}
53
+ {file && <div className="file-list"><FileItem file={file} onRemove={removeFile} /></div>}
54
+ {file && !result && !error && (
55
+ <div className="options-panel animate-in">
56
+ <h4>◈ Teks Watermark</h4>
57
+ <div className="input-group">
58
+ <label>Masukkan teks yang ingin dijadikan watermark</label>
59
+ <input type="text" placeholder="Contoh: RAHASIA, DRAFT, CONFIDENTIAL" value={text} onChange={(e) => setText(e.target.value)} />
60
+ </div>
61
+ </div>
62
+ )}
63
+ {processing && <ProgressBar progress={progress} status="Menambahkan watermark..." />}
64
+ {error && <div className="result-card animate-in" style={{ borderColor: 'rgba(239,68,68,0.3)' }}><h4>✕ Error</h4><p style={{ color: 'var(--danger)', fontSize: '14px' }}>{error}</p></div>}
65
+ {result && (
66
+ <div className="result-card success animate-in">
67
+ <h4>✓ Watermark Ditambahkan!</h4>
68
+ <p style={{ color: 'var(--text-secondary)', fontSize: '14px', marginBottom: '14px' }}>Teks "{text}" berhasil ditambahkan ke seluruh halaman.</p>
69
+ <button className="btn-primary" onClick={handleSave}>Simpan File</button>
70
+ </div>
71
+ )}
72
+ {file && !processing && !result && !error && <button className="btn-primary" onClick={handleWatermark} disabled={!text.trim()}>Tambahkan Watermark</button>}
73
+ </div>
74
+ </div>
75
+ </>
76
+ );
77
+ }