Repository navigation
drawSvg: the initial fill is not applied, and fill="none" does not override an inherited fill #165
Description
What were you trying to do?
Draw an SVG logo exported from a design tool with page.drawSvg(). The logo's wordmark is one <path> without a fill attribute. It relies on SVG's initial fill, which is black.
How did you attempt to do it?
I called page.drawSvg(svg, { x, y, width, height }) with the unmodified SVG markup.
What actually happened?
The wordmark was missing from the PDF. Reducing it gives two cases where drawSvg paints differently from the SVG spec and from browsers:
- A shape with no
fillon itself or an ancestor is not filled. A shape that relies on the initial fill is therefore not drawn at all. fill="none"does not override a fill inherited from an ancestor, so the shape is filled with the ancestor's color.
The related missing initial stroke-width of <rect>, <circle> and <ellipse> is reported in #164.
What did you expect to happen?
The initial fill should apply: black. fill="none" should mean "not filled", whatever an ancestor sets.
- A black square (operator
f). - A blue outline only (
S).
How can we reproduce the issue?
import { PDFDocument } from '@cantoo/pdf-lib'
// Draws one SVG on a 100×100 page and returns the operator that paints it:
// f = fill, S = stroke, B = fill and stroke; null = nothing painted.
const paint = async (body, rootAttributes = '') => {
const pdfDoc = await PDFDocument.create()
const page = pdfDoc.addPage([100, 100])
page.drawSvg(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" ${rootAttributes}>${body}</svg>`,
{ x: 0, y: 100, width: 100, height: 100 },
)
// A page nothing was drawn on has no content stream.
const content = (page.node.normalizedEntries().Contents?.asArray() ?? [])
.map((ref) => pdfDoc.context.lookup(ref).getContentsString())
.join('')
return content.match(/^([fSB])$/m)?.[1] ?? null
}
// 1. No fill set anywhere: the initial fill is black. expected f, actual null
console.log('1', await paint('<rect width="10" height="10"/>'))
// 2. fill="none" overrides the fill inherited from the root. expected S, actual B
console.log('2', await paint('<g fill="none" stroke="blue" stroke-width="1"><rect width="10" height="10"/></g>', 'fill="red"'))Output with 2.11.1 (node repro.mjs):
1 null
2 B
Cause. In src/api/svg.ts, parseColor returns undefined for none (line 464). The attribute parser then falls back to the inherited value: fill: fillRaw?.rgb || inherited.fill (line 534), and the same for stroke (line 539). So "none" and "not set" can't be told apart, and nothing supplies the initial fill at the root.
Possible fix.
- Start the root with the initial fill (black).
- Represent
noneas an explicit value (e.g.null) that is inherited like a color but paints nothing, instead ofundefined.
Version
2.11.1
What environment are you running pdf-lib in?
Node (v26); the same happens in the browser.
Checklist
- My report includes a Short, Self Contained, Correct (Compilable) Example.
- I have attached all PDFs, images, and other files needed to run my SSCCE. (None are needed.)
Activity