phenx/php-svg-lib
A lightweight PHP library for reading, parsing, and rendering SVG files. It handles paths, basic shapes, gradients, text, styles, and CSS, and is best known as the SVG engine behind Dompdf, letting PHP applications embed vector graphics in generated PDFs without external dependencies.
Install it directly only if you need SVG parsing separate from a PDF engine — most projects get it transitively through dompdf/dompdf.
composer require phenx/php-svg-lib
Requires ext-dom and pulls in sabberworm/php-css-parser (for <style> blocks and style="") and phenx/php-font-lib (for text metrics). Read three files in this order, they are the whole mental model of the package:
src/Surface/SurfaceInterface.php — the complete contract of drawing operations the library can emit. Everything else is just a producer of these calls.src/Surface/CPdf.php — the reference implementation (used by Dompdf). If you are writing your own backend, port this file method by method.src/Document.php and src/Node/ — parsing, node tree, style resolution.First use case — render an SVG into a PDF and confirm the plumbing works:
use Svg\Document;
use Svg\Surface\CPdf; // bundled R&OS CPDF wrapper implementing SurfaceInterface
$doc = Document::load(__DIR__ . '/logo.svg');
// Only needed when the root uses percentages or has no units.
$doc->setWidth(200);
$doc->setHeight(100);
$surface = new CPdf([0, 0, 595, 842]); // verify constructor args in src/Surface/CPdf.php
$doc->render($surface);
file_put_contents('logo.pdf', $surface->output());
If that works, your next step is almost never "more features" — it's a logging surface (see below) so you can see exactly what the library asks of your backend.
Write your own SurfaceInterface to target a new backend. This is the intended extension point. Map the calls to your engine's primitives: save/restore → q/Q (or gsave/grestore), transform → cm, moveTo/lineTo/curveTo/rectangle → m/l/c/re, fill/stroke/clip → f/S/W n, setFillColor/setStrokeColor → rg/RG, setFillAlpha/setStrokeAlpha → an ExtGState you allocate lazily, setFont+text → BT /Fn Tf Td Tj ET. Keep it thin; all SVG semantics (units, transforms, inheritance) are already resolved before you're called.
Decorator-over-reimplementation for debugging. Don't re-implement the interface from scratch just to inspect it — wrap the CPdf surface and log every call with its arguments. You will immediately see which <path> emits garbage coordinates, whether a gradient degrades to a solid color, and whether save/restore are balanced.
final class TracingSurface implements \Svg\Surface\SurfaceInterface
{
public array $ops = [];
public function __construct(private \Svg\Surface\SurfaceInterface $inner) {}
public function moveTo($x, $y) { $this->ops[] = ['moveTo', $x, $y]; $this->inner->moveTo($x, $y); }
// ...forward the rest verbatim, appending to $this->ops
}
Parse once, render many. Document::load() runs DOM parsing, CSS parsing, and style cascade — order of magnitude more expensive than render(). For icon sheets, map pins, or invoice logos rendered per page, memoize the Document keyed by md5_file($svg) and only create a fresh surface per target.
Handle sizing yourself. The library emits user units; it does not scale to a viewport. For predictable output, either pass absolute width/height via setWidth()/setHeight(), or ignore them entirely and call $surface->save(); $surface->translate(...); $surface->scale(...); around render() with a transform you compute from viewBox. Rasterizing at 96 DPI and 300 DPI produces the same op stream — the scale factor is your job.
Sanitize CSS upstream, not downstream. Inline presentation attributes and style="" are reliable; complex selectors, @media, custom properties, and calc() are largely ignored (silently). If a designer's export renders wrong, run it through SVGO/svgo-style flattening first so every property becomes a presentation attribute on the element.
Integrating with Dompdf: don't touch this package at all. Dompdf's Dompdf\Adapter\CPDF implements the surface. Instead, control it via Dompdf options — chroot and remote-asset settings govern whether <image href="..."> inside the SVG resolves, and base64 data URIs are the most portable way to embed raster art. Treat the Composer constraint as owned by Dompdf, not by you.
SurfaceInterface grows across majors. A backend written against 0.5 can fatal on 1.x when a method is added. Pin with a caret constraint, add a test that instantiates your surface class, and re-read SurfaceInterface on every minor bump. Prefer a decorator over a from-scratch implementation so upgrades surface at compile time rather than at render time.viewBox, or a width="100%" root without setWidth() typically yields a blank or clipped page rather than an exception. Wrap loads in libxml_use_internal_errors(true) and check libxml_get_errors(); after render(), assert your surface recorded at least one fill or stroke.<image href> URLs — that's SSRF, plus potential local file disclosure if chroot-style restrictions aren't in place. Reject or rewrite external refs on upload; keep libxml_disable_entity_loader-era hardening in mind for old libxml.feGaussianBlur, feDropShadow) are not applied, mask and pattern support is weak, textPath is absent, text doesn't wrap or do full bidi, vector-effect and em/% font sizing are unreliable. Ask designers for SVGs without filters, or pre-flatten them with Inkscape/ImageMagick before they reach PHP.opacity ≠ fill-opacity. Combined element opacity requires a transparency group in the backend; many custom surfaces just multiply fill alpha, which looks close but differs where strokes overlap. If visual parity matters, test overlapping semi-transparent shapes specifically.getTextWidth() backed by actual font files (php-font-lib). If your PDF host lacks the font, sizes drift silently — embed fonts and test with the exact production font set.src/Surface/CPdf.php for how LinearGradient/RadialGradient are dispatched before promising gradient fidelity to anyone.How can I help you explore Laravel packages today?