|
| 1 | +# @svg-pdf/libpdf |
| 2 | + |
| 3 | +Embeds an SVG document as real, sharp vector content into a PDF made with [`@libpdf/core`](https://libpdf.documenso.com/) — not a rasterized image, so it stays crisp at any zoom. |
| 4 | + |
| 5 | +This package is an **adapter**: it parses SVG using [`@svg-pdf/core`](https://www.npmjs.com/package/@svg-pdf/core) (a DOM-free parser that turns SVG into a neutral instruction list) and translates the result into `@libpdf/core` drawing calls. Anything the parser can't handle is skipped individually with a warning, instead of failing the whole conversion. |
| 6 | + |
| 7 | +## Install |
| 8 | + |
| 9 | +`@libpdf/core` — the library that actually writes PDF files — is a **peer dependency**: it won't come along automatically, so install both yourself: |
| 10 | + |
| 11 | +``` |
| 12 | +npm install @svg-pdf/libpdf @libpdf/core |
| 13 | +``` |
| 14 | + |
| 15 | +This is done deliberately rather than bundling a private copy of `@libpdf/core`, because _you_ are the one who creates the PDF document object using `@libpdf/core` and then hands it to `@svg-pdf/libpdf` to draw into — if the two ended up using separately-installed copies of `@libpdf/core`, they wouldn't recognize each other's objects as "the same kind of thing," causing confusing bugs. |
| 16 | + |
| 17 | +## Quick start |
| 18 | + |
| 19 | +```ts |
| 20 | +import { PDF as LibPDF } from '@libpdf/core'; |
| 21 | +import { embedSvgInPdf } from '@svg-pdf/libpdf'; |
| 22 | +import * as fs from 'fs'; |
| 23 | + |
| 24 | +const svgText = '<svg viewBox="0 0 200 100"><rect width="10" height="10"/></svg>'; |
| 25 | + |
| 26 | +const doc = LibPDF.create(); |
| 27 | +const { warnings } = await embedSvgInPdf(doc, { |
| 28 | + svgText, |
| 29 | + rotation: 0, |
| 30 | + // optional: pageSize, orientation ('portrait' | 'landscape'), margin |
| 31 | +}); |
| 32 | + |
| 33 | +if (warnings.length > 0) { |
| 34 | + console.warn('Unsupported SVG features were skipped:', warnings); |
| 35 | +} |
| 36 | + |
| 37 | +fs.writeFileSync('output.pdf', await doc.save()); |
| 38 | +``` |
| 39 | + |
| 40 | +Create an empty PDF document, call `embedSvgInPdf` to draw your SVG into it as one page, then save the PDF. `embedSvgInPdf` never throws just because your SVG uses an unsupported feature — it skips that one piece and adds a message to `warnings`. The only time it throws is if the SVG text itself can't be parsed (not valid XML). |
| 41 | + |
| 42 | +## Fetching external `<image>` URLs |
| 43 | + |
| 44 | +An `<image href="https://example.com/photo.png">` pointing at the web is **not** fetched by default — it's skipped with a warning instead. This is a deliberate safety default: automatically fetching arbitrary URLs found inside someone else's SVG could be abused to make your server request things it shouldn't reach (a class of attack called SSRF). |
| 45 | + |
| 46 | +If you trust your SVGs, pass a `fetchImage` function and decide for yourself what's safe to fetch: |
| 47 | + |
| 48 | +```ts |
| 49 | +await embedSvgInPdf(doc, { |
| 50 | + svgText, |
| 51 | + rotation: 0, |
| 52 | + fetchImage: async (url) => { |
| 53 | + const res = await fetch(url); |
| 54 | + if (!res.ok) return null; |
| 55 | + return { |
| 56 | + bytes: new Uint8Array(await res.arrayBuffer()), |
| 57 | + mimeType: res.headers.get('content-type') ?? 'image/png', |
| 58 | + }; |
| 59 | + }, |
| 60 | +}); |
| 61 | +``` |
| 62 | + |
| 63 | +## Embedding custom fonts |
| 64 | + |
| 65 | +By default, text is drawn using one of PDF's 14 built-in "standard" fonts (like Helvetica), matched to your SVG's `font-family`/`font-weight`/`font-style`. To embed a real, custom font instead, pass a `fetchFont` function — called once per distinct font actually used, not once per letter: |
| 66 | + |
| 67 | +```ts |
| 68 | +await embedSvgInPdf(doc, { |
| 69 | + svgText, |
| 70 | + rotation: 0, |
| 71 | + fetchFont: async ({ fontFamily, fontWeight, fontStyle }) => { |
| 72 | + if (fontFamily === 'Poppins') return fs.readFileSync('./fonts/Poppins-Regular.ttf'); |
| 73 | + return null; // falls back to the closest standard font |
| 74 | + }, |
| 75 | +}); |
| 76 | +``` |
| 77 | + |
| 78 | +If the SVG already embeds its own font inline via `@font-face { src: url(data:font/ttf;base64,...) }`, that's used automatically — no `fetchFont` needed. |
| 79 | + |
| 80 | +## Embedding non-JPEG/PNG images |
| 81 | + |
| 82 | +JPEG and PNG are embedded as-is. Any other format (WebP, for example) needs converting to PNG first — this happens automatically via `OffscreenCanvas` in a browser/Worker, but that API doesn't exist in plain Node.js. In Node, supply your own `normalizeImage` function (for example, using `sharp`): |
| 83 | + |
| 84 | +```ts |
| 85 | +import sharp from 'sharp'; |
| 86 | + |
| 87 | +await embedSvgInPdf(doc, { |
| 88 | + svgText, |
| 89 | + rotation: 0, |
| 90 | + normalizeImage: async (bytes, mimeType) => sharp(bytes).png().toBuffer(), |
| 91 | +}); |
| 92 | +``` |
| 93 | + |
| 94 | +`@svg-pdf/libpdf` deliberately doesn't bundle `sharp` or any other Node-only image library itself, to stay lightweight and runnable unmodified in a browser. |
| 95 | + |
| 96 | +## Using the parser standalone |
| 97 | + |
| 98 | +If you just want the parsed, structured representation of an SVG — without turning it into a PDF — use [`@svg-pdf/core`](https://www.npmjs.com/package/@svg-pdf/core) directly via its `parseSvgDocument` function. |
| 99 | + |
| 100 | +## What's supported |
| 101 | + |
| 102 | +Shapes and grouping, gradients and `<pattern>`, `<marker>`, `<clipPath>`, fills/strokes/blending, real CSS selector matching inside `<style>`, best-effort `<text>` including `<textPath>`, `<image>`, and `<a href>` link regions. The [supported-features doc](https://github.com/delylabs/svg-pdf/blob/main/docs/supported-features.md) in the repo has the full, up-to-date list of what's supported, what's out of scope and why, and what's not supported yet. |
| 103 | + |
| 104 | +## License |
| 105 | + |
| 106 | +MIT. |
0 commit comments