jbtronics/dompdf-font-loader-bundle
A Symfony bundle that simplifies using custom fonts with Dompdf. Configure your font directories or assets and it automatically registers them, so generated PDFs can use custom fonts without manual font installation or @font-face setup.
Read this first: this package is a Symfony bundle, not a Laravel package. It will install via Composer, but nothing in Laravel will auto-discover or boot it. There is no service provider, no config publish tag, no facade. Your job as a Laravel developer is to decide whether you want (a) the underlying idea — pre-generating dompdf font metrics during a build/deploy step — or (b) the actual code inside the bundle, wired manually.
Given the package has 2 stars, 0 dependents, and a single maintainer, treat it as a reference implementation of a pattern rather than a dependency you install and forget.
dompdf needs .ufm/.ufm + .php metric files for every custom TTF before it can measure text. FontMetrics::registerFont() parses the TTF at runtime, which is slow (tens to hundreds of ms per face) and happens on the first PDF render in every process — including every queue worker and every FPM child. This bundle's contribution is: declare your font files in config, then hook cache:warmup so the metric files are generated once, ahead of time, into dompdf's font cache directory.
That's the entire value proposition. In Laravel terms, it's "move font registration out of the request/worker and into php artisan."
composer require jbtronics/dompdf-font-loader-bundle
Then read, in this order, before writing any code:
src/DependencyInjection/Configuration.php — the exact config tree (font family → file path → style/weight). Do not guess the keys.Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface) — this is the code you'll be re-implementing as an Artisan command.src/ — this is the class you'll actually bind in a service provider.README.md for the config sample you'll translate.You have a branded invoice PDF. dompdf currently renders it in Helvetica because your @font-face/font-family: Inter silently falls back. You want Inter (regular + bold) embedded and measured correctly, without paying TTF parsing on each render.
Steps:
resources/fonts/inter/Inter-Regular.ttf and Inter-Bold.ttf.storage/fonts or storage/app/dompdf-fonts) and make sure it's writable and persistent — it is a cache, but regenerating it is expensive.font-family: 'Inter', sans-serif;.The critical constraint: the warmup step and the render step must agree on the font cache directory and the font file paths. Path is part of the cache key in dompdf's metrics index. A path that differs between your deploy container and your web container is the #1 failure mode.
If you're on barryvdh/laravel-dompdf, you already have config/dompdf.php with font_dir, font_cache, and chroot. Set all three deliberately, then treat the bundle as documentation for how to populate font_cache.
If you want the bundle's loader logic, bind it yourself in a provider:
// app/Providers/DompdfFontServiceProvider.php
public function register(): void
{
$this->app->singleton(DompdfFontLoader::class, function ($app) {
return new DompdfFontLoader(
fontDir: resource_path('fonts'),
// confirm the real constructor signature against src/
);
});
}
Confirm the constructor against src/ — Symfony bundles often take a ContainerInterface or an iterable $fonts tag-collection, neither of which exists in Laravel. If the constructor expects tagged services, pass a plain array.
This is the pattern most worth copying, and it's ~20 lines:
class WarmDompdfFonts extends Command
{
protected $signature = 'dompdf:fonts:warm';
public function handle(): int
{
$metrics = (new FontMetrics(config('dompdf.font_cache')));
$metrics->setFontDir(config('dompdf.font_dir'));
foreach (config('dompdf.custom_fonts', []) as $family => $faces) {
foreach ($faces as $style => $path) {
$metrics->registerFont(
[$family, $style], // e.g. ['Inter', 'bold']
$path,
);
}
}
$this->info('Dompdf font metrics warmed.');
return self::SUCCESS;
}
}
Run it in composer.json's post-install-cmd/post-deploy, or as a discrete deploy step — not in a php artisan migrate-adjacent hook that might run on every node concurrently against a shared volume.
Treat warmup as a build artifact. In CI:
composer install --no-dev
php artisan dompdf:fonts:warm # writes into storage/fonts
Then ship storage/fonts into the image, or run the warm step once per release on a single node before draining traffic. Avoid warming in a queue worker's boot() — you'll get a thundering herd of file writes on the same .ufm paths.
If you can't control the deploy step (shared hosting, Heroku-ish ephemeral filesystems), fall back to registering fonts once per process via a memoized singleton, and accept the first-request cost:
$this->app->singleton('dompdf.fonts.registered', function () { /* register */ return true; });
Do this before $pdf->loadHTML(), never after — metrics are consumed during layout.
Map config keys to exactly what appears in your CSS. font-family: 'Inter' must match the registered family and the @font-face font-family value. If your CSS says Inter, sans-serif and metrics are registered under inter, you get a silent fallback with correct-looking output — the worst kind of bug.
Assert on the output, not on file existence: render a small HTML fixture with a custom glyph (e.g. an em-dash and a €) and check $pdf->output() contains the font name in the embedded font list. A metrics file existing does not mean dompdf is using it.
bundles.php registration, and cache:warmup hook have no equivalent. Everything is manual. Budget for that up front..otf with CFF outlines, .woff, and .woff2 will fail or silently misbehave. Convert to TTF with fonttools/ttfautohint first. Variable fonts are not reliably supported — instantiate static weights.font_cache (dompdf 2.x) or the fontDir option.resources/fonts/Inter-Regular.ttf and /var/www/html/resources/fonts/Inter-Regular.ttf produce different cache entries. Normalize to absolute paths inside the command and never interpolate base_path() inconsistently between environments.chroot will bite you. dompdf's chroot restricts file access. If fonts live outside it, registration fails with a confusing "file not found"-ish error. Either put fonts inside chroot or widen it — widening is a security tradeoff, so prefer the former.['Inter','bold'] and ['Inter','normal'] are distinct entries; registering only regular gives you faux-bold or a fallback.max_execution_time: 30 web request is how you get 500s that only appear on the first request after deploy. This is exactly what the warmup step exists to prevent — don't skip it.app/Support/ is a defensible choice for a production Laravel app — MIT license permits it, and you lose nothing but the Symfony wiring you weren't using anyway.FontMetrics. Once you understand registerFont([$family, $style], $path) and where font_cache points, you no longer need this package — that's the takeaway.composer install on autoscaling groups. N nodes × concurrent write to the same .ufm is a race. Warm once, or warm into a per-node temp dir and let it be cold-start cost.How can I help you explore Laravel packages today?