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 } }