Skip to content

Added missing features to SwiftDraw - #142

Open
sakamura wants to merge 99 commits into
swhitty:mainfrom
Misoservices:main
Open

sakamura wants to merge 99 commits into
swhitty:mainfrom
Misoservices:main

Conversation

@sakamura

@sakamura sakamura commented Oct 9, 2026

Copy link
Copy Markdown

Good day, awesome developers! I had to use an agent to make a dozen upgrades to the code. Now I strictly do not recommend you merge all the files as-is! These are agent-based, and very little human look got done, it would not be good practice. It also adds up some internal roadmap and reasons, and internal project names (I don't mind them being public), so a healthy housekeeping is really required.

But this is a really good starting point for all the features.

  • SD1 — stroke-dasharray and stroke-dashoffset
  • SD2 — feGaussianBlur rendered
  • SD3 — reference cycles never crash
  • SD4 — tolerant parsing: a bad value drops the attribute, not the document (and SD15 — an invalid element is skipped, not the document)
  • SD5 — patterns complete
  • SD6 — gradients to spec
  • SD7 — display and visibility
  • SD8 — clip paths and masks to spec
  • SD9 — preserveAspectRatio
  • SD10 — CSS cascade
  • SD16 1. LayerTree.CommandOptimizer.filterStateCommand does not treat .setFillPattern as changing the fill
  • SD16 2. A paint with a fallback, fill="url(#g) #000000" (also style=, url(#g) none; Inkscape writes it), fails to parse
  • SD12 — filter primitives beyond blur
  • SD13 — units and percentages on all geometry

I might continue to improve my branch, so you can look it up, but it should be relatively stable from this point on.

Thank you very much for providing your code in a liberal license! Happy continuation!

sakamura and others added 30 commits October 8, 2026 10:55
Marks this fork as an altered version (zlib clause 2), adds CLAUDE.md for
cloud sessions and Backdrop/ROADMAP.md: 14 items (SD1-SD14) in three waves,
from Backdrop's measured gap catalogue against 0.29.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014secrjJFVQjHWzLji64mzq
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BMaJqhjWrmunNiNcMMuJQD
…the document

hsl()/hsla(), case-insensitive keywords, rebeccapurple, 4/8-digit hex with alpha,
!important, unit-bearing transforms, opacity clamping, SVG 2 plain href on image,
gradients and anchors; invalid presentation attributes are ignored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX57MfEEMwiafw7LcjGooN
Honour x/y, patternUnits (default objectBoundingBox), patternContentUnits,
viewBox (xMidYMid meet) and patternTransform, and inherit attributes and
content along the href / xlink:href chain with cycle protection. A pattern
without width/height (or an id) no longer fails the document: it paints
nothing, per SVG 1.1 §13.3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oF1EUUqMrqHSnUp3s5qPW
…e platforms)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX57MfEEMwiafw7LcjGooN
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VTWP7xSKgyh1SgNiikyvi
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VTWP7xSKgyh1SgNiikyvi
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VTWP7xSKgyh1SgNiikyvi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de budget; assert on layer tree

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014secrjJFVQjHWzLji64mzq
- Unknown patternUnits / patternContentUnits values drop the attribute
  instead of failing the document.
- Percentages on x/y/width/height resolve against the viewport (viewBox
  size, else width/height) under userSpaceOnUse; they stay bbox fractions
  under objectBoundingBox, including when inherited through href.
- A non-finite or non-invertible patternTransform, or a non-finite tile,
  paints nothing.
- The filled shape's bounding box is computed only when objectBoundingBox
  units need it.
- Tests for each, plus a CoreGraphics pixel test for the Y phase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013oF1EUUqMrqHSnUp3s5qPW
- style tokenizer skips empty/bare declarations instead of failing the document
- gradientTransform none/junk is ignored
- accept '! important' with whitespace
- invalid href falls back to xlink:href
- negative stroke-width dropped
- more tests (hsl edge cases, #rrggbbaa, LayerTree alpha), fix test licence header

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX57MfEEMwiafw7LcjGooN
- CGText isolates filter layers with saveGState/restoreGState and warns when a blur is dropped;
  the optimizer no longer elides state after a filter layer
- clamp and check every conversion from stdDeviation and regions; huge values hit the cap
- empty clip draws and allocates nothing; vImage errors skip the copy; calloc the temp buffer
- over the pixel cap, render the layer at a reduced scale instead of unfiltered
- skip no-op passes, pass-through needs no offscreen, clip to the region once
- deviations follow rotation; vector contexts are rasterized at 2x
- text-only objectBoundingBox blurs draw unfiltered without the unsupported warning
- remove the unused inherited filter state; tests for the above

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VTWP7xSKgyh1SgNiikyvi
x/y, patternUnits, patternContentUnits, patternTransform, viewBox, href
inheritance of attributes and content; degenerate patterns paint nothing.
Reviewed; CI green on 11f2810; Backdrop corpus: 0 regressions, 0 shipping
bank changes, 2 patterned eggs fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ef tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014secrjJFVQjHWzLji64mzq
<use> cycles (including a use of its own ancestor, dropped at the first use
per SVG 1.1 §5.6), mask and pattern href cycles are refused; depth cap 16 and
a 20,000-expansion budget bound the work. Reviewed; CI green on 136287e;
Backdrop corpus: 0 drawings changed, draw time unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bad attribute value drops the attribute, never the document: hsl/hsla,
case-insensitive and CSS named colours, inherit, none for clip-path/mask/
filter, units in transforms, empty values and declarations, !important,
opacity clamp, unknown enums, SVG 2 href; 4/8-digit hex fixed. Reviewed;
CI green on 086d008; Backdrop corpus 0 regressions, 1 newly parsed file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict with SD4 in parsePresentationAttributes resolved by the integrator:
SD1's dash parsing, wrapped in SD4's lenient(). Reviewed; CI green on 1002717;
Backdrop corpus 0 regressions, 3 drawings now dashed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	DOM/Sources/Parser.XML.Element.swift
The review fix put the reset in the radial-gradient fill branch, which never
sets a dash, and left the radial-gradient stroke without one: a stroke-only
shape is the lone push/pop pair the optimizer strips, so the dash leaked into
the caller's context in CGText output. Test covers linear and radial strokes
(fails without the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- groups containing text have unknown bounds: objectBoundingBox filters are dropped, never the text
- clip the offscreen input to the filter region (rotation/skew); the edge is anti-aliased once by the parent
- clamp box sizes per axis to the bitmap dimension
- skip the offscreen when every box size is 1
- PDF smoke test, blur-then-clip pixel test, text and kernel tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VTWP7xSKgyh1SgNiikyvi
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtABiZpyZ4snBX6gF7Etfv
sakamura and others added 24 commits October 8, 2026 21:35
- A <clipPath> holding only a <use> or <text> child clipped everything
  away (both are dropped by makeClipShape, so the shape list was empty);
  the empty-rect clip now applies only when every child is hidden or
  there are none, and such an element draws unclipped as on main.
- testHiddenTextAndImageDrawNothing counted commands, and the
  LayerTreeProvider draws no path for visible text either; it now
  checks the layer tree against a visible control.
- testForeignObjectWithSVGNamespaceChild asserted >= 1 with a sibling
  that alone gives 1; it now pins exactly the sibling.
- New: display does not apply to gradients, <pattern> or <filter>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Selectors (descendant, child, sibling, compound, attribute,
:first-child) with specificity and source order, an indexed and
memoized matcher, tolerant sheets (one bad declaration drops only
itself), and transform, mask and clip-rule from CSS. Head b8aa11d after
its third round (SF Symbol layer classes kept in the export, counted
matcher tests, CSS-wide keywords dropped, single-pass style= parsing);
CI 20/20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
display="none" removes the element and its subtree before any layer
state is built; display is ignored on <mask>, <clipPath>, <pattern>,
gradients and <filter>; visibility parsed and inherited (a child may
show again); hidden <clipPath> children do not contribute, and one
whose children are all hidden clips everything; editor-namespace
subtrees are dropped. Head d521b91: the author's fix round (bac0eda)
plus an integrator commit keeping a <clipPath> that holds only a <use>
or <text> unclipped (bac0eda cleared 76 Noto glyphs that way) and
replacing two tests that could not fail. CI 20/20.

Integrator resolution with SD10: makeLayer computes the cascaded
presentation attributes once, builds the state from them, returns
before makeBaseLayer for display:none, and passes the attributes on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clipPathUnits, maskUnits and maskContentUnits with objectBoundingBox
resolved in the builder, mask regions, <use> and <text> inside a
<clipPath> (text, mixed clip-rules or a clip-path on the <clipPath>
become an alpha mask), clip-path on a <clipPath>, clip-rule taken from
the clip's children, a bad clipPathUnits dropping only the attribute.
Head 0c31a83: the author's round 1 fixes (f6c5c96) plus an integrator
commit bounding bbox measurement (document-wide step budget) and
clip-path nesting (ReferenceGuard.maxDepth). CI 20/20.

Integrator resolutions with SD10 and SD7:
- makeBaseLayer resolves the clip from the cascaded attributes
  (makeClip(id:bounds:)) and passes them to createMaskLayer, which now
  takes the element too (for its bounding box).
- clip members skip hidden visibility as well as display:none (SD7) and
  read their transform from the cascade (SD10).
- the mask state keeps display inline (SD7).
- <clipPath style=""> uses SD10's parseStyleDeclarations.
- SD10's testClipRuleFromStyleSheet and the .holed sample rule move the
  clip-rule onto the clip child, as the roadmap asked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and SD14 set aside

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…views

Pattern fills leave a stale fill colour in the command optimizer, and a
paint with a fallback colour paints black.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llbacks (url(#id) colour)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
Lengths on rect, circle, ellipse, line, use, image, text and nested <svg>
resolve %, px, pt, pc, mm, cm, in, em and ex (SVG 1.1 §7.10): percentages
against the nearest viewport, em/ex against the element's font-size.
Root and nested <svg> sizes are stored as Float instead of Int; a root with
only width or height takes the other side from the viewBox ratio; a nested
<svg> without a size is 100% of its viewport and its children are parsed
once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCaoqhVNCJtviDcLpUVR7S
DOM tests for every length attribute, LayerTree command-stream tests,
CoreGraphics pixel tests (macOS CI) and Samples.bundle/units-percent.svg.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCaoqhVNCJtviDcLpUVR7S
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCaoqhVNCJtviDcLpUVR7S
… spaces, href test)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
…, CGText image warning, inherited pattern fit test)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
…uote in font-family, matcher counter, empty !important partition)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
…lipPath visibility/CSS/child clip-path, cascade in bbox, union check, maskIsClip equality, gradientUnits dropped)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
…is not supported)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HMyo4bwzHoq66MiywTcNGG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feOffset, feFlood, feComposite (over, in, out, atop, xor, lighter,
arithmetic), feMerge, feBlend (all Compositing 1 modes) and
feColorMatrix (matrix, saturate, hueRotate, luminanceToAlpha), wired by
in / in2 / result with SourceGraphic and SourceAlpha.

- DOM: DOM.Filter.Primitive records each primitive's inputs, result,
  subregion (x/y/width/height in primitiveUnits) and the inherited
  color-interpolation-filters. Unreadable attributes fall back to the
  spec defaults; nothing new throws.
- LayerTree: FilterLayer carries the primary primitive tree resolved to
  user space: inputs as indices (missing, forward and unknown references
  take the previous result; Background*/FillPaint/StrokePaint are
  transparent black), subregions per Filter Effects 1 §9.4 within the
  filter region, flood colours through the colour converter.
- FilterBitmap: platform-independent kernels on premultiplied RGBA8,
  linearRGB maths decoded per pixel so storage stays sRGB encoded, and
  the tree evaluator with result liveness.
- CoreGraphics: SD2's blur chains keep their in-place path; other filters
  evaluate the tree on the offscreen. The visible area grows by offsets
  as well as blurs; the pixel budget is shared by the live bitmaps.
- CGText still degrades with a comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtxFp9AizfH7h4sa3PLmEb
- Point-wise kernels compute each run of equal input pixels once
  (transparent areas, solid fills): about 10x faster per megapixel.
- feOffset copies row segments for whole-pixel offsets and uses 8-bit
  fixed-point weights, skipping uniform neighbourhoods, otherwise.
- pow, cos and sin in Double: their Float overloads are missing on
  Android and Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtxFp9AizfH7h4sa3PLmEb
- FilterBitmap.flood stored its pixel through a pointer into a local
  array; optimized builds (Swift 6.4, -O) kept only the first byte, so
  every feFlood was transparent in Release (17 SD12 tests fail under
  `swift test -c release`; Debug CI never saw it). Quantize directly.
- A primary tree over 64 primitives draws unfiltered: each primitive is
  a full pass over the region, so a hostile chain stalled every frame.
- testSamplesResolveEveryFilter skips when Samples.bundle is not on
  disk (device and emulator runs).
- CI: a macOS job builds and tests in release with the newest Xcode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feOffset, feFlood, feComposite, feMerge, feBlend and feColorMatrix render
through CoreGraphics, wired by in/in2/result, primary tree only. Integrator
commit be9bf84: floods were transparent under -O (release CI job added),
a primary tree over 64 primitives draws unfiltered, the samples test skips
off the source tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@swhitty

swhitty commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Thanks @sakamura (and claude) I'll go through this, particularly interested in the feGaussianBlur implementation 🙏🏻

@sakamura

Copy link
Copy Markdown
Author

Awesome. I'm glad you appreciate it. I needed these to properly support some SVG. These were my priorities. You can find the different branches on my fork for each individual feature. If you need something else, please ask me, I will try to provide it. Cheers!

pablogb added a commit to pablogb/SwiftDraw that referenced this pull request Oct 21, 2026
swhitty#142: dashes and dash offsets, Gaussian blur and the
other filter primitives, reference cycles, tolerant parsing, patterns,
gradients, display and visibility, clip and mask units,
preserveAspectRatio, the CSS cascade, units and percentages, invalid
elements skipped. Its sources replace this branch's own <image href>,
#RGBA/#RRGGBBAA and stroke-dasharray changes, which it covers; their
tests stay where they still apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pablogb added a commit to pablogb/SwiftDraw that referenced this pull request Oct 21, 2026
Removes its Backdrop roadmap, CLAUDE.md and CI job, and says in the
README that this is an altered version, as the zlib licence asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pablogb added a commit to pablogb/SwiftDraw that referenced this pull request Oct 21, 2026
From the review of swhitty#142's parsing (and two older faults):
- Path data stops at its first error; `M0 0Z!` looped forever (also in
  0.29.0).
- A selector of more than 32 compounds is dropped; matching stops after
  2 M evaluations per document, which also bounds its memo.
- Defs and stylesheets are read once, by the root: per nested <svg> it
  was exponential. A <svg> nested more than 16 deep is skipped, and the
  SAX parser skips elements deeper than 256 levels, so every recursive
  walker fits a 512 KB thread.
- The first 10 parse errors are logged, then a count, each line in one
  write.
- Fast paths in removingImportant and scanUnit: corpus parse time back
  from 2.42 s to 1.81 s (upstream 1.74 s).
- <line> coordinates default to 0; SAX text is appended in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pablogb added a commit to pablogb/SwiftDraw that referenced this pull request Oct 21, 2026
From the review of swhitty#142's drawing (and older faults):
- Gradient spread periods, filter layers and tile counts clamp before
  converting to Int.
- One depth cap (200, a <use>/mask/pattern hop counting 12) on the
  builder's stack; <switch> is iterative. Fits a 512 KB thread.
- Expansion is bounded by 250,000 elements built inside references, not
  20,000 references; patterns, their commands and CGPatterns are cached
  per id (a 22 s file now takes 0.13 s).
- Filter offscreens share one live budget and a per-draw budget; filters
  are resolved once per id and past 256 primitives or inputs draw
  unfiltered.
- In PDF, masks and clip paths that need one clip with an image mask
  instead of a blend PDF cannot express (which painted black over
  unclipped content); a shapes-only nested clip-path is a second vector
  clip.
- Pattern matrices from the full user-to-base transform: patterns were
  upside down and misplaced (also in 0.29.0).
- Filters in PDF at 300 ppi; one cascade per element; glyph paths placed
  as made (20,000 glyphs: 24 s to 0.35 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants