manager = extension_loaded('gd') ? new ImageManager(new GdDriver) : new ImageManager(new ImagickDriver); } catch (\Throwable) { $this->manager = null; } } /** * @return array{existing:list,missing:list,target_variants:list} */ public function plan(Artwork $artwork, bool $force = false): array { $disk = Storage::disk($this->storage->objectDiskName()); $existing = []; $missing = []; foreach ($this->paths->variantNames() as $variant) { $objectPath = $this->paths->objectPath($artwork, $variant); if ($disk->exists($objectPath)) { $existing[] = $variant; continue; } $missing[] = $variant; } return [ 'existing' => $existing, 'missing' => $missing, 'target_variants' => $force ? $this->paths->variantNames() : $missing, ]; } /** * @return array{existing:list,missing:list,target_variants:list,generated:int,skipped:int,generated_variants:list,generated_paths:list,failed:array} */ public function generate(Artwork $artwork, bool $force = false): array { $this->assertImageManager(); $plan = $this->plan($artwork, $force); $targetVariants = $plan['target_variants']; if ($targetVariants === []) { return $plan + [ 'generated' => 0, 'skipped' => count($plan['existing']), 'generated_variants' => [], 'generated_paths' => [], 'failed' => [], ]; } ['path' => $sourcePath, 'temporary' => $temporaryPath] = $this->resolveSourcePath($artwork); $generatedVariants = []; $generatedPaths = []; $failed = []; try { foreach ($targetVariants as $variant) { $config = $this->paths->variantConfig($variant); if ($config === null) { $failed[$variant] = 'Unknown featured thumbnail variant.'; continue; } try { $image = $this->manager->read($sourcePath)->cover((int) $config['width'], (int) $config['height']); $encoded = (string) $image->encode(new WebpEncoder((int) $config['quality'])); $objectPath = $this->paths->objectPath($artwork, $variant); $this->storage->putObjectContents($objectPath, $encoded, 'image/webp'); $generatedVariants[] = $variant; $generatedPaths[] = $objectPath; } catch (\Throwable $exception) { $failed[$variant] = $exception->getMessage(); } } } finally { if ($temporaryPath !== null && $temporaryPath !== '') { File::delete($temporaryPath); } } if ($generatedPaths !== []) { $this->cdnPurge->purgeArtworkObjectPaths($generatedPaths, [ 'artwork_id' => $artwork->id, 'reason' => 'featured_artwork_thumbnails_generated', 'force' => $force, ]); } return $plan + [ 'generated' => count($generatedVariants), 'skipped' => max(0, count($targetVariants) - count($generatedVariants) - count($failed)) + count($plan['existing']), 'generated_variants' => $generatedVariants, 'generated_paths' => $generatedPaths, 'failed' => $failed, ]; } private function assertImageManager(): void { if ($this->manager !== null) { return; } throw new RuntimeException('Image processing is not available on this environment.'); } /** * @return array{path:string,temporary:?string} */ private function resolveSourcePath(Artwork $artwork): array { $localPath = $this->locator->resolveLocalPath($artwork); if ($this->isUsableSourceFile($localPath)) { return ['path' => $localPath, 'temporary' => null]; } $objectPath = $this->locator->resolveObjectPath($artwork); $extension = strtolower((string) pathinfo($objectPath, PATHINFO_EXTENSION)); if (! in_array($extension, self::ALLOWED_SOURCE_EXTENSIONS, true)) { throw new RuntimeException('Original artwork source is not a supported image file.'); } $contents = $this->storage->readObject($objectPath); if (! is_string($contents) || $contents === '' || ! $this->isImageContents($contents)) { throw new RuntimeException('Original artwork source is missing or is not a valid image.'); } $temporaryPath = tempnam(sys_get_temp_dir(), 'featured-artwork-'); if ($temporaryPath === false) { throw new RuntimeException('Unable to allocate a temporary source file for featured thumbnail generation.'); } $targetPath = $temporaryPath.($extension !== '' ? '.'.$extension : ''); if (! @rename($temporaryPath, $targetPath)) { File::delete($temporaryPath); throw new RuntimeException('Unable to prepare a temporary source file for featured thumbnail generation.'); } if (file_put_contents($targetPath, $contents) === false) { File::delete($targetPath); throw new RuntimeException('Unable to write the temporary source file for featured thumbnail generation.'); } return ['path' => $targetPath, 'temporary' => $targetPath]; } private function isUsableSourceFile(string $path): bool { if ($path === '' || ! is_file($path) || ! is_readable($path)) { return false; } $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION)); if (! in_array($extension, self::ALLOWED_SOURCE_EXTENSIONS, true)) { return false; } $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = strtolower((string) $finfo->file($path)); return str_starts_with($mime, 'image/'); } private function isImageContents(string $contents): bool { $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = strtolower((string) $finfo->buffer($contents)); return str_starts_with($mime, 'image/'); } }