← Blog

Text on path, foreignObject, and other complex SVG features

Three SVG features that need special handling on conversion Features beyond simple shapes and text textPath text laid out along a path foreignObject embedded HTML / MathML / arbitrary XML use cloned references to defined symbols marker arrowheads, terminators on paths pattern repeating fill pattern switch pick the first child the renderer can render animate / animateTransform / animateMotion SMIL animations

Beyond the basic shape and text primitives, SVG has a substantial library of advanced features. The conversion handles most of them, with the notable exception of foreignObject — which renders HTML inside SVG and effectively requires a browser engine.

textPath — text along a curve

SVG can lay text along an arbitrary path:

<defs>
  <path id="curve" d="M 50 100 Q 200 0 350 100"/>
</defs>
<text>
  <textPath href="#curve">Text following a curve</textPath>
</text>

Each character is positioned along the path with rotation matching the path's tangent at that point. Used for circular labels, ribbon-style banners, decorative typography.

PDF doesn't have a native "text on path" operator. The renderer handles textPath by laying out glyphs along the path and emitting positioned, rotated text in the output PDF. The result is vector text that follows the path correctly.

For paths with sharp curvature changes (sharp corners), per-character placement may produce visible gaps or overlaps; smooth curves work cleanly.

foreignObject — the hard one

<foreignObject> embeds non-SVG content (HTML, MathML, RDF) inside an SVG. The browser uses its HTML/MathML rendering engine to draw the embedded content, applying SVG transformations to it.

This is most often used for HTML content inside SVG (rich-text labels, complex interactive elements). It's also the standard way MathML equations get displayed in SVG-based scientific publishing.

The conversion does not render foreignObject content. The reasons:

SVGs with foreignObject content render with the foreignObject regions blank in the output PDF. The surrounding vector content renders correctly.

Workarounds:

  1. Some desktop SVG editors with HTML support can render foreignObject in limited cases; export to PDF directly from the editor.
  2. Replace foreignObject with native SVG text before conversion — works if the foreignObject just contains styled text.
  3. Render the SVG in a browser first, then save the result as PDF (Chrome's "Save as PDF" or headless Chrome via Puppeteer). The browser handles foreignObject natively.

use — cloned symbol references

The <use> element creates a copy of another element by ID:

<defs>
  <circle id="dot" r="5" fill="red"/>
</defs>
<use href="#dot" x="50" y="50"/>
<use href="#dot" x="100" y="50"/>
<use href="#dot" x="150" y="50"/>

The same circle appears at three positions. SVG defines this for re-use efficiency: the symbol is defined once, instantiated many times. Most renderers flatten use at render time — each instance becomes a separate copy in the rendered output.

The output PDF preserves this behavior. Each use becomes a distinct draw operation in the content stream. PDF's "Form XObjects" could provide deduplication (define once, draw many times), but most general-purpose SVG-to-PDF pipelines don't emit Form XObjects for use; the result is more verbose than strictly necessary, though typically not large.

Marker — arrowheads and decorations

SVG markers attach decorations to path endpoints and vertices:

<defs>
  <marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="5"
          orient="auto">
    <path d="M 0 0 L 10 5 L 0 10 z" fill="black"/>
  </marker>
</defs>
<path d="M 50 50 L 200 50" stroke="black" marker-end="url(#arrow)"/>

Arrowheads on flowchart connectors, terminators on technical drawings, decorative dots on graph lines. Markers are computed at render time: the SVG renderer evaluates the path's endpoint, places the marker oriented to match the path tangent.

Markers convert correctly: each one becomes a vector element placed and rotated at the appropriate path position in the output PDF.

Pattern — repeating fills

SVG patterns tile a small graphic across a larger area:

<defs>
  <pattern id="dots" x="0" y="0" width="20" height="20"
           patternUnits="userSpaceOnUse">
    <circle cx="10" cy="10" r="3" fill="black"/>
  </pattern>
</defs>
<rect width="200" height="200" fill="url(#dots)"/>

PDF supports patterns natively as Pattern color spaces. The conversion typically maps SVG patterns onto PDF patterns when feasible, keeping the result vector and sharp at any zoom; complex patterns may rasterize.

switch — fallback content

The <switch> element renders the first child that the renderer can handle. Used for graceful degradation: provide complex content for capable renderers, simpler fallbacks for limited ones.

The renderer evaluates the children's "feature requirements" (the requiredFeatures attribute, mostly deprecated in SVG 2, and requiredExtensions) and picks the first one supported. SVGs designed to gracefully degrade do so during conversion.

Animation — SMIL

SVG's animation elements (<animate>, <animateTransform>, <animateMotion>) drive property changes over time. Browsers play them as actual animations.

PDFs are static. The conversion captures the SVG's initial appearance — the state of every animated property at the start of any animation. The result is what the SVG looks like before any motion has played.

For animated SVGs where the meaningful content is at a later time (a reveal animation that ends with the visible content), pre-evaluate the animation in a browser, screenshot the final state, and convert that. Or remove the animation tags and set the final values directly on the elements.

CDATA and XML comments

SVG content inside CDATA sections is processed normally. Comments (<!-- ... -->) are ignored. Both are handled correctly during conversion.