PeachPDF renders SVG through its own vector scene graph, reusing the same PDF path/fill/stroke/gradient/clip primitives already used for CSS backgrounds and borders — SVG content is never rasterized to a bitmap. Full SVG 1.0 coverage is now supported (minus a handful of PDF-incompatible features such as animation, scripting, and filters): all basic shapes, fill-rule, stroke dash/cap/join, rotate()/skewX()/skewY() transforms, full preserveAspectRatio and nested viewports, objectBoundingBox/spreadMethod/currentColor gradients, the style cascade (style= and <style>), <switch>/<a> links, <marker>, <pattern>, <mask>, <image>, and <text>/<tspan>/<tref>. See supported-svg-features.md for the full compatibility matrix.
straight lines (M/L/Z) path d="M50,10 L90,90 L10,90 Z" | cubic Bézier (C) path d="M10,80 C10,20 90,20 90,80" | elliptical arc (A) path d="M10,60 A40,40 0 0 1 90,60" | multiple subpaths one path, two "M...Z" subpaths |
circle circle cx=50 cy=50 r=35 | polygon (pentagon) polygon points="..." | overlapping circles + opacity two circles, opacity="0.7" each | polygon (5-point star) polygon points="..." |
linearGradient, 2 stops linearGradient x1/y1/x2/y2, 2 stops | linearGradient, 3 stops diagonal, 3 stops | radialGradient, centered radialGradient cx/cy/r | radialGradient + gradientTransform gradientTransform squishes the radial into an ellipse |
stroke only, default miterlimit stroke-width=4 stroke-miterlimit=10 | thick stroke, low miterlimit stroke-width=10 stroke-miterlimit=1 | fill + stroke combined fill and stroke on the same shape | stroke-only star fill="none" stroke="#c0392b" |
gradient stroke (rounded rect + ellipse) stroke="url(#gradient)" on a rounded rect and an ellipse - a gradient stroke sharing a page with solid strokes |
transparent gradient stroke beside opaque left fades to transparent; the opaque green-gold frame must stay fully visible | gradient stroke inside padded wrapper wrapper padding no longer drops the stroke | gradient stroke inside rounded bordered wrapper border-radius + border wrapper no longer drops the stroke | two gradient-stroked panels in a row a transparent gradient ring never masks a later one on the same page |
nested group opacity (0.7 × 0.7) g opacity="0.7" > g opacity="0.7" | group transform: translate + scale transform="translate(15,15) scale(0.6)" | horizontal mirror via scale(-1,1) transform="scale(-1,1) translate(-100,0)" | use + per-instance transform two <use> of the same <circle>, each transformed |
group opacity over overlapping text g opacity="0.5" over two overlapping <text> runs — no double-darkening at the overlap | use opacity of a container use opacity="0.5" of a <g> of overlapping rects — composited once | nested svg opacity nested <svg opacity="0.5"> with overlapping circles |
clip-path references a <clipPath> that itself contains a <use> of a shape defined once in <defs> — the same pattern used by the peach illustrations below. A clip shape's own transform (directly, via <use transform>, or via a wrapping <g>) is applied to the clip geometry, so one shape can be reused at several positions. With clipPathUnits="objectBoundingBox" the 0..1 clip geometry auto-scales to the referencing element's own bounding box.
gradient clipped to a circle clipPath > use > circle | gradient clipped to a star clipPath > use > polygon | use for simple repetition use xlink:href="#dot" x=.. y=.. | one clip shape reused at 4 positions clipPath > use transform="translate(..)" ×4 |
clip + group opacity combined g clip-path="url(#clip3)" opacity="0.8" | objectBoundingBox: clip auto-scales to the element clipPathUnits="objectBoundingBox" (0..1 diamond) |
rect with rounded corners rect x y width height rx=15 | ellipse (percentage rx) ellipse rx="35%" (percentage length) | line, round linecap line x1 y1 x2 y2 stroke-linecap="round" | polyline (open, unclosed) polyline points="..." fill="none" |
stroke-dasharray + dashoffset stroke-dasharray="14,8" stroke-dashoffset="4" | stroke-linecap: butt/round/square stroke-linecap: butt, round, square | stroke-linejoin: miter/round/bevel stroke-linejoin: miter, round, bevel | dashed rounded-rect border dasharray + linecap + linejoin combined |
fill-rule="nonzero" (default) same-direction inner square: solid (no hole) | fill-rule="evenodd" identical path, evenodd: donut (hole visible) | fill-opacity, stroke fully opaque fill-opacity="0.4" (stroke unaffected) | stroke-opacity, fill fully opaque stroke-opacity="0.4" (fill unaffected) |
invalid fill-rule inherits, doesn't reset to nonzero g fill-rule="evenodd" > path fill-rule="not-a-rule": still a donut | invalid stroke-linecap/linejoin inherit from group group's round cap/join survive an invalid override |
invalid fill/stroke inherit from group g fill/stroke set > circle fill/stroke="not-a-color": still purple with dark outline | invalid fill-opacity/stroke-opacity inherit from group group's 0.4 fill/stroke opacity survive an invalid override, not reset to 1.0 |
rotate(angle, cx, cy) transform="rotate(30,50,50)" | rotate(-angle, cx, cy) transform="rotate(-45,50,50)" | skewX() transform="skewX(20)" | skewY() transform="skewY(-20)" |
objectBoundingBox (default) no gradientUnits: x1/x2 are 0..1 fractions of the rect's own box | spreadMethod="repeat" narrow x1..x2 range tiles across the rect | spreadMethod="reflect" same idea, mirrored at each repeat | radial fx/fy off-center highlight fx/fy offset from cx/cy: sphere-shading highlight |
default: xMidYMid meet 2:1 viewBox into a square box: letterboxed | xMidYMid slice same viewBox, slice: cropped, fills the box | preserveAspectRatio="none" stretched independently per axis: distorted | nested <svg>, own viewport nested svg establishes its own coordinate system |
style= overrides presentation attribute fill="red" style="fill:#2ecc71": style wins | currentColor fill="currentColor" resolves the ancestor's CSS color | <style> class selector (standalone) .hi/.lo rules, via <img> | <style> id selector (standalone) #target rule, via <img> |
inline <svg><style> a <style> nested in an inline <svg> now applies (issue #159) | HTML <style> cascades into SVG a document-level rule matching SVG shapes applies (SVG 2 §6) | combinator selector "g rect" descendant combinator, via the full selector engine | var() custom property fill:var(--c) resolves through the CSS cascade |
switch: first child wins no requiredFeatures evaluation: always shows child 1 | switch: skips an unbuildable child <metadata> isn't renderable, so child 2 is used | <a href>: real PDF link becomes a clickable PDF link annotation | <a> with no href renders children normally, just isn't a link |
marker-end, orient="auto" marker rotates to follow the path's tangent | marker-start/mid/end on every vertex one marker def, placed at start/mid/end vertices | markerUnits="strokeWidth" (default) same marker, scales with each line's own stroke-width | orient: fixed angle orient="45": same fixed rotation at every vertex |
checkerboard pattern of 4 <rect> tiles, patternUnits="userSpaceOnUse" | polka dots dot pattern clipped to the circle's own geometry | patternTransform="rotate(45)" stripe tile rotated 45° via patternTransform | pattern filling a star shape pattern respects the star's own fill geometry |
linear-gradient luminance fade gradient mask fades the purple rect over a yellow backdrop | radial vignette radial mask: spotlight/vignette fade | vector shape as mask a shape, not just a gradient, as the mask's luminance | <text> as mask content gradient shows only through the letter shapes |
raster data:image/png a real embedded raster image XObject | vector data:image/svg+xml stays real vector content, never rasterized | preserveAspectRatio="none" on <image> raster image stretched non-uniformly to fit its box | <image> with clip-path clip-path applies to <image> like any other element |
<text x y fill font-size> plain positioned text, baseline at (x, y) | text-anchor: start/middle/end all three anchored at x=50 (dashed guideline) | <tspan> restyles mid-run tspan flows right after "Hello ", own fill+weight | <tspan x y>: new line + font variety own x/y starts a new line; size/weight/style vary |
font-* inherited from an ancestor <g> text inherits family/size/weight from <g>; 1.5em = 24 | per-character x list + rotate each glyph its own x + rotation (SVG §10.4) | nested <tspan> along a <textPath> restyled tspan laid along the curve too |
letter-spacing & word-spacing both properties reach the same GSUB/GPOS pipeline HTML text uses | text-transform uppercase / capitalize / lowercase | font-variant-caps: small-caps real GSUB smcp/c2sc substitution, gated on font support | text-decoration-line/-style/-color own value only (not inherited), flows across a plain descendant tspan |
The identical SVG markup rendered two ways: embedded directly in the HTML, and encoded as a base64 data: URI on an <img> tag. Both go through the same vector renderer.
inline <svg> <svg>...</svg> inline in the HTML body | <img src="data:..."> same markup, base64 data: URI |
Two original peach illustrations, built entirely from the elements above: cubic-curve paths, radial gradients for shading, and a clipPath + use for the cross-section.
Whole Peachpath (M/C/Z) body + leaf + stroked stem, radialGradient shading, radial blush with alpha stops | Peach SliceclipPath + use round the flesh, opacity-grouped striations, pit path, use for the skin outline |
Peach Branch & Blossoms<marker> blossoms/leaf tip along a curved branch, italic <text> caption | Peach Basketpattern-woven basket, rotate() on individual peaches, gradient drop-shadow via mask |