Skip to content

Internals Images and SVG

Dragon edited this page Jun 3, 2026 · 1 revision

Internals: Images and SVG

This page describes the internal pipelines for raster images and SVG Form XObjects in phppdf.

Images

JPEG and PNG flow through Image::fromFile() / Image::fromBytes() / Image::fromBase64() into a single internal pipeline:

  • JPEG: embedded as-is with /Filter /DCTDecode. Supports RGB, Gray, and CMYK color spaces.
  • PNG (RGB / Gray / Palette): the IDAT chunks are concatenated and re-emitted as /Filter /FlateDecode with the appropriate /DecodeParms (predictor 15 with the original BitsPerComponent / Colors / Columns).
  • PNG with palette: the PLTE chunk is emitted as an /Indexed color space.
  • PNG with alpha (RGB+Alpha / Gray+Alpha / Palette + tRNS): the alpha channel is split into a separate single-component grayscale XObject and attached to the main image via /SMask (soft mask). The reader composites them on render.

Each image is registered in the document as a Form-like XObject (/Type /XObject /Subtype /Image) and referenced by name in the content stream. Caching is path-based and instance-based: the same path string used N times reuses one embed; the same Image instance reused N times reuses one embed.

SVG

SVG inputs (<svg>...</svg> or <?xml ... <svg>) are embedded as Form XObjects (/Type /XObject /Subtype /Form) rather than as raster images, so they remain vector at any zoom.

The pipeline is:

  1. XML parsed via libxml.
  2. Limits enforced: max 5 MiB raw, max nesting depth 32, max 50 000 elements, no cycles in <use> references, root must be <svg> in the SVG namespace, viewBox or width / height required.
  3. SVG painter state walked depth-first, emitting a PDF content stream into a new Form XObject.
  4. viewBox + preserveAspectRatio resolved to a cm (concat-matrix) operator at the Form XObject's start, so the embedded coordinate space maps correctly onto the placement rectangle.
  5. Cached per-document like raster images.

Opacity rendering

SVG fill-opacity, stroke-opacity, and opacity (which multiplies both) are emitted as graphics state parameter dictionaries (/ExtGState entries):

  • A ca value for fill alpha.
  • A CA value for stroke alpha.
  • One /ExtGState per unique alpha pair, named /Gs1, /Gs2, etc., and referenced in the content stream via gs operators.

This is the standard PDF way to express partial transparency.

SVG gradients

<linearGradient> and <radialGradient> are painted as PDF Shading Patterns (/PatternType 2, shading type 2 for linear and type 3 for radial), each registered in the document's pattern registry and named /Pn. The content stream switches the color space to /Pattern before painting: /Pattern cs ... /Pn scn for fill and /Pattern CS ... /Pn SCN for stroke, so the existing path terminators (f, S, B, f*, B*) are reused unchanged.

Pattern matrix. A PDF pattern's /Matrix is relative to the form XObject's base coordinate system, not the current painting CTM. The renderer accumulates the full chain: viewBox prologue cm + group and shape transforms, then for gradientUnits="objectBoundingBox" the shape bounding-box affine map, then the gradient's own gradientTransform. The result is written as the pattern's /Matrix, giving correct placement regardless of nesting depth.

Color function. Two-stop gradients use a single FunctionType 2 (exponential, N=1) interpolating between the two endpoint RGB triples. Three or more stops use a stitching FunctionType 3 wrapping one FunctionType 2 per adjacent pair, with Bounds and Encode derived from the stop offsets. Extend [true true] on the shading dictionary implements the pad spread (colors clamp beyond the gradient ends).

Stop opacity. When all stops share the same stop-opacity, that value is folded into the existing ExtGState ca / CA mechanism (the same per-pair alpha dictionaries used for fill-opacity / stroke-opacity). When opacity varies across stops, the color shading is painted with alpha forced to 1 inside an outer Form XObject, and a parallel alpha shading (a /DeviceGray shading whose stop "colors" are the per-stop opacities) is sub-rendered into a luminance /SMask Form, reusing the soft-mask infrastructure. The alpha-shading /Matrix must NOT include the shape CTM (the color matrix does): the alpha shading lives in the SMask Form's user space while the color shading lives in the outer Form's post-viewBox space - two opposite /Matrix conventions.

Spread methods. spreadMethod="reflect" and "repeat" are implemented by rewriting the gradient: the geometry is extended and the stops are replicated outward in PAD mode (the ShadingBuilder itself is unchanged). For radial gradients the extent is measured from the center (cx, cy), not the focal point (fx, fy).

href stop inheritance. A gradient element that has href (or xlink:href) pointing to another gradient inherits its <stop> children from the target. Inheritance is resolved before rendering with cycle detection; a cycle causes the gradient to be skipped silently.

Supported

  • All path commands: M, L, H, V, C, S, Q, T, A, Z and their lowercase (relative) variants. Arcs are converted to cubic Bezier curves.
  • Basic shapes: rect (with optional rx / ry rounded corners), circle, ellipse, line, polyline, polygon.
  • Transforms: matrix, translate, scale, rotate (with optional center), skewX, skewY; composition left-to-right.
  • viewBox + preserveAspectRatio (all 9 alignments x meet | slice; none stretches).
  • Groups (<g>), <defs> + <use> references with cycle detection.
  • Paint state: solid fill / stroke (147 named CSS colors, hex, rgb(), rgba(), currentColor), stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit, stroke-dasharray + stroke-dashoffset, fill-rule (nonzero | evenodd), fill-opacity, stroke-opacity, opacity (multiplicative).
  • Linear and radial gradients (<linearGradient> / <radialGradient>): objectBoundingBox and userSpaceOnUse units, gradientTransform, href stop inheritance, multi-stop, on fill and stroke, uniform AND per-stop stop-opacity, and spreadMethod pad / reflect / repeat.
  • <pattern> tiling fills and strokes (/PatternType 1): patternUnits objectBoundingBox / userSpaceOnUse, patternTransform, nested viewBox, href inheritance; pattern children may be shapes, groups, and <use> (text and image inside a pattern are stripped).
  • <clipPath> (clip-path="url(#id)"): native PDF clipping (W / W* n), clipPathUnits userSpaceOnUse / objectBoundingBox, any element, shapes + <use>, clip-rule nonzero / evenodd, union of children. Clip transforms are baked into the coordinates (never emitted as cm).
  • <mask> luminance soft masks: PDF /SMask + Form XObject /Group /S /Transparency, maskUnits + maskContentUnits (objectBoundingBox + userSpaceOnUse), any maskable element. The mask Form's /Matrix is identity with the /BBox projected into user space (a PDF Form /Matrix is concatenated with the CTM active at the /gs, the opposite convention from a tiling pattern).
  • <symbol> + <marker>: <use> of a <symbol viewBox + preserveAspectRatio> resolves to a group with the viewBox-to-use-box matrix; marker-start / marker-mid / marker-end on line / polyline / polygon / path with markerUnits stroke / userSpace, orient num / auto / auto-start-reverse, refX / refY, emitted inline (q / cm / Q), never as an indirect object.
  • <text>, <tspan>, <textPath> as real selectable PDF text (BT / Tf / Tm / Tj / ET): text-anchor, font-size / weight / style, dx / dy, fill + stroke, opacity, transform, inheritance through <g>. Standard 14 fonts and any registered custom TTF / OTF family (matched by font-family, with automatic glyph subsetting via the shared GlyphUsage). <textPath> lays glyphs along a referenced <path>, rotated to the tangent, with startOffset (absolute / percentage) and text-anchor. The Y flip is applied via the text matrix Tm [1 0 0 -1 bx by].
  • <image>: PNG / JPEG data URIs (see subsection below).
  • <filter> (filter="url(#id)"): hybrid pure-PHP raster (see subsection below).
  • Presentation attributes, inline style="...", AND <style> CSS (simple + compound selectors: type / .class / #id / * / rect.foo, comma groups; cascade by specificity then source order). Precedence: inline style > CSS rule > direct attribute > inherited. CSS is resolved entirely at parse time.

SVG embedded images

An SVG <image> element whose href attribute is a PNG or JPEG data URI (data:image/png;base64,... or data:image/jpeg;base64,...) is handled as follows:

  • The base64 payload is decoded at parse time and handed to Image::fromBytes, producing a raster XObject (Image + optional SMask for PNG alpha, same pipeline as standalone images).
  • Each distinct data URI is deduped by content hash within the SVG Form XObject. The resulting child XObject is registered in the form's /Resources/XObject dictionary as /Im0, /Im1, etc. (0-based), and drawn with the PDF Do operator.
  • objectCount is recursive: form object + sum of each distinct child image's object count. A PNG with alpha contributes 2 objects (image + SMask); a JPEG contributes 1.
  • The placement matrix is [fw 0 0 -fh fx fy+fh], where fw/fh are the rendered width/height in points and fx/fy are the top-left corner. The -fh flip accounts for PDF's top-row-first raster orientation within the renderer's y-down coordinate space.
  • preserveAspectRatio with slice installs a rectangular clip path around the viewport before the Do call, matching the clip behavior used for the top-level SVG.
  • transform and opacity on the <image> element go through the same matrix-concat and ExtGState mechanisms used for other SVG elements.
  • External href values (local file paths or http(s) URLs) are silently ignored - the renderer has no network or filesystem access at emit time. Other data URI formats (GIF, WebP, svg+xml) are also ignored, as are images with non-positive computed width or height.

SVG filters

An element carrying filter="url(#id)" is rendered with a hybrid raster strategy (the approach Batik / Illustrator take; no other pure-PHP PDF library implements SVG filters):

  • The filter region is rasterized in pure deterministic PHP - no GD drawing, so byte-identity golden tests stay stable across platforms. The result is embedded as an image XObject (/DeviceRGB samples) plus a /DeviceGray /SMask, both /FlateDecode, and placed back with a region-local cm.
  • Text inside a filtered subtree is NOT rasterized: it is redrawn sharp (vector) on top of the raster via renderTextOnly, so it stays selectable and crisp.
  • The pipeline is a named-buffer graph (SourceGraphic / SourceAlpha / per-primitive result), with null-in chaining (first primitive defaults to SourceGraphic, later ones to the previous result). Blur / composite math runs on premultiplied alpha; color math runs in linearRGB via ColorSpace LUTs.
  • First-wave primitives: feGaussianBlur (3-pass box blur), feOffset, feFlood, feMerge, feBlend (normal / multiply / screen / darken / lighten, on premultiplied color), feComposite (Porter-Duff over / in / out / atop / xor + arithmetic), feColorMatrix (matrix / saturate / hueRotate / luminanceToAlpha), and feDropShadow (expanded to blur + offset + flood + merge).
  • Resolution is Document::setSvgFilterResolution(int $dpi) (default 300, capped at 2000 px per side). pxPerUnit = dpi / 72 in viewBox-local coordinates - it must NOT include the viewBox-to-unit prologue scale, or the raster collapses.
  • ext-gd is optional (composer suggest, not require): used only to decode a JPEG referenced inside a filter. PNG decodes through the built-in PngMetadata / PngFilters path to raw samples.
  • ImageEmbedder::svgHasFilterPaint pre-reserves count * 2 objects in objectCount (image + SMask per filtered element) so the xref stays correct.

Known limits: primitiveUnits="objectBoundingBox" is parsed but subregion coords are treated as userSpaceOnUse; color-interpolation-filters="sRGB" is not wired (always linearRGB); clip / mask / pattern / filter nested inside a filter are ignored; stroke inside a filter is a v1 approximation (perpendicular quads, square joins); the heavier primitives below are deferred.

Not supported (skipped silently per SVG fallback spec)

  • Heavy filter primitives: feTurbulence, feDisplacementMap, feConvolveMatrix, feMorphology, feImage, feTile, and the lighting primitives (feDiffuseLighting / feSpecularLighting). BackgroundImage / FillPaint / StrokePaint filter inputs are transparent.
  • Mesh gradients. fill="url(#x)" referencing an unsupported or unresolvable paint server falls back to black.
  • Scripts, animations (SMIL), foreignObject.
  • <image> is supported only for PNG / JPEG data URIs (see subsection above). External href values (local paths or http(s) URLs) and other data URI formats (GIF, WebP, svg+xml) are ignored - the renderer has no network or filesystem access at emit time.

See also

Clone this wiki locally