Skip to content

Instantly share code, notes, and snippets.

@dotcypress
Last active June 27, 2023 14:05
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dotcypress/fa4b5c739b02f4374402 to your computer and use it in GitHub Desktop.
Save dotcypress/fa4b5c739b02f4374402 to your computer and use it in GitHub Desktop.
SVG path to CGPath converter
//
// 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()
}
@jasorod
Copy link

jasorod commented Aug 29, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment