yilanboy / preview
Generate the preview image
Requires
- php: ^8.4
- ext-fileinfo: *
- ext-gd: *
Requires (Dev)
- laravel/pint: ^1.29
- pestphp/pest: ^4.0.0
- phpstan/phpstan: ^2.1
This package is auto-updated.
Last update: 2026-06-30 04:53:57 UTC
README
A simple package to generate a preview image.
Installation
Install the package with composer.
composer require yilanboy/preview
Then create an image generator.
use Yilanboy\Preview\Canvas\Solid; use Yilanboy\Preview\Canvas\Enums\Margin; use Yilanboy\Preview\Canvas\Enums\Format; use Yilanboy\Preview\Canvas\Enums\Size; use Yilanboy\Preview\Generator; use Yilanboy\Preview\Text\Enums\Font; use Yilanboy\Preview\Text\Enums\FontSize; use Yilanboy\Preview\Text\TextBlock; new Generator() ->size(Size::OpenGraph) ->margin(Margin::Medium) ->format(Format::PNG) ->background(new Solid('#777bb3')) ->title(new TextBlock( text: 'Preview', color: 'white', fontSize: FontSize::Large, font: Font::Inter, )) ->description(new TextBlock( text: 'A simple PHP package to create preview image', color: 'white', fontSize: FontSize::Small, font: Font::Inter, )) ->output();
This code will display the following image on the web page.
Examples
The most common use for this package is generating an Open Graph image — the
preview card shown when a link is shared on social media. Instead of designing one by hand for every
blog post, generate it from the post's own title and description. (Generator defaults to
Size::OpenGraph, so the dimensions are already right.)
Laravel
Serve it on the fly. Point a route at each post and render the image straight to the response:
use App\Models\Post; use Yilanboy\Preview\Canvas\Enums\Format; use Yilanboy\Preview\Canvas\Gradient; use Yilanboy\Preview\Generator; use Yilanboy\Preview\Text\Enums\FontSize; use Yilanboy\Preview\Text\TextBlock; Route::get('/posts/{post}/og.png', function (Post $post) { $format = Format::PNG; $image = new Generator() ->format($format) ->background(new Gradient(from: '#1e3a8a', to: '#9333ea')) ->title(new TextBlock(text: $post->title, color: 'white', fontSize: FontSize::Medium)) ->bytes(); return response($image, 200, [ 'Content-Type' => $format->mimeType(), ]); })->name('posts.og');
Then reference the route in your page's <head> so social platforms pick it up:
<meta property="og:image" content="{{ route('posts.og', $post) }}">
Or generate it once, ahead of time. If you'd rather not render on every request, build the image when a post is published and save it to disk, then serve the static file:
$image = new Generator() ->background(new Gradient(from: '#1e3a8a', to: '#9333ea')) ->title(new TextBlock(text: $post->title, color: 'white', fontSize: FontSize::Medium)) ->bytes(); Storage::put("images/posts/{$post->id}/og.png", $image);
Canvas
Size
Pick a preset that matches where the image will be embedded. Generator defaults to Size::OpenGraph.
use Yilanboy\Preview\Canvas\Enums\Size; $generator->size(Size::Square);
| Preset | Dimensions | Where it's used |
|---|---|---|
OpenGraph |
1200 × 630 | Facebook, generic Open Graph previews |
Square |
1080 × 1080 | Instagram, LinkedIn square posts |
Landscape |
1920 × 1080 | 16:9 landscape, slide / hero images |
Portrait |
1080 × 1920 | 9:16 vertical, stories and reels |
YouTube |
1280 × 720 | YouTube video thumbnails |
If no preset fits, set the width and height yourself. Both must be at least 1, otherwise an
InvalidInput exception is thrown. size() and dimensions() set the same canvas size, so the last call wins.
$generator->dimensions(width: 800, height: 418);
Margin
Text is inset from the canvas edges by a fixed pixel margin. Generator defaults to Margin::Medium (60px).
use Yilanboy\Preview\Canvas\Enums\Margin; $generator->margin(Margin::Large);
| Preset | Pixels |
|---|---|
None |
0 |
Small |
30 |
Medium |
60 |
Large |
90 |
ExtraLarge |
120 |
Backgrounds
Generator::background() accepts anything implementing the Background interface. Three implementations ship with the
package.
Solid — a flat color.
use Yilanboy\Preview\Canvas\Solid; $generator->background(new Solid('#777bb3'));
Gradient — two colors interpolated across the canvas.
use Yilanboy\Preview\Canvas\Gradient; use Yilanboy\Preview\Canvas\Enums\GradientDirection; $generator->background(new Gradient( from: '#1e3a8a', to: '#9333ea', direction: GradientDirection::Diagonal, ));
GradientDirection cases: Vertical (default) · Horizontal · Diagonal.
Image — render a bitmap behind your text.
use Yilanboy\Preview\Canvas\Image; use Yilanboy\Preview\Canvas\Enums\ImageFit; $generator->background(new Image( path: __DIR__.'/cover.jpg', fit: ImageFit::Cover, opacity: 0.6, tint: '#000000', ));
ImageFit cases: Cover (default) · Contain · Stretch · Tile.
opacity is a float between 0.0 and 1.0 (default 1.0). When opacity < 1.0, the canvas is filled with tint
first so the tint color shows through the partially transparent image — use it to darken or wash the background. tint
defaults to #000000.
See all three modes interactively in the playground (next section).
Text
TextBlock
TextBlock is a final readonly class, so it's immutable. Every constructor
argument is named and defaulted — to vary a field, construct a new instance:
$base = new TextBlock(text: 'Hello'); $red = new TextBlock(text: 'Hello', color: 'red'); $big = new TextBlock(text: 'Hello', fontSize: FontSize::Huge);
If no FontSize preset fits, pass a custom size in pixels instead. It must be at least 1, otherwise an
InvalidInput exception is thrown.
$custom = new TextBlock(text: 'Hello', fontSize: 42);
Available customization enums live under Yilanboy\Preview\Text\Enums:
| Enum | Cases |
|---|---|
Font |
Inter · InterMedium · Roboto · RobotoMedium · JetBrainsMono · JetBrainsMonoMedium · NotoSans · NotoSansMedium · NotoSansSC · NotoSansSCMedium · NotoSansTC · NotoSansTCMedium · NotoSansJP · NotoSansJPMedium |
FontSize |
ExtraSmall (24) · Small (32) · Medium (50) · Large (64) · ExtraLarge (80) · Huge (100) |
Alignment |
Left · Center · Right |
LineHeight |
Snug (1.15) · Normal (1.3) · Relaxed (1.5) · Loose (1.75) |
All 14 bundled fonts are static TrueType instances shipped under SIL OFL, with separate files per weight because GD
cannot select a weight from a variable font: each family ships a Regular (400) and a Medium (500) variant (the *Medium
cases). Inter is the 24pt optical cut. NotoSansTC covers Latin + Traditional Chinese, NotoSansSC covers Latin +
Simplified Chinese, and NotoSansJP covers Latin + Japanese; NotoSans, Inter, Roboto, and JetBrainsMono (a
monospaced family) are Latin-only.
Currently, the text supports English, Chinese (Traditional and Simplified), and Japanese.
Alignment controls how each line is positioned horizontally within the margins. TextBlock defaults to
Alignment::Left.
use Yilanboy\Preview\Text\Enums\Alignment; use Yilanboy\Preview\Text\TextBlock; $generator->title(new TextBlock( text: 'Preview', alignment: Alignment::Center, ));
Line Height
When text wraps to multiple lines, LineHeight controls the spacing between them. The value is a unit-less multiplier
of the text's font size (CSS line-height semantics). TextBlock defaults to LineHeight::Normal (1.3×).
use Yilanboy\Preview\Text\Enums\LineHeight; use Yilanboy\Preview\Text\TextBlock; $generator->description(new TextBlock( text: 'A longer description that wraps to multiple lines for demonstration purposes.', lineHeight: LineHeight::Loose, ));
| Preset | Multiplier |
|---|---|
Snug |
1.15× |
Normal |
1.3× |
Relaxed |
1.5× |
Loose |
1.75× |
Custom Fonts
The font argument also accepts a filesystem path to your own font file, instead of a bundled Font case.
use Yilanboy\Preview\Text\TextBlock; new TextBlock( text: 'Hello', font: __DIR__.'/fonts/MyFont.ttf', );
Only TrueType (.ttf) files are supported. OpenType (.otf) is rejected. A path is accepted only when all of
the following hold:
- the file exists and is readable;
- the extension is
.ttf(case-insensitive); - the file's first 4 bytes are the TrueType
sfntheader (0x00010000) — this is what rejects an.otfrenamed to.ttf, whose header isOTTO.
If the path is not a valid TrueType font, the constructor throws an InvalidInput exception with the message
The font path is not a valid TrueType font file. Validation runs in the constructor, so an invalid TextBlock can
never exist — construction fails fast.
Security: the font path is read straight off disk and is treated as trusted input. It must come from you, the developer — never from unsanitised end-user input, which would enable arbitrary file reads and file-existence probing.
Output
There are three ways to produce the final image:
output()renders it, sets the HTTPContent-Typeheader from the format's MIME type, and writes the bytes straight to the response. This is convenient for plain PHP scripts; in frameworks like Laravel or Symfony, preferbytes()and return a framework response instead.save($path)renders it and writes the bytes to a file.bytes()renders it and returns the encoded bytes as a string, which is useful for framework responses or object storage.
$generator->output(); // serve in the HTTP response $generator->save('preview.png'); // write to a file $image = $generator->bytes(); // return encoded image bytes
format() selects the encoding. Format has three cases — PNG (default), JPEG, and WEBP.
use Yilanboy\Preview\Canvas\Enums\Format; $generator->format(Format::JPEG);
Note: the format set via
format()always wins —save()does not look at the file extension. The configured format is the single source of truth, soformat(Format::JPEG)->save('preview.png')writes JPEG bytes into a file namedpreview.png. Keep the extension and format in sync yourself.
Exceptions
Everything the library throws lives under Yilanboy\Preview\Exceptions. Invalid input (bad colors, font paths,
dimensions, etc.) throws InvalidInput, and render-time GD failures throw RenderFailure. Both implement the
PreviewException marker interface and still extend their SPL parents (InvalidArgumentException /
RuntimeException), so existing catch blocks keep working.
use Yilanboy\Preview\Exceptions\PreviewException; try { $generator->save($path); } catch (PreviewException $e) { // anything this library threw }
Start a Local Server to Show the Image
There is a playground.php file in examples folder. Start a local server to open an interactive form where you can
edit the title, description, font, and font size, switch between Solid / Gradient / Image backgrounds, preview
gradients live via CSS, and tweak image opacity and tint with sliders.
php -S localhost:8000 examples/playground.php
Then open your browser and visit localhost:8000.
Development
Run tests:
composer tests
Format code with Pint (only dirty files):
composer fmt
Run all checks (Pint + PHPStan):
composer check
Snapshot Testing
The image tests use snapshot testing: each test generates a PNG and compares it against a stored reference image in
tests/Fixtures/. The comparison is tolerant rather than pixel-exact — it ignores anti-aliasing noise that differs
between macOS and Linux FreeType builds, while still catching real rendering changes (different text, color, or layout).
When you make an intentional change to rendering output, regenerate the fixtures by setting the UPDATE_SNAPSHOTS
environment variable (any non-empty value other than 0 works):
UPDATE_SNAPSHOTS=1 composer tests
Each snapshot test detects the variable and overwrites its fixture in tests/Fixtures/ with the freshly generated
image. Always open the regenerated PNGs and confirm they look correct before committing — once a fixture is updated,
it becomes the new source of truth.
