Skip to content

phpdftk/svg

Pure-PHP SVG 2 parser. Produces a typed tree (SvgDocument, Group, Path, Text\TextElement, Text\Tspan, Shape\Rect, Shape\Circle, Shape\Ellipse, Shape\Line, Shape\Polyline, Shape\Polygon, GenericElement, …) with on-demand attribute parsing, including the transform attribute (Value\Transform), the d path-data grammar (Path\PathData), SVG 2 §13 presentation-attribute / paint accessors (fill(), stroke(), Value\Paint, Value\Color), and the SVG 2 §11.6 text positioning + CSS Fonts 4 font accessors.

Terminal window
composer require phpdftk/svg
use Phpdftk\Svg\Parser;
use Phpdftk\Svg\Shape\Rect;
$doc = (new Parser())->parse(<<<'SVG'
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
<rect x="10" y="20" width="30" height="40"/>
</svg>
SVG);
$doc->widthAttribute(); // "200"
$doc->viewBox(); // null (none declared)
foreach ($doc->findByTag('rect') as $r) {
// $r is a Phpdftk\Svg\Shape\Rect
$r->x(); $r->y(); $r->width(); $r->height();
}

The parser uses DOMDocument::loadXML with LIBXML_NONET and explicitly leaves LIBXML_NOENT off:

  • External entities (<!ENTITY x SYSTEM "file://…"> or http://…) are NOT substituted. Classic XXE payloads return empty content rather than leaking file or network data.
  • No network access during parse (no DTD or entity fetches).
  • XInclude directives pass through as generic elements; the parser never calls DOMDocument::xinclude().

A SecurityTest suite covers these properties — regressions break CI before they ship.

The parser is feature-complete for the v1 element set: secure XML loader, SvgDocument (with findById id→element index), Group, all five basic shapes, the transform attribute (Value\Transform), <path> with the full SVG 2 §9.3.9 d-grammar, SVG 2 §13 presentation / paint accessors, <text>/<tspan> + CSS Fonts 4 font accessors, <defs>/<symbol>/<use> with intra-document href resolution, <clipPath>/<mask>/<image>, gradients with cycle-safe href chaining, and the optional Css\CssBridge for <style> + presentation-attribute cascade resolution (requires phpdftk/css).

Length attributes carry absolute CSS units, which resolve to user units at 96dpi (CSS Values 4 §6.2): width="2cm" is 75.59 user units, and in, mm, pt, pc, Q and px resolve likewise. Percentages and font-relative units (em, ex) need viewport or font context the attribute accessor does not carry, so their numeric part is used as-is.

transform-origin (Value\TransformOrigin) pivots an element’s transform. SVG elements have no associated CSS layout box, so unlike CSS boxes their initial transform-origin is 0 0 rather than 50% 50% — an untouched element rotates about its reference box’s origin, not its centre. Percentages resolve against that box’s size, lengths offset from its origin, and position keywords name its edges. Declarations that do not match the CSS Transforms 1 §6 grammar — a second component naming an axis already taken, such as left left or top 100% — are dropped whole, back to the 0 0 initial.

transform-box (CSS Transforms 1 §7) chooses that reference box: view-box (the initial) is the nearest SVG viewport, fill-box the object bounding box, and stroke-box that box grown by half the stroke. SVG elements have no CSS layout box, so content-box resolves as fill-box and border-box as stroke-box.

For an inline <svg> in an HTML document, document stylesheet rules reach SVG elements too: the subtree generates no boxes of its own, so the box generator projects the cascaded value of the SVG-relevant properties onto each descendant before the subtree is handed to the renderer. An element’s own presentation attribute always wins over the projected value.

Presentation attributes take part in that cascade. Per SVG 2 §6.7 each one is an author-origin declaration of specificity zero, synthesised into a stylesheet ordered ahead of every other author sheet — so a stylesheet rule out-ranks an attribute, and, more importantly, an INHERITED property set as an attribute reaches descendants: <g fill="green"><rect/></g> fills the rect green. The same holds for a standalone SVG document through Css\CssBridge.

var() resolves inside a presentation attribute (CSS Variables 1 §3), since SVG 2 §6.7 makes those attributes into properties: <circle r="var(--radii)"> reads --radii off the element’s cascade, and a reference that resolves to nothing with no fallback is invalid at computed-value time, so the attribute is dropped. Attributes that are not properties — viewBox, points, preserveAspectRatio, href — keep their literal text.

<use> instantiates the element it references by deep-cloning it into a shadow tree whose host is the <use> (SVG 2 §5.6). For an inline <svg> the clone is built during the cascade projection, which is what lets the instance INHERIT from the <use> — the sprite idiom <use href="#icon" fill="currentColor"> reaches the shapes inside #icon, which styling the referenced element where it sits (usually inside <defs>) can never do.

The clone is cascaded while still detached from the document, which is the shadow BOUNDARY: a document-tree selector like #test rect cannot match inside the instance, while a selector wholly inside the tree (.inside rect) still does. Ids are stripped from the clone before it is attached, since a shadow node is not addressable by id.

Circular references are dropped rather than expanded (SVG 2 §5.6.2): the referenced element being the <use> itself or one of its ancestors, and the mutual case where two <use> elements reference each other’s containers. Instantiation is additionally capped at 8 levels of nesting and 20,000 cloned elements per <svg>.

A url(#…) or href="#…" reference carries a URL fragment, so it is percent-decoded before it names anything: url(#%66%6f%6f) and url(#foo) resolve to the same element. SvgDocument::findByFragment is that entry point; findById stays a literal id lookup.

The translator that emits PDF from the parsed tree ships as phpdftk/svg-to-pdf — currently at 95.40% in-scope WPT pass on the SVG corpus.