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 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,90 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { open } from '@tauri-apps/plugin-dialog';
3
+ import { invoke } from '@tauri-apps/api/core';
4
+
5
+ export interface PdfFile {
6
+ name: string;
7
+ size: number;
8
+ path: string;
9
+ }
10
+
11
+ interface DropzoneProps {
12
+ onFilesSelected: (files: PdfFile[]) => void;
13
+ multiple?: boolean;
14
+ }
15
+
16
+ export default function Dropzone({ onFilesSelected, multiple = false }: DropzoneProps) {
17
+ const [isDragOver, setIsDragOver] = useState(false);
18
+
19
+ const handleDragOver = useCallback((e: React.DragEvent) => {
20
+ e.preventDefault();
21
+ setIsDragOver(true);
22
+ }, []);
23
+
24
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
25
+ e.preventDefault();
26
+ setIsDragOver(false);
27
+ }, []);
28
+
29
+ const handleDrop = useCallback(async (e: React.DragEvent) => {
30
+ e.preventDefault();
31
+ setIsDragOver(false);
32
+
33
+ // In Tauri, dropped files have a path property
34
+ const droppedFiles = Array.from(e.dataTransfer.files);
35
+ const pdfFiles: PdfFile[] = [];
36
+
37
+ for (const f of droppedFiles) {
38
+ if (!f.name.toLowerCase().endsWith('.pdf')) continue;
39
+ const path = (f as any).path as string | undefined;
40
+ if (path) {
41
+ const size = await invoke<number>('get_pdf_info', { inputPath: path }).catch(() => f.size);
42
+ pdfFiles.push({ name: f.name, size, path });
43
+ }
44
+ }
45
+
46
+ if (pdfFiles.length > 0) {
47
+ onFilesSelected(pdfFiles);
48
+ }
49
+ }, [onFilesSelected]);
50
+
51
+ const handleClick = async () => {
52
+ try {
53
+ const selected = await open({
54
+ multiple: multiple,
55
+ filters: [{ name: 'PDF Files', extensions: ['pdf'] }],
56
+ });
57
+
58
+ if (!selected) return;
59
+
60
+ const paths = Array.isArray(selected) ? selected : [selected];
61
+ const pdfFiles: PdfFile[] = [];
62
+
63
+ for (const p of paths) {
64
+ const name = p.split('/').pop() || 'document.pdf';
65
+ const size = await invoke<number>('get_pdf_info', { inputPath: p }).catch(() => 0);
66
+ pdfFiles.push({ name, size, path: p });
67
+ }
68
+
69
+ if (pdfFiles.length > 0) {
70
+ onFilesSelected(pdfFiles);
71
+ }
72
+ } catch (err) {
73
+ console.error('Gagal membuka file dialog:', err);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <div
79
+ className={`dropzone ${isDragOver ? 'drag-over' : ''}`}
80
+ onDragOver={handleDragOver}
81
+ onDragLeave={handleDragLeave}
82
+ onDrop={handleDrop}
83
+ onClick={handleClick}
84
+ >
85
+ <span className="drop-icon">📁</span>
86
+ <h3>Seret & Lepas File PDF di Sini</h3>
87
+ <p>atau klik untuk memilih file dari komputer Anda</p>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,27 @@
1
+ import type { PdfFile } from './Dropzone';
2
+
3
+ interface FileItemProps {
4
+ file: PdfFile;
5
+ onRemove: () => void;
6
+ }
7
+
8
+ function formatSize(bytes: number): string {
9
+ if (bytes < 1024) return bytes + ' B';
10
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
11
+ return (bytes / 1048576).toFixed(2) + ' MB';
12
+ }
13
+
14
+ export default function FileItem({ file, onRemove }: FileItemProps) {
15
+ return (
16
+ <div className="file-item animate-in">
17
+ <div className="file-icon">📄</div>
18
+ <div className="file-info">
19
+ <div className="file-name">{file.name}</div>
20
+ <div className="file-size">{formatSize(file.size)}</div>
21
+ </div>
22
+ <button className="file-remove" onClick={onRemove} title="Hapus file">
23
+
24
+ </button>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,18 @@
1
+ interface ProgressBarProps {
2
+ progress: number;
3
+ status: string;
4
+ }
5
+
6
+ export default function ProgressBar({ progress, status }: ProgressBarProps) {
7
+ return (
8
+ <div className="progress-container animate-in">
9
+ <div className="progress-info">
10
+ <span className="status-badge processing">⏳ {status}</span>
11
+ <span>{Math.round(progress)}%</span>
12
+ </div>
13
+ <div className="progress-bar-wrapper">
14
+ <div className="progress-bar" style={{ width: `${progress}%` }} />
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,92 @@
1
+ import { NavLink } from 'react-router-dom';
2
+
3
+ const groups = [
4
+ {
5
+ title: 'Menu',
6
+ items: [
7
+ { path: '/', label: 'Dashboard', icon: '⊞' },
8
+ ],
9
+ },
10
+ {
11
+ title: 'Atur Halaman',
12
+ items: [
13
+ { path: '/merge', label: 'Gabung PDF', icon: '⊕' },
14
+ { path: '/split', label: 'Pisah PDF', icon: '✂' },
15
+ { path: '/delete-pages', label: 'Hapus Halaman', icon: '✕' },
16
+ { path: '/reorder', label: 'Susun Ulang', icon: '⇅' },
17
+ { path: '/rotate', label: 'Rotasi PDF', icon: '↻' },
18
+ { path: '/crop', label: 'Crop PDF', icon: '⬡' },
19
+ { path: '/page-numbers', label: 'Nomor Halaman', icon: '#' },
20
+ ],
21
+ },
22
+ {
23
+ title: 'Ukuran & Kualitas',
24
+ items: [
25
+ { path: '/compress', label: 'Kompres PDF', icon: '↓' },
26
+ { path: '/grayscale', label: 'Grayscale', icon: '◑' },
27
+ { path: '/watermark', label: 'Watermark', icon: '◈' },
28
+ ],
29
+ },
30
+ {
31
+ title: 'Konversi',
32
+ items: [
33
+ { path: '/to-image', label: 'PDF → Gambar', icon: '▣' },
34
+ { path: '/image-to-pdf', label: 'Gambar → PDF', icon: '⊞' },
35
+ { path: '/to-text', label: 'PDF → Teks', icon: 'T' },
36
+ { path: '/ocr', label: 'OCR Scan', icon: '◎' },
37
+ ],
38
+ },
39
+ {
40
+ title: 'Keamanan',
41
+ items: [
42
+ { path: '/protect', label: 'Proteksi PDF', icon: '⊘' },
43
+ { path: '/unlock', label: 'Buka Kunci', icon: '⊙' },
44
+ ],
45
+ },
46
+ {
47
+ title: 'Informasi',
48
+ items: [
49
+ { path: '/info', label: 'Info PDF', icon: 'ⓘ' },
50
+ ],
51
+ },
52
+ ];
53
+
54
+ interface SidebarProps {
55
+ theme: 'light' | 'dark';
56
+ onToggleTheme: () => void;
57
+ }
58
+
59
+ export default function Sidebar({ theme, onToggleTheme }: SidebarProps) {
60
+ return (
61
+ <aside className="sidebar">
62
+ <div className="sidebar-logo">
63
+ <div className="logo-icon">P</div>
64
+ <h1>PDF Tools</h1>
65
+ </div>
66
+ <nav className="sidebar-nav">
67
+ {groups.map((group) => (
68
+ <div key={group.title}>
69
+ <div className="nav-section-title">{group.title}</div>
70
+ {group.items.map((t) => (
71
+ <NavLink
72
+ key={t.path}
73
+ to={t.path}
74
+ end={t.path === '/'}
75
+ className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
76
+ >
77
+ <span className="icon">{t.icon}</span>
78
+ {t.label}
79
+ </NavLink>
80
+ ))}
81
+ </div>
82
+ ))}
83
+ </nav>
84
+ <div className="sidebar-footer">
85
+ <button className="theme-toggle" onClick={onToggleTheme}>
86
+ <span>{theme === 'light' ? '☀ Light' : '● Dark'}</span>
87
+ <span style={{ fontSize: '11px', opacity: 0.5 }}>Toggle</span>
88
+ </button>
89
+ </div>
90
+ </aside>
91
+ );
92
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ type Theme = 'light' | 'dark';
4
+
5
+ export function useTheme() {
6
+ const [theme, setTheme] = useState<Theme>(() => {
7
+ if (typeof window !== 'undefined') {
8
+ const saved = localStorage.getItem('pdf-tools-theme') as Theme;
9
+ if (saved) return saved;
10
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
11
+ }
12
+ return 'light';
13
+ });
14
+
15
+ useEffect(() => {
16
+ document.documentElement.setAttribute('data-theme', theme);
17
+ localStorage.setItem('pdf-tools-theme', theme);
18
+ }, [theme]);
19
+
20
+ const toggleTheme = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));
21
+
22
+ return { theme, toggleTheme };
23
+ }