Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ jobs:
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: Execute tests
run: XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=85
run: XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=70
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ composer require justbetter/statamic-glide-directive
```

## Usage
This package adds a Blade directive. You can use an asset in the directive, and it will render the image according to the presets defined in the config. Heres an example:
This package adds a Blade directive. You can use an asset in the directive, and it will render the image according to the presets defined in the config. Here's an example:

```php
@responsive($image, [
Expand All @@ -31,16 +31,14 @@ To allow images to change on resize, include this in your head:
@include('statamic-glide-directive::partials.head')
```

We recommend generating your presets using:
```bash
php please assets:generate-presets
```
### Image Generation

For performance, consider using Redis for your queue connection. If kept on sync, images will be generated on the fly, affecting page load times. When using Redis, images will also be created on the fly while processing jobs in the queue. If an image doesn't have a Glide preset ready, the original image URL will be used for the first page load.
Images are served directly through custom routes that properly handle the content type and caching. When a preset image is requested, it's generated on demand and stored in the public directory.
If an image preset hasn't been generated yet, a placeholder will be used temporarily until the optimized version is ready.

To ensure that the image generation does not block the response, we're using the `dispatchAfterResponse` method when generating the resizes:
```php
GenerateGlideImageJob::dispatchAfterResponse($asset, $preset, $fit, $format);
We recommend pre-generating your presets for optimal performance:
```bash
php please assets:generate-presets
```

## Config
Expand Down Expand Up @@ -78,7 +76,7 @@ Configure which sources to use. By default, only WebP sources are used. You can
'sources' => 'webp',
```

### Publish
### Publish Configuration
```bash
php artisan vendor:publish --provider="JustBetter\ImageOptimize\ServiceProvider"
```
```
5 changes: 4 additions & 1 deletion config/glide-directive.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@
'default_queue' => env('STATAMIC_GLIDE_DIRECTIVE_DEFAULT_QUEUE', 'default'),

// Set the threshold width to use for the image source sets.
'image_resize_threshold' => 480
'image_resize_threshold' => 480,

// Set the storage prefix to use for the image source sets.
'storage_prefix' => 'storage/glide-image',
];
8 changes: 4 additions & 4 deletions resources/views/image.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ class="{{ $class }}"
height="{{ $height }}"
/>
@else
@isset($presets['webp'])
@if(isset($presets['webp']))
<source
srcset="{{ $presets['webp'] }}"
sizes="32px"
type="image/webp"
>
@endisset
@isset($presets[$image->mimeType()])
@endif
@if(isset($presets[$image->mimeType()]) && $image->mimeType() !== 'image/webp')
<source
srcset="{{ $presets[$image->mimeType()] }}"
sizes="32px"
type="{{ $image->mimeType() }}"
>
@endisset
@endif
<img
{!! $attributes ?? '' !!}
class="{{ $class }}"
Expand Down
21 changes: 21 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* Because these routes should mimic static files, no middlewares are applied.
*/

use Illuminate\Support\Facades\Route;
use JustBetter\GlideDirective\Controllers\ImageController;

$patterns = [
'file' => '.*',
'format' => '\..+',
];

Route::get('glide-image/placeholder/{file}', [ImageController::class, 'placeholder'])
->where($patterns)
->name('glide-image.placeholder');

Route::get('img/'.config('justbetter.glide-directive.storage_prefix', 'storage/glide-image').'/{preset}/{fit}/{s}/{file}{format}', [ImageController::class, 'getImageByPreset'])
->where($patterns)
->name('glide-image.preset');
115 changes: 115 additions & 0 deletions src/Controllers/ImageController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace JustBetter\GlideDirective\Controllers;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use JustBetter\GlideDirective\Responsive;
use League\Glide\Server;
use League\Glide\Signatures\Signature;
use League\Glide\Signatures\SignatureException;
use Statamic\Assets\Asset;
use Statamic\Contracts\Assets\Asset as AssetContract;
use Statamic\Contracts\Imaging\ImageManipulator;
use Statamic\Facades\Asset as AssetFacade;
use Statamic\Facades\Image;
use Statamic\Imaging\ImageGenerator;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class ImageController extends Controller
{
protected ?AssetContract $asset;

protected array $params;

public function __construct(protected ImageGenerator $imageGenerator, protected Server $server) {}

public function placeholder(Request $request, string $file, string $webp = ''): Response
{
/** @var ?Asset $asset */
$asset = AssetFacade::findByUrl(Str::start($file, '/'));
$this->asset = $asset;

if (! $this->asset) {
abort(404);
}

$presets = Responsive::getPresets($this->asset);
$base64Image = $presets['placeholder'] ?? '';
$base64Content = preg_replace('/^data:image\/\w+;base64,/', '', $base64Image);
$imageData = base64_decode($base64Content);
$mimeType = $this->asset->mimeType();

return response($imageData)
->header('Content-Type', $mimeType)
->header('Content-Disposition', 'inline');
}

public function getImageByPreset(Request $request, string $preset, string $fit, string $signature, string $file, string $format): BinaryFileResponse
{
/** @var ?Asset $asset */
$asset = AssetFacade::findByUrl(Str::start($file, '/'));
$this->asset = $asset;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can directly set $this->asset here, i think the typehint in the comment is not needed then too

$this->params = ['s' => $signature, 'preset' => $preset, 'fit' => $fit, 'format' => $format];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe after merge; We can create a ImageRequest to validate the parameters, now it will probably have an empty string in one of these variables if not filled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one of these parameters is not filled it means the given path is not right, which already results in a 404.
Haven't been able to get this to go wrong at least.


if (! $this->asset) {
abort(404);
}

try {
$signatureFactory = new Signature(config('app.key'));
$signatureFactory->validateRequest($this->asset->url(), $this->params);
} catch (SignatureException $e) {
abort(404);
}
$path = $this->buildImage();
$cachePath = config('statamic.assets.image_manipulation.cache_path');
$publicPath = $cachePath.'/'.$path;

if (! file_exists($publicPath)) {
abort(404);
}

$contentType = $this->asset->mimeType();

return new BinaryFileResponse($publicPath, 200, [
'Content-Type' => $contentType,
'Cache-Control' => 'public, max-age=31536000',
]);
}

protected function buildImage(): ?string
{
if (! $this->asset) {
return null;
}

$this->server->setSource(Storage::build(['driver' => 'local', 'root' => public_path()])->getDriver());
$this->server->setSourcePathPrefix('/');
$this->server->setCachePathPrefix(config('justbetter.glide-directive.storage_prefix', 'storage/glide-image').'/'.$this->params['preset'].'/'.$this->params['fit'].'/'.$this->params['s']);
$this->server->setCachePathCallable($this->getCachePathCallable());

$path = $this->server->makeImage($this->asset->url(), $this->params);

return $path;
}

protected function getCachePathCallable(): ?Closure
{
$server = $this->server;
$asset = $this->asset;
$params = $this->params;

if (! $asset) {
return null;
}

return function () use ($server, $asset, $params) {
return $server->getCachePathPrefix().$asset->url().$params['format'];
};
}
}
42 changes: 0 additions & 42 deletions src/Jobs/GenerateGlideImageJob.php

This file was deleted.

45 changes: 11 additions & 34 deletions src/Responsive.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@

use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use JustBetter\GlideDirective\Jobs\GenerateGlideImageJob;
use League\Glide\Signatures\SignatureFactory;
use Statamic\Assets\Asset;
use Statamic\Contracts\Imaging\ImageManipulator;
use Statamic\Facades\Glide;
use Statamic\Facades\Image;
use Statamic\Facades\URL;
use Statamic\Fields\Value;
use Statamic\Statamic;
use Statamic\Support\Str;

class Responsive
{
Expand Down Expand Up @@ -68,7 +65,7 @@ public static function getPresets(Asset $asset): array
$index = 0;

foreach ($configPresets as $preset => $data) {
if(!($data['w'] ?? false)) {
if (! ($data['w'] ?? false)) {
continue;
}

Expand All @@ -89,7 +86,7 @@ public static function getPresets(Asset $asset): array
}

if (self::canUseMimeTypeSource()) {
if ($glideUrl = self::getGlideUrl($asset, $preset, $fit ?? $data['fit'], $asset->mimeType())) {
if ($glideUrl = self::getGlideUrl($asset, $preset, $fit ?? $data['fit'], $asset->extension())) {
$presets[$asset->mimeType()] .= $glideUrl.' '.$size;

if ($preset !== 'placeholder') {
Expand Down Expand Up @@ -125,37 +122,17 @@ public static function getPresets(Asset $asset): array
protected static function getGlideUrl(Asset $asset, string $preset, string $fit, ?string $format = null): ?string
{
if ($preset === 'placeholder') {
return Statamic::tag('glide:data_url')->params([
'preset' => $preset,
'src' => $asset->url(),
'format' => $format,
'fit' => $fit,
])->fetch();
}

$manipulator = self::getManipulator($asset, $preset, $fit, $format);

if (is_string($manipulator)) {
return null;
return route('glide-image.placeholder', [
'file' => ltrim($asset->url(), '/'),
]);
}

$params = $manipulator->getParams();

$manipulationCacheKey = 'asset::'.$asset->id().'::'.md5(json_encode($params) ? json_encode($params) : '');

if ($cachedUrl = Glide::cacheStore()->get($manipulationCacheKey)) {
$url = Str::ensureLeft(config('statamic.assets.image_manipulation.route'), '/').'/'.$cachedUrl;

return URL::encode($url);
}

if (config('queue.default') === 'redis') {
GenerateGlideImageJob::dispatch($asset, $preset, $fit, $format);
} else {
GenerateGlideImageJob::dispatchAfterResponse($asset, $preset, $fit, $format);
}
$signatureFactory = SignatureFactory::create(config('app.key'));
$params = $signatureFactory->addSignature($asset->url(), ['preset' => $preset, 'fit' => $fit, 'format' => '.'.$format]);

return null;
return route('glide-image.preset', array_merge($params, [
'file' => ltrim($asset->url(), '/'),
]));
}

protected static function getManipulator(Asset $item, string $preset, string $fit, ?string $format = null): ImageManipulator|string
Expand Down
8 changes: 8 additions & 0 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public function boot(): void
->bootConfig()
->bootDirectives()
->bootViews()
->bootRoutes()
->bootClasses();
}

Expand Down Expand Up @@ -40,6 +41,13 @@ protected function bootDirectives(): static
return $this;
}

protected function bootRoutes(): static
{
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');

return $this;
}

protected function bootViews(): static
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'statamic-glide-directive');
Expand Down
Loading