getMimeType() ?? ''; if (!str_starts_with($mime, 'image/')) { return null; } $sourcePath = $file->getPathname(); $extension = strtolower((string) $file->getClientOriginalExtension()); return $this->createThumbnail($sourcePath, $mime, $extension, $scopeFolder, $disk); } public function createForAttachment(Attachment $attachment, bool $force = false): ?array { if (!$force && $attachment->thumbnail_path) { $thumbDisk = Storage::disk($attachment->disk); if ($thumbDisk->exists($attachment->thumbnail_path)) { return null; } } $mime = $attachment->mime_type ?? ''; if (!str_starts_with($mime, 'image/')) { return null; } $disk = Storage::disk($attachment->disk); if (!$disk->exists($attachment->path)) { return null; } $sourcePath = $disk->path($attachment->path); $scopeFolder = $this->resolveScopeFolder($attachment); $extension = strtolower((string) ($attachment->extension ?? '')); return $this->createThumbnail($sourcePath, $mime, $extension, $scopeFolder, $attachment->disk); } private function resolveScopeFolder(Attachment $attachment): string { if ($attachment->thread_id) { return "threads/{$attachment->thread_id}"; } if ($attachment->post_id) { return "posts/{$attachment->post_id}"; } return 'misc'; } private function createThumbnail( string $sourcePath, string $mime, string $extension, string $scopeFolder, string $diskName ): ?array { if (!$this->settingBool('attachments.create_thumbnails', true)) { return null; } $maxWidth = $this->settingInt('attachments.thumbnail_max_width', 300); $maxHeight = $this->settingInt('attachments.thumbnail_max_height', 300); if ($maxWidth <= 0 || $maxHeight <= 0) { return null; } $info = @getimagesize($sourcePath); if (!$info) { return null; } [$width, $height] = $info; if ($width <= 0 || $height <= 0) { return null; } if ($width <= $maxWidth && $height <= $maxHeight) { return null; } $ratio = min($maxWidth / $width, $maxHeight / $height); $targetWidth = max(1, (int) round($width * $ratio)); $targetHeight = max(1, (int) round($height * $ratio)); $sourceImage = $this->createImageFromFile($sourcePath, $mime); if (!$sourceImage) { return null; } $thumbImage = imagecreatetruecolor($targetWidth, $targetHeight); if (!$thumbImage) { imagedestroy($sourceImage); return null; } if (in_array($mime, ['image/png', 'image/gif'], true)) { imagecolortransparent($thumbImage, imagecolorallocatealpha($thumbImage, 0, 0, 0, 127)); imagealphablending($thumbImage, false); imagesavealpha($thumbImage, true); } imagecopyresampled( $thumbImage, $sourceImage, 0, 0, 0, 0, $targetWidth, $targetHeight, $width, $height ); $quality = $this->settingInt('attachments.thumbnail_quality', 85); $thumbBinary = $this->renderImageBinary($thumbImage, $mime, $quality); imagedestroy($sourceImage); imagedestroy($thumbImage); if ($thumbBinary === null) { return null; } $filename = Str::uuid()->toString(); if ($extension !== '') { $filename .= ".{$extension}"; } $thumbPath = "attachments/{$scopeFolder}/thumbs/{$filename}"; Storage::disk($diskName)->put($thumbPath, $thumbBinary); return [ 'path' => $thumbPath, 'mime' => $mime, 'size' => strlen($thumbBinary), ]; } private function createImageFromFile(string $path, string $mime) { return match ($mime) { 'image/jpeg', 'image/jpg' => @imagecreatefromjpeg($path), 'image/png' => @imagecreatefrompng($path), 'image/gif' => @imagecreatefromgif($path), 'image/webp' => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null, default => null, }; } private function renderImageBinary($image, string $mime, int $quality): ?string { ob_start(); $success = false; if (in_array($mime, ['image/jpeg', 'image/jpg'], true)) { $success = imagejpeg($image, null, max(10, min(95, $quality))); } elseif ($mime === 'image/png') { $compression = (int) round(9 - (max(10, min(95, $quality)) / 100) * 9); $success = imagepng($image, null, $compression); } elseif ($mime === 'image/gif') { $success = imagegif($image); } elseif ($mime === 'image/webp' && function_exists('imagewebp')) { $success = imagewebp($image, null, max(10, min(95, $quality))); } $data = ob_get_clean(); if (!$success) { return null; } return $data !== false ? $data : null; } private function settingBool(string $key, bool $default): bool { $value = Setting::query()->where('key', $key)->value('value'); if ($value === null) { return $default; } return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true); } private function settingInt(string $key, int $default): int { $value = Setting::query()->where('key', $key)->value('value'); if ($value === null) { return $default; } return (int) $value; } }