Skip to content

SVG to PDF

Render SVG to PDF via the geometric painter. Output stays vector — paths, gradients, transforms map straight to PDF content-stream operators, with no intermediate raster.

Terminal window
composer require phpdftk/svg-to-pdf
use Phpdftk\Pdf\Writer\PdfWriter;
use Phpdftk\Svg\Parser as SvgParser;
use Phpdftk\SvgToPdf\SvgRenderer;
$svg = (new SvgParser())->parse(<<<'SVG'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" viewBox="0 0 200 100">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="red"/>
<stop offset="1" stop-color="blue"/>
</linearGradient>
</defs>
<rect width="200" height="100" fill="url(#g)"/>
<text x="100" y="60" text-anchor="middle" font-family="serif" font-size="24" fill="white">Hello</text>
</svg>
SVG);
$writer = new PdfWriter();
$page = $writer->addPage(595.0, 842.0);
$renderer = new SvgRenderer($page, $writer);
$renderer->draw($svg, x: 100, y: 100, width: 400, height: 200);
file_put_contents('out.pdf', $writer->toBytes());

Shapes — <rect> (with corner radii), <circle>, <ellipse>, <line>, <polyline>, <polygon>, <path> with the full SVG 2 §9.3.9 d-grammar (M, m, L, l, H, h, V, v, C, c, S, s, Q, q, T, t, A, a, Z, z). Path-data parser is byte-recovery-safe — malformed segments stop cleanly without infinite-looping (closes WPT path stress tests).

Paint — fill / stroke with CSS Color 3+4 syntax, fill-opacity / stroke-opacity / opacity (group). Stroke styling: stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit, stroke-dasharray, stroke-dashoffset. paint-order: stroke fill reordering. fill-rule: evenodd / nonzero.

Gradients — <linearGradient> and <radialGradient> with N-stop colour distributions mapped to PDF Type-3 stitching functions. gradientUnits (objectBoundingBox / userSpaceOnUse), gradientTransform, spreadMethod (pad / reflect / repeat). Cycle-safe xlink:href / href chaining — gradient inheritance through arbitrary chains without stack overflow.

Patterns — fill="url(#id)" referencing a <pattern> (SVG 2 §13.3). The tile is clipped to the shape and its children replicated across the shape’s bounding box, painted inside the element’s own transform wrap so the pattern follows the element’s CTM (patternUnits objectBoundingBox / userSpaceOnUse supported). Pattern viewBox / patternTransform are not yet applied.

Composition — <g> with transform (matrix, translate, scale, rotate, skewX, skewY, with arbitrary composition), <defs> + <symbol> + <use> template expansion with intra-document href resolution (cycle detection prevents pathological documents from looping), <clipPath> (including transformed clip paths and clipPathUnits="objectBoundingBox"), CSS clip-path: <basic-shape> || <geometry-box> on a graphics element or <g> — inset() / circle() / ellipse() / polygon() / rect() / xywh() resolved against the element’s fill box (which is what content-box / padding-box / border-box / margin-box all reduce to for an element with no CSS layout box), its stroke box, or the nearest SVG viewport for view-box (a container reports no bounding box yet, so a shape on a <g> leaves it unclipped rather than clipping it to a wrong rectangle) — <mask> (soft-mask via PDF ExtGState SMask), <image> (see below).

<image> — SVG 2 §8.6. The element establishes a new viewport at its x / y / width / height and clips the referenced resource to it. Hrefs resolve from href or legacy xlink:href, and may be a local file path, a data: URI (base64 or percent-encoded), or an http(s):// URL when a phpdftk/resource-loader is supplied.

Raster resources (PNG / JPEG) embed as PDF image XObjects. SVG resources are rendered as vectors — the referenced document’s own tree is painted into the image viewport by a separate translator, so its url(#…) references resolve in its own id space rather than the host document’s, and reference cycles are depth-capped. Sizing follows the CSS Images 3 §5.3 default sizing algorithm: an omitted width or height comes from the resource’s intrinsic size, or from the other axis via its intrinsic ratio. The viewBox-to-viewport mapping takes preserveAspectRatio from the <image> element when it declares one (SVG 2 §8.6 makes it override the referenced root’s), otherwise from an activated <view>, otherwise from the referenced root. A …#viewId fragment on the href activates that <view>’s viewBox and preserveAspectRatio per SVG 2 §18.3.

Inside a referenced resource, vw / vh / vmin / vmax resolve against the <image> box — that resource’s own outermost viewport is its initial containing block (CSS Values 4 §6.1). Geometry declared through style="width: …" rather than the presentation attribute is not yet read (SVG 2 §10.1 geometry properties).

Text — <text> and <tspan> with SVG 2 §11.6 positioning lists (x, y, dx, dy, rotate per-glyph). text-anchor (start / middle / end). font-family, font-size, font-weight, font-style, font-stretch. CSS Text Decoration 4 §6 text-shadow with multi-layer back-to-front emission. fill: none; stroke: ... outline text.

Animations — SMIL elements (<animate>, <animateMotion>, <animateTransform>, <set>) parsed and pinned at t=0 — the SVG renders as it appears at document load, with no timeline. PDF is static; this is by design.

CSS bridge — Optional phpdftk/css integration projects the SVG document’s <style> block + presentation-attribute cascade into per-element resolved values. Inline style="..." overrides authored CSS, presentation attributes lose to authored CSS (per CSS 2.1 §6.4.4).

phpdftk/svg-to-pdfDOMPDF SVGmPDF SVG
Vector output (no rasterisation)✓~ Some shapes rasterised~
N-stop gradients via PDF stitching✓✗ Two-stop only~
Path d-grammar (full SVG 2 §9.3.9)✓~ Common subset~
<use> / <defs> / <symbol>✓~~
<clipPath> / <mask>✓✗~
Text positioning (SVG 2 §11.6 lists)✓✗✗
text-shadow on <text>✓ Multi-layer✗✗
Cycle-safe gradient href chains✓✗✗
<pattern> fill (tiled, CTM-following)✓~~
Standalone (no PDF-engine coupling)✓✗✗

SVG WPT in-scope pass: 95.40% (166 / 174). Documented at SVG WPT substrate.

Hand-authored conformance fixtures live at packages/svg-to-pdf/tests/Conformance/fixtures/ — 15 fixtures covering shapes, gradients, text, masks, opacity, transforms, clip-path, symbol/use, combined scenes. All 15 pass (ignored list empty).

  • Filter effects (<filter>). Parsed but defer to no-op at paint time. Lit up by Phase 4C raster compositor.
  • <foreignObject> HTML content. Parsed as opaque foreign content; rendering deferred to the html-to-pdf recursion that’s still being wired.
  • text-shadow blur radius. Parsed but not rasterised (same posture as html-to-pdf’s text-shadow — both defer blur to Phase 4C). Sharp-offset shadows render correctly.
  • Pattern viewBox / patternTransform. <pattern> fill tiles its children in the referencing element’s user space; a pattern-local viewBox scale or patternTransform is not yet applied (the svg-matrix-058..069 fixtures need it).

Roadmap: docs/plans/html-and-svg.md.