aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/Controllers/EditorController.php
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/Controllers/EditorController.php')
-rw-r--r--src/app/Controllers/EditorController.php157
1 files changed, 157 insertions, 0 deletions
diff --git a/src/app/Controllers/EditorController.php b/src/app/Controllers/EditorController.php
new file mode 100644
index 0000000..7cfb9b9
--- /dev/null
+++ b/src/app/Controllers/EditorController.php
@@ -0,0 +1,157 @@
+<?php
+
+declare(strict_types=1);
+// Editor: webcam/upload image capture, overlay selection, and server-side
+// GD compositing to create posts.
+
+namespace App\Controllers;
+
+use App\Csrf;
+use App\Flash;
+use App\Models\Post;
+
+class EditorController
+{
+ private Post $post;
+
+ public function __construct()
+ {
+ $this->post = new Post();
+ }
+
+ public function show(): void
+ {
+ if (!isset($_SESSION['user_id'])) {
+ header('Location: /login');
+ return;
+ }
+
+ $overlaysDir = \dirname(__DIR__, 2) . '/public/assets/overlays';
+ $overlayFiles = glob($overlaysDir . '/*.png');
+ // Map filesystem paths to URL paths the browser can load
+ $overlays = array_map(static fn($path) => '/assets/overlays/' . basename($path), $overlayFiles);
+
+ $content = __DIR__ . '/../Views/editor/index.php';
+ include __DIR__ . '/../Views/layouts/main.php';
+ }
+
+ public function store(): void
+ {
+ header('Content-Type: application/json');
+
+ if (!isset($_SESSION['user_id'])) {
+ http_response_code(401);
+ echo json_encode(['error' => 'Not authenticated.']);
+ return;
+ }
+
+ $input = json_decode(file_get_contents('php://input'), true);
+ if (!$input) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid request.']);
+ return;
+ }
+
+ if (!Csrf::validate($input['csrf_token'] ?? '')) {
+ http_response_code(403);
+ echo json_encode(['error' => 'Invalid CSRF token.']);
+ return;
+ }
+
+ $imageData = $input['image_data'] ?? '';
+ $overlayName = $input['overlay'] ?? '';
+ $overlayScale = (float) ($input['overlay_scale'] ?? 1.0);
+
+ // Validate overlay exists — use basename() to prevent path traversal
+ $overlaysDir = \dirname(__DIR__, 2) . '/public/assets/overlays';
+ $overlayPath = $overlaysDir . '/' . basename($overlayName);
+ if (!file_exists($overlayPath) || !str_ends_with($overlayPath, '.png')) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid overlay.']);
+ return;
+ }
+
+ $overlayScale = max(0.1, min(2.0, $overlayScale));
+
+ // Strip the data URL prefix (e.g. "data:image/jpeg;base64,") to get raw base64
+ $base64 = preg_replace('#^data:image/\w+;base64,#', '', $imageData);
+ $decoded = base64_decode($base64, true);
+ if ($decoded === false) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid image data.']);
+ return;
+ }
+
+ $base = @imagecreatefromstring($decoded);
+ if ($base === false) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Could not process image.']);
+ return;
+ }
+
+ $outputPath = $this->composite($base, $overlayPath, $overlayScale);
+
+
+ if ($outputPath === null) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to save image.']);
+ return;
+ }
+
+ // Store relative path from the web root so it works as a URL
+ $relativePath = 'uploads/posts/' . basename($outputPath);
+ $this->post->create($_SESSION['user_id'], $relativePath);
+
+ echo json_encode(['success' => true, 'redirect' => '/gallery']);
+ }
+
+ /**
+ * Composite the base image with an overlay using GD.
+ * Returns the saved file path, or null on failure.
+ */
+ private function composite(\GdImage $base, string $overlayPath, float $scale): ?string
+ {
+ $canvasSize = 640;
+
+ // Create a square canvas and fill with white
+ $canvas = imagecreatetruecolor($canvasSize, $canvasSize);
+ $white = imagecolorallocate($canvas, 255, 255, 255);
+ imagefill($canvas, 0, 0, $white);
+
+ // Resize base image to cover the 640x640 canvas (center-crop)
+ $srcW = imagesx($base);
+ $srcH = imagesy($base);
+ // Pick the largest square that fits inside the source image
+ $cropSize = min($srcW, $srcH);
+ $srcX = (int) (($srcW - $cropSize) / 2);
+ $srcY = (int) (($srcH - $cropSize) / 2);
+ imagecopyresampled($canvas, $base, 0, 0, $srcX, $srcY, $canvasSize, $canvasSize, $cropSize, $cropSize);
+
+ // Load the overlay PNG with alpha transparency
+ $overlay = imagecreatefrompng($overlayPath);
+ // Enable alpha blending so transparent overlay pixels don't overwrite the base
+ imagealphablending($canvas, true);
+
+ $overlayW = imagesx($overlay);
+ $overlayH = imagesy($overlay);
+ $scaledW = (int) ($overlayW * $scale);
+ $scaledH = (int) ($overlayH * $scale);
+ // Center the overlay on the canvas
+ $destX = (int) (($canvasSize - $scaledW) / 2);
+ $destY = (int) (($canvasSize - $scaledH) / 2);
+
+ imagecopyresampled($canvas, $overlay, $destX, $destY, 0, 0, $scaledW, $scaledH, $overlayW, $overlayH);
+
+
+ // Save as JPEG
+ $uploadsDir = \dirname(__DIR__, 2) . '/uploads/posts';
+
+ $filename = $_SESSION['user_id'] . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.jpg';
+ $path = $uploadsDir . '/' . $filename;
+
+ $ok = imagejpeg($canvas, $path, 85);
+
+
+ return $ok ? $path : null;
+ }
+}