Skip to content
Inkoxide

Search covers every page and heading of the docs. Use ↑ ↓ and ↵.

Drawing: SVG and Canvas

Edit this page

<Svg width={120} height={60} viewBox="0 0 120 60">
  <Rect x={0} y={20} width={30} height={40} rx={3} fill="#c7d2fe" />
  <Rect x={45} y={0} width={30} height={60} rx={3} fill="#4f46e5" />
  <Path d="M0 58 L120 58" stroke="#9ca3af" strokeWidth={1} />
</Svg>

<Canvas width={120} height={60} paint={(p, w, h) => p.moveTo(0, h).lineTo(w, 0).lineWidth(2).stroke('#4f46e5')} />

The SVG primitives take SVG attributes in camelCase (strokeWidth, stopColor). Canvas draws in code with a pdfkit-style painter. An SVG file can also go straight into <Image src={svgBytes} />.

Text in a drawing is a Text inside the <Svg> (or a G, Defs, ClipPath or Marker in it), with Tspans for spans of it. It is real PDF text: it can be selected, searched and copied.

<Svg width={160} height={60}>
  <Rect width={160} height={60} rx={6} fill="#eef2ff" />
  <Text x={80} y={26} textAnchor="middle" fontWeight="bold" fill="#4f46e5">
    Revenue <Tspan fill="#16a34a">+12%</Tspan>
  </Text>
  <Text x={80} y={46} textAnchor="middle" fontSize={9} letterSpacing={1}>
    Q3 2026
  </Text>
</Svg>
  • Position: x and y, with textAnchor (start, middle, end) and dominantBaseline (alphabetic, middle, central, hanging, text-before-edge, …). A list (x={[10, 20, 30]}) places each character, and dx, dy and rotate shift and turn them. textLength fits the text to a width, by its spacing or, with lengthAdjust="spacingAndGlyphs", its glyphs too.
  • Font: fontFamily (a stack: "Brand, Inter"), fontWeight, fontStyle and fontSize, on the text, a Tspan or a G around it; otherwise the text style around the drawing, as for any Text. The font is one registered with Font.register (or bundled), matched like other text, so a missing family falls back with a warning.
  • Style: fill and stroke with colours, gradients and patterns, letterSpacing, wordSpacing, textDecoration (underline, overline, line-through), direction (rtl sets the base direction of the text and mirrors its anchor) and, on a Tspan, baselineShift (super, sub or a length) and alignmentBaseline, which places the span on another baseline than the text's dominantBaseline. A Text in an <Svg> takes these SVG props: style and className are for text outside drawings.
  • Strings and Tspan go in it; anything else is an error.

Markers draw a shape at the vertices of a Path, Line, Polyline or Polygon: define a Marker in Defs and reference it with markerStart, markerMid or markerEnd.

<Svg width={120} height={40}>
  <Defs>
    <Marker id="arrow" viewBox="0 0 10 10" refX={10} refY={5} markerWidth={6} markerHeight={6} orient="auto-start-reverse">
      <Path d="M0 0L10 5L0 10Z" fill="context-stroke" />
    </Marker>
  </Defs>
  <Line x1={10} y1={20} x2={110} y2={20} stroke="#4f46e5" strokeWidth={2} markerStart="url(#arrow)" markerEnd="url(#arrow)" />
</Svg>

refX and refY are the point of the marker placed on the vertex, markerWidth and markerHeight its viewport (3 by default), and orient turns it with the path ('auto'), the same but reversed at the start ('auto-start-reverse'), or by a number of degrees. A marker is scaled by the stroke width unless markerUnits="userSpaceOnUse". context-stroke and context-fill paint it with the shape's stroke or fill. A RadialGradient takes fr, the radius of its focal circle, next to fx and fy.

The Canvas painter builds a path (moveTo, lineTo, bezierCurveTo, quadraticCurveTo, rect, roundedRect, circle, ellipse, polygon, path) and paints it with fill, stroke or fillAndStroke, in the current state: fillColor, strokeColor, opacities, lineWidth, lineCap, lineJoin, miterLimit (10 by default, as in PDF), dash, and the transforms translate, rotate and scale. save() and restore() keep and bring back the state and the transforms.

<Canvas
  width={120}
  height={60}
  paint={(p, w, h) => {
    const sky = p.linearGradient(0, 0, 0, h).stop(0, '#4f46e5').stop(1, '#c7d2fe');
    p.save().roundedRect(0, 0, w, h, 8).clip();
    p.rect(0, 0, w, h).fill(sky);
    p.restore().circle(w / 2, h / 2, 12).fill(p.radialGradient(56, 26, 0, 60, 30, 12).stop(0, 'white').stop(1, '#f59e0b'));
  }}
/>
  • clip(rule?) clips what is painted after it to the current path, until restore(); 'evenodd' picks the even-odd rule. Clips nest.

  • linearGradient(x1, y1, x2, y2) and radialGradient(x1, y1, r1, x2, y2, r2) (from the first circle to the second) return a CanvasGradient; stop(offset, color, opacity?) adds its colours. Pass it to fill, stroke, fillAndStroke, fillColor or strokeColor in place of a colour. Its coordinates are in the user space where it paints, transforms included, as in pdfkit.

  • text(str, x, y, options?) draws one line of text with the fill colour or gradient, its top at y (as in pdfkit). font(name, size?) sets the font: a registered font id ('Inter-Bold'), a family name, or a pdfkit standard name ('Helvetica-Bold'), which picks the canvas's family in that weight and style; fontSize(n) sets the size (12 by default). Until font() the text is in the font around the canvas. The options (CanvasTextOptions) are width with align (left, center, right; justify, as in pdfkit, is left, since a line does not wrap), baseline (top, middle, bottom, alphabetic, hanging), characterSpacing, wordSpacing, underline, strike, fill (default true) and stroke (with the stroke colour and line style). Text does not wrap: a line break in str is an error.

    <Canvas width={160} height={40} paint={(p, w) => {
      p.font('Inter-Bold', 14).fillColor('#4f46e5').text('Total', 0, 4);
      p.font('Inter').text('$1,280.00', 0, 4, { width: w, align: 'right' });
    }} />

In an SVG, whether in <Svg> or an image file:

  • Text (<text>, <tspan>, <a>) is drawn as PDF text with the document's fonts, in nested SVG images too. Each span gets the declared font that matches its font-family, font-weight and font-style as CSS matches them (the first declared font when no family matches), with the fallback fonts for characters it lacks; a font that only an SVG file needs goes in fallbackFonts (metadata), so that the document declares it. Style sheets, xml:space, positions and rotations, anchors, baselines, baseline-shift, spacing, text-decoration and textLength work, also in clip paths, masks, patterns, markers and use copies. Text on a path (<textPath>) and vertical text (writing-mode of tb, tb-rl, vertical-rl or vertical-lr) fail the render with INVALID_IMAGE: convert them to outlines in your design tool. sideways-rl and sideways-lr are not refused: the text is drawn horizontally. font-variant, font-stretch, font-kerning and font-feature-settings are ignored, and font properties set on a <use> do not reach the text it copies (its fill and stroke do). A <text> element that an XML entity expands to fails with INVALID_IMAGE too: write the element in the SVG itself. An entity inside a <text> that is in the SVG (<text>&co; report</text>) is fine. An !important rule that names both path and polygon also sets the paint of the SVG's text. Colour emoji fonts draw in colour, and emoji the document maps to images (emojiImages) are those images, as in other text; an emoji image that is itself an SVG with text is left out there, but its text still copies. An SVG with text in a document that declares no fonts fails validation with INVALID_IMAGE, and an SVG holds at most 1,000,000 characters of text, counting every copy. Its text counts toward the other limits of the SVG too, as it is drawn, so an SVG that fails one fails validation, not only the render. An SVG without width, height and viewBox takes its size from what it draws, its text left out, so the same with any fonts. Where nothing sets a font size it is 16, as in browsers.
  • Filters (filter: drop shadows, blurs) are not applied: the element is drawn without them.
  • Images inside the SVG must be embedded as data: URLs (JPEG, PNG, GIF, WebP or SVG). Links to files or URLs are left out, and an embedded image over 50 megapixels fails the render.
  • Size: the width and height are 0.00001 to 100,000 px each, and the viewBox width and height at least 0.00001. Outside these the SVG fails the render with INVALID_IMAGE; a nested SVG image is left out.
  • Huge numbers elsewhere in the SVG (transform="scale(1e30)", a gradient or a shape at 1e30) are written as at most ±10¹¹ points, the largest number the PDF writer emits anywhere, so PDF readers can parse every number. What is drawn that far out is off the page either way.

Synced from packages/render/GUIDE.md in the inkoxide repository.