diff --git a/ajax/process_document.php b/ajax/process_document.php new file mode 100644 index 0000000..633010f --- /dev/null +++ b/ajax/process_document.php @@ -0,0 +1,249 @@ + 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } +if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; +if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; +if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php"; +if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php"; +if (!$res) die("Include of main fails"); + +require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; +require_once __DIR__.'/../class/upload_token.class.php'; + +header('Content-Type: application/json; charset=utf-8'); + +function fail($msg, $code = 400) +{ + http_response_code($code); + echo json_encode(array('success' => false, 'error' => $msg)); + exit; +} + +// Token validieren +$token = (string) ($_REQUEST['token'] ?? ''); +$tok = BerichtUploadToken::fetchValid($db, $token); +if (!$tok) { + fail('Token ungültig oder abgelaufen', 403); +} + +// Upload-Ziel ermitteln +$upload_dir = $tok->getUploadDir(); +if (!$upload_dir) { + fail('Objekt nicht gefunden', 404); +} + +// Seiten prüfen +if (empty($_FILES['pages']) || !is_array($_FILES['pages']['tmp_name'])) { + fail('Keine Seiten hochgeladen'); +} + +$pages = $_FILES['pages']; +$page_count = count($pages['tmp_name']); + +if ($page_count === 0) { + fail('Keine Seiten hochgeladen'); +} + +// Temp-Verzeichnis für Verarbeitung +$tmpdir = sys_get_temp_dir().'/bericht_scan_'.uniqid(); +if (!mkdir($tmpdir, 0755, true)) { + fail('Temp-Verzeichnis konnte nicht erstellt werden'); +} + +$cleanup = function() use ($tmpdir) { + // Temp-Dateien aufräumen + array_map('unlink', glob($tmpdir.'/*')); + @rmdir($tmpdir); +}; + +// Seiten speichern und optimieren +$image_files = array(); +for ($i = 0; $i < $page_count; $i++) { + if (empty($pages['tmp_name'][$i])) continue; + + $ext = strtolower(pathinfo($pages['name'][$i], PATHINFO_EXTENSION)); + if (!in_array($ext, array('jpg', 'jpeg', 'png'))) { + $cleanup(); + fail('Nur JPG/PNG erlaubt'); + } + + // Seite nummeriert speichern + $page_file = sprintf('%s/page_%03d.jpg', $tmpdir, $i + 1); + + // Mit ImageMagick optimieren (Kontrast, Größe) + $tmp_file = $pages['tmp_name'][$i]; + + // Prüfen ob convert (ImageMagick) verfügbar ist + $convert_bin = '/usr/bin/convert'; + if (!file_exists($convert_bin)) { + $convert_bin = trim(shell_exec('which convert 2>/dev/null')); + } + + if ($convert_bin && file_exists($convert_bin)) { + // Bild optimieren: Kontrast erhöhen, Größe begrenzen, als JPG speichern + $cmd = escapeshellcmd($convert_bin).' ' + .escapeshellarg($tmp_file) + .' -resize 2400x2400\> ' // Max 2400px, Seitenverhältnis beibehalten + .' -normalize ' // Kontrast optimieren + .' -quality 92 ' + .escapeshellarg($page_file).' 2>&1'; + exec($cmd, $output, $ret); + + if ($ret !== 0 || !file_exists($page_file)) { + // Fallback: einfach kopieren + copy($tmp_file, $page_file); + } + } else { + // Kein ImageMagick: einfach kopieren + copy($tmp_file, $page_file); + } + + if (file_exists($page_file)) { + $image_files[] = $page_file; + } +} + +if (empty($image_files)) { + $cleanup(); + fail('Keine gültigen Seiten verarbeitet'); +} + +// Ziel-Dateiname +if (!is_dir($upload_dir)) dol_mkdir($upload_dir); +$timestamp = dol_print_date(dol_now(), '%Y%m%d_%H%M%S'); +$pdf_filename = 'scan_'.$timestamp.'_'.uniqid().'.pdf'; +$pdf_path = $upload_dir.'/'.$pdf_filename; + +// PDF erstellen +$pdf_created = false; + +// Methode 1: ocrmypdf (bevorzugt - erzeugt durchsuchbares PDF) +$ocrmypdf_bin = getDolGlobalString('BERICHT_OCRMYPDF_BIN', '/usr/bin/ocrmypdf'); +if (!file_exists($ocrmypdf_bin)) { + $ocrmypdf_bin = trim(shell_exec('which ocrmypdf 2>/dev/null')); +} + +if ($ocrmypdf_bin && file_exists($ocrmypdf_bin)) { + // Erst Bilder zu PDF zusammenfügen mit img2pdf oder convert + $img2pdf_bin = trim(shell_exec('which img2pdf 2>/dev/null')); + $temp_pdf = $tmpdir.'/temp.pdf'; + + if ($img2pdf_bin && file_exists($img2pdf_bin)) { + // img2pdf ist schneller und verlustfrei + $cmd = escapeshellcmd($img2pdf_bin).' -o '.escapeshellarg($temp_pdf); + foreach ($image_files as $f) { + $cmd .= ' '.escapeshellarg($f); + } + exec($cmd.' 2>&1', $output, $ret); + } elseif ($convert_bin && file_exists($convert_bin)) { + // Fallback: ImageMagick convert + $cmd = escapeshellcmd($convert_bin); + foreach ($image_files as $f) { + $cmd .= ' '.escapeshellarg($f); + } + $cmd .= ' '.escapeshellarg($temp_pdf); + exec($cmd.' 2>&1', $output, $ret); + } + + if (file_exists($temp_pdf)) { + // OCR mit ocrmypdf + $lang = getDolGlobalString('BERICHT_OCR_LANG', 'deu+eng'); + $cmd = escapeshellcmd($ocrmypdf_bin) + .' -l '.escapeshellarg($lang) + .' --rotate-pages ' // Automatische Rotation + .' --deskew ' // Schräglage korrigieren + .' --clean ' // Hintergrund bereinigen + .' --optimize 1 ' // Leichte Optimierung + .' '.escapeshellarg($temp_pdf) + .' '.escapeshellarg($pdf_path) + .' 2>&1'; + exec($cmd, $output, $ret); + + if ($ret === 0 && file_exists($pdf_path)) { + $pdf_created = true; + } + } +} + +// Methode 2: Fallback mit ImageMagick (kein OCR) +if (!$pdf_created && $convert_bin && file_exists($convert_bin)) { + $cmd = escapeshellcmd($convert_bin); + foreach ($image_files as $f) { + $cmd .= ' '.escapeshellarg($f); + } + $cmd .= ' -quality 92 '.escapeshellarg($pdf_path).' 2>&1'; + exec($cmd, $output, $ret); + + if ($ret === 0 && file_exists($pdf_path)) { + $pdf_created = true; + } +} + +// Methode 3: Fallback mit TCPDF (PHP-only) +if (!$pdf_created) { + // TCPDF sollte in Dolibarr verfügbar sein + if (file_exists(DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php')) { + require_once DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf.php'; + + $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false); + $pdf->SetCreator('Dolibarr Bericht-Modul'); + $pdf->SetAuthor('Dokumenten-Scanner'); + $pdf->SetTitle('Gescanntes Dokument'); + $pdf->SetMargins(0, 0, 0); + $pdf->SetAutoPageBreak(false); + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + foreach ($image_files as $img) { + $pdf->AddPage(); + $size = getimagesize($img); + if ($size) { + $w = $size[0]; + $h = $size[1]; + // Bild auf A4 skalieren (210x297mm) + $ratio = min(210 / ($w * 0.264583), 297 / ($h * 0.264583)); + $pdf->Image($img, 0, 0, $w * 0.264583 * $ratio, $h * 0.264583 * $ratio); + } + } + + $pdf->Output($pdf_path, 'F'); + + if (file_exists($pdf_path)) { + $pdf_created = true; + } + } +} + +// Aufräumen +$cleanup(); + +if (!$pdf_created) { + fail('PDF konnte nicht erstellt werden. Bitte prüfen Sie die Server-Konfiguration (ImageMagick/ocrmypdf).'); +} + +// Token-Counter erhöhen +$tok->incrementCount(); + +// Erfolg +echo json_encode(array( + 'success' => true, + 'filename' => $pdf_filename, + 'pages' => count($image_files), + 'ocr' => ($ocrmypdf_bin && file_exists($ocrmypdf_bin)) ? true : false, +)); diff --git a/mobile_upload.php b/mobile_upload.php index 901f056..bba1554 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -323,6 +323,197 @@ body { color: #fff; } .btn-save:active { background: #4cae4c; } + +/* Dokumenten-Scanner Styles */ +.scanner-view { + position: fixed; + inset: 0; + background: #1a1a1f; + z-index: 1000; + display: flex; + flex-direction: column; +} +.scanner-view.hidden { display: none; } + +.scanner-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #222; + border-bottom: 1px solid #333; +} +.scanner-header h2 { + margin: 0; + font-size: 16px; + color: #5cb85c; +} +.scanner-header .page-count { + font-size: 14px; + color: #888; +} + +.scanner-camera-area { + flex: 1; + position: relative; + background: #000; + display: flex; + align-items: center; + justify-content: center; +} +.scanner-camera-area video { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Seiten-Vorschau-Leiste am unteren Rand */ +.scanner-pages-strip { + background: #222; + padding: 8px; + display: flex; + gap: 8px; + overflow-x: auto; + min-height: 80px; + align-items: center; +} +.scanner-page-thumb { + width: 60px; + height: 80px; + border-radius: 4px; + object-fit: cover; + border: 2px solid #444; + flex-shrink: 0; + position: relative; +} +.scanner-page-thumb.selected { + border-color: #5cb85c; +} +.scanner-page-wrapper { + position: relative; + flex-shrink: 0; +} +.scanner-page-delete { + position: absolute; + top: -6px; + right: -6px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #d9534f; + color: #fff; + border: none; + font-size: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} +.scanner-page-number { + position: absolute; + bottom: 2px; + left: 2px; + background: rgba(0,0,0,0.7); + color: #fff; + font-size: 10px; + padding: 1px 4px; + border-radius: 2px; +} + +.scanner-controls { + display: flex; + gap: 12px; + padding: 16px; + background: #222; + border-top: 1px solid #333; +} +.scanner-controls button { + flex: 1; + padding: 14px; + font-size: 15px; + font-weight: 600; + border-radius: 8px; + border: none; + cursor: pointer; +} +.scanner-btn-capture { + background: #5cb85c; + color: #fff; +} +.scanner-btn-capture:active { background: #4cae4c; } +.scanner-btn-done { + background: #337ab7; + color: #fff; +} +.scanner-btn-done:active { background: #2868a0; } +.scanner-btn-done:disabled { + background: #444; + color: #888; + cursor: not-allowed; +} +.scanner-btn-cancel { + background: #444; + color: #fff; + flex: 0.5; +} + +/* Scanner Vorschau (einzelne Seite) */ +.scanner-preview { + position: absolute; + inset: 0; + background: #000; + display: flex; + flex-direction: column; + z-index: 10; +} +.scanner-preview.hidden { display: none; } +.scanner-preview img { + flex: 1; + width: 100%; + object-fit: contain; +} +.scanner-preview-buttons { + display: flex; + gap: 12px; + padding: 16px; + background: #222; +} +.scanner-preview-buttons button { + flex: 1; + padding: 14px; + font-size: 15px; + font-weight: 600; + border-radius: 8px; + border: none; + cursor: pointer; +} + +/* Upload-Fortschritt */ +.scanner-uploading { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.9); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 20; +} +.scanner-uploading.hidden { display: none; } +.scanner-uploading .spinner { + width: 48px; + height: 48px; + border: 4px solid #333; + border-top-color: #5cb85c; + border-radius: 50%; + animation: spin 1s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +.scanner-uploading p { + margin-top: 16px; + color: #fff; + font-size: 14px; +}
@@ -336,6 +527,8 @@ body { @@ -365,6 +558,45 @@ body { + + +