Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
c347c4e
Backdrop fork: altered-version notice, session rules, roadmap
sakamura Oct 8, 2026
daea987
Detect reference cycles for use, mask and pattern; cap reference depth
claude Oct 8, 2026
6d53eba
Fix reference cycle test compilation
claude Oct 8, 2026
424dcd1
SD1: render stroke-dasharray and stroke-dashoffset
claude Oct 8, 2026
7e7a9e4
SD4: tolerant parsing — bad attribute values drop the attribute, not …
claude Oct 8, 2026
681da61
SD5: complete <pattern> support
claude Oct 8, 2026
40be7cf
SD4: qualify XMLParser in tests (Foundation.XMLParser clashes on Appl…
claude Oct 8, 2026
0fa109f
SD2: render feGaussianBlur through an offscreen filter layer
claude Oct 8, 2026
647d2fa
SD2: tests and sample for feGaussianBlur
claude Oct 8, 2026
93b3f79
SD2: split box size expression for the type checker
claude Oct 8, 2026
dbc2c63
Roadmap: wave 1 in review, SD15 added
sakamura Oct 8, 2026
303b425
Drop <use> of an ancestor at the first use; tighten depth cap, add no…
claude Oct 8, 2026
1002717
SD1: reset dash after stroke, reject non-finite values, more tests
claude Oct 8, 2026
11f2810
SD5 review: tolerant units, viewport percentages, degenerate transforms
claude Oct 8, 2026
086d008
SD4: integrator review fixes
claude Oct 8, 2026
2582f78
SD2: integrator review fixes
claude Oct 8, 2026
0ac7bd9
Merge pull request #4: SD5 complete pattern support
sakamura Oct 8, 2026
e25d937
Roadmap: SD5 merged
sakamura Oct 8, 2026
52218de
Merge remote-tracking branch 'origin/main' into sd3-reference-cycles
claude Oct 8, 2026
136287e
Merge main; bound pattern href chains with ReferenceGuard; pattern hr…
claude Oct 8, 2026
1e46fb1
Merge pull request #1: SD3 reference cycles never crash
sakamura Oct 8, 2026
80a21eb
Roadmap: SD3 merged
sakamura Oct 8, 2026
bb859d8
Merge pull request #3: SD4 tolerant parsing
sakamura Oct 8, 2026
0ad4ffe
Merge pull request #2: SD1 stroke-dasharray and stroke-dashoffset
sakamura Oct 8, 2026
983e027
SD1 follow-up: reset the dash after a radial-gradient stroke
sakamura Oct 8, 2026
111c801
Roadmap: SD1 and SD4 merged, wave 2 partly open
sakamura Oct 8, 2026
183fd79
Merge remote-tracking branch 'origin/main' into sd2-gaussian-blur
claude Oct 8, 2026
977b7de
SD2: second integrator review fixes
claude Oct 8, 2026
fac1594
SD15: skip an invalid element instead of failing the document
claude Oct 8, 2026
a4a0c5a
SD15: spell out key paths for Xcode 16.2
claude Oct 8, 2026
b7cf5a5
SD9: preserveAspectRatio on root svg, nested svg and image
claude Oct 8, 2026
0d9b250
SD15: qualify XMLParser in tests (ambiguous with Foundation on macOS)
claude Oct 8, 2026
3d0b9a7
SD9: compare pixels with an explicit red in the CoreGraphics tests
claude Oct 8, 2026
9d76868
SD6: gradients to spec
claude Oct 8, 2026
b1f09d8
SD10: CSS cascade with selectors, specificity and tolerant sheets
claude Oct 8, 2026
d8f7cca
Merge pull request #5: SD2 feGaussianBlur rendered
sakamura Oct 8, 2026
e375b7a
Roadmap: SD2 merged, wave 2 in review, SD7 and SD8 open
sakamura Oct 8, 2026
0b95e2e
Merge remote-tracking branch 'origin/main' into sd15-skip-invalid-ele…
claude Oct 8, 2026
55e5fb7
Merge remote-tracking branch 'origin/main' into sd9-preserve-aspect-r…
claude Oct 8, 2026
a619d38
Merge remote-tracking branch 'origin/main' into sd10-css-cascade
claude Oct 8, 2026
ff4a131
Merge remote-tracking branch 'origin/main' into sd6-gradients
claude Oct 8, 2026
0abbd28
SD15: skip bad defs, clipPath, mask, gradient and filter elements; pl…
claude Oct 8, 2026
db236ef
SD7: honour display and visibility, drop foreign-namespace subtrees
claude Oct 8, 2026
316b8aa
SD7: fix raw string literals in tests
claude Oct 8, 2026
e50ee39
SD15: make the stderr log stream stateless (no shared global written …
claude Oct 8, 2026
7412615
SD9: review fixes — root slice clip, degenerate viewports, CGText ima…
claude Oct 8, 2026
1c9ca93
SD7: shapes.svg ns:g subtree is now dropped, update command count
claude Oct 8, 2026
818c1dc
SD15: keep parseGraphicsElements free of extra frames (nested svg rec…
claude Oct 8, 2026
f5bcf24
SD8: clip paths and masks to spec
claude Oct 8, 2026
029e35a
SD6 review: memoized gradients, stop cap, exact radial periods
claude Oct 8, 2026
af85330
SD9: account for the unflipped bitmap in the root slice pixel test
claude Oct 8, 2026
7ae0ff2
SD7: avoid Optional.none ambiguity in clip child display check
claude Oct 8, 2026
6d690ad
SD10 review: cascade fixes, indexed matcher, stop properties
claude Oct 8, 2026
faaf1a3
SD8: tests and sample for clip paths and masks
claude Oct 8, 2026
0000ed3
SD15: keep the recursive def walkers' frames small (deep nesting over…
claude Oct 8, 2026
7d0fa17
SD7: treat prefixed elements with an undeclared prefix as foreign on …
claude Oct 8, 2026
feb0ca9
Merge remote-tracking branch 'origin/main' into sd8-clip-mask-units
claude Oct 9, 2026
f998120
SD8: merge main (SD2); the mask region clip shows in the filter order…
claude Oct 9, 2026
0822303
SD7: shapes.svg command count depends on the platform's handling of a…
claude Oct 9, 2026
f827f0b
SD7: shapes.svg command count accepts both XML parser behaviours for …
claude Oct 9, 2026
dec4c81
Merge pull request #8: SD6 gradients to spec
sakamura Oct 9, 2026
98e44f2
Merge pull request #7: SD9 preserveAspectRatio
sakamura Oct 9, 2026
e05a08b
Merge pull request #6: SD15 skip an invalid element, not the document
sakamura Oct 9, 2026
713e692
Roadmap: SD6, SD9 and SD15 merged; SD7, SD8 and SD10 back for fixes; …
sakamura Oct 9, 2026
e87ea91
Merge remote-tracking branch 'origin/main' into sd10-css-cascade
claude Oct 9, 2026
371a04a
Revert "SD7: shapes.svg command count accepts both XML parser behavio…
claude Oct 9, 2026
bb2ccaa
Revert "SD7: shapes.svg command count depends on the platform's handl…
claude Oct 9, 2026
70362d7
Revert "SD7: treat prefixed elements with an undeclared prefix as for…
claude Oct 9, 2026
892fbb8
Merge origin/main into sd7-display-visibility
claude Oct 9, 2026
dbf21cb
Merge remote-tracking branch 'origin/main' into sd8-clip-mask-units
claude Oct 9, 2026
0c25127
SD7: shapes.svg declares xmlns:ns as SVG, expect 165 commands
claude Oct 9, 2026
f6c5c96
SD8: integrator review round 1
claude Oct 9, 2026
b8aa11d
SD10 third review: SF Symbol layer classes, counted matcher tests
claude Oct 9, 2026
bac0eda
SD7: display ignored on mask, hidden clipPath clips everything, skip …
claude Oct 9, 2026
0c31a83
SD8: bound bbox measurement and clip-path nesting (integrator)
sakamura Oct 9, 2026
d521b91
SD7: keep unclippable clip children, real visibility tests (integrator)
sakamura Oct 9, 2026
602d09e
Merge pull request #9: SD10 CSS cascade
sakamura Oct 9, 2026
337bef8
Merge pull request #10: SD7 display and visibility
sakamura Oct 9, 2026
1130a09
Merge pull request #11: SD8 clip paths and masks to spec
sakamura Oct 9, 2026
e70a69d
Roadmap: SD10, SD7 and SD8 merged, wave 2 complete; SD16 follow-ups
sakamura Oct 9, 2026
f2986cb
Roadmap: wave 3 priorities — SD16 and SD13 next, SD12 optional, SD11 …
sakamura Oct 9, 2026
1ded1e0
Roadmap: SD16 leads with two rendering bugs found against FreeSVG pre…
sakamura Oct 9, 2026
abbc94f
SD16: reset tracked fill on pattern fills; parse and resolve paint fa…
claude Oct 9, 2026
bd72415
SD13: units and percentages on geometry, fractional root sizes
claude Oct 9, 2026
c9a801f
SD13: tests, sample, absolute units on stroke-width
claude Oct 9, 2026
c7b26c4
SD13: macOS SVG size test expects em/ex resolved against font-size
claude Oct 9, 2026
4ca027b
SD16: SD6 follow-ups (gradient id index, linear periods, mixed colour…
claude Oct 9, 2026
d5af73b
SD16: SD9 follow-ups (root clip-path with slice, pattern fit overflow…
claude Oct 9, 2026
d54dedb
SD16: SD10 follow-ups (functional pseudo-class after SF class, open q…
claude Oct 9, 2026
5db2700
SD16: SD7, SD8 and SD15 follow-ups (hidden leaf, display:none root, c…
claude Oct 9, 2026
c55297c
SD16: filter + mask-type clip pixel test uses a blurred bar (feFlood …
claude Oct 9, 2026
394179c
Merge pull request #12: SD16 paint fallbacks and follow-ups
sakamura Oct 9, 2026
bc6ad10
Merge pull request #13: SD13 units and nested svg parsing
sakamura Oct 9, 2026
b66e422
roadmap: SD16 and SD13 merged
sakamura Oct 9, 2026
3062c88
SD12: filter primitives beyond blur
claude Oct 9, 2026
93b3cb6
SD12: faster pixel kernels; Android and Windows math
claude Oct 9, 2026
be9bf84
SD12: floods under -O, primary tree cap, release CI
sakamura Oct 9, 2026
69a9fa5
Merge pull request #14: SD12 filter primitives beyond blur
sakamura Oct 9, 2026
5f60a24
roadmap: SD12 merged; run the tests optimized too
sakamura Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage_report.lcov

# Apps ship optimized builds: SD12's floods were transparent under -O while every Debug job passed
xcode_release:
runs-on: macos-26
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select the newest Xcode
run: echo "DEVELOPER_DIR=$(ls -d /Applications/Xcode_*.app | sort -V | tail -1)/Contents/Developer" >> "$GITHUB_ENV"
- name: Version
run: swift --version
- name: Build
run: swift build -c release --build-tests -Xswiftc -enable-testing
- name: Test
run: swift test -c release --skip-build -Xswiftc -enable-testing

xcode_26_2:
runs-on: macos-26
env:
Expand Down
233 changes: 233 additions & 0 deletions Backdrop/ROADMAP.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# CLAUDE.md — Misoservices/SwiftDraw (Backdrop's fork)

This is a fork of [swhitty/SwiftDraw](https://github.com/swhitty/SwiftDraw) (zlib licence) kept by Misoservices
for **Backdrop / MisoScape**, an Apple-platform app that draws user-placed SVG motifs through SwiftDraw's
CoreGraphics renderer. The fork exists to render the SVG that real editors (Inkscape, Illustrator, Figma) emit
and that SwiftDraw 0.29.0 drops or draws wrong. The work list is **[Backdrop/ROADMAP.md](Backdrop/ROADMAP.md)**;
every session takes exactly one item from it.

## Build and test

- `swift build --build-tests && swift test --skip-build` — works on Linux (Swift 6.0–6.3) and macOS.
- **Also run the tests optimized:** `swift test -c release -Xswiftc -enable-testing`. Apps ship Release builds,
and SD12's floods were transparent under `-O` while every Debug job passed; CI's `xcode_release` job runs it.
- **Cloud sessions run on Linux, where CoreGraphics does not exist.** Everything under
`#if canImport(CoreGraphics)` is neither compiled nor tested there. The fork's GitHub Actions (`build.yml`)
runs macOS 26 / Xcode 26.6, four Linux toolchains and Windows on every push: **push your branch early and
treat a green macOS job as the only proof that CoreGraphics code compiles and passes.** If you cannot read the
CI result from the session, say so in the PR description rather than claiming it passed.
- If `swift` is missing, install a Linux toolchain (swiftly, or the swift.org tarball for 6.2+) and report the
exact commands you used in the PR so the environment's setup script can adopt them.

## How the code is laid out (read before changing anything)

- `DOM/Sources/` — XML → `DOM.*` model (`Parser.XML.*.swift`). Platform-independent.
- `SwiftDraw/Sources/LayerTree/` — DOM → `LayerTree` (`LayerTree.Builder*.swift`), then
`LayerTree.CommandGenerator` turns layers into a flat stream of `RendererCommand`s
(`SwiftDraw/Sources/Renderer/Renderer.swift`). Platform-independent and the best place to test.
- Renderers execute commands: `Renderer.CoreGraphics.swift` (the one Backdrop uses), `Renderer.CGText*.swift`
(Swift code generation), `Renderer.SFSymbol*.swift`, `Renderer.SVG.swift`, `Renderer.LayerTree.swift`.
**A new `RendererCommand` case must be handled by every renderer**: implement it for CoreGraphics and CGText;
elsewhere implement it or degrade deliberately (and say which in the PR).
- Tests: `SwiftDraw/Tests/` (XCTest; `Renderer/MockRenderer.swift` records command streams — assert on those so
the test runs on Linux), `DOM/Tests/`. `Samples.bundle/` holds sample SVGs.

## Rules

1. **One roadmap item per branch and PR.** Branch `sd<N>-<slug>` (e.g. `sd1-dasharray`) from `main`; open the PR
against `Misoservices/SwiftDraw` `main`, never against upstream. Keep the diff to the item: no reformatting, no
drive-by refactors, so each PR can later be offered upstream on its own.
2. **Never fail the whole document for one bad attribute.** SwiftDraw today throws on many legal-but-unexpected
values and the entire drawing vanishes. The fork's direction: drop the attribute (use the spec default), keep
the document. Never introduce a new throw path for input an editor could plausibly produce.
3. **Follow the SVG 1.1 spec (SVG 2 where editors already emit it), and cite the section** you implemented in
the PR description. When the spec and browsers disagree, match Chrome/Safari and say so.
4. **Tests are the deliverable.** Every behaviour gets a test asserting on the DOM, the LayerTree or the
`MockRenderer` command stream (Linux-runnable), plus a CoreGraphics test where pixels matter (macOS CI).
Add a small sample SVG to `Samples.bundle/` for each visible feature.
5. Match the surrounding code: 4-space indentation, upstream's naming and file layout, the existing licence
header on new files. Add new enum cases and `switch` arms at the end to keep parallel branches mergeable.
6. **Do not edit `Backdrop/ROADMAP.md`** — several sessions run in parallel and it would conflict. Put status,
decisions, spec citations and anything left undone in the PR description; the integrator updates the roadmap.
7. zlib licence clause 2: altered versions must be plainly marked. Keep the notice at the top of `README.md`;
never remove or alter the licence header of an existing file.
8. Performance matters: Backdrop renders motifs while the user drags sliders. Anything that allocates an offscreen
bitmap (filters) must size it to the filter region in device pixels, not the whole canvas.
5 changes: 4 additions & 1 deletion DOM/Sources/DOM.Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

package extension DOM {

enum Color: Equatable {
enum Color: Hashable {
case none
case currentColor
case keyword(Keyword)
Expand Down Expand Up @@ -189,6 +189,8 @@ package extension DOM {
case whitesmoke
case yellow
case yellowgreen
// CSS Color 4
case rebeccapurple
}
}
}
Expand Down Expand Up @@ -345,6 +347,7 @@ package extension DOM.Color.Keyword {
case .whitesmoke: return (245, 245, 245)
case .yellow: return (255, 255, 0)
case .yellowgreen: return (154, 205, 50)
case .rebeccapurple: return (102, 51, 153)
}
}
}
4 changes: 4 additions & 0 deletions DOM/Sources/DOM.Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ package extension DOM {

package var attributes = PresentationAttributes()
package var style = PresentationAttributes()
// stylesheet declarations matched at parse time; nil for elements built in code
package var matchedStyle: StyleSheet.Matched?
// style="" declarations marked !important
package var importantStyle = PresentationAttributes()
}

final class Line: GraphicsElement {
Expand Down
132 changes: 129 additions & 3 deletions DOM/Sources/DOM.Filter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,141 @@ package extension DOM {
final class Filter: Element {
package var id: String

package var effects: [Effect]
// one per filter primitive, in document order (SD12)
package var primitives: [Primitive]

package var effects: [Effect] {
primitives.map(\.effect)
}

package var x: DOM.Coordinate?
package var y: DOM.Coordinate?
package var width: DOM.Coordinate?
package var height: DOM.Coordinate?
package var filterUnits: Units?
package var primitiveUnits: Units?

package init(id: String) {
self.id = id
self.effects = []
self.primitives = []
}

package enum Effect: Hashable {
case gaussianBlur(stdDeviation: DOM.Float)
// stdDeviationY is nil when stdDeviation has a single value
case gaussianBlur(stdDeviation: DOM.Float, stdDeviationY: DOM.Float? = nil)

// a filter primitive SwiftDraw cannot render yet
case unsupported(name: String)

// Filter Effects 1 §9.18 feOffset, in primitiveUnits
case offset(dx: DOM.Float, dy: DOM.Float)

// Filter Effects 1 §9.13 feFlood: currentColor is resolved while parsing
case flood(color: DOM.Color, opacity: DOM.Float)

// Filter Effects 1 §9.8 feComposite: `in` composited onto `in2`
case composite(CompositeOperator)

// Filter Effects 1 §9.16 feMerge: the inputs are the <feMergeNode> children
case merge

// Filter Effects 1 §9.5 feBlend: `in` blended onto `in2`
case blend(BlendMode)

// Filter Effects 1 §9.6 feColorMatrix
case colorMatrix(ColorMatrix)
}

// A filter primitive with its wiring (Filter Effects 1 §9.2 in / in2 / result) and subregion (§9.4)
package struct Primitive: Hashable {
package var effect: Effect

// `in` then `in2`, or one per <feMergeNode>; nil where the attribute is missing
package var inputs: [Input?]
package var result: String?

// primitive subregion in primitiveUnits: user units, or fractions of the bounding box;
// nil takes the default subregion
package var x: DOM.Coordinate?
package var y: DOM.Coordinate?
package var width: DOM.Coordinate?
package var height: DOM.Coordinate?

// the computed color-interpolation-filters, inherited from the <filter> and its ancestors
package var colorInterpolation: ColorInterpolation

package init(effect: Effect,
inputs: [Input?] = [],
result: String? = nil,
colorInterpolation: ColorInterpolation = .linearRGB) {
self.effect = effect
self.inputs = inputs
self.result = result
self.colorInterpolation = colorInterpolation
}
}

package enum Input: Hashable {
case sourceGraphic
case sourceAlpha
case backgroundImage
case backgroundAlpha
case fillPaint
case strokePaint
case result(String)
}

package enum CompositeOperator: Hashable {
case over
case `in`
case out
case atop
case xor
// Filter Effects 1: the sum of both inputs
case lighter
// result = k1·i1·i2 + k2·i1 + k3·i2 + k4 on premultiplied values
case arithmetic(k1: DOM.Float, k2: DOM.Float, k3: DOM.Float, k4: DOM.Float)
}

// Compositing and Blending 1 §10 blend modes
package enum BlendMode: String, Hashable {
case normal
case multiply
case screen
case overlay
case darken
case lighten
case colorDodge = "color-dodge"
case colorBurn = "color-burn"
case hardLight = "hard-light"
case softLight = "soft-light"
case difference
case exclusion
case hue
case saturation
case color
case luminosity
}

package enum ColorMatrix: Hashable {
// 20 values, row-major 4x5; any other count is a pass through
case matrix([DOM.Float])
case saturate(DOM.Float)
// degrees
case hueRotate(DOM.Float)
case luminanceToAlpha
}

// Filter Effects 1 §10 color-interpolation-filters
package enum ColorInterpolation: String, Hashable {
case auto
case sRGB
case linearRGB
}

package enum Units: String {
case userSpaceOnUse
case objectBoundingBox
}
}
}
1 change: 1 addition & 0 deletions DOM/Sources/DOM.Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ package extension DOM {

package var x: Coordinate?
package var y: Coordinate?
package var preserveAspectRatio: PreserveAspectRatio?

package init(href: URL) {
self.href = href
Expand Down
11 changes: 9 additions & 2 deletions DOM/Sources/DOM.LinearGradient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ package extension DOM {

package var stops: [Stop]
package var gradientUnits: Units?
package var gradientTransform: [Transform]
package var gradientTransform: [Transform]?
package var spreadMethod: SpreadMethod?

//references another LinearGradient element id within defs
package var href: URL?

package init(id: String) {
self.id = id
self.stops = []
self.gradientTransform = []
}

package struct Stop: Equatable {
Expand Down Expand Up @@ -83,4 +83,11 @@ package extension DOM.LinearGradient {
case userSpaceOnUse
case objectBoundingBox
}

// SVG 1.1 §13.2.2
enum SpreadMethod: String {
case pad
case reflect
case `repeat`
}
}
14 changes: 11 additions & 3 deletions DOM/Sources/DOM.Pattern.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,23 @@ package extension DOM {
package var id: String
package var x: Coordinate?
package var y: Coordinate?
package var width: Coordinate
package var height: Coordinate
package var width: Coordinate?
package var height: Coordinate?

package var patternUnits: Units?
package var patternContentUnits: Units?

package var childElements: [DOM.GraphicsElement] = []

package init(id: String, width: Coordinate, height: Coordinate) {
package var href: URL?
package var viewBox: DOM.SVG.ViewBox?
package var preserveAspectRatio: DOM.PreserveAspectRatio?
package var patternTransform: [Transform]?
/// Names of the x / y / width / height attributes written as percentages; their value
/// is stored as a fraction and resolved against the viewport under userSpaceOnUse.
package var percentageAttributes: Set<String> = []

package init(id: String, width: Coordinate? = nil, height: Coordinate? = nil) {
self.id = id
self.width = width
self.height = height
Expand Down
Loading