Last active
September 23, 2025 19:34
-
-
Save godismyjudge95/468c14da0e0a689ef294e7caab03d1e4 to your computer and use it in GitHub Desktop.
Statamic Image Service
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Tags; | |
| use App\Services\ImageService; | |
| use Illuminate\Support\Collection; | |
| use Statamic\Contracts\Assets\Asset; | |
| use Statamic\Tags\Tags; | |
| /** TODO: replace this with official solution once this is merged - https://github.com/statamic/cms/pull/10204 */ | |
| class GenerateImage extends Tags | |
| { | |
| /** | |
| * The {{ generate_image }} tag. | |
| * | |
| * @return string | |
| */ | |
| public function index() | |
| { | |
| $params = collect($this->params); | |
| if (empty($params->get('src'))) { | |
| return; | |
| } | |
| return $this->generate($params->pull('src'), $params); | |
| } | |
| /** | |
| * The {{ generate_image:* }} tag. | |
| * | |
| * @return string | |
| */ | |
| public function wildcard() | |
| { | |
| $tag = explode(':', $this->tag, 2)[1]; | |
| $src = $this->context->get($tag); | |
| return $this->generate($src, collect($this->params)); | |
| } | |
| public function generate(string|array|Asset $src, Collection $params) | |
| { | |
| $absolute = $params->pull('absolute', false); | |
| $service = ImageService::for($src); | |
| $params | |
| ->filter() | |
| ->each(fn($value, $param) => $service->$param($value)); | |
| $url = $service->generate(); | |
| if ($absolute && !str_starts_with($url, 'http')) { | |
| return config('app.url') . $url; | |
| } | |
| return $url; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Services; | |
| use Exception; | |
| use Statamic\Contracts\Assets\Asset; | |
| use Statamic\Facades\Asset as AssetFacade; | |
| use Statamic\Facades\Image; | |
| enum ImageProvider: string | |
| { | |
| case CLOUDFLARE = 'cloudflare'; | |
| case IMAGOR = 'imagor'; | |
| case GLIDE = 'glide'; | |
| } | |
| class ImageService | |
| { | |
| public array $params = []; | |
| /** | |
| * @var array{ | |
| * hash: string, | |
| * trim: string, | |
| * crop: string, | |
| * fit: string, | |
| * stretch: string, | |
| * size: string, | |
| * padding: string, | |
| * halign: string, | |
| * valign: string, | |
| * smart: string, | |
| * filters: array, | |
| * image: string, | |
| * } | |
| */ | |
| public array $imagor = [ | |
| 'hash' => '', | |
| 'trim' => '', | |
| 'crop' => '', | |
| 'fit-in' => '', | |
| 'stretch' => '', | |
| 'size' => '', | |
| 'padding' => '', | |
| 'halign' => '', | |
| 'valign' => '', | |
| 'smart' => '', | |
| 'filters' => [], | |
| 'image' => '', | |
| ]; | |
| public array $hooks = []; | |
| public function __construct( | |
| public string|array|Asset $src, | |
| public ?ImageProvider $provider = null, | |
| public ?string $domain = null, | |
| ) { | |
| $this->domain ??= config('services.images.domain'); | |
| $this->provider ??= ImageProvider::tryFrom(config('services.images.provider')) ?? ImageProvider::GLIDE; | |
| // TODO: Remove this after this is resolved - https://github.com/statamic/cms/issues/8637 | |
| if (($this->src['extension'] ?? '') === 'svg') { | |
| return $this->src['url'] ?? $this->src; | |
| } | |
| if (is_array($this->src) && isset($this->src['id'])) { | |
| $this->src = AssetFacade::find($this->src['id']) ?? $this->src; | |
| } | |
| if (is_string($this->src)) { | |
| if (str_contains($this->src, '::')) { | |
| $this->src = AssetFacade::findById($this->src) ?? $this->src; | |
| } else if (str_starts_with($this->src, 'http') || str_starts_with($this->src, '/')) { | |
| $this->src = AssetFacade::findByUrl($this->src) ?? $this->src; | |
| } | |
| } | |
| $this->fit('crop_focal'); | |
| $this->format('auto'); | |
| $this->quality(85); | |
| if ($this->src instanceof Asset && $focus = $this->src->get('focus')) { | |
| $parts = explode('-', $focus); | |
| $x = intval($parts[0]); | |
| $x = $x === 0 ? 0 : $x / 100; | |
| $y = max(intval($parts[1]) - 20, 0); | |
| $y = $y === 0 ? 0 : $y / 100; | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| // https://developers.cloudflare.com/images/transform-images/transform-via-url/#gravity | |
| $this->params['gravity'] = "{$x}x{$y}"; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| // https://github.com/cshum/imagor#filters | |
| $this->imagor['filters']['focal'] = [$x, $y]; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $x *= 100; | |
| $y *= 100; | |
| $this->params['fit'] = "crop-{$x}-{$y}"; | |
| break; | |
| } | |
| } | |
| } | |
| public static function for( | |
| string|array|Asset $src, | |
| ?ImageProvider $provider = null, | |
| string $domain = 'https://selandgroup.com', | |
| ) { | |
| return new self($src, $provider, $domain); | |
| } | |
| public function generate() | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $mark_params = ['mark', 'markw', 'markh', 'markfit', 'markx', 'marky', 'markpad', 'markpos', 'markalpha']; | |
| collect($this->hooks)->each(fn($hook) => $hook()); | |
| $params = collect($this->params) | |
| ->except($mark_params) | |
| ->map(fn($v, $k) => "{$k}={$v}")->join(','); | |
| $src = "{$this->domain}{$this->src}"; | |
| if ($this->src instanceof Asset) { | |
| $src = ltrim($this->src->url, '/'); | |
| } | |
| $url = "{$this->domain}/cdn-cgi/image/{$params}/{$src}"; | |
| // if ($this->params['mark'] ?? null) { | |
| // $new_url = app(ImageGenerator::class) | |
| // ->generateByUrl($url, Arr::only($this->params, $mark_params)); | |
| // // collect($this->params) | |
| // // ->only($mark_params) | |
| // // ->each(fn ($value, $param) => $manipulator->$param($value)); | |
| // dd([ | |
| // $url, | |
| // $new_url | |
| // ]); | |
| // $url = $new_url; | |
| // } | |
| return $url; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| collect($this->hooks)->each(fn($hook) => $hook()); | |
| $this->imagor['filters'] = collect($this->imagor['filters']) | |
| ->map(fn($value, $key) => $key . '(' . implode(',', $value) . ')') | |
| ->prepend('filters') | |
| ->join(':'); | |
| $path = collect($this->imagor)->join('/'); | |
| if ($token = config('services.infomedia_images.token')) { | |
| $path = strtr( | |
| base64_encode(hash_hmac('sha1', $path, $token, true)), | |
| '/+', | |
| '_-' | |
| ) . '/' . $path; | |
| } else { | |
| $path = 'unsafe/' . $path; | |
| } | |
| return config('services.infomedia_images.endpoint') . '/' . $path; | |
| break; | |
| case ImageProvider::GLIDE: | |
| collect($this->hooks)->each(fn($hook) => $hook()); | |
| $manipulator = Image::manipulate($this->src); | |
| collect($this->params)->each(fn($value, $param) => $manipulator->$param($value)); | |
| return $manipulator->build(); | |
| break; | |
| } | |
| } | |
| public function background(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['background'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['background_color'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['bg'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function blur(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['blur'] = (intval($value) / 100) * 250; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['blur'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['blur'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function brightness(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['brightness'] = (intval($value) + 100) / 100; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['brightness'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['brightness'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function contrast(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['contrast'] = (intval($value) + 100) / 100; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['contrast'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['contrast'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function crop(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| [$width, $height, $x, $y] = explode(',', $value); | |
| $this->params['trim'] = "{$x};{$y};" . (intval($x) + intval($width)) . ';' . (intval($y) + intval($height)); | |
| break; | |
| case ImageProvider::IMAGOR: | |
| [$width, $height, $x, $y] = explode(',', $value); | |
| $this->imagor['crop'] = "{$x}x{$y}:" . (intval($x) + intval($width)) . 'x' . (intval($y) + intval($height)); | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['crop'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function dpr(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| case ImageProvider::GLIDE: | |
| $this->params['dpr'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| throw new Exception('Not implemented'); | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function filter(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| throw new Exception('Not implemented'); | |
| break; | |
| case ImageProvider::IMAGOR: | |
| if (!in_array($value, ['greyscale'])) { | |
| throw new Exception('Unsupported filter value'); | |
| } | |
| $this->imagor['filters']['grayscale'] = ''; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['fitler'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function fit(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| // https://developers.cloudflare.com/images/transform-images/transform-via-url/#fit | |
| $mappings = [ | |
| 'contain' => 'contain', | |
| 'max' => 'scale-down', | |
| 'fill' => 'pad', | |
| // 'stretch' => '', | |
| 'crop' => 'cover', | |
| 'crop_focal' => 'cover', | |
| ]; | |
| if (!array_key_exists($value, $mappings)) { | |
| throw new Exception('Unsupported fit parameter'); | |
| } | |
| $this->params['fit'] = $mappings[$value]; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| switch ($value) { | |
| case 'contain': // Resizes to fit within, without cropping, distorting, or altering aspect ratio | |
| $this->imagor['fit'] = 'fit-in'; | |
| break; | |
| case 'max': // Resizes to fit within, without cropping, distorting, or altering aspect ratio, will not increase size of image | |
| throw new Exception('Not implemented'); | |
| break; | |
| case 'fill': // Resize to fit within, without cropping or distorting the image, reaming space is filled with bg color | |
| throw new Exception('Not implemented'); | |
| break; | |
| case 'stretch': // Stretches to fit dimensions exactly, will NOT maintain the aspect ratio | |
| $this->imagor['stretch'] = 'stretch'; | |
| break; | |
| case 'crop': // Resize to fill the boundaries, crops any excess | |
| break; | |
| case 'crop_focal': // Same as crop but with a focal point | |
| break; | |
| } | |
| break; | |
| case ImageProvider::GLIDE: | |
| if ($value !== 'crop_focal') { | |
| $this->params['fit'] = $value; | |
| } | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function flip(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| throw new Exception('Not implemented'); | |
| break; | |
| case ImageProvider::IMAGOR: | |
| if (!in_array($value, ['v', 'h', 'both'])) { | |
| throw new Exception('Invalid flip value'); | |
| } | |
| $this->hooks['resize'] = $this->imagorResize(...); | |
| break; | |
| case ImageProvider::GLIDE: | |
| if (!in_array($value, ['v', 'h', 'both'])) { | |
| throw new Exception('Invalid flip value'); | |
| } | |
| $this->params['flip'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function format(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $value = $value === 'jpg' ? 'jpeg' : $value; | |
| // https://developers.cloudflare.com/images/transform-images/transform-via-url/#format | |
| if (!in_array($value, ['auto', 'avif', 'webp', 'jpeg', 'baseline-jpeg', 'json'])) { | |
| throw new Exception('Unsupported format value'); | |
| } | |
| $this->params['format'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $value = $value === 'jpg' ? 'jpeg' : $value; | |
| // https://github.com/cshum/imagor#filters | |
| if (!in_array($value, ['auto', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'avif', 'jp2'])) { | |
| throw new Exception('Unsupported format value'); | |
| } | |
| if ($value === 'auto') { | |
| return $this; | |
| } | |
| $this->imagor['filters']['format'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $value = $value === 'jpeg' ? 'jpg' : $value; | |
| // https://statamic.dev/tags/glide#parameters | |
| if (!in_array($value, ['auto', 'jpg', 'pjpg', 'png', 'gif', 'webp', 'avif'])) { | |
| throw new Exception('Unsupported format value'); | |
| } | |
| if ($value === 'auto') { | |
| return $this; | |
| } | |
| $this->params['format'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function gamma(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| case ImageProvider::GLIDE: | |
| $this->params['gamma'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| throw new Exception('Not implemented'); | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function height(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['height'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->params['height'] = $value; | |
| $this->hooks['resize'] = $this->imagorResize(...); | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['height'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function mark(string $value) | |
| { | |
| $this->params['mark'] = $value; | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->hooks['watermarks'] = $this->imagorWatermark(...); | |
| break; | |
| case ImageProvider::GLIDE: | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function markw(string $value) | |
| { | |
| $this->params['markw'] = $value; | |
| return $this; | |
| } | |
| public function markh(string $value) | |
| { | |
| $this->params['markh'] = $value; | |
| return $this; | |
| } | |
| public function markfit(string $value) | |
| { | |
| $this->params['markfit'] = $value; | |
| return $this; | |
| } | |
| public function markx(string $value) | |
| { | |
| $this->params['markx'] = $value; | |
| return $this; | |
| } | |
| public function marky(string $value) | |
| { | |
| $this->params['marky'] = $value; | |
| return $this; | |
| } | |
| public function markpad(string $value) | |
| { | |
| $this->params['markpad'] = $value; | |
| return $this; | |
| } | |
| public function markpos(string $value) | |
| { | |
| $this->params['markpos'] = $value; | |
| return $this; | |
| } | |
| public function markalpha(string $value) | |
| { | |
| $this->params['markalpha'] = $value; | |
| return $this; | |
| } | |
| public function orientation(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| if (!in_array($value, [0, 90, 180, 270])) { | |
| throw new Exception('Unsupported orientation value'); | |
| } | |
| $this->params['rotate'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| if (!in_array($value, [0, 90, 180, 270])) { | |
| throw new Exception('Unsupported orientation value'); | |
| } | |
| $this->imagor['filters']['orient'] = $value; | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['orient'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function pixelate(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| case ImageProvider::IMAGOR: | |
| throw new Exception('Not implemented'); | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['pixelate'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function quality(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| case ImageProvider::GLIDE: | |
| $this->params['quality'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['quality'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function sharpen(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| case ImageProvider::GLIDE: | |
| $this->params['sharpen'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->imagor['filters']['sharpen'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function width(string $value) | |
| { | |
| switch ($this->provider) { | |
| case ImageProvider::CLOUDFLARE: | |
| $this->params['width'] = $value; | |
| break; | |
| case ImageProvider::IMAGOR: | |
| $this->params['width'] = $value; | |
| $this->hooks['resize'] = $this->imagorResize(...); | |
| break; | |
| case ImageProvider::GLIDE: | |
| $this->params['width'] = $value; | |
| break; | |
| } | |
| return $this; | |
| } | |
| public function square(string $value) | |
| { | |
| $this->width($value); | |
| $this->height($value); | |
| return $this; | |
| } | |
| public function preset(?string $value = null) | |
| { | |
| if (!$value) { | |
| return $this; | |
| } | |
| if ($this->provider === ImageProvider::GLIDE) { | |
| $this->params['preset'] = $value; | |
| return $this; | |
| } | |
| $presets = Image::manipulationPresets(); | |
| if (!array_key_exists($value, $presets)) { | |
| throw new Exception("Preset {$value} does not exist"); | |
| } | |
| $preset = $presets[$value]; | |
| if (!is_array($preset)) { | |
| throw new Exception("Preset {$value} is not an array"); | |
| } | |
| foreach ($preset as $param => $val) { | |
| $param = match ($param) { | |
| 'w' => 'width', | |
| 'h' => 'height', | |
| 'fm' => 'format', | |
| 'q' => 'quality', | |
| default => $param, | |
| }; | |
| if (method_exists($this, $param)) { | |
| $this->$param($val); | |
| } else { | |
| throw new Exception("Preset {$value} has unsupported parameter {$param}"); | |
| } | |
| } | |
| return $this; | |
| } | |
| private function imagorResize() | |
| { | |
| $width = $this->params['width'] ?? null; | |
| $height = $this->params['height'] ?? null; | |
| if ($this->params['square'] ?? null) { | |
| $width = $height = $this->params['square']; | |
| } | |
| if ($this->params['flip']) { | |
| $width = $width && in_array($this->params['flip'], ['h', 'both']) ? "-{$width}" : null; | |
| $height = $height && in_array($this->params['flip'], ['v', 'both']) ? "-{$height}" : null; | |
| } | |
| $this->imagor['size'] = "{$width}x{$height}"; | |
| } | |
| private function imagorWatermark() | |
| { | |
| $image = $this->params['mark']; | |
| $x = $this->params['markx'] ?? match ($this->params['markpos'] ?? null) { | |
| 'top-left', 'left', 'bottom-left' => 'left', | |
| 'top-right', 'right', 'bottom-right' => 'right', | |
| 'top', 'bottom' => 'center', | |
| default => $this->params['markpad'], | |
| }; | |
| $y = $this->params['marky'] ?? match ($this->params['markpos'] ?? null) { | |
| 'top-left', 'top', 'top-right' => 'top', | |
| 'bottom-left', 'bottom', 'bottom-right' => 'bottom', | |
| 'left', 'right' => 'center', | |
| default => $this->params['markpad'], | |
| }; | |
| $alpha = $this->params['markalpha'] ?? null; | |
| // TODO: calculate the ratio of the width to the image width | |
| $w_ratio = $this->params['markw'] ?? null; | |
| $h_ratio = $this->params['markh'] ?? null; | |
| $this->imagor['filters']['watermark'] = [$image, $x, $y, $alpha, $w_ratio, $h_ratio]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment