Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Sources/SVGPath+CoreGraphics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import Foundation
// MARK: SVGPath to CGPath

public extension CGPath {
static func from(svgPath: String) throws -> CGPath {
try from(svgPath: SVGPath(string: svgPath))
static func from(svgPath: String, with options: SVGPath.ParseOptions = .default) throws -> CGPath {
try from(svgPath: SVGPath(string: svgPath, with: options))
}

static func from(svgPath: SVGPath) -> CGPath {
Expand Down
52 changes: 33 additions & 19 deletions Sources/SVGPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,29 @@
import Foundation

public struct SVGPath: Hashable, Sendable {
public struct ParseOptions: Sendable {
public static let `default` = Self()

public var invertYAxis: Bool

public init(invertYAxis: Bool = true) {
self.invertYAxis = invertYAxis
}
}

public var commands: [SVGCommand]

public init(commands: [SVGCommand]) {
self.commands = commands
}

public init(string: String) throws {
public init(string: String, with options: ParseOptions = .default) throws {
var token: UnicodeScalar = " "
var commands = [SVGCommand]()
var numbers = ArraySlice<Double>()
var number = ""
var isRelative = false
let yAxisSign = options.invertYAxis ? -1.0 : 1.0

func assertArgs(_ count: Int) throws -> [Double] {
if numbers.count < count {
Expand All @@ -57,19 +68,19 @@ public struct SVGPath: Hashable, Sendable {

func moveTo() throws -> SVGCommand {
let numbers = try assertArgs(2)
return .moveTo(SVGPoint(x: numbers[0], y: -numbers[1]))
return .moveTo(SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]))
}

func lineTo() throws -> SVGCommand {
let numbers = try assertArgs(2)
return .lineTo(SVGPoint(x: numbers[0], y: -numbers[1]))
return .lineTo(SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]))
}

func lineToVertical() throws -> SVGCommand {
let numbers = try assertArgs(1)
return .lineTo(SVGPoint(
x: isRelative ? 0 : commands.lastPoint.x,
y: -numbers[0]
y: yAxisSign * numbers[0]
))
}

Expand All @@ -84,8 +95,8 @@ public struct SVGPath: Hashable, Sendable {
func quadCurve() throws -> SVGCommand {
let numbers = try assertArgs(4)
return .quadratic(
SVGPoint(x: numbers[0], y: -numbers[1]),
SVGPoint(x: numbers[2], y: -numbers[3])
SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]),
SVGPoint(x: numbers[2], y: yAxisSign * numbers[3])
)
}

Expand All @@ -100,15 +111,15 @@ public struct SVGPath: Hashable, Sendable {
if !isRelative {
control += lastPoint
}
return .quadratic(control, SVGPoint(x: numbers[0], y: -numbers[1]))
return .quadratic(control, SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]))
}

func cubicCurve() throws -> SVGCommand {
let numbers = try assertArgs(6)
return .cubic(
SVGPoint(x: numbers[0], y: -numbers[1]),
SVGPoint(x: numbers[2], y: -numbers[3]),
SVGPoint(x: numbers[4], y: -numbers[5])
SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]),
SVGPoint(x: numbers[2], y: yAxisSign * numbers[3]),
SVGPoint(x: numbers[4], y: yAxisSign * numbers[5])
)
}

Expand All @@ -125,8 +136,8 @@ public struct SVGPath: Hashable, Sendable {
}
return .cubic(
control,
SVGPoint(x: numbers[0], y: -numbers[1]),
SVGPoint(x: numbers[2], y: -numbers[3])
SVGPoint(x: numbers[0], y: yAxisSign * numbers[1]),
SVGPoint(x: numbers[2], y: yAxisSign * numbers[3])
)
}

Expand All @@ -137,7 +148,7 @@ public struct SVGPath: Hashable, Sendable {
rotation: numbers[2] * .pi / 180,
largeArc: numbers[3] != 0,
sweep: numbers[4] != 0,
end: SVGPoint(x: numbers[5], y: -numbers[6])
end: SVGPoint(x: numbers[5], y: yAxisSign * numbers[6])
))
}

Expand Down Expand Up @@ -231,16 +242,19 @@ public extension SVGPath {

public var prettyPrinted: Bool
public var wrapWidth: Int
public var invertYAxis: Bool

public init(prettyPrinted: Bool = true, wrapWidth: Int = .max) {
public init(prettyPrinted: Bool = true, wrapWidth: Int = .max, invertYAxis: Bool = true) {
self.prettyPrinted = prettyPrinted
self.wrapWidth = wrapWidth
self.invertYAxis = invertYAxis
}
}

func string(with options: WriteOptions) -> String {
var output = ""
var width = 0
let yAxisSign = options.invertYAxis ? -1.0 : 1.0

func append(_ string: String) {
let spaced = width > 0 && (
Expand Down Expand Up @@ -269,19 +283,19 @@ public extension SVGPath {
for command in commands {
switch command {
case let .moveTo(point):
append("M", point.x, -point.y)
append("M", point.x, yAxisSign * point.y)
case let .lineTo(point):
append("L", point.x, -point.y)
append("L", point.x, yAxisSign * point.y)
case let .cubic(c1, c2, point):
append("C", c1.x, -c1.y, c2.x, -c2.y, point.x, -point.y)
append("C", c1.x, yAxisSign * c1.y, c2.x, yAxisSign * c2.y, point.x, yAxisSign * point.y)
case let .quadratic(control, point):
append("Q", control.x, -control.y, point.x, -point.y)
append("Q", control.x, yAxisSign * control.y, point.x, yAxisSign * point.y)
case let .arc(arc):
let rad = arc.radius, end = arc.end
let rot = arc.rotation / .pi * 180
let large = arc.largeArc ? 1.0 : 0
let sweep = arc.sweep ? 1.0 : 0
append("A", rad.x, rad.y, rot, large, sweep, end.x, -end.y)
append("A", rad.x, rad.y, rot, large, sweep, end.x, yAxisSign * end.y)
case .end:
append("Z")
}
Expand Down
15 changes: 15 additions & 0 deletions Tests/SVGPathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ import SVGPath
import XCTest

class SVGPathTests: XCTestCase {
func testTriangleWithoutInvertingYAxis() throws {
let parseOptions = SVGPath.ParseOptions(invertYAxis: false)
let svgPath = try SVGPath(string: "M150 0 L75 200 L225 200 Z", with: parseOptions)
let expected = SVGPath(commands: [
.moveTo(.init(x: 150, y: 0)),
.lineTo(.init(x: 75, y: 200)),
.lineTo(.init(x: 225, y: 200)),
.end,
])
XCTAssertEqual(svgPath, expected)

let writeOptions = SVGPath.WriteOptions(invertYAxis: false)
XCTAssertEqual(svgPath.string(with: writeOptions), "M150 0 L75 200 L225 200 Z")
}

func testTriangle() throws {
let svgPath = try SVGPath(string: "M150 0 L75 200 L225 200 Z")
let expected = SVGPath(commands: [
Expand Down