bericht/ajax/process_document.php
Eduard Wisch aa58b5692c
All checks were successful
Deploy bericht / deploy (push) Successful in 1s
Dokumenten-Scanner mit OCR-Unterstützung
Neuer "Dokument scannen" Button in der Mobile-Upload-Seite:
- Eigene Scanner-View für mehrseitige Dokumente
- Seiten-Vorschau-Leiste mit Thumbnails
- Seiten hinzufügen/löschen vor dem Upload
- Backend-Verarbeitung zu PDF

OCR-Verarbeitung (wenn verfügbar):
- ocrmypdf für durchsuchbares PDF (deu+eng)
- Automatische Rotation und Schräglagekorrektur
- Fallback auf ImageMagick oder TCPDF

Server-Konfiguration (optional für OCR):
  apt install tesseract-ocr tesseract-ocr-deu ocrmypdf

[deploy]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-13 09:59:14 +02:00

249 lines
8.4 KiB
PHP

<?php
/* Verarbeitet gescannte Dokumentseiten zu einem durchsuchbaren PDF.
* KEIN Dolibarr-Login nötig — Authentifizierung über Token in der URL.
* Verwendet ocrmypdf für OCR (optional), Fallback auf einfaches Bild-PDF.
*
* POST: token, pages[] (multipart)
*/
if (!defined('NOLOGIN')) define('NOLOGIN', '1');
if (!defined('NOCSRFCHECK')) define('NOCSRFCHECK', '1');
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) $res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME']; $tmp2 = realpath(__FILE__); $i = strlen($tmp) - 1; $j = strlen($tmp2) - 1;
while ($i > 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,
));