GSoC 2026: Feature-Complete SVG Import and Rendering Support #4183
jsjgdh
started this conversation in
Student Project Reports
Replies: 1 comment
Week 1
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hi there,
I'm Kulratan (@jsjgdh ), and I worked with Graphite as a Google Summer of Code (GSoC) 2026 contributor. My project was titled "Complete SVG Import and Rendering Support", focused on expanding Graphite's SVG pipeline into a standards-compliant system that imports, edits, and renders complex SVG files faithfully.
Introduction
SVG is the standard interchange format for 2D vector assets on the web. Graphite already had a modern node-based non-destructive architecture, but importing real-world SVG files often meant lost formatting, missing styles, unhandled gradient mappings, dropped markers, and no support for specialized layout like text following curves.
The goal of this GSoC project was to close those gaps by implementing missing SVG specification features, adding new procedural node capabilities to Graphite's computational node graph, and making round-tripping between SVG and Graphite's internal representation lossless in both directions.
Benefits
fx/fy/fr), and angular gradients, plusgradientUnits(userSpaceOnUsevsobjectBoundingBox) support.Final Report
The project went well overall, closing many of the fidelity gaps between real-world SVG files and Graphite's internal representation. Text can flow along curves, gradients survive a round-trip through import and export without being flattened, arrowheads and decorations no longer vanish on import, and embedded bitmaps land in the layer tree exactly where the SVG says they should. Below is a walkthrough of the six contributions made throughout GSoC 2026.
Unified SVG 2 gradient model & gradient units (2PRs)
When this work started, Graphite stored a gradient's shape as an enum: linear or radial, nothing else. SVG radial gradients with a focal circle (
fx,fy,fr) were silently flattened during import because the focal data had nowhere to live, so every imported radial gradient collapsed to a plain center-focused one.The unified two-circle model
Before any code was written, my mentor Keavon designed the representation this project would adopt. His insight was that every gradient SVG can express is a subset of one simple description: a transform (which Graphite already had), a curvature of circle A, a curvature of circle B, and one more boolean for angular/conic/sweep gradients. Only 2–3 scalar parameters describe any gradient, so no enum is needed to pick between types at all:
1/radius, so a line (infinite radius) has curvature 0. Two zero-curvature circles are exactly a linear gradient.fx/fy/fr) exactly.angularflag marks conic gradients, which SVG cannot represent yet. Those render nothing today, but the model is a useful superset: once shader-based rasterization lands, Graphite will support even more gradients than SVG does.Since node attributes have defaults when absent, gradients that don't specify curvatures are automatically read as 0, meaning linear gradients (the most common form) need zero extra storage.
My mentor Keavon also specified the subtlety that makes the model practical. Raw curvature lives in
[-inf, inf], which is hostile to UI sliders and animation, so instead of storing it directly, the value gets reparameterized with an arctangent mapping,atan(curvature) · 2/pie, compressing the range onto[-1, 1]: a line maps to 0, a unit circle to 0.5, a point to 1. Because the range is finite, a gradient can be animated smoothly between linear and radial, something an enum could never allow.My work on this PR was turning that design into reality across Graphite: extending the gradient data structures with the geometry fields and their serialization, threading the two-circle parameters through every conversion between ramps, items, and attributes; deriving the linear/radial form from the geometry in the renderer and SVG exporter instead of reading an enum attribute; making focal data (
fx/fy/fr) round-trip without loss; and keeping all of it compiling and tested while master underwent major refactors of the paint and rendering architecture mid-project.Focal gradient round-tripping
With the model in place, importing
<radialGradient fx=... fy=... fr=...>became lossless. The focal center rides as a whole-gradient attribute, and the focal radiusfr(relative to the outer radius) derives circle A's curvature. Exporting walks the same path in reverse: unit-spacefx/fy/frattributes are emitted and the placement travels in thegradientTransform. That encoding matters, and I checked it against the SVG 2 specification: withgradientUnits="userSpaceOnUse"the focal coordinates are interpreted after applyinggradientTransform, confirming the chosen representation.One bug slipped in along the way. The first version baked geometry attributes onto every imported radial gradient, which accidentally froze the properties panel's Linear/Radial switch for imported files. The fix was to bake focal geometry only when the SVG actually carries focal data (
fr > 0or an off-center focal point); plain radials stay enum-free, so the form input keeps governing them and switching forms remains live.Gradient units import & export
Added full support for both coordinate systems:
userSpaceOnUse: gradient coordinates resolve in the user space at the point of reference, unaffected by the target geometry's aspect ratio.objectBoundingBox: coordinates map onto the bounding box of the shape, so a gradient stretches with non-square geometry, including the matrix math of converting between the two spaces during both import and export.W3C SVG 2 compliant text on path
Probably the hardest single feature of the project: full text-on-path support matching the SVG 2 specification.
The core difficulty is that text must traverse a curve at uniform speed, but Bezier parameter
tdoes not correspond to arc length. Movingtlinearly makes glyphs bunch up and stretch out. To solve this I built an arc-length lookup table (ArcLengthLut) overkurbo::BezPathsegments, sampling each curve segment with adaptive tolerance and building a mapping from distance-along-curvesto the exact(segment, t)parameter pair, from which instantaneous tangents and normals are computed.Glyph placement sits on top of that, integrated with the
parleyandskrifatext layout engines: each glyph is positioned along the baseline curve, rotated by the local tangent angle, and shifted by its offsets and baseline adjustments per the SVG spec. The full set of SVG text-path control attributes got proper enum implementations:side(left/right of the path direction),text-anchor(start/middle/end along the curve),method(align vs stretch),spacing(exact vs auto), andlengthAdjust(spacing vs spacing-and-glyphs). The document value types and graph-operation handlers were extended as well, so stroke, fill, color ramps, and gradient attributes survive text-on-path import and export rather than being stripped.chrome_101vXxoECA.mp4
Procedural "Attach Markers" node
In SVG, path endpoints and vertices can carry arrowheads or decorative ornaments via the
<marker>element. Rather than hard-coding this into the importer, the cleaner home for the capability in Graphite is a procedural node, so I created Attach Markers in the Repeat category, usable on any vector data regardless of where it came from.The geometry question is how a marker should orient where two segments meet. Snapping to either segment's direction looks wrong at corners; the node extracts per-vertex records containing position plus incoming and outgoing tangents, then computes the normalized bisector angle so markers rotate smoothly across both sharp corners and smooth turns. Placement is controlled with start/mid/end checkboxes,
scale,auto-orient(follow path direction vs fixed rotation), and an angle offset. Thanks to Graphite's generic node macros, the artwork attached at each transform can be anything: vectors, rasters, or simple colored primitives, which makes the node more flexible than SVG's own marker element.SVG text decorations
Standard typography leans heavily on underlines, overlines, and strikethroughs, and naive implementations draw them at guessed positions, which looks wrong across fonts. This implementation reads exact typographic metrics instead of guessing: each glyph run exposes its font's designed values through the layout engine's run metrics (underpinned by
parley's font stack), so underlines sit at the font's own underline offset with its underline thickness, overlines at the ascent height, and strikethroughs at the strikeout offset and size. The three booleans were added to the Text node as regular node inputs, which surfaces them as checkboxes in the properties panel automatically. Decoration geometry generation hooks into the path builder and to-path conversion so decorations inherit the parent text's fill, stroke, and transforms, and round-trip through SVG import/export.chrome_lZ4mTDDGYN.mp4
Embedded raster image import
SVGs frequently embed bitmap previews, textures, and photos as Base64 data URIs inside
<image>elements. Previously, these simply vanished on import. The SVG import pipeline now decodes embedded rasters across PNG, JPEG, GIF, and WebP, registers them in Graphite's document resource storage, and instantiates them as image nodes with affine transforms correctly composed from the declared width/height and the SVG viewport matrix.chrome_bRhjT5Juk6.mp4
What I took away from the project
kurbo,glam, andparley.graphenenode graph, designing nodes that operate generically over vector and graphic primitives.Future Work
I look forward to continuing as an active contributor to Graphite.
Acknowledgements
Thank you to my mentors and the entire Graphite team for their guidance, thorough code reviews, and support throughout this Google Summer of Code!
All reactions