├── .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 | 
6 | [](https://swiftpackageindex.com/richardpiazza/SwiftSVG)
7 | [](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 ""
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 | //
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------