Skip to content

Instantly share code, notes, and snippets.

@godismyjudge95
Last active September 23, 2025 19:34
Show Gist options
  • Select an option

  • Save godismyjudge95/468c14da0e0a689ef294e7caab03d1e4 to your computer and use it in GitHub Desktop.

Select an option

Save godismyjudge95/468c14da0e0a689ef294e7caab03d1e4 to your computer and use it in GitHub Desktop.
Statamic Image Service
<?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;
}
}
<?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