Drawing: SVG and Canvas
<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:
xandy, withtextAnchor(start,middle,end) anddominantBaseline(alphabetic,middle,central,hanging,text-before-edge, …). A list (x={[10, 20, 30]}) places each character, anddx,dyandrotateshift and turn them.textLengthfits the text to a width, by its spacing or, withlengthAdjust="spacingAndGlyphs", its glyphs too. - Font:
fontFamily(a stack:"Brand, Inter"),fontWeight,fontStyleandfontSize, on the text, aTspanor aGaround it; otherwise the text style around the drawing, as for anyText. The font is one registered withFont.register(or bundled), matched like other text, so a missing family falls back with a warning. - Style:
fillandstrokewith colours, gradients and patterns,letterSpacing,wordSpacing,textDecoration(underline,overline,line-through),direction(rtlsets the base direction of the text and mirrors its anchor) and, on aTspan,baselineShift(super,subor a length) andalignmentBaseline, which places the span on another baseline than the text'sdominantBaseline. ATextin an<Svg>takes these SVG props:styleandclassNameare for text outside drawings. - Strings and
Tspango 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, untilrestore();'evenodd'picks the even-odd rule. Clips nest. -
linearGradient(x1, y1, x2, y2)andradialGradient(x1, y1, r1, x2, y2, r2)(from the first circle to the second) return aCanvasGradient;stop(offset, color, opacity?)adds its colours. Pass it tofill,stroke,fillAndStroke,fillColororstrokeColorin 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 aty(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). Untilfont()the text is in the font around the canvas. The options (CanvasTextOptions) arewidthwithalign(left,center,right;justify, as in pdfkit, isleft, since a line does not wrap),baseline(top,middle,bottom,alphabetic,hanging),characterSpacing,wordSpacing,underline,strike,fill(defaulttrue) andstroke(with the stroke colour and line style). Text does not wrap: a line break instris 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 itsfont-family,font-weightandfont-styleas 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 infallbackFonts(metadata), so that the document declares it. Style sheets,xml:space, positions and rotations, anchors, baselines,baseline-shift, spacing,text-decorationandtextLengthwork, also in clip paths, masks, patterns, markers andusecopies. Text on a path (<textPath>) and vertical text (writing-modeoftb,tb-rl,vertical-rlorvertical-lr) fail the render withINVALID_IMAGE: convert them to outlines in your design tool.sideways-rlandsideways-lrare not refused: the text is drawn horizontally.font-variant,font-stretch,font-kerningandfont-feature-settingsare 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 withINVALID_IMAGEtoo: write the element in the SVG itself. An entity inside a<text>that is in the SVG (<text>&co; report</text>) is fine. An!importantrule that names bothpathandpolygonalso 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 withINVALID_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 withoutwidth,heightandviewBoxtakes 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
viewBoxwidth and height at least 0.00001. Outside these the SVG fails the render withINVALID_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.