|
| 1 | +# SVG Path Support |
| 2 | + |
| 3 | +## Problem Statement |
| 4 | + |
| 5 | +Users want to draw SVG path data onto PDF pages. A common use case is sewing patterns and technical drawings stored as SVG that need to be rendered to PDF. |
| 6 | + |
| 7 | +**User request from Reddit:** |
| 8 | + |
| 9 | +> I am currently using both PDFKit and pdfjs to create printable sewing patterns from SVG data. I currently have to take all my SVG path data, put it into an A0 PDF, load that PDF into a canvas element, then chop up the canvas image data into US letter sizes. |
| 10 | +
|
| 11 | +## Goals |
| 12 | + |
| 13 | +1. Parse SVG path `d` attribute strings and render them via `PathBuilder` |
| 14 | +2. Support all SVG path commands (M, L, H, V, C, S, Q, T, A, Z) in both absolute and relative forms |
| 15 | +3. Integrate cleanly with existing `PathBuilder` API |
| 16 | +4. Flip the Y-axis by default so raw SVG coordinates map correctly into PDF space (with an opt-out) |
| 17 | + |
| 18 | +## Non-Goals |
| 19 | + |
| 20 | +- Full SVG document parsing (elements like `<text>`, `<image>`, `<use>`, CSS, filters) |
| 21 | +- SVG transforms (users can apply PDF transforms separately) |
| 22 | +- SVG units conversion (assume unitless = points, like the rest of our API) |
| 23 | +- Page tiling/splitting (users handle this themselves with our primitives) |
| 24 | + |
| 25 | +## Scope |
| 26 | + |
| 27 | +**In scope:** |
| 28 | + |
| 29 | +- SVG path `d` string parser |
| 30 | +- All path commands: M, m, L, l, H, h, V, v, C, c, S, s, Q, q, T, t, A, a, Z, z |
| 31 | +- Arc-to-bezier conversion for the `A` command |
| 32 | +- `PathBuilder.appendSvgPath()` instance method |
| 33 | +- `PDFPage.drawSvgPath()` convenience method |
| 34 | + |
| 35 | +**Out of scope:** |
| 36 | + |
| 37 | +- Helper to extract paths from SVG documents (maybe later as a separate utility) |
| 38 | +- Viewbox/coordinate system transforms |
| 39 | +- Stroke/fill style parsing from SVG attributes |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Desired Usage |
| 44 | + |
| 45 | +### Basic: Draw SVG path data |
| 46 | + |
| 47 | +```typescript |
| 48 | +// Convenience method on PDFPage - fill by default |
| 49 | +page.drawSvgPath("M 10 10 L 100 10 L 100 100 Z", { |
| 50 | + color: rgb(1, 0, 0), |
| 51 | +}); |
| 52 | + |
| 53 | +// With stroke |
| 54 | +page.drawSvgPath("M 10 10 C 20 20, 40 20, 50 10", { |
| 55 | + borderColor: rgb(0, 0, 0), |
| 56 | + borderWidth: 2, |
| 57 | +}); |
| 58 | +``` |
| 59 | + |
| 60 | +### Using PathBuilder for more control |
| 61 | + |
| 62 | +```typescript |
| 63 | +// When you need to choose fill vs stroke explicitly |
| 64 | +page |
| 65 | + .drawPath() |
| 66 | + .appendSvgPath("M 10 10 L 100 10 L 100 100 Z") |
| 67 | + .stroke({ borderColor: rgb(0, 0, 0) }); |
| 68 | +``` |
| 69 | + |
| 70 | +### Chaining with existing PathBuilder methods |
| 71 | + |
| 72 | +```typescript |
| 73 | +page |
| 74 | + .drawPath() |
| 75 | + .moveTo(0, 0) |
| 76 | + .appendSvgPath("l 50 50 c 10 10 20 20 30 10") // relative commands continue from current point |
| 77 | + .lineTo(200, 200) |
| 78 | + .close() |
| 79 | + .stroke(); |
| 80 | +``` |
| 81 | + |
| 82 | +### Complex paths (sewing patterns, icons) |
| 83 | + |
| 84 | +```typescript |
| 85 | +// Heart shape |
| 86 | +page |
| 87 | + .drawPath() |
| 88 | + .appendSvgPath("M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30 Q 90,60 50,90 Q 10,60 10,30 Z") |
| 89 | + .fill({ color: rgb(1, 0, 0) }); |
| 90 | + |
| 91 | +// Multiple subpaths |
| 92 | +page |
| 93 | + .drawPath() |
| 94 | + .appendSvgPath("M 0 0 L 100 0 L 100 100 L 0 100 Z M 25 25 L 75 25 L 75 75 L 25 75 Z") |
| 95 | + .fill({ windingRule: "evenodd" }); // Creates a square with a square hole |
| 96 | +``` |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Architecture |
| 101 | + |
| 102 | +``` |
| 103 | +┌─────────────────────────────────────────────────────────────┐ |
| 104 | +│ PathBuilder.fromSvgPath() / .appendSvgPath() │ |
| 105 | +│ (Entry points - high-level API) │ |
| 106 | +├─────────────────────────────────────────────────────────────┤ |
| 107 | +│ src/svg/path-parser.ts │ |
| 108 | +│ (Parse d string → command objects) │ |
| 109 | +├─────────────────────────────────────────────────────────────┤ |
| 110 | +│ src/svg/path-executor.ts │ |
| 111 | +│ (Execute commands via callback interface) │ |
| 112 | +│ - Handles relative → absolute conversion │ |
| 113 | +│ - Handles smooth curve reflection │ |
| 114 | +│ - Handles arc → bezier conversion │ |
| 115 | +├─────────────────────────────────────────────────────────────┤ |
| 116 | +│ PathBuilder (existing) │ |
| 117 | +│ (moveTo, lineTo, curveTo, quadraticCurveTo, close) │ |
| 118 | +└─────────────────────────────────────────────────────────────┘ |
| 119 | +``` |
| 120 | + |
| 121 | +The `src/svg/` module is intentionally decoupled from `PathBuilder`. The executor takes a callback interface, so it can drive any path-building target (PathBuilder, canvas, testing, etc.). |
| 122 | + |
| 123 | +### New Files |
| 124 | + |
| 125 | +| File | Purpose | |
| 126 | +| -------------------------- | ---------------------------------------------------------------------------------- | |
| 127 | +| `src/svg/path-parser.ts` | Tokenize and parse SVG path `d` strings into command objects | |
| 128 | +| `src/svg/path-executor.ts` | Execute parsed commands with state tracking (relative coords, smooth curves, arcs) | |
| 129 | +| `src/svg/arc-to-bezier.ts` | Arc endpoint → center parameterization and bezier approximation | |
| 130 | +| `src/svg/index.ts` | Public exports | |
| 131 | + |
| 132 | +### Modified Files |
| 133 | + |
| 134 | +| File | Changes | |
| 135 | +| --------------------------------- | --------------------------------------- | |
| 136 | +| `src/api/drawing/path-builder.ts` | Add `appendSvgPath()` instance method | |
| 137 | +| `src/api/pdf-page.ts` | Add `drawSvgPath()` convenience method | |
| 138 | +| `src/index.ts` | Export svg utilities for advanced users | |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +## SVG Path Command Reference |
| 143 | + |
| 144 | +| Command | Parameters | Description | PathBuilder equivalent | |
| 145 | +| ------- | ------------------------------- | ---------------- | --------------------------------------------- | |
| 146 | +| M/m | x y | Move to | `moveTo(x, y)` | |
| 147 | +| L/l | x y | Line to | `lineTo(x, y)` | |
| 148 | +| H/h | x | Horizontal line | `lineTo(x, currentY)` | |
| 149 | +| V/v | y | Vertical line | `lineTo(currentX, y)` | |
| 150 | +| C/c | x1 y1 x2 y2 x y | Cubic bezier | `curveTo(...)` | |
| 151 | +| S/s | x2 y2 x y | Smooth cubic | Reflect last CP, then `curveTo(...)` | |
| 152 | +| Q/q | x1 y1 x y | Quadratic bezier | `quadraticCurveTo(...)` | |
| 153 | +| T/t | x y | Smooth quadratic | Reflect last CP, then `quadraticCurveTo(...)` | |
| 154 | +| A/a | rx ry angle large-arc sweep x y | Elliptical arc | Convert to bezier curves | |
| 155 | +| Z/z | (none) | Close path | `close()` | |
| 156 | + |
| 157 | +**Lowercase = relative coordinates** (offset from current point) |
| 158 | +**Uppercase = absolute coordinates** |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Test Plan |
| 163 | + |
| 164 | +### Unit Tests |
| 165 | + |
| 166 | +**Parser tests (`src/svg/path-parser.test.ts`):** |
| 167 | + |
| 168 | +- Basic commands: M, L, H, V, C, Q, Z |
| 169 | +- Relative commands: m, l, h, v, c, q, z |
| 170 | +- Smooth curves: S, s, T, t |
| 171 | +- Arcs: A, a (various flag combinations) |
| 172 | +- Number formats: integers, decimals, negative, scientific notation |
| 173 | +- Whitespace variations: spaces, commas, no separators |
| 174 | +- Repeated commands (implicit repetition) |
| 175 | +- Invalid input handling (malformed paths) |
| 176 | + |
| 177 | +**Executor tests (`src/svg/path-executor.test.ts`):** |
| 178 | + |
| 179 | +- Relative to absolute conversion |
| 180 | +- Smooth curve control point reflection |
| 181 | +- Arc to bezier conversion accuracy |
| 182 | +- State tracking across commands |
| 183 | + |
| 184 | +**Arc conversion tests (`src/svg/arc-to-bezier.test.ts`):** |
| 185 | + |
| 186 | +- Various arc flag combinations (large-arc, sweep) |
| 187 | +- Degenerate cases (zero radii, same start/end point) |
| 188 | +- Accuracy of bezier approximation |
| 189 | + |
| 190 | +### Integration Tests |
| 191 | + |
| 192 | +**PathBuilder integration:** |
| 193 | + |
| 194 | +- `fromSvgPath()` produces correct operators |
| 195 | +- `appendSvgPath()` continues from current point |
| 196 | +- Chaining with other PathBuilder methods |
| 197 | +- Complex real-world paths (icons, shapes) |
| 198 | + |
| 199 | +### Visual Tests |
| 200 | + |
| 201 | +- Generate PDFs with various SVG paths |
| 202 | +- Compare with SVG rendered in browser |
| 203 | +- Test paths from real-world sources (Font Awesome icons, map data) |
| 204 | + |
| 205 | +### Edge Cases |
| 206 | + |
| 207 | +- Empty path string |
| 208 | +- Path with only M command (no drawing) |
| 209 | +- Very large coordinates |
| 210 | +- Very small arc radii (degenerate to line) |
| 211 | +- Zero-length arcs |
| 212 | +- Arcs with rx=0 or ry=0 (should become lines per SVG spec) |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +## Open Questions |
| 217 | + |
| 218 | +1. **Error handling**: Should malformed paths throw or silently skip bad commands? |
| 219 | + |
| 220 | +- **Recommendation**: Skip bad commands with console warning, continue parsing. Matches browser behavior. |
| 221 | + |
| 222 | +2. **Coordinate precision**: Should we round coordinates? |
| 223 | + |
| 224 | +- **Recommendation**: No rounding, preserve full precision. PDF handles it fine. |
| 225 | + |
| 226 | +--- |
| 227 | + |
| 228 | +## Future Enhancements (Not in this plan) |
| 229 | + |
| 230 | +- `parseSvgPaths(svgDocument: string)`: Extract `<path>` elements with basic styles |
| 231 | +- Transform parsing (`transform` attribute) |
| 232 | +- Style extraction (`fill`, `stroke`, `stroke-width` attributes) |
| 233 | +- Support for other SVG shape elements (`<rect>`, `<circle>`, `<ellipse>`, `<polygon>`) |
0 commit comments