PHPackages                             yilanboy/preview - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Image &amp; Media](/categories/media)
4. /
5. yilanboy/preview

ActiveLibrary[Image &amp; Media](/categories/media)

yilanboy/preview
================

Generate the preview image

2.3.0(1mo ago)4113MITPHPPHP ^8.4CI passing

Since Dec 21Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/yilanboy/preview)[ Packagist](https://packagist.org/packages/yilanboy/preview)[ RSS](/packages/yilanboy-preview/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (7)Versions (19)Used By (0)

Preview
=======

[](#preview)

A simple package to generate a preview image.

Installation
------------

[](#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.

[![preview](images/preview.png)](images/preview.png)

Examples
--------

[](#examples)

The most common use for this package is generating an [Open Graph](https://ogp.me) 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

[](#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 `` so social platforms pick it up:

```

```

**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
------

[](#canvas)

### Size

[](#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);
```

PresetDimensionsWhere it's used`OpenGraph`1200 × 630Facebook, generic Open Graph previews`Square`1080 × 1080Instagram, LinkedIn square posts`Landscape`1920 × 108016:9 landscape, slide / hero images`Portrait`1080 × 19209:16 vertical, stories and reels`YouTube`1280 × 720YouTube video thumbnailsIf 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

[](#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);
```

PresetPixels`None`0`Small`30`Medium`60`Large`90`ExtraLarge`120### Backgrounds

[](#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
----

[](#text)

### TextBlock

[](#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`:

EnumCases`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

[](#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,
));
```

PresetMultiplier`Snug`1.15×`Normal`1.3×`Relaxed`1.5×`Loose`1.75×### Custom Fonts

[](#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 `sfnt` header (`0x00010000`) — this is what rejects an `.otf` renamed to `.ttf`, whose header is `OTTO`.

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
------

[](#output)

There are three ways to produce the final image:

- `output()` renders it, sets the HTTP `Content-Type` header 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, prefer `bytes()` 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);
```

`quality()` changes the output quality for lossy formats (`JPEG` and `WEBP`). It accepts values from `0` to `100`.

```
$generator
    ->format(Format::WEBP)
    ->quality(85);
```

> **Note:** `quality()` does not apply to `PNG`. PNG compression is lossless, so changing compression affects encoding speed and file size, not visual image quality.

> **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, so `format(Format::JPEG)->save('preview.png')` writes JPEG bytes into a file named `preview.png`. Keep the extension and format in sync yourself.

Exceptions
----------

[](#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
--------------------------------------

[](#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](http://localhost:8000).

Development
-----------

[](#development)

Run tests:

```
composer tests
```

Format code with Pint (only dirty files):

```
composer fmt
```

Run all checks (Pint + PHPStan):

```
composer check
```

### Snapshot Testing

[](#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.

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance92

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity65

Established project with proven stability

 Bus Factor1

Top contributor holds 97% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~57 days

Recently: every ~7 days

Total

11

Last Release

35d ago

Major Versions

1.0.1 → 2.0.0-beta.12026-06-09

PHP version history (2 changes)1.0.0PHP ^8.2

2.0.0-beta.1PHP ^8.4

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/27554321?v=4)[Allen](/maintainers/yilanboy)[@yilanboy](https://github.com/yilanboy)

---

Top Contributors

[![yilanboy](https://avatars.githubusercontent.com/u/27554321?v=4)](https://github.com/yilanboy "yilanboy (96 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")

---

Tags

imagepreview

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/yilanboy-preview/health.svg)

```
[![Health](https://phpackages.com/badges/yilanboy-preview/health.svg)](https://phpackages.com/packages/yilanboy-preview)
```

###  Alternatives

[intervention/image

PHP Image Processing

14.4k214.2M2.8k](/packages/intervention-image)[league/glide

Wonderfully easy on-demand image manipulation library with an HTTP based API.

2.6k54.0M162](/packages/league-glide)[liip/imagine-bundle

This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.

1.7k40.6M269](/packages/liip-imagine-bundle)[spatie/image

Manipulate images with an expressive API

1.4k63.1M206](/packages/spatie-image)[intervention/image-laravel

Laravel Integration of Intervention Image

1589.8M226](/packages/intervention-image-laravel)[intervention/gif

PHP GIF Encoder/Decoder

6131.5M16](/packages/intervention-gif)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
