├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── SwiftSVG │ ├── Circle.swift │ ├── CommandRepresentable.swift │ ├── Container.swift │ ├── CoreAttributes.swift │ ├── Element.swift │ ├── Ellipse.swift │ ├── Extensions │ └── Point+SwiftSVG.swift │ ├── Fill.swift │ ├── Group.swift │ ├── Internal │ ├── EllipseProcessor.swift │ ├── PathProcessor.swift │ ├── PolygonProcressor.swift │ ├── PolylineProcessor.swift │ └── RectangleProcessor.swift │ ├── Line.swift │ ├── Path.Command.swift │ ├── Path.Component.swift │ ├── Path.ComponentParser.swift │ ├── Path.swift │ ├── Polygon.swift │ ├── Polyline.swift │ ├── PresentationAttributes.swift │ ├── Rectangle.swift │ ├── SVG+Swift2D.swift │ ├── SVG.swift │ ├── Stroke.swift │ ├── StylingAttributes.swift │ ├── Text.swift │ └── Transformation.swift └── Tests └── SwiftSVGTests ├── CommandRepresentableTests.swift ├── Extensions.swift ├── PathDataTests.swift ├── PointTests.swift ├── Resources └── quad01.svg ├── SVGTests.swift └── TranformationTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | macos-build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build (macOS) 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | 21 | ubuntu-build: 22 | 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Build (Ubuntu) 28 | run: swift build -v 29 | - name: Run tests 30 | run: swift test -v 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Richard Piazza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Swift2D", 6 | "repositoryURL": "https://github.com/richardpiazza/Swift2D.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f9e993c0075013339c5c132ab01fc78750b8774f", 10 | "version": "2.1.0" 11 | } 12 | }, 13 | { 14 | "package": "XMLCoder", 15 | "repositoryURL": "https://github.com/CoreOffice/XMLCoder.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c438dad94f6a243b411b70a4b4bac54595064808", 19 | "version": "0.15.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftSVG", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftSVG", 12 | targets: ["SwiftSVG"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | .package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.15.0"), 18 | .package(url: "https://github.com/richardpiazza/Swift2D.git", from: "2.1.0"), 19 | ], 20 | targets: [ 21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 22 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 23 | .target( 24 | name: "SwiftSVG", 25 | dependencies: [ 26 | "XMLCoder", 27 | "Swift2D" 28 | ] 29 | ), 30 | .testTarget( 31 | name: "SwiftSVGTests", 32 | dependencies: [ 33 | "SwiftSVG" 34 | ], 35 | resources: [ 36 | .process("Resources") 37 | ] 38 | ), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftSVG 2 | 3 | A Swift SVG parsing library 4 | 5 | ![](https://github.com/richardpiazza/SwiftSVG/workflows/Swift/badge.svg?branch=main) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Frichardpiazza%2FSwiftSVG%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/richardpiazza/SwiftSVG) 7 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Frichardpiazza%2FSwiftSVG%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/richardpiazza/SwiftSVG) 8 | 9 | ## Usage 10 | 11 | **SwiftSVG** is distributed using the [Swift Package Manager](https://swift.org/package-manager). To install it into a project, add it as a dependency within your `Package.swift` manifest: 12 | 13 | ```swift 14 | let package = Package( 15 | ... 16 | dependencies: [ 17 | .package(url: "https://github.com/richardpiazza/SwiftSVG.git", from: "0.10.0") 18 | ], 19 | ... 20 | ) 21 | ``` 22 | 23 | Then import the **SwiftSVG** packages wherever you'd like to use it: 24 | 25 | ```swift 26 | import SwiftSVG 27 | ``` 28 | 29 | ## Features 30 | 31 | SVG (Scalable Vector Graphics) is an XML-based markup language for describing two-dimensional vector graphics. 32 | The text-based files contain a series of shapes and paths forming images. 33 | **SwiftSVG** parses & builds SVG files so the data can be interpreted and used. (For instance: [VectorPlus](https://github.com/richardpiazza/VectorPlus)) 34 | 35 | An `SVG` is most commonly initialized using an existing file (`URL`) or `Data`. 36 | 37 | ```swift 38 | let url: URL 39 | let svg1 = try SVG.make(from: url) 40 | 41 | let data: Data 42 | let svg2 = try SVG.make(with: data) 43 | ``` 44 | 45 | ## Contributions 46 | 47 | Checkout 48 | 49 | * [Contributor Guide](https://github.com/richardpiazza/.github/blob/main/CONTRIBUTING.md) 50 | 51 | * [Code of Conduct](https://github.com/richardpiazza/.github/blob/main/CODE_OF_CONDUCT.md) 52 | 53 | * [Swift Style Guide](https://github.com/richardpiazza/.github/blob/main/SWIFT_STYLE_GUIDE.md) 54 | 55 | ## License 56 | 57 | This project is released under an [MIT License](https://github.com/richardpiazza/SwiftSVG/blob/master/LICENSE). 58 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Circle.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import XMLCoder 3 | 4 | /// Basic shape, used to draw circles based on a center point and a radius. 5 | /// 6 | /// The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the 7 | /// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform 8 | /// in the same manner as the geometry of the element. 9 | /// 10 | /// ## Documentation 11 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle) 12 | /// | [W3](https://www.w3.org/TR/SVG11/shapes.html#CircleElement) 13 | public struct Circle: Element { 14 | 15 | /// The x-axis coordinate of the center of the circle. 16 | public var x: Double = 0.0 17 | /// The y-axis coordinate of the center of the circle. 18 | public var y: Double = 0.0 19 | /// The radius of the circle. 20 | public var r: Double = 0.0 21 | 22 | // CoreAttributes 23 | public var id: String? 24 | 25 | // PresentationAttributes 26 | public var fillColor: String? 27 | public var fillOpacity: Double? 28 | public var fillRule: Fill.Rule? 29 | public var strokeColor: String? 30 | public var strokeWidth: Double? 31 | public var strokeOpacity: Double? 32 | public var strokeLineCap: Stroke.LineCap? 33 | public var strokeLineJoin: Stroke.LineJoin? 34 | public var strokeMiterLimit: Double? 35 | public var transform: String? 36 | 37 | // StylingAttributes 38 | public var style: String? 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case x = "cx" 42 | case y = "cy" 43 | case r = "r" 44 | case id 45 | case fillColor = "fill" 46 | case fillOpacity = "fill-opacity" 47 | case fillRule = "fill-rule" 48 | case strokeColor = "stroke" 49 | case strokeWidth = "stroke-width" 50 | case strokeOpacity = "stroke-opacity" 51 | case strokeLineCap = "stroke-linecap" 52 | case strokeLineJoin = "stroke-linejoin" 53 | case strokeMiterLimit = "stroke-miterlimit" 54 | case transform 55 | case style 56 | } 57 | 58 | public init() { 59 | } 60 | 61 | public init(x: Double, y: Double, r: Double) { 62 | self.x = x 63 | self.y = y 64 | self.r = r 65 | } 66 | 67 | // MARK: - CustomStringConvertible 68 | public var description: String { 69 | let desc = "" 71 | } 72 | } 73 | 74 | // MARK: - DynamicNodeEncoding 75 | extension Circle: DynamicNodeEncoding { 76 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 77 | return .attribute 78 | } 79 | } 80 | 81 | // MARK: - DynamicNodeDecoding 82 | extension Circle: DynamicNodeDecoding { 83 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 84 | return .attribute 85 | } 86 | } 87 | 88 | // MARK: - DirectionalCommandRepresentable 89 | extension Circle: DirectionalCommandRepresentable { 90 | public func commands(clockwise: Bool) throws -> [Path.Command] { 91 | return EllipseProcessor(circle: self).commands(clockwise: clockwise) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/CommandRepresentable.swift: -------------------------------------------------------------------------------- 1 | /// Elements conforming to `CommandRepresentable` can be expressed in the form of `Path.Command`s. 2 | public protocol CommandRepresentable { 3 | func commands() throws -> [Path.Command] 4 | } 5 | 6 | public protocol DirectionalCommandRepresentable: CommandRepresentable { 7 | func commands(clockwise: Bool) throws -> [Path.Command] 8 | } 9 | 10 | public extension DirectionalCommandRepresentable { 11 | /// Defaults to anti/counter-clockwise commands. 12 | func commands() throws -> [Path.Command] { 13 | return try commands(clockwise: false) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Container.swift: -------------------------------------------------------------------------------- 1 | public protocol Container { 2 | var circles: [Circle]? { get set } 3 | var ellipses: [Ellipse]? { get set } 4 | var groups: [Group]? { get set } 5 | var lines: [Line]? { get set } 6 | var paths: [Path]? { get set } 7 | var polygons: [Polygon]? { get set } 8 | var polylines: [Polyline]? { get set } 9 | var rectangles: [Rectangle]? { get set } 10 | var texts: [Text]? { get set } 11 | } 12 | 13 | internal enum ContainerKeys: String, CodingKey { 14 | case circles = "circle" 15 | case ellipses = "ellipse" 16 | case groups = "g" 17 | case lines = "line" 18 | case paths = "path" 19 | case polylines = "polyline" 20 | case polygons = "polygon" 21 | case rectangles = "rect" 22 | case texts = "text" 23 | } 24 | 25 | public extension Container { 26 | var containerDescription: String { 27 | var contents: String = "" 28 | 29 | let circles = self.circles?.compactMap({ $0.description }) ?? [] 30 | circles.forEach({ contents.append("\n\($0)") }) 31 | 32 | let ellipses = self.ellipses?.compactMap({ $0.description }) ?? [] 33 | ellipses.forEach({ contents.append("\n\($0)") }) 34 | 35 | let groups = self.groups?.compactMap({ $0.description }) ?? [] 36 | groups.forEach({ contents.append("\n\($0)") }) 37 | 38 | let lines = self.lines?.compactMap({ $0.description }) ?? [] 39 | lines.forEach({ contents.append("\n\($0)") }) 40 | 41 | let paths = self.paths?.compactMap({ $0.description }) ?? [] 42 | paths.forEach({ contents.append("\n\($0)") }) 43 | 44 | let polylines = self.polylines?.compactMap({ $0.description }) ?? [] 45 | polylines.forEach({ contents.append("\n\($0)") }) 46 | 47 | let polygons = self.polygons?.compactMap({ $0.description }) ?? [] 48 | polygons.forEach({ contents.append("\n\($0)") }) 49 | 50 | let rectangles = self.rectangles?.compactMap({ $0.description }) ?? [] 51 | rectangles.forEach({ contents.append("\n\($0)") }) 52 | 53 | let texts = self.texts?.compactMap({ $0.description }) ?? [] 54 | texts.forEach({ contents.append("\n\($0)") }) 55 | 56 | return contents 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/CoreAttributes.swift: -------------------------------------------------------------------------------- 1 | public protocol CoreAttributes { 2 | var id: String? { get set } 3 | } 4 | 5 | internal enum CoreAttributesKeys: String, CodingKey { 6 | case id 7 | } 8 | 9 | public extension CoreAttributes { 10 | var coreDescription: String { 11 | if let id = self.id { 12 | return "\(CoreAttributesKeys.id.rawValue)=\"\(id)\"" 13 | } else { 14 | return "" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Element.swift: -------------------------------------------------------------------------------- 1 | public protocol Element: CoreAttributes, PresentationAttributes, StylingAttributes { 2 | } 3 | 4 | public extension Element { 5 | var attributeDescription: String { 6 | var components: [String] = [] 7 | 8 | if !coreDescription.isEmpty { 9 | components.append(coreDescription) 10 | } 11 | if !presentationDescription.isEmpty { 12 | components.append(presentationDescription) 13 | } 14 | if !stylingDescription.isEmpty { 15 | components.append(stylingDescription) 16 | } 17 | 18 | return components.joined(separator: " ") 19 | } 20 | } 21 | 22 | public extension CommandRepresentable where Self: Element { 23 | /// When a `Path` is accessed on an element, the path that is returned should have the supplied transformations 24 | /// applied. 25 | /// 26 | /// For instance, if 27 | /// * a `Path.data` contains relative elements, 28 | /// * and `transformations` contains a `.translate` 29 | /// 30 | /// Than the path created will not only use 'absolute' instructions, but those instructions will be modified to 31 | /// include the required transformation. 32 | func path(applying transformations: [Transformation] = []) throws -> Path { 33 | var _transformations = transformations 34 | _transformations.append(contentsOf: self.transformations) 35 | 36 | let commands = try self.commands().map({ $0.applying(transformations: _transformations) }) 37 | 38 | var path = Path(commands: commands) 39 | path.fillColor = fillColor 40 | path.fillOpacity = fillOpacity 41 | path.strokeColor = strokeColor 42 | path.strokeOpacity = strokeOpacity 43 | path.strokeWidth = strokeWidth 44 | 45 | return path 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Ellipse.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import XMLCoder 3 | 4 | /// SVG basic shape, used to create ellipses based on a center coordinate, and both their x and y radius. 5 | /// 6 | /// The arc of an ‘ellipse’ element begins at the "3 o'clock" point on the radius and progresses towards the 7 | /// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same 8 | /// manner as the geometry of the element. 9 | public struct Ellipse: Element { 10 | 11 | /// The x position of the ellipse. 12 | public var x: Double = 0.0 13 | /// The y position of the ellipse. 14 | public var y: Double = 0.0 15 | /// The radius of the ellipse on the x axis. 16 | public var rx: Double = 0.0 17 | /// The radius of the ellipse on the y axis. 18 | public var ry: Double = 0.0 19 | 20 | // CoreAttributes 21 | public var id: String? 22 | 23 | // PresentationAttributes 24 | public var fillColor: String? 25 | public var fillOpacity: Double? 26 | public var fillRule: Fill.Rule? 27 | public var strokeColor: String? 28 | public var strokeWidth: Double? 29 | public var strokeOpacity: Double? 30 | public var strokeLineCap: Stroke.LineCap? 31 | public var strokeLineJoin: Stroke.LineJoin? 32 | public var strokeMiterLimit: Double? 33 | public var transform: String? 34 | 35 | // StylingAttributes 36 | public var style: String? 37 | 38 | enum CodingKeys: String, CodingKey { 39 | case x = "cx" 40 | case y = "cy" 41 | case rx 42 | case ry 43 | case id 44 | case fillColor = "fill" 45 | case fillOpacity = "fill-opacity" 46 | case fillRule = "fill-rule" 47 | case strokeColor = "stroke" 48 | case strokeWidth = "stroke-width" 49 | case strokeOpacity = "stroke-opacity" 50 | case strokeLineCap = "stroke-linecap" 51 | case strokeLineJoin = "stroke-linejoin" 52 | case strokeMiterLimit = "stroke-miterlimit" 53 | case transform 54 | case style 55 | } 56 | 57 | public init() { 58 | } 59 | 60 | public init(x: Double, y: Double, rx: Double, ry: Double) { 61 | self.x = x 62 | self.y = y 63 | self.rx = rx 64 | self.ry = ry 65 | } 66 | 67 | // MARK: - CustomStringConvertible 68 | public var description: String { 69 | let desc = "" 71 | } 72 | } 73 | 74 | // MARK: - DynamicNodeEncoding 75 | extension Ellipse: DynamicNodeEncoding { 76 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 77 | return .attribute 78 | } 79 | } 80 | 81 | // MARK: - DynamicNodeDecoding 82 | extension Ellipse: DynamicNodeDecoding { 83 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 84 | return .attribute 85 | } 86 | } 87 | 88 | // MARK: - DirectionalCommandRepresentable 89 | extension Ellipse: DirectionalCommandRepresentable { 90 | public func commands(clockwise: Bool) throws -> [Path.Command] { 91 | return EllipseProcessor(ellipse: self).commands(clockwise: clockwise) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Extensions/Point+SwiftSVG.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | extension Point { 4 | static var nan: Point { 5 | return Point(x: Double.nan, y: Double.nan) 6 | } 7 | 8 | var hasNaN: Bool { 9 | return x.isNaN || y.isNaN 10 | } 11 | 12 | /// Returns a copy of the instance with the **x** value replaced with the provided value. 13 | func with(x value: Double) -> Point { 14 | return Point(x: value, y: y) 15 | } 16 | 17 | /// Returns a copy of the instance with the **y** value replaced with the provided value. 18 | func with(y value: Double) -> Point { 19 | return Point(x: x, y: value) 20 | } 21 | 22 | /// Adjusts the **x** value by the provided amount. 23 | /// 24 | /// This will explicitly check for `.isNaN`, and if encountered, will simply 25 | /// use the provided value. 26 | func adjusting(x value: Double) -> Point { 27 | return (x.isNaN) ? with(x: value) : with(x: x + value) 28 | } 29 | 30 | /// Adjusts the **y** value by the provided amount. 31 | /// 32 | /// This will explicitly check for `.isNaN`, and if encountered, will simply 33 | /// use the provided value. 34 | func adjusting(y value: Double) -> Point { 35 | return (y.isNaN) ? with(y: value) : with(y: y + value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Fill.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | public struct Fill { 4 | 5 | public var color: String? 6 | public var opacity: Double? 7 | public var rule: Rule = .nonZero 8 | 9 | public init() {} 10 | 11 | /// Presentation attribute defining the algorithm to use to determine the inside part of a shape. 12 | /// 13 | /// The default `Rule` is `.nonzero`. 14 | public enum Rule: String, Codable, CaseIterable { 15 | /// The value evenodd determines the "insideness" of a point in the shape by drawing a ray from that point to 16 | /// infinity in any direction and counting the number of path segments from the given shape that the ray 17 | /// crosses. If this number is odd, the point is inside; if even, the point is outside. 18 | case evenOdd = "evenodd" 19 | /// The value nonzero determines the "insideness" of a point in the shape by drawing a ray from that point to 20 | /// infinity in any direction, and then examining the places where a segment of the shape crosses the ray. 21 | /// Starting with a count of zero, add one each time a path segment crosses the ray from left to right and 22 | /// subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if 23 | /// the result is zero then the point is outside the path. Otherwise, it is inside. 24 | case nonZero = "nonzero" 25 | 26 | public init(from decoder: Decoder) throws { 27 | let container = try decoder.singleValueContainer() 28 | let rawValue = try container.decode(String.self) 29 | guard let rule = Rule(rawValue: rawValue) else { 30 | print("Attempts to decode Fill.Rule with rawValue: '\(rawValue)'") 31 | self = .nonZero 32 | return 33 | } 34 | 35 | self = rule 36 | } 37 | } 38 | } 39 | 40 | extension Fill.Rule: CustomStringConvertible { 41 | public var description: String { 42 | return self.rawValue 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Group.swift: -------------------------------------------------------------------------------- 1 | import XMLCoder 2 | 3 | /// A container used to group other SVG elements. 4 | /// 5 | /// Grouping constructs, when used in conjunction with the ‘desc’ and ‘title’ elements, provide information 6 | /// about document structure and semantics. 7 | /// 8 | /// ## Documentation 9 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g) 10 | /// | [W3](https://www.w3.org/TR/SVG11/struct.html#Groups) 11 | public struct Group: Container, Element { 12 | 13 | // Container 14 | public var circles: [Circle]? 15 | public var ellipses: [Ellipse]? 16 | public var groups: [Group]? 17 | public var lines: [Line]? 18 | public var paths: [Path]? 19 | public var polygons: [Polygon]? 20 | public var polylines: [Polyline]? 21 | public var rectangles: [Rectangle]? 22 | public var texts: [Text]? 23 | 24 | // CoreAttributes 25 | public var id: String? 26 | public var title: String? 27 | public var desc: String? 28 | 29 | // PresentationAttributes 30 | public var fillColor: String? 31 | public var fillOpacity: Double? 32 | public var fillRule: Fill.Rule? 33 | public var strokeColor: String? 34 | public var strokeWidth: Double? 35 | public var strokeOpacity: Double? 36 | public var strokeLineCap: Stroke.LineCap? 37 | public var strokeLineJoin: Stroke.LineJoin? 38 | public var strokeMiterLimit: Double? 39 | public var transform: String? 40 | 41 | // StylingAttributes 42 | public var style: String? 43 | 44 | enum CodingKeys: String, CodingKey { 45 | case circles = "circle" 46 | case ellipses = "ellipse" 47 | case groups = "g" 48 | case lines = "line" 49 | case paths = "path" 50 | case polylines = "polyline" 51 | case polygons = "polygon" 52 | case rectangles = "rect" 53 | case texts = "text" 54 | case id 55 | case title 56 | case desc 57 | case fillColor = "fill" 58 | case fillOpacity = "fill-opacity" 59 | case fillRule = "fill-rule" 60 | case strokeColor = "stroke" 61 | case strokeWidth = "stroke-width" 62 | case strokeOpacity = "stroke-opacity" 63 | case strokeLineCap = "stroke-linecap" 64 | case strokeLineJoin = "stroke-linejoin" 65 | case strokeMiterLimit = "stroke-miterlimit" 66 | case transform 67 | case style 68 | } 69 | 70 | public init() { 71 | } 72 | 73 | // MARK: - CustomStringConvertible 74 | public var description: String { 75 | var contents: String = "" 76 | 77 | if let title = self.title { 78 | contents.append("\n\(title)") 79 | } 80 | 81 | if let desc = self.desc { 82 | contents.append("\n\(desc)") 83 | } 84 | 85 | contents.append(containerDescription) 86 | 87 | return "\(contents)\n" 88 | } 89 | } 90 | 91 | // MARK: - DynamicNodeEncoding 92 | extension Group: DynamicNodeEncoding { 93 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 94 | if let _ = ContainerKeys(stringValue: key.stringValue) { 95 | return .element 96 | } 97 | 98 | return .attribute 99 | } 100 | } 101 | 102 | // MARK: - DynamicNodeDecoding 103 | extension Group: DynamicNodeDecoding { 104 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 105 | if let _ = ContainerKeys(stringValue: key.stringValue) { 106 | return .element 107 | } 108 | 109 | return .attribute 110 | } 111 | } 112 | 113 | // MARK: - Paths 114 | public extension Group { 115 | /// A representation of all the sub-`Path`s in the `Group`. 116 | func subpaths(applying transformations: [Transformation] = []) throws -> [Path] { 117 | var _transformations = transformations 118 | _transformations.append(contentsOf: self.transformations) 119 | 120 | var output: [Path] = [] 121 | 122 | if let circles = self.circles { 123 | try output.append(contentsOf: circles.compactMap({ try $0.path(applying: _transformations) })) 124 | } 125 | 126 | if let ellipses = self.ellipses { 127 | try output.append(contentsOf: ellipses.compactMap({ try $0.path(applying: _transformations) })) 128 | } 129 | 130 | if let rectangles = self.rectangles { 131 | try output.append(contentsOf: rectangles.compactMap({ try $0.path(applying: _transformations) })) 132 | } 133 | 134 | if let polygons = self.polygons { 135 | try output.append(contentsOf: polygons.compactMap({ try $0.path(applying: _transformations) })) 136 | } 137 | 138 | if let polylines = self.polylines { 139 | try output.append(contentsOf: polylines.compactMap({ try $0.path(applying: _transformations) })) 140 | } 141 | 142 | if let paths = self.paths { 143 | try output.append(contentsOf: paths.map({ try $0.path(applying: _transformations) })) 144 | } 145 | 146 | if let groups = self.groups { 147 | try groups.forEach({ 148 | try output.append(contentsOf: $0.subpaths(applying: _transformations)) 149 | }) 150 | } 151 | 152 | return output 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Internal/EllipseProcessor.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import Foundation 3 | 4 | struct EllipseProcessor { 5 | 6 | let x: Double 7 | let y: Double 8 | let rx: Double 9 | let ry: Double 10 | 11 | /// The _optimal_ offset for control points when representing a 12 | /// circle/ellipse as 4 bezier curves. 13 | /// 14 | /// [Stack Overflow](https://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves) 15 | static func controlPointOffset(_ radius: Double) -> Double { 16 | return (Double(4.0/3.0) * tan(Double.pi / 8.0)) * radius 17 | } 18 | 19 | init(ellipse: Ellipse) { 20 | x = ellipse.x 21 | y = ellipse.y 22 | rx = ellipse.rx 23 | ry = ellipse.ry 24 | } 25 | 26 | init(circle: Circle) { 27 | x = circle.x 28 | y = circle.y 29 | rx = circle.r 30 | ry = circle.r 31 | } 32 | 33 | func commands(clockwise: Bool) -> [Path.Command] { 34 | var commands: [Path.Command] = [] 35 | 36 | let xOffset = Self.controlPointOffset(rx) 37 | let yOffset = Self.controlPointOffset(ry) 38 | 39 | let zero = Point(x: x + rx, y: y) 40 | let ninety = Point(x: x, y: y - ry) 41 | let oneEighty = Point(x: x - rx, y: y) 42 | let twoSeventy = Point(x: x, y: y + ry) 43 | 44 | var cp1: Point = .zero 45 | var cp2: Point = .zero 46 | 47 | // Starting at degree 0 (the right most point) 48 | commands.append(.moveTo(point: zero)) 49 | 50 | if clockwise { 51 | cp1 = Point(x: zero.x, y: zero.y + yOffset) 52 | cp2 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y) 53 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy)) 54 | 55 | cp1 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y) 56 | cp2 = Point(x: oneEighty.x, y: oneEighty.y + yOffset) 57 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty)) 58 | 59 | cp1 = Point(x: oneEighty.x, y: oneEighty.y - yOffset) 60 | cp2 = Point(x: ninety.x - xOffset, y: ninety.y) 61 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety)) 62 | 63 | cp1 = Point(x: ninety.x + xOffset, y: ninety.y) 64 | cp2 = Point(x: zero.x, y: zero.y - yOffset) 65 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero)) 66 | } else { 67 | cp1 = Point(x: zero.x, y: zero.y - yOffset) 68 | cp2 = Point(x: ninety.x + xOffset, y: ninety.y) 69 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety)) 70 | 71 | cp1 = Point(x: ninety.x - xOffset, y: ninety.y) 72 | cp2 = Point(x: oneEighty.x, y: oneEighty.y - yOffset) 73 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty)) 74 | 75 | cp1 = Point(x: oneEighty.x, y: oneEighty.y + yOffset) 76 | cp2 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y) 77 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy)) 78 | 79 | cp1 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y) 80 | cp2 = Point(x: zero.x, y: zero.y + yOffset) 81 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero)) 82 | } 83 | 84 | commands.append(.closePath) 85 | 86 | return commands 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Internal/PathProcessor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct PathProcessor { 4 | 5 | let data: String 6 | 7 | init(data: String) { 8 | self.data = data 9 | } 10 | 11 | func commands() throws -> [Path.Command] { 12 | let parser = Path.ComponentParser() 13 | let components = try Path.Component.components(from: data) 14 | return try parser.parse(components) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Internal/PolygonProcressor.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | struct PolygonProcessor { 4 | 5 | let points: String 6 | 7 | init(points: String) { 8 | self.points = points 9 | } 10 | 11 | func commands() throws -> [Path.Command] { 12 | let pairs = points.components(separatedBy: " ") 13 | let components = pairs.flatMap({ $0.components(separatedBy: ",") }) 14 | guard components.count > 0 else { 15 | return [] 16 | } 17 | 18 | guard components.count % 2 == 0 else { 19 | // An odd number of components means that parsing probably failed 20 | return [] 21 | } 22 | 23 | var commands: [Path.Command] = [] 24 | 25 | var firstValue: Bool = true 26 | for (idx, component) in components.enumerated() { 27 | guard let _value = Double(component) else { 28 | return commands 29 | } 30 | 31 | let value = Double(_value) 32 | 33 | if firstValue { 34 | if idx == 0 { 35 | commands.append(.moveTo(point: Point(x: value, y: .nan))) 36 | } else { 37 | commands.append(.lineTo(point: Point(x: value, y: .nan))) 38 | } 39 | firstValue = false 40 | } else { 41 | let count = commands.count 42 | guard let modified = try? commands.last?.adjustingArgument(at: 1, by: value) else { 43 | return commands 44 | } 45 | 46 | commands[count - 1] = modified 47 | firstValue = true 48 | } 49 | } 50 | 51 | commands.append(.closePath) 52 | 53 | return commands 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Internal/PolylineProcessor.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | struct PolylineProcessor { 4 | 5 | let points: String 6 | 7 | init(points: String) { 8 | self.points = points 9 | } 10 | 11 | func commands() throws -> [Path.Command] { 12 | let pairs = points.components(separatedBy: " ") 13 | let components = pairs.flatMap({ $0.components(separatedBy: ",") }) 14 | let values = components.compactMap({ Double($0) }).map({ Double($0) }) 15 | 16 | guard values.count > 2 else { 17 | // More than just a starting point is required. 18 | return [] 19 | } 20 | 21 | guard values.count % 2 == 0 else { 22 | // An odd number of components means that parsing probably failed 23 | return [] 24 | } 25 | 26 | var commands: [Path.Command] = [] 27 | 28 | let move = values.prefix(upTo: 2) 29 | let segments = values.suffix(from: 2) 30 | 31 | commands.append(.moveTo(point: Point(x: move[0], y: move[1]))) 32 | 33 | var _value: Double = .nan 34 | segments.forEach { (value) in 35 | if _value.isNaN { 36 | _value = value 37 | } else { 38 | commands.append(.lineTo(point: Point(x: _value, y: value))) 39 | _value = .nan 40 | } 41 | } 42 | 43 | let reversedSegments = segments.dropLast(2).reversed() 44 | reversedSegments.forEach { (value) in 45 | if _value.isNaN { 46 | _value = value 47 | } else { 48 | commands.append(.lineTo(point: Point(x: _value, y: value))) 49 | _value = .nan 50 | } 51 | } 52 | 53 | commands.append(.closePath) 54 | 55 | return commands 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Internal/RectangleProcessor.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | struct RectangleProcessor { 4 | 5 | let rectangle: Rectangle 6 | 7 | init(rectangle: Rectangle) { 8 | self.rectangle = rectangle 9 | } 10 | 11 | func commands(clockwise: Bool) -> [Path.Command] { 12 | var rx = rectangle.rx 13 | var ry = rectangle.ry 14 | 15 | if let _rx = rx, _rx > (rectangle.width / 2.0) { 16 | rx = rectangle.width / 2.0 17 | } 18 | 19 | if let _ry = ry, _ry > (rectangle.height / 2.0) { 20 | ry = rectangle.height / 2.0 21 | } 22 | 23 | var commands: [Path.Command] = [] 24 | 25 | switch (rx, ry) { 26 | case (.some(let radiusX), .some(let radiusY)) where radiusX != radiusY: 27 | // Use Cubic Bezier Curve to form rounded corners 28 | // TODO: Verify that the control points are right 29 | 30 | var cp1: Point = .zero 31 | var cp2: Point = .zero 32 | var point: Point = Point(x: rectangle.x + radiusX, y: rectangle.y) 33 | 34 | commands.append(.moveTo(point: point)) 35 | 36 | if clockwise { 37 | point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y) 38 | commands.append(.lineTo(point: point)) 39 | 40 | cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y) 41 | cp2 = cp1 42 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY) 43 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 44 | 45 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY) 46 | commands.append(.lineTo(point: point)) 47 | 48 | cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) 49 | cp2 = cp1 50 | point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height) 51 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 52 | 53 | point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height) 54 | commands.append(.lineTo(point: point)) 55 | 56 | cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height) 57 | cp2 = cp1 58 | point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY) 59 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 60 | 61 | point = .init(x: rectangle.x, y: rectangle.y + radiusY) 62 | commands.append(.lineTo(point: point)) 63 | 64 | cp1 = .init(x: rectangle.x, y: rectangle.y) 65 | cp2 = cp1 66 | point = .init(x: rectangle.x + radiusX, y: rectangle.y) 67 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 68 | } else { 69 | cp1 = .init(x: rectangle.x, y: rectangle.y) 70 | cp2 = cp1 71 | point = .init(x: rectangle.x, y: rectangle.y + radiusY) 72 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 73 | 74 | point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY) 75 | commands.append(.lineTo(point: point)) 76 | 77 | cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height) 78 | cp2 = cp1 79 | point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height) 80 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 81 | 82 | point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height) 83 | commands.append(.lineTo(point: point)) 84 | 85 | cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) 86 | cp2 = cp1 87 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY) 88 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 89 | 90 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY) 91 | commands.append(.lineTo(point: point)) 92 | 93 | cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y) 94 | cp2 = cp1 95 | point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y) 96 | commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point)) 97 | } 98 | case (.some(let radius), .none), (.none, .some(let radius)), (.some(let radius), _): 99 | // use Quadratic Bezier Curve to form rounded corners 100 | 101 | var cp: Point = .zero 102 | var point: Point = Point(x: rectangle.x + radius, y: rectangle.y) 103 | 104 | commands.append(.moveTo(point: point)) 105 | 106 | if clockwise { 107 | point = .init(x: (rectangle.x + rectangle.width) - radius, y: rectangle.y) 108 | commands.append(.lineTo(point: point)) 109 | 110 | cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y) 111 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius) 112 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 113 | 114 | point = .init(x: rectangle.x + rectangle.width, y: (rectangle.y + rectangle.height) - radius) 115 | commands.append(.lineTo(point: point)) 116 | 117 | cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) 118 | point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height) 119 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 120 | 121 | point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height) 122 | commands.append(.lineTo(point: point)) 123 | 124 | cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height) 125 | point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius) 126 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 127 | 128 | point = .init(x: rectangle.x, y: rectangle.y + radius) 129 | commands.append(.lineTo(point: point)) 130 | 131 | cp = .init(x: rectangle.x, y: rectangle.y) 132 | point = .init(x: rectangle.x + radius, y: rectangle.y) 133 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 134 | } else { 135 | cp = .init(x: rectangle.x, y: rectangle.y) 136 | point = .init(x: rectangle.x, y: rectangle.y + radius) 137 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 138 | 139 | point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius) 140 | commands.append(.lineTo(point: point)) 141 | 142 | cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height) 143 | point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height) 144 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 145 | 146 | point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height) 147 | commands.append(.lineTo(point: point)) 148 | 149 | cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height) 150 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radius) 151 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 152 | 153 | point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius) 154 | commands.append(.lineTo(point: point)) 155 | 156 | cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y) 157 | point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y) 158 | commands.append(.quadraticBezierCurve(cp: cp, point: point)) 159 | } 160 | case (.none, .none): 161 | // draw three line segments. 162 | commands.append(.moveTo(point: Point(x: rectangle.x, y: rectangle.y))) 163 | 164 | if clockwise { 165 | commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y))) 166 | commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height))) 167 | commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height))) 168 | } else { 169 | commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height))) 170 | commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height))) 171 | commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y))) 172 | } 173 | } 174 | 175 | commands.append(.closePath) 176 | 177 | return commands 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Line.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import XMLCoder 3 | 4 | /// SVG basic shape used to create a line connecting two points. 5 | /// 6 | /// ## Documentation 7 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) 8 | /// | [W3](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) 9 | public struct Line: Element { 10 | 11 | /// Defines the x-axis coordinate of the line starting point. 12 | public var x1: Double = 0.0 13 | /// Defines the x-axis coordinate of the line ending point. 14 | public var y1: Double = 0.0 15 | /// Defines the y-axis coordinate of the line starting point. 16 | public var x2: Double = 0.0 17 | /// Defines the y-axis coordinate of the line ending point. 18 | public var y2: Double = 0.0 19 | 20 | // CoreAttributes 21 | public var id: String? 22 | 23 | // PresentationAttributes 24 | public var fillColor: String? 25 | public var fillOpacity: Double? 26 | public var fillRule: Fill.Rule? 27 | public var strokeColor: String? 28 | public var strokeWidth: Double? 29 | public var strokeOpacity: Double? 30 | public var strokeLineCap: Stroke.LineCap? 31 | public var strokeLineJoin: Stroke.LineJoin? 32 | public var strokeMiterLimit: Double? 33 | public var transform: String? 34 | 35 | // StylingAttributes 36 | public var style: String? 37 | 38 | enum CodingKeys: String, CodingKey { 39 | case x1 40 | case y1 41 | case x2 42 | case y2 43 | case id 44 | case fillColor = "fill" 45 | case fillOpacity = "fill-opacity" 46 | case fillRule = "fill-rule" 47 | case strokeColor = "stroke" 48 | case strokeWidth = "stroke-width" 49 | case strokeOpacity = "stroke-opacity" 50 | case strokeLineCap = "stroke-linecap" 51 | case strokeLineJoin = "stroke-linejoin" 52 | case strokeMiterLimit = "stroke-miterlimit" 53 | case transform 54 | case style 55 | } 56 | 57 | public init() { 58 | } 59 | 60 | public init(x1: Double, y1: Double, x2: Double, y2: Double) { 61 | self.x1 = x1 62 | self.y1 = y1 63 | self.x2 = x2 64 | self.y2 = y2 65 | } 66 | 67 | // MARK: - CustomStringConvertible 68 | public var description: String { 69 | let desc = "" 71 | } 72 | } 73 | 74 | // MARK: - DynamicNodeEncoding 75 | extension Line: DynamicNodeEncoding { 76 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 77 | return .attribute 78 | } 79 | } 80 | 81 | // MARK: - DynamicNodeDecoding 82 | extension Line: DynamicNodeDecoding { 83 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 84 | return .attribute 85 | } 86 | } 87 | 88 | // MARK: - CommandRepresentable 89 | extension Line: CommandRepresentable { 90 | public func commands() throws -> [Path.Command] { 91 | return [ 92 | .moveTo(point: Point(x: x1, y: y1)), 93 | .lineTo(point: Point(x: x2, y: y2)), 94 | .lineTo(point: Point(x: x1, y: y1)), 95 | .closePath 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Path.Command.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import Foundation 3 | 4 | public extension Path { 5 | /// Path commands are instructions that define a path to be drawn. 6 | /// 7 | /// Each command is composed of a command letter and numbers that represent the command parameters. 8 | enum Command: Equatable, CustomStringConvertible { 9 | /// Moves the current drawing point 10 | case moveTo(point: Point) 11 | /// Draw a straight line from the current point to the point provided 12 | case lineTo(point: Point) 13 | /// Draw a smooth curve using three points (+ origin) 14 | case cubicBezierCurve(cp1: Point, cp2: Point, point: Point) 15 | /// Draw a smooth curve using two points (+ origin) 16 | case quadraticBezierCurve(cp: Point, point: Point) 17 | /// Draw a curve defined as a portion of an ellipse 18 | case ellipticalArcCurve(rx: Double, ry: Double, angle: Double, largeArc: Bool, clockwise: Bool, point: Point) 19 | /// ClosePath instructions draw a straight line from the current position to the first point in the path. 20 | case closePath 21 | 22 | public enum Prefix: Character, CaseIterable { 23 | case move = "M" 24 | case relativeMove = "m" 25 | case line = "L" 26 | case relativeLine = "l" 27 | case horizontalLine = "H" 28 | case relativeHorizontalLine = "h" 29 | case verticalLine = "V" 30 | case relativeVerticalLine = "v" 31 | case cubicBezierCurve = "C" 32 | case relativeCubicBezierCurve = "c" 33 | case smoothCubicBezierCurve = "S" 34 | case relativeSmoothCubicBezierCurve = "s" 35 | case quadraticBezierCurve = "Q" 36 | case relativeQuadraticBezierCurve = "q" 37 | case smoothQuadraticBezierCurve = "T" 38 | case relativeSmoothQuadraticBezierCurve = "t" 39 | case ellipticalArcCurve = "A" 40 | case relativeEllipticalArcCurve = "a" 41 | case close = "Z" 42 | case relativeClose = "z" 43 | 44 | public static var characterSet: CharacterSet { 45 | return CharacterSet(charactersIn: allCases.map({ String($0.rawValue) }).joined()) 46 | } 47 | } 48 | 49 | public enum Coordinates { 50 | case absolute 51 | case relative 52 | } 53 | 54 | public enum Error: Swift.Error { 55 | case message(String) 56 | case invalidAdjustment(Path.Command) 57 | case invalidArgumentPosition(Int, Path.Command) 58 | case invalidRelativeCommand 59 | } 60 | 61 | public var description: String { 62 | switch self { 63 | case .moveTo(let point): 64 | return "\(Prefix.move.rawValue)\(point.x),\(point.y)" 65 | case .lineTo(let point): 66 | return "\(Prefix.line.rawValue)\(point.x),\(point.y)" 67 | case .cubicBezierCurve(let cp1, let cp2, let point): 68 | return "\(Prefix.cubicBezierCurve.rawValue)\(cp1.x),\(cp1.y) \(cp2.x),\(cp2.y) \(point.x),\(point.y)" 69 | case .quadraticBezierCurve(let cp, let point): 70 | return "\(Prefix.quadraticBezierCurve.rawValue)\(cp.x),\(cp.y) \(point.x),\(point.y)" 71 | case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): 72 | let la = largeArc ? 1 : 0 73 | let cw = clockwise ? 1 : 0 74 | return "\(Prefix.ellipticalArcCurve.rawValue)\(rx) \(ry) \(angle) \(la) \(cw) \(point.x) \(point.y)" 75 | case .closePath: 76 | return "\(Prefix.close.rawValue)" 77 | } 78 | } 79 | 80 | /// The primary point that dictates the commands action. 81 | public var point: Point { 82 | switch self { 83 | case .moveTo(let point): return point 84 | case .lineTo(let point): return point 85 | case .cubicBezierCurve(_, _, let point): return point 86 | case .quadraticBezierCurve(_, let point): return point 87 | case .ellipticalArcCurve(_, _, _, _, _, let point): return point 88 | case .closePath: return .zero 89 | } 90 | } 91 | } 92 | } 93 | 94 | public extension Path.Command { 95 | /// Applies the provided `Transformation` to the instances values. 96 | func applying(transformation: Transformation) -> Path.Command { 97 | switch transformation { 98 | case .translate(let x, let y): 99 | switch self { 100 | case .moveTo(let point): 101 | let _point = point.adjusting(x: x).adjusting(y: y) 102 | return .moveTo(point: _point) 103 | case .lineTo(let point): 104 | let _point = point.adjusting(x: x).adjusting(y: y) 105 | return .lineTo(point: _point) 106 | case .cubicBezierCurve(let cp1, let cp2, let point): 107 | let _cp1 = cp1.adjusting(x: x).adjusting(y: y) 108 | let _cp2 = cp2.adjusting(x: x).adjusting(y: y) 109 | let _point = point.adjusting(x: x).adjusting(y: y) 110 | return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point) 111 | case .quadraticBezierCurve(let cp, let point): 112 | let _cp = cp.adjusting(x: x).adjusting(y: y) 113 | let _point = point.adjusting(x: x).adjusting(y: y) 114 | return .quadraticBezierCurve(cp: _cp, point: _point) 115 | case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): 116 | let _point = point.adjusting(x: x).adjusting(y: y) 117 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point) 118 | case .closePath: 119 | return self 120 | } 121 | case .matrix: 122 | // TODO: What should occur here? 123 | return self 124 | } 125 | } 126 | 127 | /// Applies multiple transformations in the order they are specified. 128 | func applying(transformations: [Transformation]) -> Path.Command { 129 | var command = self 130 | 131 | transformations.forEach { (transformation) in 132 | command = command.applying(transformation: transformation) 133 | } 134 | 135 | return command 136 | } 137 | } 138 | 139 | internal extension Path.Command { 140 | /// Determines if all values are provided (i.e. !.isNaN) 141 | var isComplete: Bool { 142 | switch self { 143 | case .moveTo(let point), .lineTo(let point): 144 | return !point.hasNaN 145 | case .cubicBezierCurve(let cp1, let cp2, let point): 146 | return !cp1.hasNaN && !cp2.hasNaN && !point.hasNaN 147 | case .quadraticBezierCurve(let cp, let point): 148 | return !cp.hasNaN && !point.hasNaN 149 | case .ellipticalArcCurve(let rx, let ry, let angle, _, _, let point): 150 | return !rx.isNaN && !ry.isNaN && !angle.isNaN && !point.hasNaN 151 | case .closePath: 152 | return true 153 | } 154 | } 155 | 156 | /// The last control point used in drawing the path. 157 | /// 158 | /// Only valid for curves. 159 | var lastControlPoint: Point? { 160 | switch self { 161 | case .cubicBezierCurve(_, let cp2, _): 162 | return cp2 163 | case .quadraticBezierCurve(let cp, _): 164 | return cp 165 | default: 166 | return nil 167 | } 168 | } 169 | 170 | /// A mirror representation of `lastControlPoint`. 171 | var lastControlPointMirror: Point? { 172 | guard let cp = lastControlPoint else { 173 | return nil 174 | } 175 | 176 | return Point(x: point.x + (point.x - cp.x), y: point.y + (point.y - cp.y)) 177 | } 178 | 179 | /// The total number of argument values the command requires. 180 | var arguments: Int { 181 | switch self { 182 | case .moveTo: return 2 183 | case .lineTo: return 2 184 | case .cubicBezierCurve: return 6 185 | case .quadraticBezierCurve: return 4 186 | case .ellipticalArcCurve: return 7 187 | case .closePath: return 0 188 | } 189 | } 190 | 191 | /// Adjusts a Command argument by a specified amount. 192 | /// 193 | /// A `Point` consumes two positions. So, in the example `.quadraticBezierCurve(cp: .zero, point: .zero)`: 194 | /// * position 0 = Control Point X 195 | /// * position 1 = Control Point Y 196 | /// * position 2 = Point X 197 | /// * position 3 = Point Y 198 | /// 199 | /// - parameter position: The index of the argument parameter to adjust. 200 | /// - parameter value: The value to add to the existing value. If the current value equal `.isNaN`, than the 201 | /// supplied value is used as-is. 202 | /// - throws: `Path.Command.Error` 203 | func adjustingArgument(at position: Int, by value: Double) throws -> Path.Command { 204 | switch self { 205 | case .moveTo(let point): 206 | switch position { 207 | case 0: 208 | return .moveTo(point: point.adjusting(x: value)) 209 | case 1: 210 | return .moveTo(point: point.adjusting(y: value)) 211 | default: 212 | throw Path.Command.Error.invalidArgumentPosition(position, self) 213 | } 214 | case .lineTo(let point): 215 | switch position { 216 | case 0: 217 | return .lineTo(point: point.adjusting(x: value)) 218 | case 1: 219 | return .lineTo(point: point.adjusting(y: value)) 220 | default: 221 | throw Path.Command.Error.invalidArgumentPosition(position, self) 222 | } 223 | case .cubicBezierCurve(let cp1, let cp2, let point): 224 | switch position { 225 | case 0: 226 | return .cubicBezierCurve(cp1: cp1.adjusting(x: value), cp2: cp2, point: point) 227 | case 1: 228 | return .cubicBezierCurve(cp1: cp1.adjusting(y: value), cp2: cp2, point: point) 229 | case 2: 230 | return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(x: value), point: point) 231 | case 3: 232 | return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(y: value), point: point) 233 | case 4: 234 | return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(x: value)) 235 | case 5: 236 | return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(y: value)) 237 | default: 238 | throw Path.Command.Error.invalidArgumentPosition(position, self) 239 | } 240 | case .quadraticBezierCurve(let cp, let point): 241 | switch position { 242 | case 0: 243 | return .quadraticBezierCurve(cp: cp.adjusting(x: value), point: point) 244 | case 1: 245 | return .quadraticBezierCurve(cp: cp.adjusting(y: value), point: point) 246 | case 2: 247 | return .quadraticBezierCurve(cp: cp, point: point.adjusting(x: value)) 248 | case 3: 249 | return .quadraticBezierCurve(cp: cp, point: point.adjusting(y: value)) 250 | default: 251 | throw Path.Command.Error.invalidArgumentPosition(position, self) 252 | } 253 | case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point): 254 | switch position { 255 | case 0: 256 | return .ellipticalArcCurve(rx: value, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point) 257 | case 1: 258 | return .ellipticalArcCurve(rx: rx, ry: value, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point) 259 | case 2: 260 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: value, largeArc: largeArc, clockwise: clockwise, point: point) 261 | case 3: 262 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: !value.isZero, clockwise: clockwise, point: point) 263 | case 4: 264 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: !value.isZero, point: point) 265 | case 5: 266 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(x: value)) 267 | case 6: 268 | return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(y: value)) 269 | default: 270 | throw Path.Command.Error.invalidArgumentPosition(position, self) 271 | } 272 | case .closePath: 273 | throw Path.Command.Error.invalidAdjustment(self) 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Path.Component.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Path { 4 | /// A unit of a SVG path data string. 5 | enum Component { 6 | case prefix(Command.Prefix) 7 | case value(Double) 8 | 9 | /// Interprets a `Path` `data` attribute into individual `Component`s for command processing. 10 | public static func components(from data: String) throws -> [Component] { 11 | var blocks: [String] = [] 12 | var block: String = "" 13 | 14 | data.unicodeScalars.forEach { scalar in 15 | if scalar == "e" { 16 | // Account for exponential value notation. 17 | block.append(String(scalar)) 18 | return 19 | } 20 | 21 | if CharacterSet.letters.contains(scalar) { 22 | if !block.isEmpty { 23 | blocks.append(block) 24 | block = "" 25 | } 26 | 27 | blocks.append(String(scalar)) 28 | return 29 | } 30 | 31 | if CharacterSet.whitespaces.contains(scalar) { 32 | if !block.isEmpty { 33 | blocks.append(block) 34 | block = "" 35 | } 36 | 37 | return 38 | } 39 | 40 | if CharacterSet(charactersIn: ",").contains(scalar) { 41 | if !block.isEmpty { 42 | blocks.append(block) 43 | block = "" 44 | } 45 | 46 | return 47 | } 48 | 49 | if CharacterSet(charactersIn: "-").contains(scalar) { 50 | if !block.isEmpty && block.last != "e" { 51 | // Again, account for exponential values. 52 | blocks.append(block) 53 | block = "" 54 | } 55 | 56 | block.append(String(scalar)) 57 | return 58 | } 59 | 60 | if CharacterSet(charactersIn: ".").contains(scalar) { 61 | if block.contains(".") { 62 | // Already decimal value, this is a new value 63 | blocks.append(block) 64 | block = "" 65 | } 66 | 67 | block.append(String(scalar)) 68 | return 69 | } 70 | 71 | if CharacterSet.decimalDigits.contains(scalar) { 72 | block.append(String(scalar)) 73 | return 74 | } 75 | 76 | print("Unhandled Character: \(scalar)") 77 | } 78 | 79 | if !block.isEmpty { 80 | blocks.append(block) 81 | block = "" 82 | } 83 | 84 | return blocks 85 | .filter { !$0.isEmpty } 86 | .compactMap { 87 | if let prefix = Path.Command.Prefix(rawValue: $0.first!) { 88 | return .prefix(prefix) 89 | } else if let value = Double($0) { 90 | return .value(value) 91 | } else { 92 | // throw in the future? 93 | return nil 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Path.ComponentParser.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | public extension Path { 4 | /// Utility used to construct a collection of `Path.Command` from a collection of `Path.Component`. 5 | class ComponentParser { 6 | /// The command currently being built 7 | private var command: Path.Command? 8 | /// Coordinate system being used 9 | private var coordinates: Path.Command.Coordinates = .absolute 10 | /// The argument position of the _command_ to be processed. 11 | private var position: Int = 0 12 | /// Indicates that only a single value will be processed on the next component pass. 13 | private var singleValue: Bool = false 14 | /// The originating coordinates of the path. 15 | private var pathOrigin: Point = .nan 16 | /// The last point as processed by the parser. 17 | private var currentPoint: Point = .zero 18 | 19 | public init() {} 20 | 21 | public func parse(_ components: [Path.Component]) throws -> [Path.Command] { 22 | var commands: [Path.Command] = [] 23 | 24 | try components.forEach { component in 25 | if let command = try parse(component, lastCommand: commands.last) { 26 | commands.append(command) 27 | } 28 | } 29 | 30 | return commands 31 | } 32 | 33 | private func parse(_ component: Path.Component, lastCommand: Path.Command?) throws -> Path.Command? { 34 | switch component { 35 | case .prefix(let prefix): 36 | return setup(prefix: prefix, lastCommand: lastCommand) 37 | case .value(let value): 38 | return try process(value: value, lastCommand: lastCommand) 39 | } 40 | } 41 | 42 | private func setup(prefix: Path.Command.Prefix, lastCommand: Path.Command?) -> Path.Command? { 43 | position = 0 44 | singleValue = false 45 | 46 | switch prefix { 47 | case .move: 48 | command = .moveTo(point: .nan) 49 | coordinates = .absolute 50 | case .relativeMove: 51 | command = .moveTo(point: currentPoint) 52 | coordinates = .relative 53 | case .line: 54 | command = .lineTo(point: .nan) 55 | coordinates = .absolute 56 | case .relativeLine: 57 | command = .lineTo(point: currentPoint) 58 | coordinates = .relative 59 | case .horizontalLine: 60 | command = .lineTo(point: currentPoint.with(x: .nan)) 61 | coordinates = .absolute 62 | case .relativeHorizontalLine: 63 | command = .lineTo(point: currentPoint) 64 | coordinates = .relative 65 | singleValue = true 66 | case .verticalLine: 67 | command = .lineTo(point: currentPoint.with(y: .nan)) 68 | coordinates = .absolute 69 | position = 1 70 | case .relativeVerticalLine: 71 | command = .lineTo(point: currentPoint) 72 | coordinates = .relative 73 | position = 1 74 | singleValue = true 75 | case .cubicBezierCurve: 76 | command = .cubicBezierCurve(cp1: .nan, cp2: .nan, point: .nan) 77 | coordinates = .absolute 78 | case .relativeCubicBezierCurve: 79 | command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint) 80 | coordinates = .relative 81 | case .smoothCubicBezierCurve: 82 | if case .cubicBezierCurve(_, let cp, _) = lastCommand { 83 | command = .cubicBezierCurve(cp1: cp.reflection(using: currentPoint), cp2: .nan, point: .nan) 84 | } else { 85 | command = .cubicBezierCurve(cp1: currentPoint, cp2: .nan, point: .nan) 86 | } 87 | coordinates = .absolute 88 | position = 2 89 | case .relativeSmoothCubicBezierCurve: 90 | if case .cubicBezierCurve(_, let cp, _) = lastCommand { 91 | command = .cubicBezierCurve(cp1: cp.reflection(using: cp.reflection(using: currentPoint)), cp2: currentPoint, point: currentPoint) 92 | } else { 93 | command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint) 94 | } 95 | coordinates = .relative 96 | position = 2 97 | case .quadraticBezierCurve: 98 | command = .quadraticBezierCurve(cp: .nan, point: .nan) 99 | coordinates = .absolute 100 | case .relativeQuadraticBezierCurve: 101 | command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint) 102 | coordinates = .relative 103 | case .smoothQuadraticBezierCurve: 104 | if case .quadraticBezierCurve(let cp, _) = lastCommand { 105 | command = .quadraticBezierCurve(cp: cp.reflection(using: currentPoint), point: .nan) 106 | } else { 107 | command = .quadraticBezierCurve(cp: currentPoint, point: .nan) 108 | } 109 | coordinates = .absolute 110 | position = 2 111 | case .relativeSmoothQuadraticBezierCurve: 112 | if case .quadraticBezierCurve(let cp, _) = lastCommand { 113 | command = .quadraticBezierCurve(cp: cp.reflection(using: currentPoint), point: currentPoint) 114 | } else { 115 | command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint) 116 | } 117 | coordinates = .relative 118 | position = 2 119 | case .ellipticalArcCurve: 120 | command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan) 121 | coordinates = .absolute 122 | case .relativeEllipticalArcCurve: 123 | command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: currentPoint) 124 | coordinates = .relative 125 | case .close, .relativeClose: 126 | currentPoint = pathOrigin 127 | reset() 128 | return .closePath 129 | } 130 | 131 | return nil 132 | } 133 | 134 | private func process(value: Double, lastCommand: Path.Command?) throws -> Path.Command? { 135 | if let command = command { 136 | try continueCommand(command, with: value) 137 | } else { 138 | try nextCommand(with: value, lastCommand: lastCommand) 139 | } 140 | 141 | if let command = command, command.isComplete { 142 | switch coordinates { 143 | case .relative: 144 | guard position == -1 else { 145 | return nil 146 | } 147 | 148 | fallthrough 149 | case .absolute: 150 | currentPoint = command.point 151 | if case .moveTo = command { 152 | pathOrigin = command.point 153 | } 154 | reset() 155 | return command 156 | } 157 | } else { 158 | return nil 159 | } 160 | } 161 | 162 | private func continueCommand(_ command: Path.Command, with value: Double) throws { 163 | switch command { 164 | case .moveTo, .cubicBezierCurve, .quadraticBezierCurve, .ellipticalArcCurve: 165 | self.command = try command.adjustingArgument(at: position, by: value) 166 | switch coordinates { 167 | case .absolute: 168 | position += 1 169 | case .relative: 170 | switch position { 171 | case 0...(command.arguments - 2): 172 | position += 1 173 | case command.arguments - 1: 174 | position = -1 175 | default: 176 | break //throw? 177 | } 178 | } 179 | case .lineTo: 180 | self.command = try command.adjustingArgument(at: position, by: value) 181 | switch coordinates { 182 | case .absolute: 183 | position += 1 184 | case .relative: 185 | switch position { 186 | case 0: 187 | if singleValue { 188 | singleValue = false 189 | position = -1 190 | } else { 191 | position += 1 192 | } 193 | case 1: 194 | if singleValue { 195 | singleValue = false 196 | } 197 | position = -1 198 | default: 199 | break //throw? 200 | } 201 | } 202 | case .closePath: 203 | break 204 | } 205 | } 206 | 207 | private func nextCommand(with value: Double, lastCommand: Path.Command?) throws { 208 | guard let command = lastCommand else { 209 | throw Path.Command.Error.invalidRelativeCommand 210 | } 211 | 212 | switch command { 213 | case .moveTo: 214 | switch coordinates { 215 | case .absolute: 216 | self.command = .lineTo(point: Point(x: value, y: .nan)) 217 | position = 1 218 | case .relative: 219 | let c = Path.Command.lineTo(point: command.point) 220 | self.command = try c.adjustingArgument(at: 0, by: value) 221 | position = 1 222 | } 223 | case .lineTo: 224 | switch coordinates { 225 | case .absolute: 226 | self.command = .lineTo(point: Point(x: value, y: .nan)) 227 | position = 1 228 | case .relative: 229 | let c = Path.Command.lineTo(point: command.point) 230 | self.command = try c.adjustingArgument(at: 0, by: value) 231 | position = 1 232 | } 233 | case .cubicBezierCurve: 234 | switch coordinates { 235 | case .absolute: 236 | self.command = .cubicBezierCurve(cp1: Point(x: value, y: .nan), cp2: .nan, point: .nan) 237 | position = 1 238 | case .relative: 239 | let c = Path.Command.cubicBezierCurve(cp1: command.point, cp2: command.point, point: command.point) 240 | self.command = try c.adjustingArgument(at: 0, by: value) 241 | position = 1 242 | } 243 | case .quadraticBezierCurve: 244 | switch coordinates { 245 | case .absolute: 246 | self.command = .quadraticBezierCurve(cp: Point(x: value, y: .nan), point: .nan) 247 | position = 1 248 | case .relative: 249 | let c = Path.Command.quadraticBezierCurve(cp: command.point, point: command.point) 250 | self.command = try c.adjustingArgument(at: 0, by: value) 251 | position = 1 252 | } 253 | case .ellipticalArcCurve: 254 | switch coordinates { 255 | case .absolute: 256 | self.command = .ellipticalArcCurve(rx: value, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan) 257 | position = 1 258 | case .relative: 259 | let c = Path.Command.ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: command.point) 260 | self.command = try c.adjustingArgument(at: 0, by: value) 261 | position = 1 262 | } 263 | case .closePath: 264 | break 265 | } 266 | } 267 | 268 | private func reset() { 269 | command = nil 270 | coordinates = .absolute 271 | position = 0 272 | singleValue = false 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Path.swift: -------------------------------------------------------------------------------- 1 | import XMLCoder 2 | 3 | /// Generic element to define a shape. 4 | /// 5 | /// A path is defined by including a ‘path’ element in a SVG document which contains a **d="(path data)"** 6 | /// attribute, where the **‘d’** attribute contains the moveto, line, curve (both Cubic and Quadratic Bézier), 7 | /// arc and closepath instructions. 8 | /// 9 | /// ## Documentation 10 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path) 11 | /// | [W3](https://www.w3.org/TR/SVG11/paths.html) 12 | public struct Path: Element { 13 | 14 | /// The definition of the outline of a shape. 15 | public var data: String = "" 16 | 17 | // CoreAttributes 18 | public var id: String? 19 | 20 | // PresentationAttributes 21 | public var fillColor: String? 22 | public var fillOpacity: Double? 23 | public var fillRule: Fill.Rule? 24 | public var strokeColor: String? 25 | public var strokeWidth: Double? 26 | public var strokeOpacity: Double? 27 | public var strokeLineCap: Stroke.LineCap? 28 | public var strokeLineJoin: Stroke.LineJoin? 29 | public var strokeMiterLimit: Double? 30 | public var transform: String? 31 | 32 | // StylingAttributes 33 | public var style: String? 34 | 35 | enum CodingKeys: String, CodingKey { 36 | case data = "d" 37 | case id 38 | case fillColor = "fill" 39 | case fillOpacity = "fill-opacity" 40 | case fillRule = "fill-rule" 41 | case strokeColor = "stroke" 42 | case strokeWidth = "stroke-width" 43 | case strokeOpacity = "stroke-opacity" 44 | case strokeLineCap = "stroke-linecap" 45 | case strokeLineJoin = "stroke-linejoin" 46 | case strokeMiterLimit = "stroke-miterlimit" 47 | case transform 48 | case style 49 | } 50 | 51 | public init() { 52 | } 53 | 54 | public init(data: String) { 55 | self.init() 56 | self.data = data 57 | } 58 | 59 | public init(commands: [Path.Command]) { 60 | self.init() 61 | data = commands.map({ $0.description }).joined() 62 | } 63 | 64 | // MARK: - CustomStringConvertible 65 | public var description: String { 66 | return "" 67 | } 68 | } 69 | 70 | // MARK: - DynamicNodeEncoding 71 | extension Path: DynamicNodeEncoding { 72 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 73 | return .attribute 74 | } 75 | } 76 | 77 | // MARK: - DynamicNodeDecoding 78 | extension Path: DynamicNodeDecoding { 79 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 80 | return .attribute 81 | } 82 | } 83 | 84 | // MARK: - CommandRepresentable 85 | extension Path: CommandRepresentable { 86 | public func commands() throws -> [Command] { 87 | return try PathProcessor(data: data).commands() 88 | } 89 | } 90 | 91 | // MARK: - Equatable 92 | extension Path: Equatable { 93 | public static func == (lhs: Path, rhs: Path) -> Bool { 94 | do { 95 | let lhsCommands = try lhs.commands() 96 | let rhsCommands = try rhs.commands() 97 | return lhsCommands == rhsCommands 98 | } catch { 99 | return false 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Polygon.swift: -------------------------------------------------------------------------------- 1 | import XMLCoder 2 | 3 | /// Defines a closed shape consisting of a set of connected straight line segments. 4 | /// 5 | /// The last point is connected to the first point. For open shapes, see the `Polyline` element. If an odd number of 6 | /// coordinates is provided, then the element is in error. 7 | /// 8 | /// ## Documentation 9 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon) 10 | /// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolygonElement) 11 | public struct Polygon: Element { 12 | 13 | /// The points that make up the polygon. 14 | public var points: String = "" 15 | 16 | // CoreAttributes 17 | public var id: String? 18 | 19 | // PresentationAttributes 20 | public var fillColor: String? 21 | public var fillOpacity: Double? 22 | public var fillRule: Fill.Rule? 23 | public var strokeColor: String? 24 | public var strokeWidth: Double? 25 | public var strokeOpacity: Double? 26 | public var strokeLineCap: Stroke.LineCap? 27 | public var strokeLineJoin: Stroke.LineJoin? 28 | public var strokeMiterLimit: Double? 29 | public var transform: String? 30 | 31 | // StylingAttributes 32 | public var style: String? 33 | 34 | enum CodingKeys: String, CodingKey { 35 | case points 36 | case id 37 | case fillColor = "fill" 38 | case fillOpacity = "fill-opacity" 39 | case fillRule = "fill-rule" 40 | case strokeColor = "stroke" 41 | case strokeWidth = "stroke-width" 42 | case strokeOpacity = "stroke-opacity" 43 | case strokeLineCap = "stroke-linecap" 44 | case strokeLineJoin = "stroke-linejoin" 45 | case strokeMiterLimit = "stroke-miterlimit" 46 | case transform 47 | case style 48 | } 49 | 50 | public init() { 51 | } 52 | 53 | public init(points: String) { 54 | self.points = points 55 | } 56 | 57 | // MARK: - CustomStringConvertible 58 | public var description: String { 59 | return "" 60 | } 61 | } 62 | 63 | // MARK: - DynamicNodeEncoding 64 | extension Polygon: DynamicNodeEncoding { 65 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 66 | return .attribute 67 | } 68 | } 69 | 70 | // MARK: - DynamicNodeDecoding 71 | extension Polygon: DynamicNodeDecoding { 72 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 73 | return .attribute 74 | } 75 | } 76 | 77 | // MARK: - CommandRepresentable 78 | extension Polygon: CommandRepresentable { 79 | public func commands() throws -> [Path.Command] { 80 | return try PolygonProcessor(points: points).commands() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Polyline.swift: -------------------------------------------------------------------------------- 1 | import XMLCoder 2 | 3 | /// SVG basic shape that creates straight lines connecting several points. 4 | /// 5 | /// Typically a polyline is used to create open shapes as the last point doesn't have to be connected to the first 6 | /// point. For closed shapes see the `Polygon` element. 7 | /// 8 | /// ## Documentation 9 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline) 10 | /// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolylineElement) 11 | public struct Polyline: Element { 12 | 13 | public var points: String = "" 14 | 15 | // CoreAttributes 16 | public var id: String? 17 | 18 | // PresentationAttributes 19 | public var fillColor: String? 20 | public var fillOpacity: Double? 21 | public var fillRule: Fill.Rule? 22 | public var strokeColor: String? 23 | public var strokeWidth: Double? 24 | public var strokeOpacity: Double? 25 | public var strokeLineCap: Stroke.LineCap? 26 | public var strokeLineJoin: Stroke.LineJoin? 27 | public var strokeMiterLimit: Double? 28 | public var transform: String? 29 | 30 | // StylingAttributes 31 | public var style: String? 32 | 33 | enum CodingKeys: String, CodingKey { 34 | case points 35 | case id 36 | case fillColor = "fill" 37 | case fillOpacity = "fill-opacity" 38 | case fillRule = "fill-rule" 39 | case strokeColor = "stroke" 40 | case strokeWidth = "stroke-width" 41 | case strokeOpacity = "stroke-opacity" 42 | case strokeLineCap = "stroke-linecap" 43 | case strokeLineJoin = "stroke-linejoin" 44 | case strokeMiterLimit = "stroke-miterlimit" 45 | case transform 46 | case style 47 | } 48 | 49 | public init() { 50 | } 51 | 52 | public init(points: String) { 53 | self.points = points 54 | } 55 | 56 | // MARK: - CustomStringConvertible 57 | public var description: String { 58 | return "" 59 | } 60 | } 61 | 62 | // MARK: - DynamicNodeEncoding 63 | extension Polyline: DynamicNodeEncoding { 64 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 65 | return .attribute 66 | } 67 | } 68 | 69 | // MARK: - DynamicNodeDecoding 70 | extension Polyline: DynamicNodeDecoding { 71 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 72 | return .attribute 73 | } 74 | } 75 | 76 | // MARK: - CommandRepresentable 77 | extension Polyline: CommandRepresentable { 78 | public func commands() throws -> [Path.Command] { 79 | return try PolylineProcessor(points: points).commands() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/PresentationAttributes.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | public protocol PresentationAttributes { 4 | var fillColor: String? { get set } 5 | var fillOpacity: Double? { get set } 6 | var fillRule: Fill.Rule? { get set } 7 | var strokeColor: String? { get set } 8 | var strokeWidth: Double? { get set } 9 | var strokeOpacity: Double? { get set } 10 | var strokeLineCap: Stroke.LineCap? { get set } 11 | var strokeLineJoin: Stroke.LineJoin? { get set } 12 | var strokeMiterLimit: Double? { get set } 13 | var transform: String? { get set } 14 | } 15 | 16 | internal enum PresentationAttributesKeys: String, CodingKey { 17 | case fillColor = "fill" 18 | case fillOpacity = "fill-opacity" 19 | case fillRule = "fill-rule" 20 | case strokeColor = "stroke" 21 | case strokeWidth = "stroke-width" 22 | case strokeOpacity = "stroke-opacity" 23 | case strokeLineCap = "stroke-linecap" 24 | case strokeLineJoin = "stroke-linejoin" 25 | case strokeMiterLimit = "stroke-miterlimit" 26 | case transform 27 | } 28 | 29 | public extension PresentationAttributes { 30 | var presentationDescription: String { 31 | var attributes: [String] = [] 32 | 33 | if let fillColor = self.fillColor { 34 | attributes.append("\(PresentationAttributesKeys.fillColor.rawValue)=\"\(fillColor)\"") 35 | } 36 | if let fillOpacity = self.fillOpacity { 37 | attributes.append("\(PresentationAttributesKeys.fillOpacity.rawValue)=\"\(fillOpacity)\"") 38 | } 39 | if let fillRule = self.fillRule { 40 | attributes.append("\(PresentationAttributesKeys.fillRule.rawValue)=\"\(fillRule.description)\"") 41 | } 42 | if let strokeColor = self.strokeColor { 43 | attributes.append("\(PresentationAttributesKeys.strokeColor.rawValue)=\"\(strokeColor)\"") 44 | } 45 | if let strokeWidth = self.strokeWidth { 46 | attributes.append("\(PresentationAttributesKeys.strokeWidth.rawValue)=\"\(strokeWidth)\"") 47 | } 48 | if let strokeOpacity = self.strokeOpacity { 49 | attributes.append("\(PresentationAttributesKeys.strokeOpacity.rawValue)=\"\(strokeOpacity)\"") 50 | } 51 | if let strokeLineCap = self.strokeLineCap { 52 | attributes.append("\(PresentationAttributesKeys.strokeLineCap.rawValue)=\"\(strokeLineCap.description)\"") 53 | } 54 | if let strokeLineJoin = self.strokeLineJoin { 55 | attributes.append("\(PresentationAttributesKeys.strokeLineJoin.rawValue)=\"\(strokeLineJoin.description)\"") 56 | } 57 | if let strokeMiterLimit = self.strokeMiterLimit { 58 | attributes.append("\(PresentationAttributesKeys.strokeMiterLimit.rawValue)=\"\(strokeMiterLimit)\"") 59 | } 60 | if let transform = self.transform { 61 | attributes.append("\(PresentationAttributesKeys.transform.rawValue)=\"\(transform)\"") 62 | } 63 | 64 | return attributes.joined(separator: " ") 65 | } 66 | 67 | var transformations: [Transformation] { 68 | let value = transform?.replacingOccurrences(of: " ", with: "") ?? "" 69 | guard !value.isEmpty else { 70 | return [] 71 | } 72 | 73 | let values = value.split(separator: ")").map({ $0.appending(")") }) 74 | return values.compactMap({ Transformation($0) }) 75 | } 76 | 77 | var fill: Fill? { 78 | get { 79 | if fillColor == nil && fillOpacity == nil { 80 | return nil 81 | } 82 | 83 | var fill = Fill() 84 | fill.color = fillColor ?? "black" 85 | fill.opacity = fillOpacity ?? 1.0 86 | return fill 87 | } 88 | set { 89 | fillColor = newValue?.color 90 | fillOpacity = newValue?.opacity 91 | fillRule = newValue?.rule 92 | } 93 | } 94 | 95 | var stroke: Stroke? { 96 | get { 97 | if strokeColor == nil && strokeOpacity == nil { 98 | return nil 99 | } 100 | 101 | var stroke = Stroke() 102 | stroke.color = strokeColor ?? "black" 103 | stroke.opacity = strokeOpacity ?? 1.0 104 | stroke.width = strokeWidth ?? 1.0 105 | stroke.lineCap = strokeLineCap ?? .butt 106 | stroke.lineJoin = strokeLineJoin ?? .miter 107 | stroke.miterLimit = strokeMiterLimit 108 | return stroke 109 | } 110 | set { 111 | strokeColor = newValue?.color 112 | strokeOpacity = newValue?.opacity 113 | strokeWidth = newValue?.width 114 | strokeLineCap = newValue?.lineCap 115 | strokeLineJoin = newValue?.lineJoin 116 | strokeMiterLimit = newValue?.miterLimit 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Rectangle.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | import XMLCoder 3 | 4 | /// Basic SVG shape that draws rectangles, defined by their position, width, and height. 5 | /// 6 | /// The values used for the x- and y-axis rounded corner radii are determined implicitly 7 | /// if the ‘rx’ or ‘ry’ attributes (or both) are not specified, or are specified but with invalid values. 8 | /// 9 | /// ## Documentation 10 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect) 11 | /// | [W3](https://www.w3.org/TR/SVG11/shapes.html#RectElement) 12 | public struct Rectangle: Element { 13 | 14 | /// The x-axis coordinate of the side of the rectangle which 15 | /// has the smaller x-axis coordinate value. 16 | public var x: Double = 0.0 17 | /// The y-axis coordinate of the side of the rectangle which 18 | /// has the smaller y-axis coordinate value 19 | public var y: Double = 0.0 20 | /// The width of the rectangle. 21 | public var width: Double = 0.0 22 | /// The height of the rectangle. 23 | public var height: Double = 0.0 24 | /// For rounded rectangles, the x-axis radius of the ellipse used 25 | /// to round off the corners of the rectangle. 26 | public var rx: Double? 27 | /// For rounded rectangles, the y-axis radius of the ellipse used 28 | /// to round off the corners of the rectangle. 29 | public var ry: Double? 30 | 31 | // CoreAttributes 32 | public var id: String? 33 | 34 | // PresentationAttributes 35 | public var fillColor: String? 36 | public var fillOpacity: Double? 37 | public var fillRule: Fill.Rule? 38 | public var strokeColor: String? 39 | public var strokeWidth: Double? 40 | public var strokeOpacity: Double? 41 | public var strokeLineCap: Stroke.LineCap? 42 | public var strokeLineJoin: Stroke.LineJoin? 43 | public var strokeMiterLimit: Double? 44 | public var transform: String? 45 | 46 | // StylingAttributes 47 | public var style: String? 48 | 49 | enum CodingKeys: String, CodingKey { 50 | case x 51 | case y 52 | case width 53 | case height 54 | case rx 55 | case ry 56 | case id 57 | case fillColor = "fill" 58 | case fillOpacity = "fill-opacity" 59 | case fillRule = "fill-rule" 60 | case strokeColor = "stroke" 61 | case strokeWidth = "stroke-width" 62 | case strokeOpacity = "stroke-opacity" 63 | case strokeLineCap = "stroke-linecap" 64 | case strokeLineJoin = "stroke-linejoin" 65 | case strokeMiterLimit = "stroke-miterlimit" 66 | case transform 67 | case style 68 | } 69 | 70 | public init() { 71 | } 72 | 73 | public init(x: Double, y: Double, width: Double, height: Double, rx: Double? = nil, ry: Double? = nil) { 74 | self.x = x 75 | self.y = y 76 | self.width = width 77 | self.height = height 78 | self.rx = rx 79 | self.ry = ry 80 | } 81 | 82 | // MARK: - CustomStringConvertible 83 | public var description: String { 84 | var desc = "" 93 | } 94 | } 95 | 96 | // MARK: - DynamicNodeEncoding 97 | extension Rectangle: DynamicNodeEncoding { 98 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 99 | return .attribute 100 | } 101 | } 102 | 103 | // MARK: - DynamicNodeDecoding 104 | extension Rectangle: DynamicNodeDecoding { 105 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 106 | return .attribute 107 | } 108 | } 109 | 110 | // MARK: - DirectionalCommandRepresentable 111 | extension Rectangle: DirectionalCommandRepresentable { 112 | public func commands(clockwise: Bool) throws -> [Path.Command] { 113 | return RectangleProcessor(rectangle: self).commands(clockwise: clockwise) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/SVG+Swift2D.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | public extension SVG { 4 | /// Original size of the document image. 5 | /// 6 | /// Primarily uses the `viewBox` attribute, and will fallback to the 'pixelSize' 7 | var originalSize: Size { 8 | return (viewBoxSize ?? pixelSize) ?? .zero 9 | } 10 | 11 | /// Size of the design in a square 'viewBox'. 12 | /// 13 | /// All paths created by this framework are outputted in a 'square'. 14 | var outputSize: Size { 15 | let size = (pixelSize ?? viewBoxSize) ?? .zero 16 | let maxDimension = max(size.width, size.height) 17 | return Size(width: maxDimension, height: maxDimension) 18 | } 19 | 20 | /// Size derived from the `viewBox` document attribute 21 | var viewBoxSize: Size? { 22 | guard let viewBox = self.viewBox else { 23 | return nil 24 | } 25 | 26 | let components = viewBox.components(separatedBy: .whitespaces) 27 | guard components.count == 4 else { 28 | return nil 29 | } 30 | 31 | guard let width = Double(components[2]) else { 32 | return nil 33 | } 34 | 35 | guard let height = Double(components[3]) else { 36 | return nil 37 | } 38 | 39 | return Size(width: width, height: height) 40 | } 41 | 42 | /// Size derived from the 'width' & 'height' document attributes 43 | var pixelSize: Size? { 44 | guard let width = self.width, !width.isEmpty else { 45 | return nil 46 | } 47 | 48 | guard let height = self.height, !height.isEmpty else { 49 | return nil 50 | } 51 | 52 | let widthRawValue = width.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil) 53 | let heightRawValue = height.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil) 54 | 55 | guard let w = Double(widthRawValue), let h = Double(heightRawValue) else { 56 | return nil 57 | } 58 | 59 | return Size(width: w, height: h) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/SVG.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XMLCoder 3 | 4 | /// SVG is a language for describing two-dimensional graphics in XML. 5 | /// 6 | /// The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost 7 | /// element of SVG documents, but it can also be used to embed a SVG fragment inside an SVG or HTML document. 8 | /// 9 | /// ## Documentation 10 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg) 11 | /// | [W3](https://www.w3.org/TR/SVG11/) 12 | public struct SVG: Container { 13 | 14 | public var viewBox: String? 15 | public var width: String? 16 | public var height: String? 17 | public var title: String? 18 | public var desc: String? 19 | 20 | // Container 21 | public var circles: [Circle]? 22 | public var ellipses: [Ellipse]? 23 | public var groups: [Group]? 24 | public var lines: [Line]? 25 | public var paths: [Path]? 26 | public var polygons: [Polygon]? 27 | public var polylines: [Polyline]? 28 | public var rectangles: [Rectangle]? 29 | public var texts: [Text]? 30 | 31 | /// A non-optional, non-spaced representation of the `title`. 32 | public var name: String { 33 | let name = title ?? "SVG Document" 34 | let newTitle = name.components(separatedBy: .punctuationCharacters).joined(separator: "_") 35 | return newTitle.replacingOccurrences(of: " ", with: "_") 36 | } 37 | 38 | enum CodingKeys: String, CodingKey { 39 | case width 40 | case height 41 | case viewBox 42 | case title 43 | case desc 44 | case circles = "circle" 45 | case ellipses = "ellipse" 46 | case groups = "g" 47 | case lines = "line" 48 | case paths = "path" 49 | case polylines = "polyline" 50 | case polygons = "polygon" 51 | case rectangles = "rect" 52 | case texts = "text" 53 | } 54 | 55 | public init() { 56 | } 57 | 58 | public init(width: Int, height: Int) { 59 | self.width = "\(width)px" 60 | self.height = "\(height)px" 61 | viewBox = "0 0 \(width) \(height)" 62 | } 63 | } 64 | 65 | // MARK: - CustomStringConvertible 66 | extension SVG: CustomStringConvertible { 67 | public var description: String { 68 | var contents: String = "" 69 | 70 | if let title = self.title { 71 | contents.append("\n\(title)") 72 | } 73 | 74 | if let desc = self.desc { 75 | contents.append("\n\(desc)") 76 | } 77 | 78 | contents.append(containerDescription) 79 | 80 | return "\(contents)\n" 81 | } 82 | } 83 | 84 | // MARK: - DynamicNodeEncoding 85 | extension SVG: DynamicNodeEncoding { 86 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 87 | switch key { 88 | case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox: 89 | return .attribute 90 | default: 91 | return .element 92 | } 93 | } 94 | } 95 | 96 | // MARK: - DynamicNodeDecoding 97 | extension SVG: DynamicNodeDecoding { 98 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 99 | switch key { 100 | case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox: 101 | return .attribute 102 | default: 103 | return .element 104 | } 105 | } 106 | } 107 | 108 | // MARK: - Creation 109 | public extension SVG { 110 | static func make(from url: URL) throws -> SVG { 111 | guard FileManager.default.fileExists(atPath: url.path) else { 112 | throw CocoaError(.fileNoSuchFile) 113 | } 114 | 115 | let data = try Data(contentsOf: url) 116 | return try make(with: data) 117 | } 118 | 119 | static func make(with data: Data, decoder: XMLDecoder = XMLDecoder()) throws -> SVG { 120 | return try decoder.decode(SVG.self, from: data) 121 | } 122 | } 123 | 124 | // MARK: - Paths 125 | public extension SVG { 126 | /// A collection of all `Path`s in the document. 127 | func subpaths() throws -> [Path] { 128 | var output: [Path] = [] 129 | let _transformations: [Transformation] = [] 130 | 131 | if let circles = self.circles { 132 | try output.append(contentsOf: circles.compactMap({ try $0.path(applying: _transformations) })) 133 | } 134 | 135 | if let ellipses = self.ellipses { 136 | try output.append(contentsOf: ellipses.compactMap({ try $0.path(applying: _transformations) })) 137 | } 138 | 139 | if let rectangles = self.rectangles { 140 | try output.append(contentsOf: rectangles.compactMap({ try $0.path(applying: _transformations) })) 141 | } 142 | 143 | if let polygons = self.polygons { 144 | try output.append(contentsOf: polygons.compactMap({ try $0.path(applying: _transformations) })) 145 | } 146 | 147 | if let polylines = self.polylines { 148 | try output.append(contentsOf: polylines.compactMap({ try $0.path(applying: _transformations) })) 149 | } 150 | 151 | if let paths = self.paths { 152 | try output.append(contentsOf: paths.map({ try $0.path(applying: _transformations) })) 153 | } 154 | 155 | if let groups = self.groups { 156 | try groups.forEach({ 157 | try output.append(contentsOf: $0.subpaths(applying: _transformations)) 158 | }) 159 | } 160 | 161 | return output 162 | } 163 | 164 | /// A singular path that represents all of the `Command`s within the document. 165 | func coalescedPath() throws -> Path { 166 | let paths = try subpaths() 167 | let commands = try paths.flatMap({ try $0.commands() }) 168 | return Path(commands: commands) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Stroke.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | public struct Stroke { 4 | 5 | public var color: String? 6 | public var width: Double? 7 | public var opacity: Double? 8 | public var lineCap: LineCap = .butt 9 | public var lineJoin: LineJoin = .miter 10 | public var miterLimit: Double? 11 | 12 | public init() { 13 | 14 | } 15 | 16 | /// Presentation attribute defining the shape to be used at the end of open subpaths when they are stroked. 17 | /// 18 | /// The default `LineCap` is `.butt` 19 | public enum LineCap: String, Codable, CaseIterable { 20 | /// The stroke for each subpath does not extend beyond its two endpoints. 21 | case butt 22 | /// The end of each subpath the stroke will be extended by a half circle with a diameter equal to the stroke 23 | /// width. 24 | case round 25 | /// The end of each subpath the stroke will be extended by a rectangle with a width equal to half the width of 26 | /// the stroke and a height equal to the width of the stroke. 27 | case square 28 | } 29 | 30 | /// Presentation attribute defining the shape to be used at the corners of paths when they are stroked. 31 | /// 32 | /// The default `LineJoin` is `.miter` 33 | public enum LineJoin: String, Codable, CaseIterable { 34 | /// An arcs corner is to be used to join path segments. 35 | /// 36 | /// The arcs shape is formed by extending the outer edges of the stroke at the join point with arcs that have 37 | /// the same curvature as the outer edges at the join point. 38 | case arcs 39 | /// The bevel value indicates that a bevelled corner is to be used to join path segments. 40 | case bevel 41 | /// Indicates that a sharp corner is to be used to join path segments. 42 | /// 43 | /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until 44 | /// they intersect. 45 | case miter 46 | /// A sharp corner is to be used to join path segments. 47 | /// 48 | /// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until 49 | /// they intersect. 50 | case miterClip = "miter-clip" 51 | /// The round value indicates that a round corner is to be used to join path segments. 52 | case round 53 | } 54 | } 55 | 56 | extension Stroke.LineCap: CustomStringConvertible { 57 | public var description: String { 58 | return self.rawValue 59 | } 60 | } 61 | 62 | extension Stroke.LineJoin: CustomStringConvertible { 63 | public var description: String { 64 | return self.rawValue 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/StylingAttributes.swift: -------------------------------------------------------------------------------- 1 | public protocol StylingAttributes { 2 | var style: String? { get set } 3 | } 4 | 5 | internal enum StylingAttributesKeys: String, CodingKey { 6 | case style 7 | } 8 | 9 | public extension StylingAttributes { 10 | var stylingDescription: String { 11 | if let style = self.style { 12 | return "\(StylingAttributesKeys.style.rawValue)=\"\(style)\"" 13 | } else { 14 | return "" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Text.swift: -------------------------------------------------------------------------------- 1 | import XMLCoder 2 | 3 | /// Graphics element consisting of text 4 | /// 5 | /// It's possible to apply a gradient, pattern, clipping path, mask, or filter to `Text`, like any other SVG graphics element. 6 | /// 7 | /// ## Documentation 8 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text) 9 | /// | [W3](https://www.w3.org/TR/SVG11/text.html#TextElement) 10 | public struct Text: Element { 11 | 12 | public var value: String = "" 13 | public var x: Double? 14 | public var y: Double? 15 | public var dx: Double? 16 | public var dy: Double? 17 | 18 | // CoreAttributes 19 | public var id: String? 20 | 21 | // PresentationAttributes 22 | public var fillColor: String? 23 | public var fillOpacity: Double? 24 | public var fillRule: Fill.Rule? 25 | public var strokeColor: String? 26 | public var strokeWidth: Double? 27 | public var strokeOpacity: Double? 28 | public var strokeLineCap: Stroke.LineCap? 29 | public var strokeLineJoin: Stroke.LineJoin? 30 | public var strokeMiterLimit: Double? 31 | public var transform: String? 32 | 33 | // StylingAttributes 34 | public var style: String? 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case value = "" 38 | case x 39 | case y 40 | case dx 41 | case dy 42 | case id 43 | case fillColor = "fill" 44 | case fillOpacity = "fill-opacity" 45 | case fillRule = "fill-rule" 46 | case strokeColor = "stroke" 47 | case strokeWidth = "stroke-width" 48 | case strokeOpacity = "stroke-opacity" 49 | case strokeLineCap = "stroke-linecap" 50 | case strokeLineJoin = "stroke-linejoin" 51 | case strokeMiterLimit = "stroke-miterlimit" 52 | case transform 53 | case style 54 | } 55 | 56 | public init() { 57 | } 58 | 59 | public init(value: String) { 60 | self.value = value 61 | } 62 | 63 | public var description: String { 64 | var components: [String] = [] 65 | 66 | if let x = self.x, !x.isNaN && !x.isZero { 67 | components.append(String(format: "x=\"%.5f\"", x)) 68 | } 69 | if let y = self.y, !y.isNaN && !y.isZero { 70 | components.append(String(format: "y=\"%.5f\"", y)) 71 | } 72 | if let dx = self.dx, !dx.isNaN, !dx.isZero { 73 | components.append(String(format: "dx=\"%.5f\"", dx)) 74 | } 75 | if let dy = self.dy, !dy.isNaN, !dy.isZero { 76 | components.append(String(format: "dy=\"%.5f\"", dy)) 77 | } 78 | 79 | components.append(attributeDescription) 80 | 81 | return "\(value)" 82 | } 83 | } 84 | 85 | // MARK: - DynamicNodeEncoding 86 | extension Text: DynamicNodeEncoding { 87 | public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { 88 | switch key { 89 | case CodingKeys.value: 90 | return .element 91 | default: 92 | return .attribute 93 | } 94 | } 95 | } 96 | 97 | // MARK: - DynamicNodeDecoding 98 | extension Text: DynamicNodeDecoding { 99 | public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { 100 | switch key { 101 | case CodingKeys.value: 102 | return .element 103 | default: 104 | return .attribute 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/SwiftSVG/Transformation.swift: -------------------------------------------------------------------------------- 1 | import Swift2D 2 | 3 | /// A modification that should be applied to an element and its children. 4 | /// 5 | /// If a list of transforms is provided, then the net effect is as if each transform had been specified separately in 6 | /// the order provided. 7 | /// 8 | /// For example, 9 | /// ``` 10 | /// 11 | /// 12 | /// 13 | /// ``` 14 | /// is functionally equivalent to: 15 | /// ``` 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// ``` 26 | /// 27 | /// The ‘transform’ attribute is applied to an element before processing any other coordinate or length values supplied 28 | /// for that element. 29 | /// 30 | /// ## Documentation 31 | /// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform) 32 | /// | [W3](https://www.w3.org/TR/SVG11/coords.html#TransformAttribute) 33 | public enum Transformation { 34 | /// Moves an object by x & y. (Y is assumed to be '0' if not provided) 35 | case translate(x: Double, y: Double) 36 | /// Specifies a transformation in the form of a transformation matrix of six values. 37 | case matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double) 38 | 39 | public enum Prefix: String, CaseIterable { 40 | case translate 41 | case matrix 42 | } 43 | 44 | /// Initializes a new `Transformation` with a raw SVG transformation string. 45 | public init?(_ string: String) { 46 | guard let prefix = Prefix.allCases.first(where: { string.lowercased().hasPrefix($0.rawValue) }) else { 47 | return nil 48 | } 49 | 50 | switch prefix { 51 | case .translate: 52 | guard let start = string.firstIndex(of: "(") else { 53 | return nil 54 | } 55 | 56 | guard let stop = string.lastIndex(of: ")") else { 57 | return nil 58 | } 59 | 60 | var substring = String(string[start...stop]) 61 | substring = substring.replacingOccurrences(of: "(", with: "") 62 | substring = substring.replacingOccurrences(of: ")", with: "") 63 | 64 | var components = substring.split(separator: " ", omittingEmptySubsequences: true).map({ String($0) }) 65 | components = components.flatMap({ $0.components(separatedBy: ",") }) 66 | 67 | let values = components.compactMap({ Double($0) }).map({ Double($0) }) 68 | guard values.count > 0 else { 69 | return nil 70 | } 71 | 72 | if values.count > 1 { 73 | self = .translate(x: values[0], y: values[1]) 74 | } else { 75 | self = .translate(x: values[0], y: 0.0) 76 | } 77 | case .matrix: 78 | guard let start = string.firstIndex(of: "(") else { 79 | return nil 80 | } 81 | 82 | guard let stop = string.lastIndex(of: ")") else { 83 | return nil 84 | } 85 | 86 | var substring = String(string[start...stop]) 87 | substring = substring.replacingOccurrences(of: "(", with: "") 88 | substring = substring.replacingOccurrences(of: ")", with: "") 89 | 90 | var components = substring.split(separator: " ", omittingEmptySubsequences: true).map({ String($0) }) 91 | components = components.flatMap({ $0.components(separatedBy: ",") }) 92 | 93 | let values = components.compactMap({ Double($0) }).map({ Double($0) }) 94 | guard values.count > 5 else { 95 | return nil 96 | } 97 | 98 | self = .matrix(a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5]) 99 | } 100 | } 101 | } 102 | 103 | // MARK: - CustomStringConvertible 104 | extension Transformation: CustomStringConvertible { 105 | public var description: String { 106 | switch self { 107 | case .translate(let x, let y): 108 | return "translate(\(x), \(y))" 109 | case .matrix(let a, let b, let c, let d, let e, let f): 110 | return "matrix(\(a), \(b), \(c), \(d), \(e), \(f))" 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/CommandRepresentableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftSVG 3 | 4 | final class CommandRepresentableTests: XCTestCase { 5 | 6 | func testCircle() throws { 7 | let circle = Circle(x: 50, y: 50, r: 50) 8 | let offset = EllipseProcessor.controlPointOffset(circle.r) 9 | 10 | var commands = try circle.commands(clockwise: false) 11 | 12 | var expected: [Path.Command] = [ 13 | .moveTo(point: .init(x: 100.0, y: 50.0)), 14 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 50.0 - offset), cp2: .init(x: 50.0 + offset, y: 0.0), point: .init(x: 50.0, y: 0.0)), 15 | .cubicBezierCurve(cp1: .init(x: 50.0 - offset, y: 0.0), cp2: .init(x: 0.0, y: 50.0 - offset), point: .init(x: 0.0, y: 50.0)), 16 | .cubicBezierCurve(cp1: .init(x: 0.0, y: 50.0 + offset), cp2: .init(x: 50.0 - offset, y: 100.0), point: .init(x: 50.0, y: 100.0)), 17 | .cubicBezierCurve(cp1: .init(x: 50.0 + offset, y: 100.0), cp2: .init(x: 100.0, y: 50.0 + offset), point: .init(x: 100.0, y: 50.0)), 18 | .closePath 19 | ] 20 | 21 | XCTAssertEqual(commands, expected) 22 | 23 | commands = try circle.commands(clockwise: true) 24 | 25 | expected = [ 26 | .moveTo(point: .init(x: 100.0, y: 50.0)), 27 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 50.0 + offset), cp2: .init(x: 50.0 + offset, y: 100.0), point: .init(x: 50.0, y: 100.0)), 28 | .cubicBezierCurve(cp1: .init(x: 50.0 - offset, y: 100.0), cp2: .init(x: 0.0, y: 50.0 + offset), point: .init(x: 0.0, y: 50.0)), 29 | .cubicBezierCurve(cp1: .init(x: 0.0, y: 50.0 - offset), cp2: .init(x: 50.0 - offset, y: 0.0), point: .init(x: 50.0, y: 0.0)), 30 | .cubicBezierCurve(cp1: .init(x: 50.0 + offset, y: 0.0), cp2: .init(x: 100.0, y: 50.0 - offset), point: .init(x: 100.0, y: 50.0)), 31 | .closePath 32 | ] 33 | 34 | XCTAssertEqual(commands, expected) 35 | } 36 | 37 | func testEllipse() throws { 38 | let x: Double = 50.0 39 | let y: Double = 25.0 40 | 41 | let ellipse = Ellipse(x: x, y: y, rx: 50, ry: 25) 42 | let xOffset = EllipseProcessor.controlPointOffset(ellipse.rx) 43 | let yOffset = EllipseProcessor.controlPointOffset(ellipse.ry) 44 | 45 | var commands = try ellipse.commands(clockwise: false) 46 | 47 | var expected: [Path.Command] = [ 48 | .moveTo(point: .init(x: x * 2, y: y)), 49 | .cubicBezierCurve(cp1: .init(x: x * 2, y: y - yOffset), cp2: .init(x: x + xOffset, y: 0.0), point: .init(x: x, y: 0.0)), 50 | .cubicBezierCurve(cp1: .init(x: x - xOffset, y: 0.0), cp2: .init(x: 0.0, y: y - yOffset), point: .init(x: 0.0, y: y )), 51 | .cubicBezierCurve(cp1: .init(x: 0.0, y: y + yOffset), cp2: .init(x: x - xOffset, y: y * 2), point: .init(x: x, y: y * 2)), 52 | .cubicBezierCurve(cp1: .init(x: x + xOffset, y: y * 2), cp2: .init(x: x * 2, y: y + yOffset), point: .init(x: x * 2, y: y)), 53 | .closePath 54 | ] 55 | 56 | XCTAssertEqual(commands, expected) 57 | 58 | commands = try ellipse.commands(clockwise: true) 59 | 60 | expected = [ 61 | .moveTo(point: .init(x: x * 2, y: y)), 62 | .cubicBezierCurve(cp1: .init(x: x * 2, y: y + yOffset), cp2: .init(x: x + xOffset, y: y * 2), point: .init(x: x, y: y * 2)), 63 | .cubicBezierCurve(cp1: .init(x: x - xOffset, y: y * 2), cp2: .init(x: 0.0, y: y + yOffset), point: .init(x: 0.0, y: y)), 64 | .cubicBezierCurve(cp1: .init(x: 0.0, y: y - yOffset), cp2: .init(x: x - xOffset, y: 0.0), point: .init(x: x, y: 0.0)), 65 | .cubicBezierCurve(cp1: .init(x: x + xOffset, y: 0.0), cp2: .init(x: x * 2, y: y - yOffset), point: .init(x: x * 2, y: y)), 66 | .closePath 67 | ] 68 | 69 | XCTAssertEqual(commands, expected) 70 | } 71 | 72 | func testLine() throws { 73 | let line = Line(x1: 10.0, y1: 10.0, x2: 80.0, y2: 30.0) 74 | let commands = try line.commands() 75 | 76 | let expected: [Path.Command] = [ 77 | .moveTo(point: .init(x: 10.0, y: 10.0)), 78 | .lineTo(point: .init(x: 80.0, y: 30.0)), 79 | .lineTo(point: .init(x: 10.0, y: 10.0)), 80 | .closePath 81 | ] 82 | 83 | XCTAssertEqual(commands, expected) 84 | } 85 | 86 | func testPolygon() throws { 87 | let polygon = SwiftSVG.Polygon(points: "850,75 958,137.5 958,262.5 850,325 742,262.6 742,137.5") 88 | let commands = try polygon.commands() 89 | 90 | let expected: [Path.Command] = [ 91 | .moveTo(point: .init(x: 850.0, y: 75.0)), 92 | .lineTo(point: .init(x: 958.0, y: 137.5)), 93 | .lineTo(point: .init(x: 958.0, y: 262.5)), 94 | .lineTo(point: .init(x: 850.0, y: 325.0)), 95 | .lineTo(point: .init(x: 742.0, y: 262.6)), 96 | .lineTo(point: .init(x: 742.0, y: 137.5)), 97 | .closePath 98 | ] 99 | 100 | XCTAssertRoughlyEqual(commands, expected) 101 | } 102 | 103 | func testPolyline() throws { 104 | let polyline = Polyline(points: "") 105 | let commands = try polyline.commands() 106 | 107 | let expected: [Path.Command] = [ 108 | ] 109 | 110 | XCTAssertEqual(commands, expected) 111 | } 112 | 113 | func testRectangle() throws { 114 | let rectangle = Rectangle(x: 0, y: 0, width: 100, height: 100) 115 | var commands = try rectangle.commands(clockwise: true) 116 | 117 | var expected: [Path.Command] = [ 118 | .moveTo(point: .init(x: 0.0, y: 0.0)), 119 | .lineTo(point: .init(x: 100.0, y: 0.0)), 120 | .lineTo(point: .init(x: 100.0, y: 100.0)), 121 | .lineTo(point: .init(x: 0.0, y: 100.0)), 122 | .closePath 123 | ] 124 | 125 | XCTAssertEqual(commands, expected) 126 | 127 | commands = try rectangle.commands(clockwise: false) 128 | 129 | expected = [ 130 | .moveTo(point: .init(x: 0.0, y: 0.0)), 131 | .lineTo(point: .init(x: 0.0, y: 100.0)), 132 | .lineTo(point: .init(x: 100.0, y: 100.0)), 133 | .lineTo(point: .init(x: 100.0, y: 0.0)), 134 | .closePath 135 | ] 136 | 137 | XCTAssertEqual(commands, expected) 138 | } 139 | 140 | func testRoundedRectangle() throws { 141 | let rectangle = Rectangle(x: 0, y: 0, width: 100, height: 100, rx: 15) 142 | var commands = try rectangle.commands(clockwise: true) 143 | 144 | var expected: [Path.Command] = [ 145 | .moveTo(point: .init(x: 15.0, y: 0.0)), 146 | .lineTo(point: .init(x: 85.0, y: 0.0)), 147 | .quadraticBezierCurve(cp: .init(x: 100.0, y: 0.0), point: .init(x: 100.0, y: 15.0)), 148 | .lineTo(point: .init(x: 100.0, y: 85.0)), 149 | .quadraticBezierCurve(cp: .init(x: 100.0, y: 100.0), point: .init(x: 85.0, y: 100.0)), 150 | .lineTo(point: .init(x: 15.0, y: 100.0)), 151 | .quadraticBezierCurve(cp: .init(x: 0.0, y: 100.0), point: .init(x: 0.0, y: 85.0)), 152 | .lineTo(point: .init(x: 0.0, y: 15.0)), 153 | .quadraticBezierCurve(cp: .zero, point: .init(x: 15.0, y: 0.0)), 154 | .closePath 155 | ] 156 | 157 | XCTAssertEqual(commands, expected) 158 | 159 | commands = try rectangle.commands(clockwise: false) 160 | 161 | expected = [ 162 | .moveTo(point: .init(x: 15.0, y: 0.0)), 163 | .quadraticBezierCurve(cp: .zero, point: .init(x: 0.0, y: 15.0)), 164 | .lineTo(point: .init(x: 0.0, y: 85.0)), 165 | .quadraticBezierCurve(cp: .init(x: 0.0, y: 100.0), point: .init(x: 15.0, y: 100.0)), 166 | .lineTo(point: .init(x: 85.0, y: 100.0)), 167 | .quadraticBezierCurve(cp: .init(x: 100.0, y: 100.0), point: .init(x: 100.0, y: 85.0)), 168 | .lineTo(point: .init(x: 100.0, y: 15.0)), 169 | .quadraticBezierCurve(cp: .init(x: 100.0, y: 0.0), point: .init(x: 85.0, y: 0.0)), 170 | .closePath 171 | ] 172 | 173 | XCTAssertEqual(commands, expected) 174 | } 175 | 176 | func testCubicRoundedRectangle() throws { 177 | let rectangle = Rectangle(x: 0, y: 0, width: 100, height: 100, rx: 10, ry: 20) 178 | var commands = try rectangle.commands(clockwise: true) 179 | 180 | var expected: [Path.Command] = [ 181 | .moveTo(point: .init(x: 10.0, y: 0.0)), 182 | .lineTo(point: .init(x: 90.0, y: 0.0)), 183 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 0.0), cp2: .init(x: 100.0, y: 0.0), point: .init(x: 100.0, y: 20.0)), 184 | .lineTo(point: .init(x: 100.0, y: 80.0)), 185 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 100.0), cp2: .init(x: 100.0, y: 100.0), point: .init(x: 90.0, y: 100.0)), 186 | .lineTo(point: .init(x: 10.0, y: 100.0)), 187 | .cubicBezierCurve(cp1: .init(x: 0.0, y: 100.0), cp2: .init(x: 0.0, y: 100.0), point: .init(x: 0.0, y: 80.0)), 188 | .lineTo(point: .init(x: 0.0, y: 20.0)), 189 | .cubicBezierCurve(cp1: .zero, cp2: .zero, point: .init(x: 10.0, y: 0.0)), 190 | .closePath 191 | ] 192 | 193 | XCTAssertEqual(commands, expected) 194 | 195 | commands = try rectangle.commands(clockwise: false) 196 | 197 | expected = [ 198 | .moveTo(point: .init(x: 10.0, y: 0.0)), 199 | .cubicBezierCurve(cp1: .zero, cp2: .zero, point: .init(x: 0.0, y: 20.0)), 200 | .lineTo(point: .init(x: 0.0, y: 80.0)), 201 | .cubicBezierCurve(cp1: .init(x: 0.0, y: 100.0), cp2: .init(x: 0.0, y: 100.0), point: .init(x: 10.0, y: 100.0)), 202 | .lineTo(point: .init(x: 90.0, y: 100.0)), 203 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 100.0), cp2: .init(x: 100.0, y: 100.0), point: .init(x: 100.0, y: 80.0)), 204 | .lineTo(point: .init(x: 100.0, y: 20.0)), 205 | .cubicBezierCurve(cp1: .init(x: 100.0, y: 0.0), cp2: .init(x: 100.0, y: 0.0), point: .init(x: 90.0, y: 0.0)), 206 | .closePath 207 | ] 208 | 209 | XCTAssertEqual(commands, expected) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/Extensions.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Swift2D 3 | @testable import SwiftSVG 4 | 5 | extension Bundle { 6 | static var swiftSVGTests: Bundle = .module 7 | } 8 | 9 | infix operator ~~ 10 | public protocol RoughEquatability { 11 | static func ~~ (lhs: Self, rhs: Self) -> Bool 12 | } 13 | 14 | public func XCTAssertRoughlyEqual(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) where T : RoughEquatability { 15 | let lhs: T 16 | let rhs: T 17 | do { 18 | lhs = try expression1() 19 | rhs = try expression2() 20 | } catch { 21 | XCTFail(error.localizedDescription, file: file, line: line) 22 | return 23 | } 24 | 25 | guard lhs ~~ rhs else { 26 | XCTFail(message(), file: file, line: line) 27 | return 28 | } 29 | } 30 | 31 | public extension Path.Command { 32 | func hasPrefix(_ prefix: Path.Command.Prefix) -> Bool { 33 | switch self { 34 | case .moveTo: 35 | return prefix == .move 36 | case .lineTo: 37 | return prefix == .line 38 | case .cubicBezierCurve: 39 | return prefix == .cubicBezierCurve 40 | case .quadraticBezierCurve: 41 | return prefix == .quadraticBezierCurve 42 | case .ellipticalArcCurve: 43 | return prefix == .ellipticalArcCurve 44 | case .closePath: 45 | return prefix == .close 46 | } 47 | } 48 | } 49 | 50 | extension Path.Command: RoughEquatability { 51 | public static func ~~ (lhs: Path.Command, rhs: Path.Command) -> Bool { 52 | switch (lhs, rhs) { 53 | case (.moveTo(let lPoint), .moveTo(let rPoint)): 54 | return lPoint ~~ rPoint 55 | case (.lineTo(let lPoint), .lineTo(let rPoint)): 56 | return lPoint ~~ rPoint 57 | case (.cubicBezierCurve(let lcp1, let lcp2, let lpoint), .cubicBezierCurve(let rcp1, let rcp2, let rpoint)): 58 | return (lcp1 ~~ rcp1) && (lcp2 ~~ rcp2) && (lpoint ~~ rpoint) 59 | case (.quadraticBezierCurve(let lcp, let lpoint), .quadraticBezierCurve(let rcp, let rpoint)): 60 | return (lcp ~~ rcp) && (lpoint ~~ rpoint) 61 | case (.ellipticalArcCurve(let lrx, let lry, let langle, let llargeArc, let lclockwise, let lpoint), .ellipticalArcCurve(let rrx, let rry, let rangle, let rlargeArc, let rclockwise, let rpoint)): 62 | return (lrx ~~ rrx) && ((lry ~~ rry)) && (langle ~~ rangle) && (llargeArc == rlargeArc) && (lclockwise == rclockwise) && (lpoint ~~ rpoint) 63 | case (.closePath, .closePath): 64 | return true 65 | default: 66 | return false 67 | } 68 | } 69 | } 70 | 71 | extension Double: RoughEquatability { 72 | public static func ~~ (lhs: Double, rhs: Double) -> Bool { 73 | // Float.abs is not available on some platforms. 74 | return Swift.abs(lhs - rhs) < 0.001 75 | } 76 | } 77 | 78 | extension Point: RoughEquatability { 79 | public static func ~~ (lhs: Point, rhs: Point) -> Bool { 80 | return (lhs.x ~~ rhs.x) && (lhs.y ~~ rhs.y) 81 | } 82 | } 83 | 84 | extension Array: RoughEquatability where Element == Path.Command { 85 | public static func ~~ (lhs: Array, rhs: Array) -> Bool { 86 | guard lhs.count == rhs.count else { 87 | return false 88 | } 89 | 90 | for (idx, i) in lhs.enumerated() { 91 | if !(i ~~ rhs[idx]) { 92 | return false 93 | } 94 | } 95 | 96 | return true 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/PathDataTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftSVG 3 | 4 | final class PathDataTests: XCTestCase { 5 | 6 | func testDataFormatAppleSymbols() throws { 7 | let pathData = "M 11.709 2.91016 C 17.1582 2.91016 21.6699 -1.60156 21.6699 -7.05078 " + 8 | "C 21.6699 -12.4902 17.1484 -17.0117 11.6992 -17.0117 C 6.25977 -17.0117 1.74805 -12.4902 1.74805 -7.05078 " + 9 | "C 1.74805 -1.60156 6.26953 2.91016 11.709 2.91016 Z M 11.709 1.25 C 7.09961 1.25 3.41797 -2.44141 3.41797 -7.05078 " + 10 | "C 3.41797 -11.6504 7.08984 -15.3516 11.6992 -15.3516 C 16.3086 -15.3516 20 -11.6504 20.0098 -7.05078 " + 11 | "C 20.0195 -2.44141 16.3184 1.25 11.709 1.25 Z M 11.6895 -2.41211 C 12.207 -2.41211 12.5195 -2.77344 12.5195 -3.33984 " + 12 | "L 12.5195 -6.23047 L 15.5762 -6.23047 C 16.123 -6.23047 16.5039 -6.51367 16.5039 -7.03125 " + 13 | "C 16.5039 -7.55859 16.1426 -7.86133 15.5762 -7.86133 L 12.5195 -7.86133 L 12.5195 -10.9277 " + 14 | "C 12.5195 -11.5039 12.207 -11.8555 11.6895 -11.8555 C 11.1719 -11.8555 10.8789 -11.4844 10.8789 -10.9277 " + 15 | "L 10.8789 -7.86133 L 7.83203 -7.86133 C 7.26562 -7.86133 6.89453 -7.55859 6.89453 -7.03125 " + 16 | "C 6.89453 -6.51367 7.28516 -6.23047 7.83203 -6.23047 L 10.8789 -6.23047 L 10.8789 -3.33984 " + 17 | "C 10.8789 -2.79297 11.1719 -2.41211 11.6895 -2.41211 Z" 18 | 19 | let path = Path(data: pathData) 20 | let commands = try path.commands() 21 | XCTAssertEqual(commands.count, 30) 22 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.move) }).count, 3) 23 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.close) }).count, 3) 24 | } 25 | 26 | func testDataFormatPixelmatorPro() throws { 27 | let pathData = "M96.083 307 C82.23 307 71 295.77 71 281.917 L71 145.75 C71 131.899 82.23 120.667 96.083 120.667 " + 28 | "96.083 120.667 109.056 120.667 128.522 120.667 128.522 92.001 151.578 92 155.425 92 185.826 92 210.976 92.056 243.595 92.056 " + 29 | "252.61 92.056 271.87 95.585 271.87 120.667 291.116 120.667 303.916 120.667 303.916 120.667 317.768 120.667 329 131.899 329 145.75 " + 30 | "L329 281.917 C329 295.77 317.768 307 303.916 307 L96.083 307 Z M200 264 C231.663 264 257.333 238.332 257.333 206.667 " + 31 | "257.333 175.004 231.663 149.334 200 149.334 168.335 149.334 142.666 175.004 142.666 206.667 142.666 238.332 168.335 264 200 264 Z " + 32 | "M200 235.334 C184.167 235.334 171.333 222.499 171.333 206.667 171.333 190.836 184.167 178 200 178 " + 33 | "215.831 178 228.667 190.836 228.667 206.667 228.667 222.499 215.831 235.334 200 235.334 Z" 34 | 35 | let path = Path(data: pathData) 36 | let commands = try path.commands() 37 | XCTAssertEqual(commands.count, 26) 38 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.move) }).count, 3) 39 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.close) }).count, 3) 40 | } 41 | 42 | func testDataFormatSketch() throws { 43 | let pathData = "M22,40.333 C11.875,40.333 3.667,32.125 3.667,22 C3.667,21.988 3.668,21.976 3.668,21.964 " + 44 | "L14.481,21.964 L19.74,34.229 L25.544,18.92 L27.31,21.964 L33,21.964 C34.013,21.964 34.833,21.143 34.833,20.131 " + 45 | "C34.833,19.118 34.013,18.297 33,18.297 L29.422,18.297 L24.849,10.413 L19.531,24.437 L16.898,18.297 " + 46 | "L4.041,18.297 C5.754,9.947 13.143,3.666 22,3.666 C32.125,3.666 40.333,11.875 40.333,22 " + 47 | "C40.333,32.125 32.125,40.333 22,40.333 M22,0 C9.85,0 0,9.85 0,22 C0,34.15 9.85,44 22,44 " + 48 | "C34.15,44 44,34.15 44,22 C44,9.85 34.15,0 22,0" 49 | 50 | let path = Path(data: pathData) 51 | let commands = try path.commands() 52 | XCTAssertEqual(commands.count, 23) 53 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.move) }).count, 2) 54 | XCTAssertEqual(commands.filter({ $0.hasPrefix(.close) }).count, 0) 55 | } 56 | 57 | func testRelativePath() throws { 58 | let absolute = "M217.074 360.93 C145.835 360.93 88.022 303.209 88.022 231.958 88.022 160.578 145.835 102.899 217.074 102.899 288.375 102.899 346.116 160.578 346.116 231.958 346.116 303.209 288.375 360.93 217.074 360.93 M217.074 38.459 C110.278 38.459 23.675 125.084 23.675 231.958 23.675 338.75 110.278 425.348 217.074 425.348 323.916 425.348 410.655 338.75 410.655 231.958 410.655 125.084 323.916 38.459 217.074 38.459 Z" 59 | let relative = "M217.074,360.93c-71.239,0-129.052-57.721-129.052-128.972c0-71.38,57.813-129.059,129.052-129.059c71.301,0,129.042,57.679,129.042,129.059C346.116,303.209,288.375,360.93,217.074,360.93 M217.074,38.459c-106.796,0-193.399,86.625-193.399,193.499c0,106.792,86.603,193.39,193.399,193.39c106.842,0,193.581-86.598,193.581-193.39C410.655,125.084,323.916,38.459,217.074,38.459z" 60 | 61 | let absolutePath = Path(data: absolute) 62 | let relativePath = Path(data: relative) 63 | 64 | let absoluteCommands = try absolutePath.commands() 65 | let relativeCommands = try relativePath.commands() 66 | 67 | XCTAssertEqual(absoluteCommands.count, relativeCommands.count) 68 | 69 | for i in 0.. 139 | // """ 140 | // 141 | // let absolute = """ 142 | // 143 | // 144 | // 145 | // 146 | // 147 | // """ 148 | // 149 | // let relativeData = try XCTUnwrap(relative.data(using: .utf8)) 150 | // let relativeSVG = try SVG.make(with: relativeData) 151 | // let relativeCommands = try XCTUnwrap(relativeSVG.paths?.first?.commands()) 152 | // 153 | // let absoluteData = try XCTUnwrap(absolute.data(using: .utf8)) 154 | // let absoluteSVG = try SVG.make(with: absoluteData) 155 | // let absoluteCommands = try XCTUnwrap(absoluteSVG.paths?.first?.commands()) 156 | // 157 | // for i in 0...68 { 158 | // let lhs = relativeCommands[i] 159 | // let rhs = absoluteCommands[i] 160 | // print(""" 161 | // 162 | // Relative: \(lhs) 163 | // Absolute: \(rhs) 164 | // 165 | // """) 166 | // } 167 | // 168 | // XCTAssertRoughlyEqual(relativeCommands, absoluteCommands) 169 | // } 170 | 171 | func testInvalidRelativeCommand() throws { 172 | let doc = """ 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | """ 197 | 198 | let data = try XCTUnwrap(doc.data(using: .utf8)) 199 | let svg = try SVG.make(with: data) 200 | XCTAssertNoThrow(try svg.subpaths()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/PointTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Swift2D 3 | @testable import SwiftSVG 4 | 5 | final class PointTests: XCTestCase { 6 | 7 | let rect = Rect(origin: .zero, size: Size(width: 500, height: 500)) 8 | var center: Point { rect.center } 9 | 10 | func testReflection() throws { 11 | // x=x y=y 12 | var point = Point(x: 250, y: 250) 13 | var reflection = point.reflection(using: center) 14 | XCTAssertEqual(reflection, Point(x: 250, y: 250)) 15 | 16 | // x→x y↓y 17 | point = Point(x: 150, y: 50) 18 | reflection = point.reflection(using: center) 19 | XCTAssertEqual(reflection, Point(x: 350, y: 450)) 20 | 21 | // x→x y=y 22 | point = Point(x: 150, y: 250) 23 | reflection = point.reflection(using: center) 24 | XCTAssertEqual(reflection, Point(x: 350, y: 250)) 25 | 26 | // x=x y↓y 27 | point = Point(x: 250, y: 50) 28 | reflection = point.reflection(using: center) 29 | XCTAssertEqual(reflection, Point(x: 250, y: 450)) 30 | 31 | // x←x y↑y 32 | point = Point(x: 350, y: 450) 33 | reflection = point.reflection(using: center) 34 | XCTAssertEqual(reflection, Point(x: 150, y: 50)) 35 | 36 | // x=x y↑y 37 | point = Point(x: 250, y: 450) 38 | reflection = point.reflection(using: center) 39 | XCTAssertEqual(reflection, Point(x: 250, y: 50)) 40 | 41 | // x←x y=y 42 | point = Point(x: 350, y: 250) 43 | reflection = point.reflection(using: center) 44 | XCTAssertEqual(reflection, Point(x: 150, y: 250)) 45 | 46 | // x→x y↑y 47 | point = Point(x: 150, y: 450) 48 | reflection = point.reflection(using: center) 49 | XCTAssertEqual(reflection, Point(x: 350, y: 50)) 50 | 51 | // x←x y↓y 52 | point = Point(x: 350, y: 50) 53 | reflection = point.reflection(using: center) 54 | XCTAssertEqual(reflection, Point(x: 150, y: 450)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/Resources/quad01.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Example quad01 - quadratic Bézier commands in path data 5 | Picture showing a "Q" a "T" command, 6 | along with annotations showing the control points 7 | and end points 8 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/SVGTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Swift2D 3 | @testable import SwiftSVG 4 | 5 | final class SVGTests: XCTestCase { 6 | 7 | func testSimpleDecode() throws { 8 | let doc = """ 9 | 10 | 11 | 12 | 13 | """ 14 | 15 | let data = try XCTUnwrap(doc.data(using: .utf8)) 16 | let svg = try SVG.make(with: data) 17 | 18 | XCTAssertEqual(svg.outputSize, Size(width: 1024, height: 1024)) 19 | let path = try XCTUnwrap(svg.paths?.first) 20 | XCTAssertEqual(path.id, "Padlock") 21 | XCTAssertFalse(path.data.isEmpty) 22 | let description = path.description 23 | XCTAssertTrue(description.hasPrefix(" 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | """ 39 | 40 | let data = try XCTUnwrap(doc.data(using: .utf8)) 41 | let svg = try SVG.make(with: data) 42 | XCTAssertEqual(svg.outputSize, Size(width: 2500, height: 2500)) 43 | } 44 | 45 | func testQuad01Decode() throws { 46 | let url = try XCTUnwrap(Bundle.swiftSVGTests.url(forResource: "quad01", withExtension: "svg")) 47 | let data = try Data(contentsOf: url) 48 | let svg = try SVG.make(with: data) 49 | let path = try XCTUnwrap(svg.paths?.first) 50 | XCTAssertEqual(path.data, "M200,300 Q400,50 600,300 T1000,300") 51 | let commands = try path.commands() 52 | XCTAssertEqual(commands, [ 53 | .moveTo(point: Point(x: 200, y: 300)), 54 | .quadraticBezierCurve(cp: Point(x: 400, y: 50), point: Point(x: 600, y: 300)), 55 | .quadraticBezierCurve(cp: Point(x: 800, y: 550), point: Point(x: 1000, y: 300)), 56 | ]) 57 | 58 | let primaryGroup = try XCTUnwrap(svg.groups?.first) 59 | let primaryPoints = try XCTUnwrap(primaryGroup.circles) 60 | XCTAssertEqual(primaryPoints.count, 3) 61 | 62 | let secondaryGroup = try XCTUnwrap(svg.groups?.last) 63 | let secondaryPoints = try XCTUnwrap(secondaryGroup.circles) 64 | XCTAssertEqual(secondaryPoints.count, 2) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/SwiftSVGTests/TranformationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftSVG 3 | 4 | final class TransformationTests: XCTestCase { 5 | 6 | func testTranslateInitialization() { 7 | var input: String = "translate(0.000000, 39.000000)" 8 | var transformation: Transformation? = Transformation(input) 9 | 10 | XCTAssertNotNil(transformation) 11 | if case let .translate(x, y) = transformation { 12 | XCTAssertEqual(x, 0.0, accuracy: 0.00001) 13 | XCTAssertEqual(y, 39.0, accuracy: 0.00001) 14 | } else { 15 | XCTFail() 16 | return 17 | } 18 | 19 | input = "translate(0.0 39.0)" 20 | transformation = Transformation(input) 21 | 22 | XCTAssertNotNil(transformation) 23 | if case let .translate(x, y) = transformation { 24 | XCTAssertEqual(x, 0.0, accuracy: 0.00001) 25 | XCTAssertEqual(y, 39.0, accuracy: 0.00001) 26 | } else { 27 | XCTFail() 28 | return 29 | } 30 | 31 | input = "TRANSLATE(0.0,39.0)" 32 | transformation = Transformation(input) 33 | 34 | XCTAssertNotNil(transformation) 35 | if case let .translate(x, y) = transformation { 36 | XCTAssertEqual(x, 0.0, accuracy: 0.00001) 37 | XCTAssertEqual(y, 39.0, accuracy: 0.00001) 38 | } else { 39 | XCTFail() 40 | return 41 | } 42 | } 43 | 44 | func testMatrixInitialization() { 45 | var input: String = "matrix(1 0 0 1 1449.84 322)" 46 | var transformation: Transformation? = Transformation(input) 47 | 48 | XCTAssertNotNil(transformation) 49 | if case let .matrix(a, b, c, d, e, f) = transformation { 50 | XCTAssertEqual(a, 1.0, accuracy: 0.00001) 51 | XCTAssertEqual(b, 0.0, accuracy: 0.00001) 52 | XCTAssertEqual(c, 0.0, accuracy: 0.00001) 53 | XCTAssertEqual(d, 1.0, accuracy: 0.00001) 54 | XCTAssertEqual(e, 1449.84, accuracy: 0.001) 55 | XCTAssertEqual(f, 322.0, accuracy: 0.00001) 56 | } else { 57 | XCTFail() 58 | return 59 | } 60 | 61 | input = "matrix(1, 0, 0, 1, 1449.84, 322)" 62 | transformation = Transformation(input) 63 | 64 | XCTAssertNotNil(transformation) 65 | if case let .matrix(a, b, c, d, e, f) = transformation { 66 | XCTAssertEqual(a, 1.0, accuracy: 0.00001) 67 | XCTAssertEqual(b, 0.0, accuracy: 0.00001) 68 | XCTAssertEqual(c, 0.0, accuracy: 0.00001) 69 | XCTAssertEqual(d, 1.0, accuracy: 0.00001) 70 | XCTAssertEqual(e, 1449.84, accuracy: 0.001) 71 | XCTAssertEqual(f, 322.0, accuracy: 0.00001) 72 | } else { 73 | XCTFail() 74 | return 75 | } 76 | 77 | input = "MATRIX(1,0,0,1,1449.84,322)" 78 | transformation = Transformation(input) 79 | 80 | XCTAssertNotNil(transformation) 81 | if case let .matrix(a, b, c, d, e, f) = transformation { 82 | XCTAssertEqual(a, 1.0, accuracy: 0.00001) 83 | XCTAssertEqual(b, 0.0, accuracy: 0.00001) 84 | XCTAssertEqual(c, 0.0, accuracy: 0.00001) 85 | XCTAssertEqual(d, 1.0, accuracy: 0.00001) 86 | XCTAssertEqual(e, 1449.84, accuracy: 0.001) 87 | XCTAssertEqual(f, 322.0, accuracy: 0.00001) 88 | } else { 89 | XCTFail() 90 | return 91 | } 92 | } 93 | 94 | func testCommandTransformation() throws { 95 | let translate = Transformation.translate(x: 25.0, y: 75.0) 96 | var command: Path.Command 97 | var result: Path.Command 98 | 99 | command = .moveTo(point: .init(x: 50.0, y: 50.0)) 100 | result = command.applying(transformation: translate) 101 | 102 | if case let .moveTo(point) = result { 103 | XCTAssertEqual(point, .init(x: 75.0, y: 125.0)) 104 | } else { 105 | XCTFail() 106 | return 107 | } 108 | 109 | command = .lineTo(point: .init(x: -60.0, y: 120.0)) 110 | result = command.applying(transformation: translate) 111 | 112 | if case let .lineTo(point) = result { 113 | XCTAssertEqual(point, .init(x: -35.0, y: 195.0)) 114 | } else { 115 | XCTFail() 116 | return 117 | } 118 | 119 | command = .cubicBezierCurve(cp1: .init(x: -20.0, y: -40.0), cp2: .init(x: 18.0, y: 94.0), point: .init(x: 20.0, y: 20.0)) 120 | result = command.applying(transformation: translate) 121 | 122 | if case let .cubicBezierCurve(cp1, cp2, point) = result { 123 | XCTAssertEqual(cp1, .init(x: 5.0, y: 35.0)) 124 | XCTAssertEqual(cp2, .init(x: 43.0, y: 169.0)) 125 | XCTAssertEqual(point, .init(x: 45.0, y: 95.0)) 126 | } else { 127 | XCTFail() 128 | return 129 | } 130 | 131 | command = .quadraticBezierCurve(cp: .init(x: 100.0, y: 50.0), point: .zero) 132 | result = command.applying(transformation: translate) 133 | 134 | if case let .quadraticBezierCurve(cp, point) = result { 135 | XCTAssertEqual(cp, .init(x: 125.0, y: 125.0)) 136 | XCTAssertEqual(point, .init(x: 25.0, y: 75.0)) 137 | } else { 138 | XCTFail() 139 | return 140 | } 141 | 142 | command = .closePath 143 | result = command.applying(transformation: translate) 144 | 145 | if case .closePath = result { 146 | } else { 147 | XCTFail() 148 | return 149 | } 150 | } 151 | } 152 | --------------------------------------------------------------------------------