├── .github └── FUNDING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── raster.php ├── phpunit.xml ├── routes └── web.php ├── src ├── BaseHandler.php ├── BladeHandler.php ├── Cache.php ├── Raster.php ├── ServiceProvider.php └── helpers.php └── tests ├── Pest.php ├── TestCase.php └── Unit └── ExampleTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jacksleight -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.2 (2025-02-26) 4 | 5 | - Laravel 12 support 6 | 7 | ## 0.4.1 (2025-01-06) 8 | 9 | - Browsershot 5 support 10 | 11 | ## 0.4.0 (2024-12-06) 12 | 13 | - [new] File name option 14 | 15 | ## 0.3.1 (2024-12-04) 16 | 17 | - [fix] View file paths with dots 18 | 19 | ## 0.3.0 (2024-11-25) 20 | 21 | - [new] Disk based caching 22 | 23 | ## 0.2.0 (2024-11-22) 24 | 25 | - [new] Global cache toggle 26 | - [new] Cache key option 27 | 28 | ## 0.1.1 (2024-11-15) 29 | 30 | - [fix] URL param overrides 31 | 32 | ## 0.1.0 (2024-11-15) 33 | 34 | - 🚀 Initial release 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jack Sleight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raster 2 | 3 | Rasterise views and components to images by simply adding a directive and fetching the URL. Automatic routing, scaling, caching, protection and preview mode. Zero configuration (unless you need it). 4 | 5 | ## Installation 6 | 7 | Run the following command from your project root: 8 | 9 | ```bash 10 | composer require jacksleight/laravel-raster 11 | ``` 12 | 13 | This package uses [Puppeteer](https://pptr.dev/) via [spatie/browsershot](https://spatie.be/docs/browsershot/v4/introduction) under the hood, you will also need follow the necessary Puppeteer [installation steps](https://spatie.be/docs/browsershot/v4/requirements) for your system. I can't help with Puppeteer issues or rendering inconsistencies, sorry. 14 | 15 | If you need to customise the config you can publish it with: 16 | 17 | ```bash 18 | php artisan vendor:publish --tag="raster-config" 19 | ``` 20 | 21 | ## Usage 22 | 23 | ### Layout Setup 24 | 25 | The views will be rendered inside a layout view where you can load any required CSS and other assets. By default this is a component called `layouts.raster`, but you can change it in the config file. 26 | 27 | ```blade 28 | {{-- resources/views/components/layouts/raster.blade.php --}} 29 | 30 | 31 | 32 | 33 | 34 | Raster 35 | @vite(['resources/css/app.css']) 36 | 37 | 38 | {{ $slot }} 39 | 40 | 41 | ``` 42 | 43 | ### Automatic Mode 44 | 45 | To make a view rasterizeable simply implement the `@raster` directive and then generate a URL to your image using the `raster()` helper. The data closure receives any parameters passed in the URL and should return an array of data to pass to the view. 46 | 47 | ```blade 48 | {{-- resources/views/blog/hero.blade.php --}} 49 | @raster( 50 | width: 1000, 51 | data: fn ($post) => [ 52 | 'post' => Post::find((int) $post), 53 | ], 54 | ) 55 |
56 | ... 57 |

{{ $post->title }}

58 |

{{ $post->date }}

59 |
60 | ``` 61 | 62 | ```blade 63 | {{-- resources/views/blog/show.blade.php --}} 64 | @push('head') 65 | 66 | @endpush 67 | ``` 68 | 69 | You can set [options](#options) with the directive or through the URL by chaining methods on to the helper. The options passed in the URL take priority over options set in the directive. 70 | 71 | When the view is rendered during normal non-raster requests the directive does nothing. 72 | 73 | > [!IMPORTANT] 74 | > Views rasterised using automatic mode must implement the raster directive. 75 | 76 | ### Manual Mode 77 | 78 | If you would like more control over the routing and how the requests are handled you can define your own routes that return raster responses and then generate a URL to your image using the usual `route()` helper. 79 | 80 | ```blade 81 | {{-- resources/views/blog/hero.blade.php --}} 82 |
83 | ... 84 |

{{ $post->title }}

85 |

{{ $post->date }}

86 |
87 | ``` 88 | 89 | ```php 90 | /* routes/web.php */ 91 | use JackSleight\LaravelRaster\Raster; 92 | 93 | Route::get('/blog/{post}/hero', function (Post $post) { 94 | return Raster::make('blog.hero') 95 | ->data(['post' => $post]) 96 | ->width(1000); 97 | })->name('blog.hero'); 98 | ``` 99 | 100 | ```blade 101 | {{-- resources/views/layout.blade.php --}} 102 | 103 | ``` 104 | 105 | > [!IMPORTANT] 106 | > Views rasterised using manual mode must not implement the raster directive. 107 | 108 | ## Customising Rasterised Views 109 | 110 | If you would like to make changes to the view based on whether or not it's being rasterised you can check for the `$raster` variable: 111 | 112 | ```blade 113 |
class([ 114 | 'rounded-none' => $raster ?? null, 115 | ]) }}> 116 |
117 | ``` 118 | 119 | ## Options 120 | 121 | The following options can be set with the directive or by chaining methods on to the object: 122 | 123 | * **width (int)** 124 | Width of the generated image. 125 | * **height (int, auto)** 126 | Height of the generated image. 127 | * **basis (int)** 128 | [Viewport basis](#viewport-basis) of the generated image. 129 | * **scale (int, 1)** 130 | Scale of the generated image. 131 | * **type (string, png)** 132 | Type of the generated image (`png`, `jpeg` or `pdf`). 133 | * **file (string)** 134 | File name of the response, excluding extension. 135 | * **data (array)** 136 | Array of data to pass to the view. 137 | * **preview (bool, false)** 138 | Enable [preview mode](#preview-mode). 139 | 140 | With PDF output a height is required, it will only contain one page, and dimensions are still pixels not mm/inches. If you're looking to generate actual documents from views I highly recommend checking out [spatie/laravel-pdf](https://github.com/spatie/laravel-pdf). 141 | 142 | ### Caching 143 | 144 | The following caching options can be set with the directive or by chaining methods on to the object. The `cacheId` cannot be passed as a URL parameter. You can globally disable caching by setting the `RASTER_CACHE_ENABLED` env var to `false`. By default the cache will be stored locally in `storage/app/raster`, you can change this by setting the `RASTER_CACHE_DISK` and `RASTER_CACHE_PATH` env vars. 145 | 146 | * **cache (bool, false)** 147 | Enable caching of generated images. 148 | * **cacheId (string, '_')** 149 | Cache identifier (optional, see below). 150 | 151 | File paths will use this pattern: `[cache_path]/[view_name]/[cache_id]/[params_hash].[extension]`. 152 | 153 | ## Viewport Basis 154 | 155 | When the basis option is set the image will be generated as if the viewport was that width, but the final image will match the desired width. Here's an example of how that affects output: 156 | 157 | ![Viewport Basis](https://jacksleight.dev/assets/packages/laravel-raster/viewport-basis.jpg) 158 | 159 | ## Preview Mode 160 | 161 | In preview mode the HTML will be returned from the response but with all the appropriate scaling applied. This gives you a 1:1 preview without the latency that comes from generating the actual image. 162 | 163 | ## Security & URL Signing 164 | 165 | Only views that implement the `@raster` directive can be rasterised in automatic mode, an error will be thrown before execution if they don't. It's also recommended to enable URL signing on production to ensure they can't be tampered with. You can do this by setting the `RASTER_SIGN_URLS` env var to `true`. 166 | 167 | ## Customising Browsershot 168 | 169 | If you need to customise the Browsershot instance you can pass a closure to `Raster::browsershot()` in a service provider: 170 | 171 | ```php 172 | use JackSleight\LaravelRaster\Raster; 173 | 174 | Raster::browsershot(fn ($browsershot) => $browsershot 175 | ->setOption('args', ['--disable-web-security']) 176 | ->waitUntilNetworkIdle() 177 | ); 178 | ``` 179 | 180 | ## Sponsoring 181 | 182 | This package is completely free to use. However fixing bugs, adding features and helping users takes time and effort. If you find this useful and would like to support its development any [contribution](https://github.com/sponsors/jacksleight) would be greatly appreciated. Thanks! 🙂 183 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jacksleight/laravel-raster", 3 | "autoload": { 4 | "psr-4": { 5 | "JackSleight\\LaravelRaster\\": "src" 6 | }, 7 | "files": [ 8 | "src/helpers.php" 9 | ] 10 | }, 11 | "extra": { 12 | "laravel": { 13 | "providers": [ 14 | "JackSleight\\LaravelRaster\\ServiceProvider" 15 | ] 16 | } 17 | }, 18 | "require": { 19 | "php": "^8.1|^8.2|^8.3", 20 | "laravel/framework": "^10.0|^11.0|^12.0", 21 | "spatie/browsershot": "^4.3|^5.0" 22 | }, 23 | "require-dev": { 24 | "pestphp/pest": "^3.5", 25 | "phpstan/phpstan": "^1.12" 26 | }, 27 | "config": { 28 | "allow-plugins": { 29 | "pestphp/pest-plugin": true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/raster.php: -------------------------------------------------------------------------------- 1 | 'raster', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Layout 16 | |-------------------------------------------------------------------------- 17 | */ 18 | 19 | 'layout' => 'components.layouts.raster', 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Sign URLs 24 | |-------------------------------------------------------------------------- 25 | */ 26 | 27 | 'sign_urls' => env('RASTER_SIGN_URLS', false), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Cache 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 35 | 'cache' => [ 36 | 'enabled' => env('RASTER_CACHE_ENABLED', true), 37 | 'disk' => env('RASTER_CACHE_DISK'), 38 | 'path' => env('RASTER_CACHE_PATH'), 39 | ], 40 | 41 | ]; 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'laravel-raster.'], function () { 7 | $route = config('raster.route'); 8 | Route::get($route.'/{name}', function (Request $request) { 9 | if (config('raster.sign_urls') && ! $request->hasValidSignature()) { 10 | abort(401); 11 | } 12 | 13 | return Raster::makeFromRequest($request); 14 | })->name('render'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/BaseHandler.php: -------------------------------------------------------------------------------- 1 | raster = $raster; 12 | } 13 | 14 | abstract public function hasFingerprint(): bool; 15 | } 16 | -------------------------------------------------------------------------------- /src/BladeHandler.php: -------------------------------------------------------------------------------- 1 | compileString(file_get_contents($this->raster->path())); 14 | 15 | return Str::contains($string, static::uniqueString()); 16 | } 17 | 18 | /** 19 | * @param array $args 20 | * @return array 21 | */ 22 | public function injectParams(...$params): array 23 | { 24 | if (! $this->raster->isAutomaticMode()) { 25 | return []; 26 | } 27 | 28 | $input = $this->raster->request()->all(); 29 | $merged = collect([...$params, ...$input]); 30 | collect($params) 31 | ->keys() 32 | ->unshift('data') 33 | ->unique() 34 | ->each(function ($name) use ($params, $input, &$merged) { 35 | if (($params[$name] ?? null) instanceof Closure) { 36 | $pass = $name === 'data' ? $input : $merged; 37 | $merged[$name] = app()->call($params[$name], $pass['data'] ?? []); 38 | } 39 | }); 40 | 41 | $merged->each(fn ($value, $name) => $this->raster->{$name}($value)); 42 | 43 | return $this->raster->data(); 44 | } 45 | 46 | protected static function uniqueString(): string 47 | { 48 | return '__raster_'.hash('sha1', __FILE__).'__'; 49 | } 50 | 51 | public static function compile(string $expression): string 52 | { 53 | $uniqueString = static::uniqueString(); 54 | 55 | return "handler()->injectParams({$expression}); 59 | extract(\$__raster_data); 60 | unset(\$__raster_data); 61 | } 62 | ?>"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | files = $files; 17 | $this->directory = $directory; 18 | } 19 | 20 | public function get(string $name, string $id, array $params) 21 | { 22 | $path = $this->getFilePath($name, $id, $params); 23 | 24 | if ($this->files->exists($path)) { 25 | return $this->files->get($path); 26 | } 27 | } 28 | 29 | public function put(string $name, string $id, array $params, string $data) 30 | { 31 | $path = $this->getFilePath($name, $id, $params); 32 | 33 | $directory = dirname($path); 34 | if (! $this->files->exists($directory)) { 35 | $this->files->makeDirectory($directory, 0777, true, true); 36 | } 37 | 38 | return $this->files->put($path, $data); 39 | } 40 | 41 | public function forget(array|string $names, array|string $ids = '_') 42 | { 43 | foreach (Arr::wrap($names) as $name) { 44 | foreach (Arr::wrap($ids) as $id) { 45 | $this->files->deleteDirectory($this->getDirectoryPath($name, $id)); 46 | } 47 | } 48 | } 49 | 50 | public function flush() 51 | { 52 | if (! $this->files->exists($this->directory)) { 53 | return; 54 | } 55 | 56 | foreach ($this->files->directories($this->directory) as $directory) { 57 | $this->files->deleteDirectory($directory); 58 | } 59 | } 60 | 61 | protected function getDirectoryPath(string $name, string $id) 62 | { 63 | return $this->directory.'/'.$name.'/'.$id; 64 | } 65 | 66 | protected function getFilePath(string $name, string $id, array $params) 67 | { 68 | $extension = match ($params['type']) { 69 | 'jpeg' => 'jpg', 70 | 'png' => 'png', 71 | 'pdf' => 'pdf', 72 | }; 73 | 74 | return $this->directory.'/'.$name.'/'.$id.'/'.md5(serialize($params)).'.'.$extension; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Raster.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | protected array $data = []; 30 | 31 | protected ?Request $request; 32 | 33 | protected ?int $width; 34 | 35 | protected ?int $height; 36 | 37 | protected ?int $basis; 38 | 39 | protected int $scale = 1; 40 | 41 | protected string $type = 'png'; 42 | 43 | protected string $file; 44 | 45 | protected bool $preview = false; 46 | 47 | protected bool $cache = false; 48 | 49 | protected string $cacheId = '_'; 50 | 51 | protected static Closure $browsershot; 52 | 53 | protected static $extensions = [ 54 | 'blade.php' => BladeHandler::class, 55 | ]; 56 | 57 | protected $route = 'laravel-raster.render'; 58 | 59 | public static function make(string $name): static 60 | { 61 | return new static($name); 62 | } 63 | 64 | public static function makeFromRequest(?Request $request): static 65 | { 66 | return new static($request->route()->parameter('name'), $request); 67 | } 68 | 69 | /** 70 | * @param array $data 71 | */ 72 | protected function __construct(string $name, ?Request $request = null) 73 | { 74 | $this->name = $name; 75 | $this->request = $request; 76 | 77 | $this->path = View::getFinder()->find($this->name); 78 | 79 | $extension = Str::after(pathinfo($this->path, PATHINFO_BASENAME), '.'); 80 | if (! $handler = static::$extensions[$extension] ?? null) { 81 | throw new \Exception('Unsupported view type: '.$extension); 82 | } 83 | $this->handler = new $handler($this); 84 | } 85 | 86 | public function name(): string 87 | { 88 | return $this->name; 89 | } 90 | 91 | public function path(): string 92 | { 93 | return $this->path; 94 | } 95 | 96 | public function handler(): BaseHandler 97 | { 98 | return $this->handler; 99 | } 100 | 101 | public function request(): Request 102 | { 103 | return $this->request; 104 | } 105 | 106 | /** 107 | * @param array|null $data 108 | * @return static|array 109 | */ 110 | public function data(?array $data = null): static|array 111 | { 112 | if (func_num_args() > 0) { 113 | $this->data = $data ?? []; 114 | 115 | return $this; 116 | } 117 | 118 | return $this->data; 119 | } 120 | 121 | public function width(?int $width = null): static|int|null 122 | { 123 | if (func_num_args() > 0) { 124 | $this->width = $width; 125 | 126 | return $this; 127 | } 128 | 129 | return $this->width; 130 | } 131 | 132 | public function height(?int $height = null): static|int|null 133 | { 134 | if (func_num_args() > 0) { 135 | $this->height = $height; 136 | 137 | return $this; 138 | } 139 | 140 | return $this->height; 141 | } 142 | 143 | public function basis(?int $basis = null): static|int|null 144 | { 145 | if (func_num_args() > 0) { 146 | $this->basis = $basis; 147 | 148 | return $this; 149 | } 150 | 151 | return $this->basis; 152 | } 153 | 154 | public function scale(?int $scale = null): static|int 155 | { 156 | if (func_num_args() > 0) { 157 | $this->scale = $scale ?? 1; 158 | 159 | return $this; 160 | } 161 | 162 | return $this->scale; 163 | } 164 | 165 | public function type(?string $type = null): static|string 166 | { 167 | if (func_num_args() > 0) { 168 | $this->type = $type ?? 'png'; 169 | 170 | return $this; 171 | } 172 | 173 | return $this->type; 174 | } 175 | 176 | public function file(?string $file = null): static|string 177 | { 178 | if (func_num_args() > 0) { 179 | $this->file = $file; 180 | 181 | return $this; 182 | } 183 | 184 | return $this->file; 185 | } 186 | 187 | public function preview(?bool $preview = null): static|bool 188 | { 189 | if (func_num_args() > 0) { 190 | $this->preview = $preview ?? false; 191 | 192 | return $this; 193 | } 194 | 195 | return $this->preview; 196 | } 197 | 198 | public function cache(?bool $cache = null): static|bool 199 | { 200 | if (func_num_args() > 0) { 201 | $this->cache = $cache ?? false; 202 | 203 | return $this; 204 | } 205 | 206 | return $this->cache; 207 | } 208 | 209 | public function cacheId(?string $cacheId = null): static|string 210 | { 211 | if (func_num_args() > 0) { 212 | $this->cacheId = $cacheId; 213 | 214 | return $this; 215 | } 216 | 217 | return $this->cacheId; 218 | } 219 | 220 | public function render(): string 221 | { 222 | if ($this->isAutomaticMode() && ! $this->hasFingerprint()) { 223 | throw new \Exception('View must implement raster'); 224 | } elseif ($this->isManualMode() && $this->hasFingerprint()) { 225 | throw new \Exception('View must not implement raster'); 226 | } 227 | 228 | $html = $this->renderHtml(); 229 | 230 | if (! isset($this->width)) { 231 | if (isset($this->basis)) { 232 | $this->width = $this->basis; 233 | } else { 234 | throw new \Exception('Width or basis must be set'); 235 | } 236 | } 237 | if ($this->type === 'pdf' && ! isset($this->height)) { 238 | throw new \Exception('Height must be set for PDF output'); 239 | } 240 | 241 | if ($this->preview) { 242 | return $this->renderPreview($html); 243 | } 244 | 245 | $cache = app(Cache::class); 246 | $params = $this->gatherParams(); 247 | $shouldCache = config('raster.cache.enabled') && $this->cache; 248 | 249 | if ($shouldCache && $data = $cache->get($this->name, $this->cacheId, $params)) { 250 | return $data; 251 | } 252 | 253 | $data = $this->renderImage($html); 254 | 255 | if ($shouldCache) { 256 | $cache->put($this->name, $this->cacheId, $params, $data); 257 | } 258 | 259 | return $data; 260 | } 261 | 262 | /** 263 | * @param array $data 264 | */ 265 | protected function renderHtml(): string 266 | { 267 | $layout = config('raster.layout'); 268 | 269 | View::share('raster', $this); 270 | $html = $this->renderView($layout, [], $this->renderView($this->name(), $this->data())); 271 | View::share('raster', null); 272 | 273 | return $html; 274 | } 275 | 276 | /** 277 | * @param array $data 278 | */ 279 | protected function renderView(string $name, array $data, ?string $slot = null): string 280 | { 281 | if (Str::before($name, '.') === 'components') { 282 | return Blade::render(<<<'HTML' 283 | 284 | {{ $slot }} 285 | 286 | HTML, [ 287 | 'name' => Str::after($name, 'components.'), 288 | 'data' => new ComponentAttributeBag($data), 289 | 'slot' => new ComponentSlot($slot ?? ''), 290 | ]); 291 | } 292 | 293 | if ($slot) { 294 | return Blade::render(<<<'HTML' 295 | @extends($name, $data) 296 | @section('slot', $slot) 297 | HTML, [ 298 | 'name' => $name, 299 | 'data' => $data, 300 | ]); 301 | } 302 | 303 | return view($name, $data)->render(); 304 | } 305 | 306 | protected function renderPreview(string $html): string 307 | { 308 | $style = ''; 309 | $script = ""; 310 | 311 | return $html.$style.$script; 312 | } 313 | 314 | protected function renderImage(string $html): string 315 | { 316 | $callback = static::$browsershot ?? fn ($browsershot) => $browsershot; 317 | 318 | $raster = $callback(new Browsershot) 319 | ->setHtml($html) 320 | ->setOption('addStyleTag', json_encode(['content' => $this->makeStyle()])) 321 | ->showBackground(); 322 | 323 | if ($this->type === 'pdf') { 324 | $width = ($this->width / 96 * 25.4) * $this->scale; 325 | $height = ($this->height / 96 * 25.4) * $this->scale; 326 | 327 | return $raster 328 | ->paperSize($width, $height) 329 | ->scale($this->scale) 330 | ->pages(1) 331 | ->pdf(); 332 | } 333 | 334 | if (isset($this->height)) { 335 | $raster->windowSize($this->width, $this->height); 336 | } else { 337 | $raster->windowSize($this->width, 1)->fullPage(); 338 | } 339 | 340 | return $raster 341 | ->deviceScaleFactor($this->scale) 342 | ->setScreenshotType($this->type) 343 | ->screenshot(); 344 | } 345 | 346 | public function makeStyle(bool $preview = false): string 347 | { 348 | $fontSize = isset($this->basis) 349 | ? 16 * $this->width / $this->basis 350 | : 16; 351 | 352 | return collect([ 353 | ':root { font-size: '.$fontSize.'px; }', 354 | ])->when($preview, fn ($style) => $style->merge([ 355 | ':root { min-height: 100vh; display: flex; background: black; }', 356 | 'body { width: '.$this->width.'px; margin: auto; }', 357 | ]))->join(' '); 358 | } 359 | 360 | protected function hasFingerprint(): bool 361 | { 362 | return $this->handler->hasFingerprint($this->path); 363 | } 364 | 365 | public function toResponse($request): Response 366 | { 367 | $data = $this->render(); 368 | 369 | if ($this->preview) { 370 | return response($data); 371 | } 372 | 373 | $mime = match (true) { 374 | $this->type === 'jpeg' => 'image/jpeg', 375 | $this->type === 'png' => 'image/png', 376 | $this->type === 'pdf' => 'application/pdf', 377 | default => throw new \Exception('Unsupported image type: '.$this->type), 378 | }; 379 | $ext = $this->type === 'jpeg' ? 'jpg' : $this->type; 380 | 381 | $file = isset($this->file) 382 | ? ($this->file.'.'.$ext) 383 | : (Str::replace('.', '-', $this->name).'.'.$ext); 384 | 385 | return response($data) 386 | ->header('Content-Type', $mime) 387 | ->header('Content-Disposition', 'inline; filename="'.$file.'"'); 388 | } 389 | 390 | public function toUrl(): string 391 | { 392 | $params = $this->gatherParams(); 393 | 394 | $defaults = (new \ReflectionClass($this)) 395 | ->getDefaultProperties(); 396 | $params = collect($params) 397 | ->filter(fn ($value, $key) => $value !== ($defaults[$key] ?? null)) 398 | ->all(); 399 | 400 | return config('raster.sign_urls') 401 | ? URL::signedRoute($this->route, $params) 402 | : route($this->route, $params); 403 | } 404 | 405 | protected function gatherParams(): array 406 | { 407 | return [ 408 | 'name' => $this->name, 409 | 'data' => app('url')->formatParameters($this->data), 410 | 'width' => $this->width ?? null, 411 | 'height' => $this->height ?? null, 412 | 'basis' => $this->basis ?? null, 413 | 'scale' => $this->scale, 414 | 'type' => $this->type, 415 | 'preview' => $this->preview, 416 | 'cache' => $this->cache, 417 | ]; 418 | } 419 | 420 | public function __toString(): string 421 | { 422 | return $this->toUrl(); 423 | } 424 | 425 | public function isAutomaticMode(): bool 426 | { 427 | return isset($this->request); 428 | } 429 | 430 | public function isManualMode(): bool 431 | { 432 | return ! isset($this->request); 433 | } 434 | 435 | public static function browsershot(Closure $browsershot): void 436 | { 437 | static::$browsershot = $browsershot; 438 | } 439 | 440 | public static function extension(string $extension, string $class): void 441 | { 442 | static::$extensions[$extension] = $class; 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Cache::class, function () { 14 | $config = config('raster.cache'); 15 | 16 | return $config['disk'] 17 | ? new Cache(Storage::disk($config['disk']), $config['path']) 18 | : new Cache(Storage::build([ 19 | 'driver' => 'local', 20 | 'root' => storage_path('app/raster'), 21 | ])); 22 | }); 23 | } 24 | 25 | public function boot(): void 26 | { 27 | $this->publishes([ 28 | __DIR__.'/../config/raster.php' => config_path('raster.php'), 29 | ], 'raster-config'); 30 | 31 | $this->mergeConfigFrom( 32 | __DIR__.'/../config/raster.php', 'raster' 33 | ); 34 | 35 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 36 | 37 | Blade::directive('raster', function ($expression) { 38 | return BladeHandler::compile($expression); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | $data 8 | */ 9 | function raster(string $name, array $data = []): Raster 10 | { 11 | return Raster::make($name)->data($data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class)->in('Feature'); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | expect()->extend('toBeOne', function () { 28 | return $this->toBe(1); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | function something() 43 | { 44 | // .. 45 | } 46 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | --------------------------------------------------------------------------------