Skip to content

Instantly share code, notes, and snippets.

@dotcypress
Last active June 27, 2023 14:05
Show Gist options
  • Select an option

  • Save dotcypress/fa4b5c739b02f4374402 to your computer and use it in GitHub Desktop.

Select an option

Save dotcypress/fa4b5c739b02f4374402 to your computer and use it in GitHub Desktop.

Revisions

  1. dotcypress revised this gist Nov 6, 2015. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions SVGPath.swift
    Original file line number Diff line number Diff line change
    @@ -22,8 +22,8 @@ public extension CGPath {
    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)
    command.control1.x, command.control1.y,
    command.point.x, command.point.y)
    case .CubeCurve:
    CGPathAddCurveToPoint(path, nil,
    command.control1.x, command.control1.y,
  2. dotcypress renamed this gist Nov 6, 2015. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. dotcypress created this gist Nov 6, 2015.
    293 changes: 293 additions & 0 deletions GVGPath.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,293 @@
    //
    // 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()
    }