- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
293 lines
10 KiB
PHP
293 lines
10 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Thumbnail Generation Job
|
|
| Handles thumbnail generation for videos and images
|
|
|*******************************************************************************************************************/
|
|
|
|
class ThumbnailGenerationJob extends BaseJob
|
|
{
|
|
/**
|
|
* Handle thumbnail generation
|
|
* @param array $data Thumbnail generation data
|
|
* @return array Generation result
|
|
*/
|
|
public function handle($data)
|
|
{
|
|
$this->validateData($data, ['file_key', 'file_path', 'file_type']);
|
|
|
|
$fileKey = $data['file_key'];
|
|
$filePath = $data['file_path'];
|
|
$fileType = $data['file_type'];
|
|
$sizes = $data['sizes'] ?? ['small' => [160, 120], 'medium' => [320, 240], 'large' => [640, 480]];
|
|
|
|
$this->logProgress('Starting thumbnail generation', [
|
|
'file_key' => $fileKey,
|
|
'file_path' => $filePath,
|
|
'file_type' => $fileType,
|
|
'sizes' => array_keys($sizes)
|
|
]);
|
|
|
|
try {
|
|
if (!file_exists($filePath)) {
|
|
throw new Exception("Source file not found: {$filePath}");
|
|
}
|
|
|
|
$thumbnailDir = _FPATH . 'f_data/thumbnails/' . substr($fileKey, 0, 2) . '/' . $fileKey . '/';
|
|
if (!is_dir($thumbnailDir)) {
|
|
mkdir($thumbnailDir, 0755, true);
|
|
}
|
|
|
|
$results = [];
|
|
|
|
if ($fileType === 'video') {
|
|
$results = $this->generateVideoThumbnails($filePath, $thumbnailDir, $fileKey, $sizes);
|
|
} elseif (in_array($fileType, ['image', 'photo'])) {
|
|
$results = $this->generateImageThumbnails($filePath, $thumbnailDir, $fileKey, $sizes);
|
|
} else {
|
|
throw new Exception("Unsupported file type for thumbnail generation: {$fileType}");
|
|
}
|
|
|
|
// Update database with thumbnail information
|
|
$this->updateThumbnailRecord($fileKey, $results);
|
|
|
|
$this->logProgress('Thumbnail generation completed', [
|
|
'file_key' => $fileKey,
|
|
'thumbnails_generated' => count($results)
|
|
]);
|
|
|
|
return [
|
|
'file_key' => $fileKey,
|
|
'thumbnails' => $results,
|
|
'thumbnail_dir' => $thumbnailDir
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logError('Thumbnail generation failed', [
|
|
'file_key' => $fileKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate video thumbnails using FFmpeg
|
|
* @param string $videoPath Video file path
|
|
* @param string $outputDir Output directory
|
|
* @param string $fileKey File key
|
|
* @param array $sizes Thumbnail sizes
|
|
* @return array Generation results
|
|
*/
|
|
private function generateVideoThumbnails($videoPath, $outputDir, $fileKey, $sizes)
|
|
{
|
|
$ffmpegPath = $this->findFFmpegPath();
|
|
$results = [];
|
|
|
|
foreach ($sizes as $sizeName => $dimensions) {
|
|
$width = $dimensions[0];
|
|
$height = $dimensions[1];
|
|
$outputFile = $outputDir . $fileKey . "_{$sizeName}.jpg";
|
|
|
|
// Generate thumbnail at 10% of video duration
|
|
$command = sprintf(
|
|
'%s -i %s -ss 00:00:10 -vframes 1 -vf "scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black" -q:v 2 %s 2>&1',
|
|
escapeshellarg($ffmpegPath),
|
|
escapeshellarg($videoPath),
|
|
$width,
|
|
$height,
|
|
$width,
|
|
$height,
|
|
escapeshellarg($outputFile)
|
|
);
|
|
|
|
$output = [];
|
|
$returnCode = 0;
|
|
exec($command, $output, $returnCode);
|
|
|
|
if ($returnCode === 0 && file_exists($outputFile)) {
|
|
$results[$sizeName] = [
|
|
'success' => true,
|
|
'file' => $outputFile,
|
|
'size' => filesize($outputFile),
|
|
'width' => $width,
|
|
'height' => $height
|
|
];
|
|
} else {
|
|
$results[$sizeName] = [
|
|
'success' => false,
|
|
'error' => 'FFmpeg thumbnail generation failed',
|
|
'output' => implode("\n", $output)
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Generate image thumbnails using GD
|
|
* @param string $imagePath Image file path
|
|
* @param string $outputDir Output directory
|
|
* @param string $fileKey File key
|
|
* @param array $sizes Thumbnail sizes
|
|
* @return array Generation results
|
|
*/
|
|
private function generateImageThumbnails($imagePath, $outputDir, $fileKey, $sizes)
|
|
{
|
|
$results = [];
|
|
|
|
// Get image info
|
|
$imageInfo = getimagesize($imagePath);
|
|
if (!$imageInfo) {
|
|
throw new Exception("Unable to read image information");
|
|
}
|
|
|
|
$sourceWidth = $imageInfo[0];
|
|
$sourceHeight = $imageInfo[1];
|
|
$mimeType = $imageInfo['mime'];
|
|
|
|
// Create source image resource
|
|
switch ($mimeType) {
|
|
case 'image/jpeg':
|
|
$sourceImage = imagecreatefromjpeg($imagePath);
|
|
break;
|
|
case 'image/png':
|
|
$sourceImage = imagecreatefrompng($imagePath);
|
|
break;
|
|
case 'image/gif':
|
|
$sourceImage = imagecreatefromgif($imagePath);
|
|
break;
|
|
case 'image/webp':
|
|
$sourceImage = imagecreatefromwebp($imagePath);
|
|
break;
|
|
default:
|
|
throw new Exception("Unsupported image format: {$mimeType}");
|
|
}
|
|
|
|
if (!$sourceImage) {
|
|
throw new Exception("Failed to create image resource");
|
|
}
|
|
|
|
foreach ($sizes as $sizeName => $dimensions) {
|
|
$targetWidth = $dimensions[0];
|
|
$targetHeight = $dimensions[1];
|
|
|
|
// Calculate dimensions maintaining aspect ratio
|
|
$ratio = min($targetWidth / $sourceWidth, $targetHeight / $sourceHeight);
|
|
$newWidth = (int)($sourceWidth * $ratio);
|
|
$newHeight = (int)($sourceHeight * $ratio);
|
|
|
|
// Create thumbnail
|
|
$thumbnail = imagecreatetruecolor($targetWidth, $targetHeight);
|
|
|
|
// Handle transparency for PNG and GIF
|
|
if ($mimeType === 'image/png' || $mimeType === 'image/gif') {
|
|
imagealphablending($thumbnail, false);
|
|
imagesavealpha($thumbnail, true);
|
|
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
|
|
imagefill($thumbnail, 0, 0, $transparent);
|
|
} else {
|
|
// Fill with white background for JPEG
|
|
$white = imagecolorallocate($thumbnail, 255, 255, 255);
|
|
imagefill($thumbnail, 0, 0, $white);
|
|
}
|
|
|
|
// Calculate centering offsets
|
|
$offsetX = (int)(($targetWidth - $newWidth) / 2);
|
|
$offsetY = (int)(($targetHeight - $newHeight) / 2);
|
|
|
|
// Resize and copy
|
|
imagecopyresampled(
|
|
$thumbnail, $sourceImage,
|
|
$offsetX, $offsetY, 0, 0,
|
|
$newWidth, $newHeight, $sourceWidth, $sourceHeight
|
|
);
|
|
|
|
// Save thumbnail
|
|
$outputFile = $outputDir . $fileKey . "_{$sizeName}.jpg";
|
|
|
|
if (imagejpeg($thumbnail, $outputFile, 85)) {
|
|
$results[$sizeName] = [
|
|
'success' => true,
|
|
'file' => $outputFile,
|
|
'size' => filesize($outputFile),
|
|
'width' => $targetWidth,
|
|
'height' => $targetHeight
|
|
];
|
|
} else {
|
|
$results[$sizeName] = [
|
|
'success' => false,
|
|
'error' => 'Failed to save thumbnail'
|
|
];
|
|
}
|
|
|
|
imagedestroy($thumbnail);
|
|
}
|
|
|
|
imagedestroy($sourceImage);
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Update thumbnail record in database
|
|
* @param string $fileKey File key
|
|
* @param array $results Thumbnail generation results
|
|
*/
|
|
private function updateThumbnailRecord($fileKey, $results)
|
|
{
|
|
try {
|
|
$db = $this->getDatabase();
|
|
|
|
$thumbnailData = [];
|
|
foreach ($results as $sizeName => $result) {
|
|
if ($result['success']) {
|
|
$thumbnailData[$sizeName] = [
|
|
'file' => basename($result['file']),
|
|
'width' => $result['width'],
|
|
'height' => $result['height'],
|
|
'size' => $result['size']
|
|
];
|
|
}
|
|
}
|
|
|
|
$updateData = [
|
|
'thumbnails_generated' => 1,
|
|
'thumbnail_data' => json_encode($thumbnailData),
|
|
'thumbnail_updated' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Update appropriate table based on file type
|
|
$db->doUpdate('db_videofiles', 'file_key', $updateData, $fileKey);
|
|
|
|
$this->logProgress('Thumbnail record updated', [
|
|
'file_key' => $fileKey,
|
|
'thumbnails' => array_keys($thumbnailData)
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logError('Failed to update thumbnail record', [
|
|
'file_key' => $fileKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find FFmpeg path
|
|
* @return string FFmpeg path
|
|
*/
|
|
private function findFFmpegPath()
|
|
{
|
|
$paths = ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', 'ffmpeg'];
|
|
|
|
foreach ($paths as $path) {
|
|
if (shell_exec("which {$path}") || file_exists($path)) {
|
|
return $path;
|
|
}
|
|
}
|
|
|
|
return 'ffmpeg'; // Fallback to PATH
|
|
}
|
|
} |