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.
This commit is contained in:
183
api/services/ImageHandler.php
Normal file
183
api/services/ImageHandler.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user