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 { + + +
@@ -573,6 +805,207 @@ function updateCameraCounter() { cameraCounter.textContent = text; } +// === DOKUMENTEN-SCANNER === + +const openScannerBtn = document.getElementById('open-scanner-btn'); +const scannerView = document.getElementById('scanner-view'); +const scannerStream = document.getElementById('scanner-stream'); +const scannerCanvas = document.getElementById('scanner-canvas'); +const scannerPreview = document.getElementById('scanner-preview'); +const scannerPreviewImage = document.getElementById('scanner-preview-image'); +const scannerPreviewDiscard = document.getElementById('scanner-preview-discard'); +const scannerPreviewAdd = document.getElementById('scanner-preview-add'); +const scannerPagesStrip = document.getElementById('scanner-pages-strip'); +const scannerPageCount = document.getElementById('scanner-page-count'); +const scannerCapture = document.getElementById('scanner-capture'); +const scannerDone = document.getElementById('scanner-done'); +const scannerCancel = document.getElementById('scanner-cancel'); +const scannerUploading = document.getElementById('scanner-uploading'); +const scannerUploadStatus = document.getElementById('scanner-upload-status'); + +let scannerMediaStream = null; +let scannedPages = []; // Array von Blobs +let currentScanBlob = null; + +// Scanner öffnen +openScannerBtn.addEventListener('click', openScannerView); + +async function openScannerView() { + try { + const constraints = { + video: { + facingMode: 'environment', + width: { ideal: 2560 }, + height: { ideal: 1920 } + }, + audio: false + }; + scannerMediaStream = await navigator.mediaDevices.getUserMedia(constraints); + scannerStream.srcObject = scannerMediaStream; + scannerView.classList.remove('hidden'); + scannedPages = []; + updateScannerUI(); + document.body.style.overflow = 'hidden'; + } catch (err) { + console.error('Scanner-Fehler:', err); + toast('Kamera nicht verfügbar: ' + err.message, true); + } +} + +// Scanner schließen +scannerCancel.addEventListener('click', closeScannerView); + +function closeScannerView() { + if (scannerMediaStream) { + scannerMediaStream.getTracks().forEach(track => track.stop()); + scannerMediaStream = null; + } + scannerStream.srcObject = null; + scannerView.classList.add('hidden'); + scannerPreview.classList.add('hidden'); + scannerUploading.classList.add('hidden'); + // Alte Seiten-Blobs freigeben + scannedPages.forEach(p => URL.revokeObjectURL(p.url)); + scannedPages = []; + document.body.style.overflow = ''; +} + +// Seite aufnehmen +scannerCapture.addEventListener('click', captureScanPage); + +function captureScanPage() { + if (!scannerMediaStream) return; + + const video = scannerStream; + const canvas = scannerCanvas; + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(video, 0, 0); + + canvas.toBlob((blob) => { + if (blob) { + currentScanBlob = blob; + showScanPreview(blob); + } + }, 'image/jpeg', 0.92); +} + +// Vorschau anzeigen +function showScanPreview(blob) { + const url = URL.createObjectURL(blob); + scannerPreviewImage.onload = () => URL.revokeObjectURL(url); + scannerPreviewImage.src = url; + scannerPreview.classList.remove('hidden'); +} + +// Seite verwerfen +scannerPreviewDiscard.addEventListener('click', () => { + currentScanBlob = null; + scannerPreview.classList.add('hidden'); +}); + +// Seite hinzufügen +scannerPreviewAdd.addEventListener('click', () => { + if (!currentScanBlob) return; + + const url = URL.createObjectURL(currentScanBlob); + scannedPages.push({ blob: currentScanBlob, url: url }); + currentScanBlob = null; + scannerPreview.classList.add('hidden'); + updateScannerUI(); +}); + +// UI aktualisieren +function updateScannerUI() { + // Counter aktualisieren + const count = scannedPages.length; + scannerPageCount.textContent = count === 1 ? '1 Seite' : count + ' Seiten'; + scannerDone.disabled = count === 0; + + // Thumbnails rendern + scannerPagesStrip.innerHTML = ''; + scannedPages.forEach((page, index) => { + const wrapper = document.createElement('div'); + wrapper.className = 'scanner-page-wrapper'; + + const img = document.createElement('img'); + img.className = 'scanner-page-thumb'; + img.src = page.url; + + const numLabel = document.createElement('span'); + numLabel.className = 'scanner-page-number'; + numLabel.textContent = index + 1; + + const delBtn = document.createElement('button'); + delBtn.className = 'scanner-page-delete'; + delBtn.textContent = '✕'; + delBtn.onclick = () => { + URL.revokeObjectURL(page.url); + scannedPages.splice(index, 1); + updateScannerUI(); + }; + + wrapper.appendChild(img); + wrapper.appendChild(numLabel); + wrapper.appendChild(delBtn); + scannerPagesStrip.appendChild(wrapper); + }); +} + +// Dokument fertigstellen und hochladen +scannerDone.addEventListener('click', finishScanning); + +async function finishScanning() { + if (scannedPages.length === 0) return; + + scannerUploading.classList.remove('hidden'); + scannerUploadStatus.textContent = 'Dokument wird hochgeladen…'; + + const fd = new FormData(); + fd.append('token', TOKEN); + fd.append('action', 'scan_document'); + + // Alle Seiten hinzufügen + scannedPages.forEach((page, i) => { + fd.append('pages[]', page.blob, 'page_' + (i + 1) + '.jpg'); + }); + + try { + const url = window.location.pathname.replace('mobile_upload.php', 'ajax/process_document.php'); + scannerUploadStatus.textContent = 'OCR-Verarbeitung läuft…'; + + const r = await fetch(url + '?token=' + encodeURIComponent(TOKEN), { + method: 'POST', + body: fd + }); + + const data = await r.json(); + + if (data.success) { + uploadedCount++; + uploadedCountEl.textContent = '(' + uploadedCount + ')'; + + const item = document.createElement('div'); + item.className = 'uploaded-item'; + item.innerHTML = '📄 ' + escapeHtml(data.filename) + ''; + uploadedList.prepend(item); + + toast('Dokument erstellt: ' + scannedPages.length + ' Seiten'); + closeScannerView(); + } else { + scannerUploading.classList.add('hidden'); + toast('Fehler: ' + (data.error || 'unbekannt'), true); + } + } catch (e) { + console.error('Upload-Fehler:', e); + scannerUploading.classList.add('hidden'); + toast('Netzwerkfehler', true); + } +} + // === UPLOAD-FUNKTIONEN === async function uploadFile(file) {