From f8476b9abc2d0467a7d936a51a154d8d73dd4b05 Mon Sep 17 00:00:00 2001 From: Dani Otero Date: Fri, 8 May 2026 13:22:18 +0200 Subject: [PATCH] Fix gradient percentage coordinates being parsed as raw pixel values SVG linearGradient and radialGradient accept percentage values for their coordinate attributes (x1, y1, x2, y2, cx, cy, r, fx, fy, fr). The parser was silently discarding the '%' suffix and treating the numeric part as a pixel coordinate, placing gradient axes far outside the bounding box and producing incorrect colours. --- DOM/Sources/Parser.XML.Attributes.swift | 7 +++- DOM/Sources/Parser.XML.LinearGradient.swift | 8 ++--- DOM/Sources/Parser.XML.RadialGradient.swift | 12 +++---- DOM/Sources/Parser.XML.Scanner.swift | 11 +++++++ DOM/Sources/Parser.XML.swift | 9 ++++++ .../Parser.XML.LinearGradientTests.swift | 32 +++++++++++++++++++ .../Parser.XML.RadialGradientTests.swift | 17 ++++++++++ 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/DOM/Sources/Parser.XML.Attributes.swift b/DOM/Sources/Parser.XML.Attributes.swift index 6834d63..8505b88 100644 --- a/DOM/Sources/Parser.XML.Attributes.swift +++ b/DOM/Sources/Parser.XML.Attributes.swift @@ -112,7 +112,12 @@ extension XMLParser { var scanner = XMLParser.Scanner(text: value) return try scanner.scanCoordinate() } - + + func parseCoordinateOrPercentage(_ value: String) throws -> DOM.Coordinate { + var scanner = XMLParser.Scanner(text: value) + return try scanner.scanCoordinateOrPercentage() + } + func parseLength(_ value: String) throws -> DOM.Length { var scanner = XMLParser.Scanner(text: value) return try scanner.scanLength() diff --git a/DOM/Sources/Parser.XML.LinearGradient.swift b/DOM/Sources/Parser.XML.LinearGradient.swift index b0b8a5f..a716798 100644 --- a/DOM/Sources/Parser.XML.LinearGradient.swift +++ b/DOM/Sources/Parser.XML.LinearGradient.swift @@ -51,10 +51,10 @@ extension XMLParser { let nodeAtt: any AttributeParser = try parseAttributes(e) let node = DOM.LinearGradient(id: try nodeAtt.parseString("id")) - node.x1 = try nodeAtt.parseCoordinate("x1") - node.y1 = try nodeAtt.parseCoordinate("y1") - node.x2 = try nodeAtt.parseCoordinate("x2") - node.y2 = try nodeAtt.parseCoordinate("y2") + node.x1 = try nodeAtt.parseCoordinateOrPercentage("x1") + node.y1 = try nodeAtt.parseCoordinateOrPercentage("y1") + node.x2 = try nodeAtt.parseCoordinateOrPercentage("x2") + node.y2 = try nodeAtt.parseCoordinateOrPercentage("y2") for n in e.children where n.name == "stop" { let att: any AttributeParser = try parseAttributes(n) diff --git a/DOM/Sources/Parser.XML.RadialGradient.swift b/DOM/Sources/Parser.XML.RadialGradient.swift index 92a370b..46b1265 100644 --- a/DOM/Sources/Parser.XML.RadialGradient.swift +++ b/DOM/Sources/Parser.XML.RadialGradient.swift @@ -51,12 +51,12 @@ extension XMLParser { let nodeAtt: any AttributeParser = try parseAttributes(e) let node = DOM.RadialGradient(id: try nodeAtt.parseString("id")) - node.r = try? nodeAtt.parseCoordinate("r") - node.cx = try? nodeAtt.parseCoordinate("cx") - node.cy = try? nodeAtt.parseCoordinate("cy") - node.fr = try? nodeAtt.parseCoordinate("fr") - node.fx = try? nodeAtt.parseCoordinate("fx") - node.fy = try? nodeAtt.parseCoordinate("fy") + node.r = try? nodeAtt.parseCoordinateOrPercentage("r") + node.cx = try? nodeAtt.parseCoordinateOrPercentage("cx") + node.cy = try? nodeAtt.parseCoordinateOrPercentage("cy") + node.fr = try? nodeAtt.parseCoordinateOrPercentage("fr") + node.fx = try? nodeAtt.parseCoordinateOrPercentage("fx") + node.fy = try? nodeAtt.parseCoordinateOrPercentage("fy") for n in e.children where n.name == "stop" { let att: any AttributeParser = try parseAttributes(n) diff --git a/DOM/Sources/Parser.XML.Scanner.swift b/DOM/Sources/Parser.XML.Scanner.swift index f27eb4d..ad4e3ba 100644 --- a/DOM/Sources/Parser.XML.Scanner.swift +++ b/DOM/Sources/Parser.XML.Scanner.swift @@ -311,6 +311,17 @@ package extension XMLParser { let unit = scanUnit() ?? .pixel return DOM.Coordinate(double.apply(unit: unit)) } + + package mutating func scanCoordinateOrPercentage() throws -> DOM.Coordinate { + let double = try scanDouble() + scanner.currentIndex = currentIndex + if scanner.scanString("%") != nil { + currentIndex = scanner.currentIndex + return DOM.Coordinate(double / 100.0) + } + let unit = scanUnit() ?? .pixel + return DOM.Coordinate(double.apply(unit: unit)) + } package mutating func scanPercentageFloat() throws -> Float { scanner.currentIndex = currentIndex diff --git a/DOM/Sources/Parser.XML.swift b/DOM/Sources/Parser.XML.swift index 029271c..5ea7438 100644 --- a/DOM/Sources/Parser.XML.swift +++ b/DOM/Sources/Parser.XML.swift @@ -65,6 +65,7 @@ package protocol AttributeValueParser { func parseFloats(_ value: String) throws -> [DOM.Float] func parsePercentage(_ value: String) throws -> DOM.Float func parseCoordinate(_ value: String) throws -> DOM.Coordinate + func parseCoordinateOrPercentage(_ value: String) throws -> DOM.Coordinate func parseLength(_ value: String) throws -> DOM.Length func parseBool(_ value: String) throws -> DOM.Bool func parseFill(_ value: String) throws -> DOM.Fill @@ -108,6 +109,10 @@ package extension AttributeParser { return try parse(key) { return try parser.parseCoordinate($0) } } + func parseCoordinateOrPercentage(_ key: String) throws -> DOM.Coordinate { + return try parse(key) { return try parser.parseCoordinateOrPercentage($0) } + } + func parseLength(_ key: String) throws -> DOM.Length { return try parse(key) { return try parser.parseLength($0) } } @@ -187,6 +192,10 @@ package extension AttributeParser { return try parse(key) { return try parser.parseCoordinate($0) } } + func parseCoordinateOrPercentage(_ key: String) throws -> DOM.Coordinate? { + return try parse(key) { return try parser.parseCoordinateOrPercentage($0) } + } + func parseLength(_ key: String) throws -> DOM.Length? { return try parse(key) { return try parser.parseLength($0) } } diff --git a/DOM/Tests/Parser.XML.LinearGradientTests.swift b/DOM/Tests/Parser.XML.LinearGradientTests.swift index 4ec14fe..47cf95d 100644 --- a/DOM/Tests/Parser.XML.LinearGradientTests.swift +++ b/DOM/Tests/Parser.XML.LinearGradientTests.swift @@ -49,6 +49,38 @@ struct ParserXMLLinearGradientTests { #expect(try XMLParser().parseLinearGradients(parent).count == 3) } + @Test + func parseCoordinatesWithPercentage() throws { + let element = XML.Element(name: "linearGradient", attributes: [ + "id": "a", + "x1": "3.309%", + "y1": "28.41%", + "x2": "103.216%", + "y2": "64.511%" + ]) + let gradient = try XMLParser().parseLinearGradient(element) + #expect(abs((gradient.x1 ?? 0) - 0.03309) < 0.0001) + #expect(abs((gradient.y1 ?? 0) - 0.2841) < 0.0001) + #expect(abs((gradient.x2 ?? 0) - 1.03216) < 0.0001) + #expect(abs((gradient.y2 ?? 0) - 0.64511) < 0.0001) + } + + @Test + func parseCoordinatesWithoutPercentage() throws { + let element = XML.Element(name: "linearGradient", attributes: [ + "id": "b", + "x1": "0", + "y1": "0.5", + "x2": "1", + "y2": "0.5" + ]) + let gradient = try XMLParser().parseLinearGradient(element) + #expect(gradient.x1 == 0) + #expect(gradient.y1 == 0.5) + #expect(gradient.x2 == 1) + #expect(gradient.y2 == 0.5) + } + @Test func parseFile() throws { let dom = try DOM.SVG.parse(fileNamed: "linearGradient.svg", in: .test) diff --git a/DOM/Tests/Parser.XML.RadialGradientTests.swift b/DOM/Tests/Parser.XML.RadialGradientTests.swift index 4700588..6a47d09 100644 --- a/DOM/Tests/Parser.XML.RadialGradientTests.swift +++ b/DOM/Tests/Parser.XML.RadialGradientTests.swift @@ -50,6 +50,23 @@ struct ParserXMLRadialGradientTests { #expect(try XMLParser().parseRadialGradients(parent).count == 3) } + @Test + func parseCoordinatesWithPercentage() throws { + let element = XML.Element.makeMockGradient() + element.attributes["r"] = "10%" + element.attributes["cx"] = "50%" + element.attributes["cy"] = "25%" + element.attributes["fx"] = "50%" + element.attributes["fy"] = "25%" + + let gradient = try XMLParser().parseRadialGradient(element) + #expect(abs((gradient.r ?? 0) - 0.1) < 0.0001) + #expect(abs((gradient.cx ?? 0) - 0.5) < 0.0001) + #expect(abs((gradient.cy ?? 0) - 0.25) < 0.0001) + #expect(abs((gradient.fx ?? 0) - 0.5) < 0.0001) + #expect(abs((gradient.fy ?? 0) - 0.25) < 0.0001) + } + @Test func parseCoordinates() throws { let element = XML.Element.makeMockGradient()