From 81723da90058ec74035691f9f07bd3c674fc05fb Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Sat, 23 May 2026 17:20:59 +0200 Subject: [PATCH 1/9] Convert tests to swift testing --- Tests/SVGViewTests/BaseTestCase.swift | 98 ++----------------------- Tests/SVGViewTests/SVGCustomTests.swift | 2 +- 2 files changed, 6 insertions(+), 94 deletions(-) diff --git a/Tests/SVGViewTests/BaseTestCase.swift b/Tests/SVGViewTests/BaseTestCase.swift index 07362d10..cba49d3d 100644 --- a/Tests/SVGViewTests/BaseTestCase.swift +++ b/Tests/SVGViewTests/BaseTestCase.swift @@ -9,10 +9,6 @@ import Foundation import Testing @testable import SVGView -#if canImport(SwiftUI) -import SwiftUI -#endif - protocol SVGTestHelper { var dir: String { get } } @@ -21,107 +17,23 @@ extension SVGTestHelper { var dir: String { "1.2T" } - func compareToReference(_ fileName: String) async throws { + func compareToReference(_ fileName: String) throws { let bundle = Bundle.module let svgURL = try #require(bundle.url(forResource: fileName, withExtension: "svg", subdirectory: "w3c/\(dir)/svg/")) let refURL = try #require(bundle.url(forResource: fileName, withExtension: "ref", subdirectory: "w3c/\(dir)/refs/")) - let svgSource = try String(contentsOf: svgURL) let node = try #require(SVGParser.parse(contentsOf: svgURL)) - let content = Serializer.serialize(node) + let content = try #require(Serializer.serialize(node)) let reference = try String(contentsOf: refURL) - Attachment.record(Attachment(svgSource, named: "\(fileName).svg")) - Attachment.record(Attachment(content, named: "\(fileName)-actual.txt")) - Attachment.record(Attachment(reference, named: "\(fileName)-expected.txt")) - Attachment.record(Attachment(unifiedDiff(actual: content, expected: reference), named: "\(fileName)-diff.txt")) - await renderedPNGAttachment(node: node, named: "\(fileName)-rendered.png") - - #expect(content == reference, "nodeContent is not equal to referenceContent. \(prettyFirstDifferenceBetweenStrings(s1: content, s2: reference))") + let nodeContent = content + let referenceContent = reference + #expect(nodeContent == referenceContent, "nodeContent is not equal to referenceContent. \(prettyFirstDifferenceBetweenStrings(s1: nodeContent, s2: referenceContent))") } func prettyFirstDifferenceBetweenStrings(s1: String, s2: String) -> String { return prettyFirstDifferenceBetweenNSStrings(s1: s1 as NSString, s2: s2 as NSString) as String } - - func unifiedDiff(actual: String, expected: String) -> String { - let actualLines = actual.components(separatedBy: "\n") - let expectedLines = expected.components(separatedBy: "\n") - var result = "--- expected\n+++ actual\n" - let lcs = longestCommonSubsequence(actualLines, expectedLines) - var i = 0, j = 0, k = 0 - while i < actualLines.count || j < expectedLines.count { - if i < actualLines.count && j < expectedLines.count && k < lcs.count && actualLines[i] == lcs[k] && expectedLines[j] == lcs[k] { - result += " \(actualLines[i])\n" - i += 1; j += 1; k += 1 - } else if j < expectedLines.count && (k >= lcs.count || expectedLines[j] != lcs[k]) { - result += "-\(expectedLines[j])\n" - j += 1 - } else { - result += "+\(actualLines[i])\n" - i += 1 - } - } - return result - } - - private func longestCommonSubsequence(_ a: [String], _ b: [String]) -> [String] { - let m = a.count, n = b.count - var dp = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1) - for i in 1...m { - for j in 1...n { - dp[i][j] = a[i-1] == b[j-1] ? dp[i-1][j-1] + 1 : max(dp[i-1][j], dp[i][j-1]) - } - } - var result: [String] = [] - var i = m, j = n - while i > 0 && j > 0 { - if a[i-1] == b[j-1] { result.append(a[i-1]); i -= 1; j -= 1 } - else if dp[i-1][j] > dp[i][j-1] { i -= 1 } - else { j -= 1 } - } - return result.reversed() - } - - /// Renders `node` to a PNG using `ImageRenderer` and records it as a test attachment. - /// Only runs on platforms that support `ImageRenderer` (macOS 13+, iOS 16+, watchOS 9+). - /// Failures are silently ignored — the render is a diagnostic aid, not part of correctness. - func renderedPNGAttachment(node: SVGNode, named name: String) async { -#if canImport(SwiftUI) - if #available(macOS 13, iOS 16, watchOS 9, *) { - let size = renderSize(for: node) - let png = await MainActor.run { () -> Data? in - let renderer = ImageRenderer(content: SVGView(svg: node).frame(width: size.width, height: size.height)) - renderer.scale = 1.0 -#if os(macOS) - guard let nsImage = renderer.nsImage, - let tiff = nsImage.tiffRepresentation, - let rep = NSBitmapImageRep(data: tiff) else { return nil } - return rep.representation(using: .png, properties: [:]) -#elseif os(iOS) - return renderer.uiImage?.pngData() -#else - return nil -#endif - } - if let png { - Attachment.record(Attachment(png, named: name)) - } - } -#endif - } - - private func renderSize(for node: SVGNode) -> CGSize { - if let viewport = node as? SVGViewport { - if let box = viewport.viewBox, box.width > 0, box.height > 0 { - return CGSize(width: box.width, height: box.height) - } - if let w = viewport.width.ideal, let h = viewport.height.ideal, w > 0, h > 0 { - return CGSize(width: w, height: h) - } - } - return CGSize(width: 480, height: 360) - } } /// Find first differing character between two strings diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift index dc454f19..2c960b2b 100644 --- a/Tests/SVGViewTests/SVGCustomTests.swift +++ b/Tests/SVGViewTests/SVGCustomTests.swift @@ -21,4 +21,4 @@ struct SVGCustomTests: SVGTestHelper { try await compareToReference("viewport-02") } -} \ No newline at end of file +} From 4dbc0a97418a33345e664cbc6d82c41608b5834d Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Sun, 24 May 2026 13:21:57 +0200 Subject: [PATCH 2/9] Improve our test helper --- Tests/SVGViewTests/BaseTestCase.swift | 49 ++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/Tests/SVGViewTests/BaseTestCase.swift b/Tests/SVGViewTests/BaseTestCase.swift index cba49d3d..2b3a4025 100644 --- a/Tests/SVGViewTests/BaseTestCase.swift +++ b/Tests/SVGViewTests/BaseTestCase.swift @@ -23,17 +23,58 @@ extension SVGTestHelper { let refURL = try #require(bundle.url(forResource: fileName, withExtension: "ref", subdirectory: "w3c/\(dir)/refs/")) let node = try #require(SVGParser.parse(contentsOf: svgURL)) - let content = try #require(Serializer.serialize(node)) + let content = Serializer.serialize(node) let reference = try String(contentsOf: refURL) - let nodeContent = content - let referenceContent = reference - #expect(nodeContent == referenceContent, "nodeContent is not equal to referenceContent. \(prettyFirstDifferenceBetweenStrings(s1: nodeContent, s2: referenceContent))") + Attachment.record(Attachment(content, named: "\(fileName)-actual.txt")) + Attachment.record(Attachment(reference, named: "\(fileName)-expected.txt")) + Attachment.record(Attachment(unifiedDiff(actual: content, expected: reference), named: "\(fileName)-diff.txt")) + + #expect(content == reference, "nodeContent is not equal to referenceContent. \(prettyFirstDifferenceBetweenStrings(s1: content, s2: reference))") } func prettyFirstDifferenceBetweenStrings(s1: String, s2: String) -> String { return prettyFirstDifferenceBetweenNSStrings(s1: s1 as NSString, s2: s2 as NSString) as String } + + func unifiedDiff(actual: String, expected: String) -> String { + let actualLines = actual.components(separatedBy: "\n") + let expectedLines = expected.components(separatedBy: "\n") + var result = "--- expected\n+++ actual\n" + let lcs = longestCommonSubsequence(actualLines, expectedLines) + var i = 0, j = 0, k = 0 + while i < actualLines.count || j < expectedLines.count { + if i < actualLines.count && j < expectedLines.count && k < lcs.count && actualLines[i] == lcs[k] && expectedLines[j] == lcs[k] { + result += " \(actualLines[i])\n" + i += 1; j += 1; k += 1 + } else if j < expectedLines.count && (k >= lcs.count || expectedLines[j] != lcs[k]) { + result += "-\(expectedLines[j])\n" + j += 1 + } else { + result += "+\(actualLines[i])\n" + i += 1 + } + } + return result + } + + private func longestCommonSubsequence(_ a: [String], _ b: [String]) -> [String] { + let m = a.count, n = b.count + var dp = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1) + for i in 1...m { + for j in 1...n { + dp[i][j] = a[i-1] == b[j-1] ? dp[i-1][j-1] + 1 : max(dp[i-1][j], dp[i][j-1]) + } + } + var result: [String] = [] + var i = m, j = n + while i > 0 && j > 0 { + if a[i-1] == b[j-1] { result.append(a[i-1]); i -= 1; j -= 1 } + else if dp[i-1][j] > dp[i][j-1] { i -= 1 } + else { j -= 1 } + } + return result.reversed() + } } /// Find first differing character between two strings From 209623adf03995dbfe117de967b5563bbbb3c606 Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Sun, 24 May 2026 13:56:25 +0200 Subject: [PATCH 3/9] Add generated png and source svg as test attachments to more easily debug --- Tests/SVGViewTests/BaseTestCase.swift | 49 ++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/Tests/SVGViewTests/BaseTestCase.swift b/Tests/SVGViewTests/BaseTestCase.swift index 2b3a4025..07362d10 100644 --- a/Tests/SVGViewTests/BaseTestCase.swift +++ b/Tests/SVGViewTests/BaseTestCase.swift @@ -9,6 +9,10 @@ import Foundation import Testing @testable import SVGView +#if canImport(SwiftUI) +import SwiftUI +#endif + protocol SVGTestHelper { var dir: String { get } } @@ -17,18 +21,21 @@ extension SVGTestHelper { var dir: String { "1.2T" } - func compareToReference(_ fileName: String) throws { + func compareToReference(_ fileName: String) async throws { let bundle = Bundle.module let svgURL = try #require(bundle.url(forResource: fileName, withExtension: "svg", subdirectory: "w3c/\(dir)/svg/")) let refURL = try #require(bundle.url(forResource: fileName, withExtension: "ref", subdirectory: "w3c/\(dir)/refs/")) + let svgSource = try String(contentsOf: svgURL) let node = try #require(SVGParser.parse(contentsOf: svgURL)) let content = Serializer.serialize(node) let reference = try String(contentsOf: refURL) + Attachment.record(Attachment(svgSource, named: "\(fileName).svg")) Attachment.record(Attachment(content, named: "\(fileName)-actual.txt")) Attachment.record(Attachment(reference, named: "\(fileName)-expected.txt")) Attachment.record(Attachment(unifiedDiff(actual: content, expected: reference), named: "\(fileName)-diff.txt")) + await renderedPNGAttachment(node: node, named: "\(fileName)-rendered.png") #expect(content == reference, "nodeContent is not equal to referenceContent. \(prettyFirstDifferenceBetweenStrings(s1: content, s2: reference))") } @@ -75,6 +82,46 @@ extension SVGTestHelper { } return result.reversed() } + + /// Renders `node` to a PNG using `ImageRenderer` and records it as a test attachment. + /// Only runs on platforms that support `ImageRenderer` (macOS 13+, iOS 16+, watchOS 9+). + /// Failures are silently ignored — the render is a diagnostic aid, not part of correctness. + func renderedPNGAttachment(node: SVGNode, named name: String) async { +#if canImport(SwiftUI) + if #available(macOS 13, iOS 16, watchOS 9, *) { + let size = renderSize(for: node) + let png = await MainActor.run { () -> Data? in + let renderer = ImageRenderer(content: SVGView(svg: node).frame(width: size.width, height: size.height)) + renderer.scale = 1.0 +#if os(macOS) + guard let nsImage = renderer.nsImage, + let tiff = nsImage.tiffRepresentation, + let rep = NSBitmapImageRep(data: tiff) else { return nil } + return rep.representation(using: .png, properties: [:]) +#elseif os(iOS) + return renderer.uiImage?.pngData() +#else + return nil +#endif + } + if let png { + Attachment.record(Attachment(png, named: name)) + } + } +#endif + } + + private func renderSize(for node: SVGNode) -> CGSize { + if let viewport = node as? SVGViewport { + if let box = viewport.viewBox, box.width > 0, box.height > 0 { + return CGSize(width: box.width, height: box.height) + } + if let w = viewport.width.ideal, let h = viewport.height.ideal, w > 0, h > 0 { + return CGSize(width: w, height: h) + } + } + return CGSize(width: 480, height: 360) + } } /// Find first differing character between two strings From 3666bb0643330b322df09a975628cde908a38906 Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Sun, 24 May 2026 15:22:23 +0200 Subject: [PATCH 4/9] Implement SVG pattern paint server with xlink:href inheritance (pservers-grad-03-b) - Add SVGPattern model (SVGPaint subclass) with CGBitmapContext tile rendering - Add draw(in: CGContext) to SVGNode (no-op), SVGRect, and SVGGroup - Add SVGParser.parseElements(_:index:) internal helper for pattern tile parsing - Register elements as paint servers in SVGIndex with xlink:href inheritance - Wire SVGPattern into SVGPaint.apply(paint:model:) switch - Add pserversGrad03B test and reference file --- GenerateReferencesCLI/cli.swift | 1 + Source/Model/Nodes/SVGGroup.swift | 11 +++ Source/Model/Nodes/SVGNode.swift | 6 ++ Source/Model/Primitives/SVGPaint.swift | 2 + Source/Model/Primitives/SVGPattern.swift | 77 +++++++++++++++++++ Source/Model/Shapes/SVGRect.swift | 17 +++- Source/Parser/SVG/SVGIndex.swift | 40 +++++++++- Source/Parser/SVG/SVGParser.swift | 12 +++ Tests/SVGViewTests/SVG11Tests.swift | 1 + .../w3c/1.1F2/refs/pservers-grad-03-b.ref | 46 +++++++++++ 10 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 Source/Model/Primitives/SVGPattern.swift create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/pservers-grad-03-b.ref diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift index 770c4fba..203f4256 100644 --- a/GenerateReferencesCLI/cli.swift +++ b/GenerateReferencesCLI/cli.swift @@ -76,6 +76,7 @@ struct cli: ParsableCommand { "paths-data-20-f", "pservers-grad-01-b", "pservers-grad-02-b", + "pservers-grad-03-b", "pservers-grad-04-b", "pservers-grad-05-b", "pservers-grad-07-b", diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift index ce143cc7..4fef4e1b 100644 --- a/Source/Model/Nodes/SVGGroup.swift +++ b/Source/Model/Nodes/SVGGroup.swift @@ -44,6 +44,17 @@ public class SVGGroup: SVGNode { serializer.add("contents", contents) } + #if !os(WASI) && !os(Linux) + override func draw(in context: CGContext) { + context.saveGState() + context.concatenate(transform) + for node in contents { + node.draw(in: context) + } + context.restoreGState() + } + #endif + #if canImport(SwiftUI) public func contentView() -> some View { SVGGroupView(model: self) diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index 1476a92c..f9602b4a 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -86,6 +86,12 @@ public class SVGNode: SerializableElement { return String(describing: type(of: self)) } + #if !os(WASI) && !os(Linux) + func draw(in context: CGContext) { + // default: no-op + } + #endif + } #if canImport(SwiftUI) diff --git a/Source/Model/Primitives/SVGPaint.swift b/Source/Model/Primitives/SVGPaint.swift index 19076133..8c775cc2 100644 --- a/Source/Model/Primitives/SVGPaint.swift +++ b/Source/Model/Primitives/SVGPaint.swift @@ -34,6 +34,8 @@ extension View { linearGradient.apply(view: self, model: model) case let radialGradient as SVGRadialGradient: radialGradient.apply(view: self, model: model) + case let pattern as SVGPattern: + pattern.apply(view: self, model: model) case let color as SVGColor: color.apply(view: self, model: model) default: diff --git a/Source/Model/Primitives/SVGPattern.swift b/Source/Model/Primitives/SVGPattern.swift new file mode 100644 index 00000000..378bdc92 --- /dev/null +++ b/Source/Model/Primitives/SVGPattern.swift @@ -0,0 +1,77 @@ +// +// SVGPattern.swift +// SVGView +// + +#if os(WASI) || os(Linux) +import Foundation +#else +import SwiftUI +#endif + +public class SVGPattern: SVGPaint { + + public let x: CGFloat + public let y: CGFloat + public let width: CGFloat + public let height: CGFloat + public let userSpace: Bool + public let patternTransform: CGAffineTransform + public let contents: [SVGNode] + + public init(x: CGFloat = 0, y: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0, + userSpace: Bool = true, patternTransform: CGAffineTransform = .identity, + contents: [SVGNode] = []) { + self.x = x + self.y = y + self.width = width + self.height = height + self.userSpace = userSpace + self.patternTransform = patternTransform + self.contents = contents + } + + #if canImport(SwiftUI) + @ViewBuilder + func apply(view: S, model: SVGShape? = nil) -> some View { + if width > 0, height > 0, !contents.isEmpty, let cgImage = renderTile() { + let image = Image(decorative: cgImage, scale: 1.0) + let bounds = model?.bounds() ?? .zero + view + .foregroundColor(.clear) + .overlay( + Rectangle() + .fill(ImagePaint(image: image, scale: 1.0)) + .frame(width: bounds.width, height: bounds.height) + .offset(x: bounds.minX, y: bounds.minY) + .mask(view) + ) + } else { + view.foregroundColor(.clear) + } + } + + private func renderTile() -> CGImage? { + let w = Int(ceil(width)) + let h = Int(ceil(height)) + guard w > 0, h > 0 else { return nil } + let colorSpace = CGColorSpaceCreateDeviceRGB() + guard let context = CGContext( + data: nil, + width: w, + height: h, + bitsPerComponent: 8, + bytesPerRow: 0, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { return nil } + // Flip coordinate system to match SVG (y increases downward) + context.translateBy(x: 0, y: CGFloat(h)) + context.scaleBy(x: 1, y: -1) + for node in contents { + node.draw(in: context) + } + return context.makeImage() + } + #endif +} diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift index 0179896c..36cc6b8c 100644 --- a/Source/Model/Shapes/SVGRect.swift +++ b/Source/Model/Shapes/SVGRect.swift @@ -47,7 +47,22 @@ public class SVGRect: SVGShape { serializer.add("rx", rx, 0).add("ry", ry, 0) super.serialize(serializer) } - + + #if !os(WASI) && !os(Linux) + override func draw(in context: CGContext) { + guard let color = fill as? SVGColor else { return } + context.saveGState() + context.setFillColor(CGColor( + red: CGFloat(color.r) / 255, + green: CGFloat(color.g) / 255, + blue: CGFloat(color.b) / 255, + alpha: CGFloat(color.opacity) + )) + context.fill(CGRect(x: x, y: y, width: width, height: height)) + context.restoreGState() + } + #endif + #if canImport(SwiftUI) public func contentView() -> some View { SVGRectView(model: self) diff --git a/Source/Parser/SVG/SVGIndex.swift b/Source/Parser/SVG/SVGIndex.swift index b1a3d188..6c7f5ecd 100644 --- a/Source/Parser/SVG/SVGIndex.swift +++ b/Source/Parser/SVG/SVGIndex.swift @@ -37,7 +37,7 @@ class SVGIndex { if let id = SVGHelper.parseId(element.attributes) { elements[id] = element switch element.name { - case "linearGradient", "radialGradient", "fill": + case "linearGradient", "radialGradient", "fill", "pattern": paints[id] = parseFill(element) default: elements[id] = element @@ -60,11 +60,49 @@ class SVGIndex { return parseLinearGradient(element) case "radialGradient": return parseRadialGradient(element) + case "pattern": + return parsePattern(element) default: return .none } } + private func getParentPattern(_ element: XMLElement) -> SVGPattern? { + if let link = element.attributes["xlink:href"]?.replacingOccurrences(of: " ", with: ""), link.hasPrefix("#") { + let id = link.replacingOccurrences(of: "#", with: "") + return paints[id] as? SVGPattern + } + return nil + } + + private func parsePattern(_ element: XMLElement) -> SVGPattern? { + let parent = getParentPattern(element) + let childElements = element.contents.compactMap { $0 as? XMLElement } + let contents: [SVGNode] + if childElements.isEmpty { + contents = parent?.contents ?? [] + } else { + contents = SVGParser.parseElements(childElements, index: self) + } + guard !contents.isEmpty else { return nil } + let x = SVGHelper.parseCGFloat(element.attributes, "x", defaultValue: parent?.x ?? 0) + let y = SVGHelper.parseCGFloat(element.attributes, "y", defaultValue: parent?.y ?? 0) + let width = SVGHelper.parseCGFloat(element.attributes, "width", defaultValue: parent?.width ?? 0) + let height = SVGHelper.parseCGFloat(element.attributes, "height", defaultValue: parent?.height ?? 0) + guard width > 0, height > 0 else { return nil } + var userSpace = parent?.userSpace ?? false + if let patternUnits = element.attributes["patternUnits"] { + userSpace = patternUnits == "userSpaceOnUse" + } + var patternTransform = parent?.patternTransform ?? .identity + if let transformStr = element.attributes["patternTransform"] { + patternTransform = SVGHelper.parseTransform(transformStr) + } + return SVGPattern(x: x, y: y, width: width, height: height, + userSpace: userSpace, patternTransform: patternTransform, + contents: contents) + } + private func getParentGradient(_ element: XMLElement) -> SVGGradient? { if let link = element.attributes["xlink:href"]?.replacingOccurrences(of: " ", with: ""), link.hasPrefix("#") { diff --git a/Source/Parser/SVG/SVGParser.swift b/Source/Parser/SVG/SVGParser.swift index 74a72efb..e799d44f 100644 --- a/Source/Parser/SVG/SVGParser.swift +++ b/Source/Parser/SVG/SVGParser.swift @@ -50,6 +50,18 @@ public struct SVGParser { return parse(context: context) } + /// Parses a list of XML elements using the given index. Used for pattern tile content. + static func parseElements(_ elements: [XMLElement], index: SVGIndex) -> [SVGNode] { + let context = SVGRootContext( + logger: SVGLogger.console, + linker: SVGLinker.none, + screen: SVGScreen.main(ppi: 96), + index: index, + defaultFontSize: 16 + ) + return elements.compactMap { parse(element: $0, parentContext: context) } + } + private static let parsers: [String:SVGElementParser] = [ "svg": SVGViewportParser(), "g": SVGGroupParser(), diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift index 41756562..77c6023e 100644 --- a/Tests/SVGViewTests/SVG11Tests.swift +++ b/Tests/SVGViewTests/SVG11Tests.swift @@ -104,6 +104,7 @@ struct SVG11Tests { @Test func pserversGrad01B() async throws { try await compareToReference("pservers-grad-01-b") } @Test func pserversGrad02B() async throws { try await compareToReference("pservers-grad-02-b") } + @Test func pserversGrad03B() async throws { try await compareToReference("pservers-grad-03-b") } @Test func pserversGrad04B() async throws { try await compareToReference("pservers-grad-04-b") } @Test func pserversGrad05B() async throws { try await compareToReference("pservers-grad-05-b") } @Test func pserversGrad07B() async throws { try await compareToReference("pservers-grad-07-b") } diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/pservers-grad-03-b.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/pservers-grad-03-b.ref new file mode 100644 index 00000000..1a33c524 --- /dev/null +++ b/Tests/SVGViewTests/w3c/1.1F2/refs/pservers-grad-03-b.ref @@ -0,0 +1,46 @@ +SVGViewport { + id: "svg-root", + viewBox: { width: 480, height: 360 }, + scaling: "none", + contents: [ + SVGDefs { }, + SVGGroup { + id: "test-body-content", + contents: [ + SVGRect { x: 20, y: 20, width: 440, height: 80 }, + SVGText { + text: "Pattern fill.", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + fill: "black", + transform: [1, 0, 0, 1, 20, 130] + }, + SVGRect { x: 20, y: 160, width: 440, height: 80 }, + SVGText { + text: "Referencing pattern fill below.", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + fill: "black", + transform: [1, 0, 0, 1, 20, 270] + } + ] + }, + SVGGroup { + contents: [ + SVGText { + id: "revision", + text: "$Revision: 1.8 $", + font: { name: "SVGFreeSansASCII,sans-serif", size: 32 }, + fill: "black", + transform: [1, 0, 0, 1, 10, 340] + } + ] + }, + SVGRect { + id: "test-frame", + x: 1, + y: 1, + width: 478, + height: 358, + stroke: { fill: "black" } + } + ] +} From 891990eaef4e901522acb419d5e3827a5f1f136f Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Sun, 24 May 2026 15:26:38 +0200 Subject: [PATCH 5/9] Add AGENTS.md and architecture/feature reference docs --- .github/docs/architecture.md | 82 ++++++++++++++++++ .github/docs/implementing-features.md | 116 ++++++++++++++++++++++++++ AGENTS.md | 35 ++++++++ 3 files changed, 233 insertions(+) create mode 100644 .github/docs/architecture.md create mode 100644 .github/docs/implementing-features.md create mode 100644 AGENTS.md diff --git a/.github/docs/architecture.md b/.github/docs/architecture.md new file mode 100644 index 00000000..e2590169 --- /dev/null +++ b/.github/docs/architecture.md @@ -0,0 +1,82 @@ +# Architecture & Parsing Pipeline + +## Overview + +``` +SVGParser.parse(xml:) + │ + ├─ SVGIndex(element:) ← traverses full XML tree FIRST + │ ├─ elements[id] = XMLElement + │ ├─ paints[id] = SVGPaint ← linearGradient, radialGradient, pattern + │ └─ CSSParser ←