Last active
June 27, 2023 14:05
-
-
Save dotcypress/fa4b5c739b02f4374402 to your computer and use it in GitHub Desktop.
SVG path to CGPath converter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SVGPath.swift | |
// SVGPath | |
// | |
// Created by Tim Wood on 1/21/15. | |
// Updated by Vitaly Domnikov 10/6/2015 | |
// Copyright (c) 2015 Tim Wood, Vitaly Domnikov. All rights reserved. | |
import Foundation | |
import CoreGraphics | |
public extension CGPath { | |
// Convert SVG path to CGPath | |
static func fromSvgPath(svgPath: String) -> CGPath? { | |
let path = CGPathCreateMutable() | |
CGPathMoveToPoint(path, nil, 0, 0) | |
let commands = SVGPath(svgPath).commands | |
for command in commands { | |
switch command.type { | |
case .Move: CGPathMoveToPoint(path, nil, command.point.x, command.point.y) | |
case .Line: CGPathAddLineToPoint(path, nil, command.point.x, command.point.y) | |
case .QuadCurve: | |
CGPathAddQuadCurveToPoint(path, nil, | |
command.control1.x, command.control1.y, | |
command.point.x, command.point.y) | |
case .CubeCurve: | |
CGPathAddCurveToPoint(path, nil, | |
command.control1.x, command.control1.y, | |
command.control2.x, command.control2.y, | |
command.point.x, command.point.y) | |
case .Close: CGPathCloseSubpath(path) | |
} | |
} | |
return path | |
} | |
} | |
// MARK: Enums | |
private enum Coordinates { | |
case Absolute | |
case Relative | |
} | |
// MARK: Class | |
public class SVGPath { | |
public var commands: [SVGCommand] = [] | |
private var builder: SVGCommandBuilder = moveTo | |
private var coords: Coordinates = .Absolute | |
private var stride: Int = 2 | |
private var numbers = "" | |
public init(_ string: String) { | |
commands.reserveCapacity(200) | |
for char in string.characters { | |
switch char { | |
case "M": use(.Absolute, 2, moveTo) | |
case "m": use(.Relative, 2, moveTo) | |
case "L": use(.Absolute, 2, lineTo) | |
case "l": use(.Relative, 2, lineTo) | |
case "V": use(.Absolute, 1, lineToVertical) | |
case "v": use(.Relative, 1, lineToVertical) | |
case "H": use(.Absolute, 1, lineToHorizontal) | |
case "h": use(.Relative, 1, lineToHorizontal) | |
case "Q": use(.Absolute, 4, quadBroken) | |
case "q": use(.Relative, 4, quadBroken) | |
case "T": use(.Absolute, 2, quadSmooth) | |
case "t": use(.Relative, 2, quadSmooth) | |
case "C": use(.Absolute, 6, cubeBroken) | |
case "c": use(.Relative, 6, cubeBroken) | |
case "S": use(.Absolute, 4, cubeSmooth) | |
case "s": use(.Relative, 4, cubeSmooth) | |
case "Z": use(.Absolute, 0, close) | |
case "z": use(.Relative, 0, close) | |
default: numbers.append(char) | |
} | |
} | |
finishLastCommand() | |
} | |
private func use(coords: Coordinates, _ stride: Int, _ builder: SVGCommandBuilder) { | |
finishLastCommand() | |
self.builder = builder | |
self.coords = coords | |
self.stride = stride | |
} | |
private func finishLastCommand() { | |
for command in take(SVGPath.parseNumbers(numbers), stride: stride, coords: coords, last: commands.last, callback: builder) { | |
commands.append(coords == .Relative ? command.relativeTo(commands.last) : command) | |
} | |
numbers = "" | |
} | |
} | |
// MARK: Numbers | |
private let numberSet = NSCharacterSet(charactersInString: "-.0123456789eE") | |
private let numberFormatter = NSNumberFormatter() | |
public extension SVGPath { | |
class func parseNumbers(numbers: String) -> [CGFloat] { | |
numberFormatter.numberStyle = .DecimalStyle | |
numberFormatter.allowsFloats = true | |
numberFormatter.decimalSeparator = "." | |
var all: [String] = [] | |
var curr = "" | |
var last = "" | |
for char in numbers.unicodeScalars { | |
let next = String(char) | |
if next == "-" && last != "" && last != "E" && last != "e" { | |
if curr.utf16.count > 0 { | |
all.append(curr) | |
} | |
curr = next | |
} else if numberSet.longCharacterIsMember(char.value) { | |
curr += next | |
} else if curr.utf16.count > 0 { | |
all.append(curr) | |
curr = "" | |
} | |
last = next | |
} | |
all.append(curr) | |
return all | |
.filter { | |
numberFormatter.numberFromString($0) != nil | |
} | |
.map { | |
CGFloat((numberFormatter.numberFromString($0)?.floatValue)!) | |
} | |
} | |
} | |
// MARK: Commands | |
public struct SVGCommand { | |
public var point: CGPoint | |
public var control1: CGPoint | |
public var control2: CGPoint | |
public var type: Kind | |
public enum Kind { | |
case Move | |
case Line | |
case CubeCurve | |
case QuadCurve | |
case Close | |
} | |
public init() { | |
let point = CGPoint() | |
self.init(point, point, point, type: .Close) | |
} | |
public init(_ x: CGFloat, _ y: CGFloat, type: Kind) { | |
let point = CGPoint(x: x, y: y) | |
self.init(point, point, point, type: type) | |
} | |
public init(_ cx: CGFloat, _ cy: CGFloat, _ x: CGFloat, _ y: CGFloat) { | |
let control = CGPoint(x: cx, y: cy) | |
self.init(control, control, CGPoint(x: x, y: y), type: .QuadCurve) | |
} | |
public init(_ cx1: CGFloat, _ cy1: CGFloat, _ cx2: CGFloat, _ cy2: CGFloat, _ x: CGFloat, _ y: CGFloat) { | |
self.init(CGPoint(x: cx1, y: cy1), CGPoint(x: cx2, y: cy2), CGPoint(x: x, y: y), type: .CubeCurve) | |
} | |
public init(_ control1: CGPoint, _ control2: CGPoint, _ point: CGPoint, type: Kind) { | |
self.point = point | |
self.control1 = control1 | |
self.control2 = control2 | |
self.type = type | |
} | |
private func relativeTo(other: SVGCommand?) -> SVGCommand { | |
if let otherPoint = other?.point { | |
return SVGCommand(control1 + otherPoint, control2 + otherPoint, point + otherPoint, type: type) | |
} | |
return self | |
} | |
} | |
// MARK: CGPoint helpers | |
private func +(a: CGPoint, b: CGPoint) -> CGPoint { | |
return CGPoint(x: a.x + b.x, y: a.y + b.y) | |
} | |
private func -(a: CGPoint, b: CGPoint) -> CGPoint { | |
return CGPoint(x: a.x - b.x, y: a.y - b.y) | |
} | |
// MARK: Command Builders | |
private typealias SVGCommandBuilder = ([CGFloat], SVGCommand?, Coordinates) -> SVGCommand | |
private func take(numbers: [CGFloat], stride: Int, coords: Coordinates, last: SVGCommand?, callback: SVGCommandBuilder) -> [SVGCommand] { | |
var out: [SVGCommand] = [] | |
var lastCommand: SVGCommand? = last | |
var nums: [CGFloat] = [0, 0, 0, 0, 0, 0]; | |
if stride == 0 { | |
lastCommand = callback(nums, lastCommand, coords) | |
out.append(lastCommand!) | |
} else { | |
let count = (numbers.count / stride) * stride | |
for var i = 0; i < count; i += stride { | |
for var j = 0; j < stride; j++ { | |
nums[j] = numbers[i + j] | |
} | |
lastCommand = callback(nums, lastCommand, coords) | |
out.append(lastCommand!) | |
} | |
} | |
return out | |
} | |
// MARK: Mm - Move | |
private func moveTo(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(numbers[0], numbers[1], type: .Move) | |
} | |
// MARK: Ll - Line | |
private func lineTo(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(numbers[0], numbers[1], type: .Line) | |
} | |
// MARK: Vv - Vertical Line | |
private func lineToVertical(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(coords == .Absolute ? last?.point.x ?? 0 : 0, numbers[0], type: .Line) | |
} | |
// MARK: Hh - Horizontal Line | |
private func lineToHorizontal(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(numbers[0], coords == .Absolute ? last?.point.y ?? 0 : 0, type: .Line) | |
} | |
// MARK: Qq - Quadratic Curve To | |
private func quadBroken(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3]) | |
} | |
// MARK: Tt - Smooth Quadratic Curve To | |
private func quadSmooth(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
var lastControl = last?.control1 ?? CGPoint() | |
let lastPoint = last?.point ?? CGPoint() | |
if (last?.type ?? .Line) != .QuadCurve { | |
lastControl = lastPoint | |
} | |
var control = lastPoint - lastControl | |
if coords == .Absolute { | |
control = control + lastPoint | |
} | |
return SVGCommand(control.x, control.y, numbers[0], numbers[1]) | |
} | |
// MARK: Cc - Cubic Curve To | |
private func cubeBroken(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand(numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5]) | |
} | |
// MARK: Ss - Smooth Cubic Curve To | |
private func cubeSmooth(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
var lastControl = last?.control2 ?? CGPoint() | |
let lastPoint = last?.point ?? CGPoint() | |
if (last?.type ?? .Line) != .CubeCurve { | |
lastControl = lastPoint | |
} | |
var control = lastPoint - lastControl | |
if coords == .Absolute { | |
control = control + lastPoint | |
} | |
return SVGCommand(control.x, control.y, numbers[0], numbers[1], numbers[2], numbers[3]) | |
} | |
// MARK: Zz - Close Path | |
private func close(numbers: [CGFloat], last: SVGCommand?, coords: Coordinates) -> SVGCommand { | |
return SVGCommand() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've forked this gist and updated the code for Swift 4 as well as added some additional parsing rules to support the SVG Paths in Google Material Icons