Files
mystuff_backend/api/services/ImageHandler.php
Lars Behrends 66f69bc90d Add PHP Media API scaffold and Docker configs
Initial project scaffold for a PHP Media API including routing, controllers, models and services under api/ (Router, Media/Cast/Image/Settings controllers, models, database/bootstrap files and automatic docs service). Adds Docker support (Dockerfile, docker-compose.yml, DOCKER_README.md, php-custom.ini), .htaccess for pretty URLs, API documentation and example payloads (API_EXAMPLES.md, api/README.md, api_examples/*.json), image handling service and logging, plus a comprehensive .gitignore. This commit provides a runnable development environment and example requests to get the API up and tested quickly.
2026-04-12 00:46:30 +02:00

184 lines
6.4 KiB
PHP

<?php
class ImageHandler {
private $uploadDir;
private $baseUrl;
public function __construct($uploadDir = null, $baseUrl = null) {
$this->uploadDir = $uploadDir ?? __DIR__ . '/../public/images/';
$this->baseUrl = $baseUrl ?? '/images/';
// Ensure upload directory exists
if (!file_exists($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
/**
* Process base64 image data and save to file
*
* @param string $base64Data Base64 encoded image data
* @param string $prefix Prefix for filename (e.g., 'poster', 'banner')
* @return string|null Relative path to saved image, or null if invalid
*/
public function saveBase64Image($base64Data, $prefix = 'image') {
error_log("ImageHandler: Starting to process base64 image, length: " . strlen($base64Data));
if (empty($base64Data)) {
error_log("ImageHandler: Empty base64 data");
return null;
}
// Check if it's already a URL (not base64)
if (filter_var($base64Data, FILTER_VALIDATE_URL)) {
error_log("ImageHandler: Data is already a URL: " . $base64Data);
return $base64Data;
}
// Check if it's already a file path
if (strpos($base64Data, '/images/') === 0) {
error_log("ImageHandler: Data is already a file path: " . $base64Data);
return $base64Data;
}
// Parse base64 data
if (preg_match('/^data:image\/(\w+);base64,(.+)$/', $base64Data, $matches)) {
$extension = $matches[1];
$base64String = $matches[2];
error_log("ImageHandler: Data URI format detected, extension: " . $extension);
} elseif (preg_match('/^\/9j\//', $base64Data)) {
// Raw base64 without data URI prefix (JPEG)
$extension = 'jpg';
$base64String = $base64Data;
error_log("ImageHandler: Raw JPEG base64 detected");
} else {
// Try to detect from the base64 string itself
$extension = $this->detectImageFormat($base64Data);
if (!$extension) {
error_log("ImageHandler: Could not detect image format, defaulting to jpg");
$extension = 'jpg';
$base64String = $base64Data;
} else {
$base64String = $base64Data;
error_log("ImageHandler: Detected format: " . $extension);
}
}
// Decode base64
$imageData = base64_decode($base64String);
if ($imageData === false) {
error_log("ImageHandler: Base64 decode failed");
return null;
}
error_log("ImageHandler: Decoded image data size: " . strlen($imageData) . " bytes");
// Skip GD validation - just check if data looks reasonable
if (strlen($imageData) < 100) {
error_log("ImageHandler: Image data too small, likely invalid");
return null;
}
// Generate unique filename
$filename = $this->generateUniqueFilename($prefix, $extension);
$filepath = $this->uploadDir . $filename;
error_log("ImageHandler: Attempting to save to: " . $filepath);
// Ensure directory exists and is writable (handle subdirectories)
$directory = dirname($filepath);
if (!file_exists($directory)) {
error_log("ImageHandler: Creating directory: " . $directory);
if (!mkdir($directory, 0755, true)) {
error_log("ImageHandler: Failed to create directory");
return null;
}
}
if (!is_writable($directory)) {
error_log("ImageHandler: Directory not writable: " . $directory);
error_log("ImageHandler: Attempting to chmod directory");
chmod($directory, 0755);
}
// Save file
$bytesWritten = file_put_contents($filepath, $imageData);
if ($bytesWritten === false) {
error_log("ImageHandler: Failed to save file to: " . $filepath);
error_log("ImageHandler: Upload directory exists: " . (file_exists($this->uploadDir) ? 'yes' : 'no'));
error_log("ImageHandler: Upload directory writable: " . (is_writable($this->uploadDir) ? 'yes' : 'no'));
return null;
}
error_log("ImageHandler: Successfully saved " . $bytesWritten . " bytes to: " . $filepath);
// Return relative path
return $this->baseUrl . $filename;
}
/**
* Detect image format from base64 string
*/
private function detectImageFormat($base64String) {
// Decode first few bytes to check magic numbers
$data = base64_decode(substr($base64String, 0, 100));
if (substr($data, 0, 3) === "\xFF\xD8\xFF") {
return 'jpg';
} elseif (substr($data, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") {
return 'png';
} elseif (substr($data, 0, 6) === "GIF87a" || substr($data, 0, 6) === "GIF89a") {
return 'gif';
} elseif (substr($data, 0, 4) === "RIFF" && substr($data, 8, 4) === "WEBP") {
return 'webp';
}
// Default to jpg for game posters
return 'jpg';
}
/**
* Validate that data is a valid image
*/
private function isValidImage($data) {
try {
$image = imagecreatefromstring($data);
if ($image !== false) {
imagedestroy($image);
return true;
}
} catch (Exception $e) {
return false;
}
return false;
}
/**
* Generate unique filename
*/
private function generateUniqueFilename($prefix, $extension) {
return $prefix . '_' . uniqid() . '_' . time() . '.' . $extension;
}
/**
* Delete an image file
*/
public function deleteImage($imagePath) {
if (empty($imagePath)) {
return false;
}
// Convert URL to filesystem path
if (strpos($imagePath, $this->baseUrl) === 0) {
$filename = substr($imagePath, strlen($this->baseUrl));
$filepath = $this->uploadDir . $filename;
if (file_exists($filepath)) {
return unlink($filepath);
}
}
return false;
}
}