diff --git a/public/index.php b/public/index.php index 344e4f0..2b0915d 100644 --- a/public/index.php +++ b/public/index.php @@ -51,6 +51,7 @@ $router->get('/belege/{id}', fn($p) => (new BelegController())->show($p)); $router->get('/belege/{id}/file', fn($p) => (new BelegController())->serveFile($p)); $router->post('/belege/{id}/delete', fn($p) => (new BelegController())->destroy($p)); $router->post('/belege/{id}/meta', fn($p) => (new BelegController())->updateMeta($p)); +$router->post('/belege/{id}/crop', fn($p) => (new BelegController())->cropImage($p)); $router->post('/belege/{id}/vorschlag/generate', fn($p) => (new BelegController())->generateVorschlag($p)); $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController())->resetVorschlag($p)); diff --git a/src/Controllers/BelegController.php b/src/Controllers/BelegController.php index abaf4c4..8f8bec0 100644 --- a/src/Controllers/BelegController.php +++ b/src/Controllers/BelegController.php @@ -260,6 +260,58 @@ class BelegController View::redirect('/belege', 'Beleg gelöscht.'); } + public function cropImage(array $params): void + { + header('Content-Type: application/json'); + $id = (int)$params['id']; + $beleg = Beleg::find($id); + + if (!$beleg) { + http_response_code(404); + echo json_encode(['error' => 'Beleg nicht gefunden.']); + return; + } + if ($beleg['locked']) { + http_response_code(403); + echo json_encode(['error' => 'GoBD: Gesperrter Beleg kann nicht verändert werden.']); + return; + } + if (!in_array($beleg['mime_type'], ['image/jpeg', 'image/png'], true)) { + http_response_code(422); + echo json_encode(['error' => 'Zuschneiden nur für JPEG und PNG möglich.']); + return; + } + + $body = json_decode(file_get_contents('php://input'), true) ?? []; + $x = (int)($body['x'] ?? 0); + $y = (int)($body['y'] ?? 0); + $w = (int)($body['w'] ?? 0); + $h = (int)($body['h'] ?? 0); + + if ($w < 10 || $h < 10) { + http_response_code(422); + echo json_encode(['error' => 'Auswahlbereich zu klein (mindestens 10×10 Pixel).']); + return; + } + + $absPath = dirname(__DIR__, 2) . '/public/' . $beleg['storage_path']; + if (!file_exists($absPath)) { + http_response_code(404); + echo json_encode(['error' => 'Datei nicht gefunden.']); + return; + } + + $svc = new ImageCropService(); + if (!$svc->cropToRect($absPath, $x, $y, $w, $h)) { + http_response_code(500); + echo json_encode(['error' => 'Zuschnitt fehlgeschlagen.']); + return; + } + + Beleg::updateHash($id, hash_file('sha256', $absPath)); + echo json_encode(['ok' => true]); + } + private function renderView(string $template, array $data): string { extract($data, EXTR_SKIP); diff --git a/src/Models/Beleg.php b/src/Models/Beleg.php index 32a5f6f..03345ed 100644 --- a/src/Models/Beleg.php +++ b/src/Models/Beleg.php @@ -218,6 +218,14 @@ class Beleg ); } + public static function updateHash(int $id, string $hash): void + { + Database::execute( + 'UPDATE belege SET file_hash = ?, updated_at = NOW() WHERE id = ?', + [$hash, $id] + ); + } + public static function saveBelegText(int $id, string $text): void { Database::execute( diff --git a/src/Services/ImageCropService.php b/src/Services/ImageCropService.php index 239eeb7..679b895 100644 --- a/src/Services/ImageCropService.php +++ b/src/Services/ImageCropService.php @@ -10,6 +10,50 @@ class ImageCropService private const PADDING = 25; // Pixel-Abstand, der nach dem Zuschnitt ringsum erhalten bleibt private const MIN_CROP_GAIN = 0.03; // Mindestens 3 % Einsparung, sonst kein Zuschnitt + /** + * Schneidet ein Bild auf den angegebenen Bereich zu (manueller Zuschnitt). + * Koordinaten und Maße beziehen sich auf das Originalbild in Pixeln. + * Gibt true zurück, wenn das Bild gespeichert wurde. + */ + public function cropToRect(string $path, int $x, int $y, int $w, int $h): bool + { + $mime = mime_content_type($path); + + $img = match ($mime) { + 'image/jpeg' => imagecreatefromjpeg($path), + 'image/png' => imagecreatefrompng($path), + default => null, + }; + + if (!$img instanceof \GdImage) { + return false; + } + + $imgW = imagesx($img); + $imgH = imagesy($img); + + // Koordinaten auf Bildgrenzen klemmen + $x = max(0, min($x, $imgW - 1)); + $y = max(0, min($y, $imgH - 1)); + $w = max(1, min($w, $imgW - $x)); + $h = max(1, min($h, $imgH - $y)); + + $cropped = imagecrop($img, ['x' => $x, 'y' => $y, 'width' => $w, 'height' => $h]); + imagedestroy($img); + + if (!$cropped instanceof \GdImage) { + return false; + } + + $saved = match ($mime) { + 'image/jpeg' => imagejpeg($cropped, $path, 92), + 'image/png' => imagepng($cropped, $path), + }; + + imagedestroy($cropped); + return $saved; + } + /** * Schneidet weißen/hellen Rand eines JPEG- oder PNG-Bilds automatisch weg. * Gibt true zurück, wenn das Bild tatsächlich verändert wurde. diff --git a/src/Views/belege/detail.php b/src/Views/belege/detail.php index ee95d14..58be0ad 100644 --- a/src/Views/belege/detail.php +++ b/src/Views/belege/detail.php @@ -88,6 +88,9 @@ $suppliers = $suppliers ?? [];