Images, video and PDF
Images, video and PDF
Images, variants and watermarks
Laracrate processes every image you upload into an optimized original plus any number of resized variants (thumbnails, medium, large, and so on). Variants are generated asynchronously in the pipeline, so the user's upload stays instant. Each variant is a real File row of its own with parent_id pointing at the original, type = image, and a variant name, so you can query and serve it like any other file. The original is never destroyed by variant generation, and the original is never watermarked.
You declare variants per file type inside a collection's config. The orchestrator (GenerateImageVariantsAction) iterates types.image.variants for the collection and dispatches GenerateImageVariantAction once per definition. A failure in one variant is logged and skipped, the rest still generate.
// config/laracrate.php 'collections' => [ 'gallery' => [ 'disk' => 'media', 'access' => 'public', 'types' => [ 'image' => [ 'variants' => [ 'thumbnail' => ['width' => 300, 'height' => 300, 'fit' => true], 'medium' => ['width' => 800, 'height' => 800], 'large' => ['width' => 1600, 'height' => 1600], ], ], ], ], ],
Where a variant lives
The variant binary is written under a variants/ subdirectory next to the original, using $file->variantKey($newName). The variant filename is the original base name plus the variant name, for example photo_thumbnail.webp. The variant row is created with $file->createVariant($name, $overrides), which inherits the parent's scope. Regeneration is idempotent: if a variant with the same name already exists it is force-deleted and rebuilt.
To read variant keys back, see the key, variantKey, and createVariant helpers covered in the Data model section, and use the model's variant accessors covered in the Working with files from your models section.
Variant options
Each variant definition accepts these keys. Per-variant values win, then the collection's types.image level, then the global defaults.image level, then a hardcoded fallback.
| Option | Type | Default | Meaning |
|---|---|---|---|
width |
int or null | null |
Target width. |
height |
int or null | null |
Target height. |
fit |
bool | false |
true crops to fill the box (cover). false scales down preserving aspect ratio without enlarging (scaleDown). |
quality |
int | 80 |
Encoder quality (0-100). |
format |
webp or jpg |
webp |
Output format. webp writes image/webp, jpg writes image/jpeg. |
watermark |
bool | false |
Bake the watermark into this variant. See below. |
Quality resolution is slightly special: a variant's own quality wins, otherwise types.image.variant_quality, then defaults.image.variant_quality (default 85), then the normal quality cascade.
Optimizing the original
The original is left untouched unless you opt in. OptimizeImageAction re-encodes a top-level image to WebP and downscales it to a maximum bounding box. It is a no-op for variants (variants are already generated optimized) and only runs when image.optimize_originals is true or the collection requests it.
// config/laracrate.php 'image' => [ 'driver' => 'imagick', // 'imagick' (recommended) or 'gd' 'optimize_originals' => false, 'max_width' => 1920, 'max_height' => 1920, 'quality' => 85, ],
The image.driver value selects the Intervention Image backend (imagick or gd) for all image work: optimization, variant generation, and watermarking. Image dimensions are captured separately by ExtractImageDimensionsAction and stored on width and height (it reads the binary with getimagesizefromstring and is a no-op once both are set).
Watermarks
A watermark is baked into the binary of specific variants at generation time. You opt in per variant with 'watermark' => true in that variant's definition. The original master file never carries a watermark, only the variants that explicitly ask for one.
// config/laracrate.php 'collections' => [ 'identity' => [ 'disk' => 'documents', 'access' => 'stream', 'types' => [ 'image' => [ 'variants' => [ 'thumbnail' => ['width' => 300], // no watermark 'display' => ['width' => 1200, 'watermark' => true], // watermarked ], ], ], ], ],
The watermark itself is configured once in the top-level watermark block. It can overlay a PNG, a text string, or both. If neither a PNG nor text is configured, applying the watermark is a no-op.
// config/laracrate.php 'watermark' => [ // PNG to overlay. Absolute path, or relative to public_path(). null skips the image. 'image_path' => env('LARACRATE_WATERMARK_IMAGE', null), // Width as a fraction of the variant width (0.0 - 1.0). 0.40 = 40% of the width. 'size' => 0.40, // PNG opacity (0-100). 'opacity' => 30, // 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'. 'position' => 'center', 'text' => [ // null = no text, a string = fixed text, or a closure(File): ?string for dynamic text. 'content' => null, // Font size as a fraction of the image width. 'font_size_ratio' => 0.0195, // CSS rgba color. 'color' => 'rgba(255, 255, 255, 0.60)', // 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'. 'position' => 'bottom-left', // Padding from the edge, in pixels. 'padding' => 20, // Path to a .ttf font. null uses the system font. 'font_path' => null, ], ],
The text content closure receives the File and may return null to skip the text, which is useful for stamping per-file data (an owner id, an order number) onto the watermark. Because a closure cannot live in env, set it by publishing the config or in a service provider.
Video and PDF previews
For non-image files Laracrate can generate an image preview so you have something to render in a gallery or list. Both video and PDF previews work the same way: the package produces a child File with variant = 'preview' and type = image, then optionally generates its own image variants (thumbnail, medium, and so on) from that preview. Both run in the pipeline and depend on external binaries, so they are opt-in per collection.
Video
Video handling has two independent steps. Dimensions and duration are read with ffprobe (ExtractVideoDimensionsAction, stored on width, height, duration). Preview frame extraction (ExtractVideoPreviewAction) pulls a single frame with ffmpeg at the configured timestamp and writes it as a JPEG preview. Both require ffmpeg and ffprobe on the server path.
// config/laracrate.php 'collections' => [ 'gallery' => [ 'disk' => 'media', 'access' => 'public', 'types' => [ 'video' => [ 'preview' => [ 'frame_at' => '00:00:01', // timestamp of the frame to grab 'variants' => [ 'thumbnail' => ['width' => 300, 'height' => 300, 'fit' => true], 'medium' => ['width' => 800, 'height' => 800], ], ], ], ], ], ],
frame_at defaults to 00:00:01 when not set. The extracted frame becomes a preview child of type image, and the variants you list under preview are generated from it (each one is in turn a child of the preview), so a video ends up with a preview frame plus its own thumbnail and medium.
Transcoding is separate and costly, so enable it only on collections that need it. TranscodeVideoAction re-encodes the original to H.264 / AAC MP4 with ffmpeg, scaling it down to fit a bounding box, and replaces the original binary in place (then re-reads dimensions). It runs when the collection declares transcode => true.
// config/laracrate.php 'video' => [ 'max_width' => 1920, 'max_height' => 1920, 'bitrate_kbps' => 2500, ],
ExtractPdfPreviewAction rasterizes one page of a PDF (page 1 by default) to a PNG and stores it as a preview child of type image. As with video, any variants you declare under preview are generated from that PNG (a thumbnail and a medium of page 1).
// config/laracrate.php 'collections' => [ 'documents' => [ 'disk' => 'documents', 'access' => 'signed', 'types' => [ 'document' => [ 'preview' => [ 'page' => 1, 'width' => 2000, 'variants' => [ 'thumbnail' => ['width' => 300], 'medium' => ['width' => 800], ], ], ], ], ], ],
The rasterization engine is selectable with pdf_preview_engine (or a per-collection engine key inside the preview block):
| Engine | Requires | Notes |
|---|---|---|
pdftoppm |
poppler-utils (pdftoppm binary) |
No Ghostscript and no ImageMagick policy.xml changes. Downscaling falls back to GD. |
imagick |
PHP imagick extension, Ghostscript (gs), and PDF enabled in ImageMagick's policy.xml |
Reads the PDF through Ghostscript. |
auto |
either of the above | Tries pdftoppm first, falls back to imagick if it is not available. |
// config/laracrate.php 'pdf_preview_engine' => 'auto',
When the engine is forced to pdftoppm and the binary is missing or fails, the action logs and returns null rather than falling back. Set the engine per collection when one disk or document type needs a specific renderer:
'preview' => ['page' => 1, 'width' => 600, 'engine' => 'pdftoppm'],