├── .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 | 
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 |
--------------------------------------------------------------------------------