Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
HTML to image rendering for PHP, powered by Rust and [Takumi](https://github.com/kane50613/takumi).
Generate Open Graph images and more from HTML/CSS without headless browser overhead.

<img src="./examples/htmlshot/output/htmlshot.png" width="480px">


## Features

- **No headless browser.** Rendering runs in-process through a native Rust library, so there is no Chromium to install, launch, or keep alive.
Expand Down Expand Up @@ -45,12 +48,18 @@ vendor/bin/htmlshot update
```

> [!NOTE]
> A natives.lock file is added to the root of the project. Commit this file to your repo to ensure that each time you'll get the same native version.
> A `natives.lock` file is added to the root of the project. Commit this file to your repo to ensure that each time you'll get the same native version.


## Documentation

- [Getting started](docs/getting-started.md) — install and render your first image
- [Fonts & styles](docs/fonts-and-styles.md) — custom fonts, inline/`<style>`/external CSS, and `tw` utilities
- [Advanced usage](docs/advanced.md) — HiDPI output and using `Context` + `Renderer` directly
- [Examples](examples/README.md) — runnable Open Graph and social-card scripts


## Quick Example
## Quick start

The fastest way in is the `HtmlShot` façade. Give it some HTML and a few
options, and it hands back the encoded image bytes:
Expand Down Expand Up @@ -81,6 +90,12 @@ Available options: `width`, `height`, `format` (`png` | `webp` | `jpeg`),
`quality` (1–100 for the lossy formats), `stylesheets` (extra CSS),
`fonts`, and `devicePixelRatio` (e.g. `2.0` for Retina output).


> [!NOTE]
> See the [`examples/`](examples/) directory for deep dive with fonts, custom styles, images and high resolution.



### Reusing a context

The façade loads fonts on every call. When you render repeatedly, build a
Expand Down Expand Up @@ -110,6 +125,7 @@ foreach ($posts as $post) {
```



## Deep Dive

### How HTML & CSS are handled
Expand Down
15 changes: 15 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Documentation

Guides for rendering HTML to images with `html-shot`.

- **[Getting started](getting-started.md)** — install the package and the native
library, then render your first image.
- **[Fonts & styles](fonts-and-styles.md)** — load custom fonts and style your
markup with inline CSS, `<style>` blocks, external stylesheets, or `tw`
utilities.
- **[Advanced usage](advanced.md)** — HiDPI / Retina output, and driving a
`Context` + `Renderer` directly for repeated rendering.

For complete, runnable scripts see the [`examples/`](../examples/) directory and
its [README](../examples/README.md). The main [project README](../README.md) has
a "Deep Dive" section describing how the engine interprets HTML and CSS.
156 changes: 156 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Advanced usage

## HiDPI / Retina output

`width` and `height` describe the logical canvas: the coordinate space your CSS
lays out in. The `devicePixelRatio` option scales the physical bitmap without
touching that layout, so a 1200×630 layout at `devicePixelRatio: 2.0` produces a
2400×1260 image, pixel-doubled and crisp on Retina displays.

```php
use HtmlShot\HtmlShot;

// 1× — 1200×630 physical pixels
$png = HtmlShot::render($html, [
'width' => 1200,
'height' => 630,
'format' => 'png',
]);

// 2× — same layout, 2400×1260 physical pixels
$retina = HtmlShot::render($html, [
'width' => 1200,
'height' => 630,
'format' => 'webp',
'devicePixelRatio' => 2.0,
]);
```

Because the layout is unchanged, font sizes, padding, and positions stay
identical between the 1× and 2× renders; only the output resolution differs.
This is the usual way to ship both a normal and an `@2x` asset from one
template. The [`banner`](../examples/banner/banner.php) example emits both.

### Choosing a format and quality

`format` accepts `'png'`, `'webp'`, or `'jpeg'`. For the lossy formats, set
`quality` from 1–100 (`0` uses the library default):

```php
$jpeg = HtmlShot::render($html, [
'width' => 1200,
'height' => 630,
'format' => 'jpeg',
'quality' => 82,
]);
```

PNG is lossless and ignores `quality`. WebP at a high quality is usually the
best size/clarity trade-off for HiDPI assets.

## Using Context and Renderer directly

The `HtmlShot` façade builds a fresh `Context` and reloads every font on each
call. When you render repeatedly (a batch of cards, or an HTTP endpoint serving
many images), build the `Context` once, load the fonts a single time, and drive
a `Renderer` directly.

```php
use HtmlShot\Context;
use HtmlShot\Renderer;

$context = new Context;
$context->loadFontFile('/fonts/Inter-Regular.ttf', family: 'Inter', weight: 400);
$context->loadFontFile('/fonts/Inter-Bold.ttf', family: 'Inter', weight: 700);

$renderer = new Renderer($context);

foreach ($posts as $post) {
$html = renderTemplate($post); // your own HTML builder

// 1× PNG
file_put_contents("{$post->slug}.png", $renderer->render($html, 1200, 630));

// 2× WebP for HiDPI displays
$webp = $renderer->render($html, 1200, 630, 'webp', devicePixelRatio: 2.0);
file_put_contents("{$post->slug}@2x.webp", $webp);
}
```

The fonts are parsed once and reused across every `render()` call, which is the
main reason to prefer this path over the façade in a loop or a long-running
process.

### `Renderer::render()` signature

```php
$renderer->render(
string $html,
int $width = 1200,
int $height = 628,
string $format = 'png',
int $quality = 0,
array $stylesheets = [],
float $devicePixelRatio = 1.0,
float $baseFontSize = 16.0,
): string;
```

Using named arguments keeps calls readable when you only need a couple of the
options:

```php
$bytes = $renderer->render(
$html,
width: 1200,
height: 630,
stylesheets: [$css],
devicePixelRatio: 2.0,
);
```

`baseFontSize` is the root font size in pixels used to resolve `rem` and the
initial `em`. It's the equivalent of the browser's `<html>` font-size, 16px by
default. Raise it to scale a whole `rem`-based template at once.

### Loading fonts onto a Context

A `Context` exposes the same loaders as the `Font` helper:

```php
// From a file
$context->loadFontFile('/fonts/Inter-Bold.ttf', family: 'Inter', weight: 700);

// From raw bytes
$context->loadFontData(file_get_contents('/fonts/Inter-Bold.ttf'), family: 'Inter', weight: 700);
```

You can also build `Font` descriptors and load them in:

```php
use HtmlShot\Font;

Font::fromFile('/fonts/Inter-Bold.ttf', family: 'Inter', weight: 700)->loadInto($context);
```

## Error handling

Render and font-loading failures throw `HtmlShot\Exception\RuntimeException`.
Wrap calls that depend on external input (user-supplied HTML, fonts read from
disk) so a single bad render doesn't take down the request:

```php
use HtmlShot\Exception\RuntimeException;

try {
$png = $renderer->render($html, 1200, 630);
} catch (RuntimeException $e) {
// log and fall back to a static placeholder
}
```

## See also

- [Getting started](getting-started.md)
- [Fonts & styles](fonts-and-styles.md)
- [`examples/`](../examples/)
155 changes: 155 additions & 0 deletions docs/fonts-and-styles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Fonts & styles

## Custom fonts

`html-shot` ships no fonts. Any text you render needs a font that covers it, so
load your own with the `Font` helper and pass them in the `fonts` option. The
`family` you give here is the name you then reference from CSS `font-family`.

```php
use HtmlShot\Font;
use HtmlShot\HtmlShot;

$png = HtmlShot::render(
'<div style="font-family: Inter; font-size: 64px; font-weight: 700;">Hello</div>',
[
'width' => 1200,
'height' => 630,
'fonts' => [
Font::fromFile('/fonts/Inter-Regular.ttf', family: 'Inter', weight: 400),
Font::fromFile('/fonts/Inter-Bold.ttf', family: 'Inter', weight: 700),
],
],
);
```

Load one descriptor per weight/style you intend to use, all under the same
`family`; the engine picks the right file based on the CSS `font-weight` and
`font-style` at render time.

### From a file or from bytes

```php
// From a path on disk
Font::fromFile('/fonts/Inter-Regular.ttf', family: 'Inter', weight: 400);

// From raw bytes, handy when the font lives in a database, cache, or archive
Font::fromData(file_get_contents('/fonts/Inter-Regular.ttf'), family: 'Inter', weight: 400);
```

Both accept `family`, `weight`, and `style` overrides:

| Argument | Default | Notes |
| --- | --- | --- |
| `family` | `''` | Override the family name; empty auto-detects from the font metadata |
| `weight` | `0` | Override weight 1–1000; `0` auto-detects |
| `style` | `''` | `'normal'`, `'italic'`, or `'oblique'`; empty auto-detects |

Supported file formats are TTF, OTF, WOFF, and WOFF2.

### Variable fonts

A single variable font file covers a range of weights. Register it once and use
any weight from its axis. The examples in this repo do exactly that with Geist:

```php
Font::fromFile(__DIR__.'/tests/fonts/Geist/variable/Geist[wght].ttf', family: 'Geist');
```

```html
<h1 style="font-family: Geist; font-weight: 800;">Bold</h1>
<p style="font-family: Geist; font-weight: 400;">Regular</p>
```

### Multiple families in one render

Register as many families as you need and switch between them in CSS:

```php
'fonts' => [
Font::fromFile($base.'/Geist/variable/Geist[wght].ttf', family: 'Geist'),
Font::fromFile($base.'/GeistMono/variable/GeistMono[wght].ttf', family: 'Geist Mono'),
],
```

```html
<span style="font-family: 'Geist Mono';">vercel/</span>
<span style="font-family: Geist;">satori</span>
```

## Styling your markup

Layout and appearance are driven entirely by CSS. There are four ways to apply
it, and they can be combined.

### 1. Inline `style` attributes

The most direct option, good for self-contained snippets:

```html
<div style="display:flex; align-items:center; justify-content:center;
width:100%; height:100%; background:#09090b; color:#fff;">
Hello
</div>
```

### 2. `<style>` blocks

A `<style>` tag inside the document is pulled out and applied as a stylesheet,
so class selectors work as expected:

```html
<style>
.card { display:flex; width:100%; height:100%; background:#0b1020; color:#fff; }
</style>
<div class="card">Hello</div>
```

### 3. External stylesheets (`stylesheets` option)

Keep styling out of the markup entirely by passing CSS through the
`stylesheets` option. This works well when your CSS lives in its own file: the
HTML carries only semantic class names.

```php
$css = file_get_contents(__DIR__.'/styles.css');

$png = HtmlShot::render('<div class="card">Hello</div>', [
'width' => 1200,
'height' => 630,
'stylesheets' => [$css], // pass any number of sheets
]);
```

The array can hold several sheets; they are applied on top of the document just
like linked stylesheets in a browser. See the
[`event-card`](../examples/event-card/) example for a complete walkthrough.

### 4. `tw` Tailwind utilities

Takumi's engine understands a `tw` attribute carrying Tailwind utility classes,
so you can lay out a card without writing any CSS:

```html
<div tw="flex h-full w-full flex-col justify-end bg-[#16130f] p-20">
<h1 tw="m-0 text-8xl font-bold tracking-tighter text-white">HtmlShot.</h1>
<p tw="mt-10 text-3xl text-[#a8a29a]">html in, image out</p>
</div>
```

Arbitrary values (`bg-[#16130f]`, `text-[12em]`) are supported. The
[`tailwind`](../examples/tailwind/) and [`stars`](../examples/stars/) examples
use this approach.

## Images and SVG

Both `<img src="...">` and CSS `background-image: url(...)` accept local file
paths and `data:` URIs; `http(s)` URLs are passed through to the engine. Inline
`<svg>` is rasterized as an image, so vector graphics render without a separate
conversion step. The [main README's "Deep Dive"](../README.md#deep-dive) covers
the exact handling.

## Next steps

- [Advanced usage](advanced.md) — HiDPI output and reusing a `Context` +
`Renderer` for repeated rendering.
Loading
Loading