diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d262bbdf..947c8c3d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,19 +6,19 @@ on:
workflow_dispatch:
jobs:
- xcode_15_3:
- runs-on: macos-14
+ xcode_16_4:
+ runs-on: macos-15
env:
- DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Version
run: swift --version
- name: Build
- run: swift test --enable-code-coverage --filter do_not_test
+ run: swift build --build-tests --enable-code-coverage
- name: Test
- run: swift test --enable-code-coverage --skip-build
+ run: swift test --skip-build --enable-code-coverage
- name: Gather code coverage
run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftDrawPackageTests.xctest/Contents/MacOS/SwiftDrawPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov
- name: Upload Coverage
@@ -27,10 +27,10 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage_report.lcov
- xcode_15_2:
- runs-on: macos-14
+ xcode_26:
+ runs-on: macos-15
env:
- DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_26.0.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -41,10 +41,10 @@ jobs:
- name: Test
run: swift test --skip-build
- xcode_14_3:
- runs-on: macos-13
+ xcode_16_3:
+ runs-on: macos-15
env:
- DEVELOPER_DIR: /Applications/Xcode_14.3.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -55,9 +55,10 @@ jobs:
- name: Test
run: swift test --skip-build
- linux_5_7:
+
+ linux_6:
runs-on: ubuntu-latest
- container: swift:5.7
+ container: swift:6.0.3
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -68,9 +69,9 @@ jobs:
- name: Test
run: swift test --skip-build
- linux_5_9:
+ linux_6_1:
runs-on: ubuntu-latest
- container: swift:5.9
+ container: swift:6.1.2
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -81,9 +82,9 @@ jobs:
- name: Test
run: swift test --skip-build
- linux_5_10:
+ linux_swift_6_2:
runs-on: ubuntu-latest
- container: swift:5.10
+ container: swiftlang/swift:nightly-6.2-noble
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -93,3 +94,11 @@ jobs:
run: swift build --build-tests
- name: Test
run: swift test --skip-build
+
+ android:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Build and Test
+ uses: skiptools/swift-android-action@v2
diff --git a/.swift-version b/.swift-version
deleted file mode 100755
index 6e636605..00000000
--- a/.swift-version
+++ /dev/null
@@ -1 +0,0 @@
-5.0
\ No newline at end of file
diff --git a/.swiftpm/SwiftDraw.xctestplan b/.swiftpm/SwiftDraw.xctestplan
index 116b305f..25b1d520 100644
--- a/.swiftpm/SwiftDraw.xctestplan
+++ b/.swiftpm/SwiftDraw.xctestplan
@@ -1,7 +1,7 @@
{
"configurations" : [
{
- "id" : "B46FBA9F-0EC4-486D-8336-66AB23577319",
+ "id" : "5E425660-B3D3-4E55-A5EE-21100C436313",
"name" : "Test Scheme Action",
"options" : {
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw-Package.xcscheme
deleted file mode 100644
index ab64e273..00000000
--- a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw-Package.xcscheme
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw.xcscheme
new file mode 100644
index 00000000..390dee42
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftDraw.xcscheme
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100755
index ddd5e424..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Change Log
-
-
-## [0.5.0](https://github.com/swhitty/SwiftDraw/releases/tag/0.5.0) (2019-03-29)
-
-- Linear gradients
-- Patterns
-
-## [0.4.0](https://github.com/swhitty/SwiftDraw/releases/tag/0.4.0) (2019-02-12)
-
-- Support for ARC segments within Path
-
-## [0.2.1](https://github.com/swhitty/SwiftDraw/releases/tag/0.2.1) (2018-11-19)
-
-- Support for Swift 4.2
-
-## [0.2](https://github.com/swhitty/SwiftDraw/releases/tag/0.2) (2017-06-12)
-
-- Adding `LayerTree` to enable optimizations and removal of redundant RenderCommands.
-- Adding `protocol Renderer`
-- Fixing element opacity errors
-- Fixing lineJoin parser errors
-
-## [0.1](https://github.com/swhitty/SwiftDraw/releases/tag/0.1) (2017-05-24)
-
-- Initial Release. Hello World.
diff --git a/CommandLine/CommandLine.swift b/CommandLine/CommandLine.swift
index 58a649f9..715ba6ab 100644
--- a/CommandLine/CommandLine.swift
+++ b/CommandLine/CommandLine.swift
@@ -31,6 +31,7 @@
import Foundation
import SwiftDraw
+import SwiftDrawDOM
extension SwiftDraw.CommandLine {
@@ -68,8 +69,8 @@ extension SwiftDraw.CommandLine {
static func printHelp() {
print("")
print("""
-swiftdraw, version 0.16.0
-copyright (c) 2023 Simon Whitty
+swiftdraw, version 0.25.0
+copyright (c) 2025 Simon Whitty
usage: swiftdraw [--format png | pdf | jpeg | swift | sfsymbol] [--size wxh] [--scale 1x | 2x | 3x]
@@ -83,17 +84,19 @@ Options:
--precision maximum number of decimal places
--output optional path of output file
- --hideUnsupportedFilters hide elements with unsupported filters.
+ --hide-unsupported-filters hide elements with unsupported filters.
Available keys for --format swift:
--api api of generated code: appkit | uikit
Available keys for --format sfsymbol:
--insets alignment of regular variant: top,left,bottom,right | auto
+ --size size category to generate: small, medium large. (default is small)
--ultralight svg file of ultralight variant
- --ultralightInsets alignment of ultralight variant: top,left,bottom,right | auto
+ --ultralight-insets alignment of ultralight variant: top,left,bottom,right | auto
--black svg file of black variant
- --blackInsets alignment of black variant: top,left,bottom,right | auto
+ --black-insets alignment of black variant: top,left,bottom,right | auto
+ --legacy use the original, less precise alignment logic from earlier swiftdraw versions.
""")
diff --git a/CommandLine/TextOutputStream+StandardError.swift b/CommandLine/TextOutputStream+StandardError.swift
deleted file mode 120000
index d98ac5d0..00000000
--- a/CommandLine/TextOutputStream+StandardError.swift
+++ /dev/null
@@ -1 +0,0 @@
-../SwiftDraw/Utilities/TextOutputStream+StandardError.swift
\ No newline at end of file
diff --git a/CommandLine/main.swift b/CommandLine/main.swift
index a58fdabc..d47a0983 100644
--- a/CommandLine/main.swift
+++ b/CommandLine/main.swift
@@ -31,6 +31,8 @@
#if canImport(Darwin)
import Darwin.POSIX
+#elseif canImport(Android)
+import Android
#else
import Glibc
#endif
diff --git a/DOM/Sources/DOM+Extensions.swift b/DOM/Sources/DOM+Extensions.swift
new file mode 100644
index 00000000..63883583
--- /dev/null
+++ b/DOM/Sources/DOM+Extensions.swift
@@ -0,0 +1,100 @@
+//
+// DOM.Element.Equality.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+
+package extension DOM.Polyline {
+ // requires even number of elements
+ convenience init(_ p: DOM.Coordinate...) {
+
+ var points = [DOM.Point]()
+
+ for index in stride(from: 0, to: p.count, by: 2) {
+ points.append(DOM.Point(p[index], p[index + 1]))
+ }
+
+ self.init(points: points)
+ }
+}
+
+package extension DOM.Polygon {
+ // requires even number of elements
+ convenience init(_ p: DOM.Coordinate...) {
+
+ var points = [DOM.Point]()
+
+ for index in stride(from: 0, to: p.count, by: 2) {
+ points.append(DOM.Point(p[index], p[index + 1]))
+ }
+
+ self.init(points: points)
+ }
+}
+
+package extension XML.Element {
+ convenience init(_ name: String, style: String) {
+ self.init(name: name, attributes: ["style": style])
+ }
+
+ convenience init(_ name: String, id: String, style: String) {
+ self.init(name: name, attributes: ["id": id, "style": style])
+ }
+}
+
+package extension DOM.SVG {
+
+ static func parse(fileNamed name: String, in bundle: Bundle) throws -> DOM.SVG {
+ guard let url = bundle.url(forResource: name, withExtension: nil) else {
+ throw Error("missing resource: \(name) in bundle: \(bundle)")
+ }
+
+ let parser = XMLParser(options: [.skipInvalidElements], filename: url.lastPathComponent)
+ let element = try XML.SAXParser.parse(contentsOf: url)
+ return try parser.parseSVG(element)
+ }
+
+ static func parse(
+ xml: String,
+ options: XMLParser.Options = [.skipInvalidElements]
+ ) throws -> DOM.SVG {
+ let element = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ let parser = XMLParser(options: options)
+ return try parser.parseSVG(element)
+ }
+
+ private struct Error: LocalizedError {
+ var errorDescription: String?
+
+ init(_ message: String) {
+ self.errorDescription = message
+ }
+ }
+}
diff --git a/SwiftDraw/DOM.Color.swift b/DOM/Sources/DOM.Color.swift
similarity index 98%
rename from SwiftDraw/DOM.Color.swift
rename to DOM/Sources/DOM.Color.swift
index 2c3f3458..044fec13 100644
--- a/SwiftDraw/DOM.Color.swift
+++ b/DOM/Sources/DOM.Color.swift
@@ -29,19 +29,19 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
-
+package extension DOM {
+
enum Color: Equatable {
case none
case currentColor
case keyword(Keyword)
- case rgbi(UInt8, UInt8, UInt8)
- case rgbf(DOM.Float, DOM.Float, DOM.Float)
+ case rgbi(UInt8, UInt8, UInt8, DOM.Float)
+ case rgbf(DOM.Float, DOM.Float, DOM.Float, DOM.Float)
case p3(DOM.Float, DOM.Float, DOM.Float)
case hex(UInt8, UInt8, UInt8)
// see: https://www.w3.org/TR/SVG11/types.html#ColorKeywords
- enum Keyword: String {
+ package enum Keyword: String {
case aliceblue
case antiquewhite
case aqua
@@ -193,7 +193,7 @@ extension DOM {
}
}
-extension DOM.Color.Keyword {
+package extension DOM.Color.Keyword {
// each color keyword maps to an rgbi
var rgbi: (UInt8, UInt8, UInt8) {
diff --git a/SwiftDraw/DOM.Element.swift b/DOM/Sources/DOM.Element.swift
similarity index 62%
rename from SwiftDraw/DOM.Element.swift
rename to DOM/Sources/DOM.Element.swift
index ea88624d..15f26f40 100644
--- a/SwiftDraw/DOM.Element.swift
+++ b/DOM/Sources/DOM.Element.swift
@@ -31,34 +31,34 @@
import Foundation
-protocol ContainerElement {
+package protocol ContainerElement {
var childElements: [DOM.GraphicsElement] { get set }
}
-protocol ElementAttributes {
+package protocol ElementAttributes {
var id: String? { get set }
var `class`: String? { get set }
}
-extension DOM {
-
+package extension DOM {
+
class Element {}
class GraphicsElement: Element, ElementAttributes {
- var id: String?
- var `class`: String?
-
- var attributes = PresentationAttributes()
- var style = PresentationAttributes()
+ package var id: String?
+ package var `class`: String?
+
+ package var attributes = PresentationAttributes()
+ package var style = PresentationAttributes()
}
final class Line: GraphicsElement {
- var x1: Coordinate
- var y1: Coordinate
- var x2: Coordinate
- var y2: Coordinate
-
- init(x1: Coordinate, y1: Coordinate, x2: Coordinate, y2: Coordinate) {
+ package var x1: Coordinate
+ package var y1: Coordinate
+ package var x2: Coordinate
+ package var y2: Coordinate
+
+ package init(x1: Coordinate, y1: Coordinate, x2: Coordinate, y2: Coordinate) {
self.x1 = x1
self.y1 = y1
self.x2 = x2
@@ -68,11 +68,11 @@ extension DOM {
}
final class Circle: GraphicsElement {
- var cx: Coordinate?
- var cy: Coordinate?
- var r: Coordinate
-
- init(cx: Coordinate?, cy: Coordinate?, r: Coordinate) {
+ package var cx: Coordinate?
+ package var cy: Coordinate?
+ package var r: Coordinate
+
+ package init(cx: Coordinate?, cy: Coordinate?, r: Coordinate) {
self.cx = cx
self.cy = cy
self.r = r
@@ -81,12 +81,12 @@ extension DOM {
}
final class Ellipse: GraphicsElement {
- var cx: Coordinate?
- var cy: Coordinate?
- var rx: Coordinate
- var ry: Coordinate
-
- init(cx: Coordinate?, cy: Coordinate?, rx: Coordinate, ry: Coordinate) {
+ package var cx: Coordinate?
+ package var cy: Coordinate?
+ package var rx: Coordinate
+ package var ry: Coordinate
+
+ package init(cx: Coordinate?, cy: Coordinate?, rx: Coordinate, ry: Coordinate) {
self.cx = cx
self.cy = cy
self.rx = rx
@@ -96,15 +96,15 @@ extension DOM {
}
final class Rect: GraphicsElement {
- var x: Coordinate?
- var y: Coordinate?
- var width: Coordinate
- var height: Coordinate
-
- var rx: Coordinate?
- var ry: Coordinate?
-
- init(x: Coordinate? = nil, y: Coordinate? = nil, width: Coordinate, height: Coordinate) {
+ package var x: Coordinate?
+ package var y: Coordinate?
+ package var width: Coordinate
+ package var height: Coordinate
+
+ package var rx: Coordinate?
+ package var ry: Coordinate?
+
+ package init(x: Coordinate? = nil, y: Coordinate? = nil, width: Coordinate, height: Coordinate) {
self.x = x
self.y = y
self.width = width
@@ -114,24 +114,24 @@ extension DOM {
}
final class Polyline: GraphicsElement {
- var points: [Point]
-
- init(points: [Point]) {
+ package var points: [Point]
+
+ package init(points: [Point]) {
self.points = points
super.init()
}
}
final class Polygon: GraphicsElement {
- var points: [Point]
-
- init(points: [Point]) {
+ package var points: [Point]
+
+ package init(points: [Point]) {
self.points = points
super.init()
}
}
final class Group: GraphicsElement, ContainerElement {
- var childElements = [GraphicsElement]()
+ package var childElements = [GraphicsElement]()
}
}
diff --git a/SwiftDraw/DOM.Filter.swift b/DOM/Sources/DOM.Filter.swift
similarity index 88%
rename from SwiftDraw/DOM.Filter.swift
rename to DOM/Sources/DOM.Filter.swift
index ca41941e..4d524b71 100644
--- a/SwiftDraw/DOM.Filter.swift
+++ b/DOM/Sources/DOM.Filter.swift
@@ -29,19 +29,19 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
-
+package extension DOM {
+
final class Filter: Element {
- var id: String
-
- var effects: [Effect]
-
- init(id: String) {
+ package var id: String
+
+ package var effects: [Effect]
+
+ package init(id: String) {
self.id = id
self.effects = []
}
- enum Effect: Equatable {
+ package enum Effect: Hashable {
case gaussianBlur(stdDeviation: DOM.Float)
}
}
diff --git a/SwiftDraw/DOM.Image.swift b/DOM/Sources/DOM.Image.swift
similarity index 80%
rename from SwiftDraw/DOM.Image.swift
rename to DOM/Sources/DOM.Image.swift
index f12d92fd..f5f5397d 100644
--- a/SwiftDraw/DOM.Image.swift
+++ b/DOM/Sources/DOM.Image.swift
@@ -28,19 +28,17 @@
//
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
+package extension DOM {
final class Image: GraphicsElement {
- var href: URL
- var width: Coordinate
- var height: Coordinate
-
- var x: Coordinate?
- var y: Coordinate?
-
- init(href: URL, width: Coordinate, height: Coordinate) {
+ package var href: URL
+ package var width: Coordinate?
+ package var height: Coordinate?
+
+ package var x: Coordinate?
+ package var y: Coordinate?
+
+ package init(href: URL) {
self.href = href
- self.width = width
- self.height = height
super.init()
}
}
diff --git a/SwiftDraw/DOM.LinearGradient.swift b/DOM/Sources/DOM.LinearGradient.swift
similarity index 70%
rename from SwiftDraw/DOM.LinearGradient.swift
rename to DOM/Sources/DOM.LinearGradient.swift
index 4794360b..7c0065ca 100644
--- a/SwiftDraw/DOM.LinearGradient.swift
+++ b/DOM/Sources/DOM.LinearGradient.swift
@@ -29,35 +29,35 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
-
+package extension DOM {
+
final class LinearGradient: Element {
- var id: String
- var x1: Coordinate?
- var y1: Coordinate?
- var x2: Coordinate?
- var y2: Coordinate?
-
- var stops: [Stop]
- var gradientUnits: Units?
- var gradientTransform: [Transform]
-
+ package var id: String
+ package var x1: Coordinate?
+ package var y1: Coordinate?
+ package var x2: Coordinate?
+ package var y2: Coordinate?
+
+ package var stops: [Stop]
+ package var gradientUnits: Units?
+ package var gradientTransform: [Transform]
+
//references another LinearGradient element id within defs
- var href: URL?
-
- init(id: String) {
+ package var href: URL?
+
+ package init(id: String) {
self.id = id
self.stops = []
self.gradientTransform = []
}
- struct Stop: Equatable {
- var offset: Float
- var color: Color
- var opacity: Float
-
- init(offset: Float, color: Color, opacity: Opacity = 1.0) {
+ package struct Stop: Equatable {
+ package var offset: Float
+ package var color: Color
+ package var opacity: Float
+
+ package init(offset: Float, color: Color, opacity: Opacity = 1.0) {
self.offset = offset
self.color = color
self.opacity = opacity
@@ -67,7 +67,7 @@ extension DOM {
}
extension DOM.LinearGradient: Equatable {
- static func ==(lhs: DOM.LinearGradient, rhs: DOM.LinearGradient) -> Bool {
+ package static func ==(lhs: DOM.LinearGradient, rhs: DOM.LinearGradient) -> Bool {
return lhs.id == rhs.id &&
lhs.x1 == rhs.x1 &&
lhs.y1 == rhs.y1 &&
@@ -77,8 +77,8 @@ extension DOM.LinearGradient: Equatable {
}
}
-extension DOM.LinearGradient {
-
+package extension DOM.LinearGradient {
+
enum Units: String {
case userSpaceOnUse
case objectBoundingBox
diff --git a/SwiftDraw/DOM.Path.swift b/DOM/Sources/DOM.Path.swift
similarity index 92%
rename from SwiftDraw/DOM.Path.swift
rename to DOM/Sources/DOM.Path.swift
index 34c95a51..165a56dc 100644
--- a/SwiftDraw/DOM.Path.swift
+++ b/DOM/Sources/DOM.Path.swift
@@ -31,20 +31,20 @@
import Foundation
-extension DOM {
-
+package extension DOM {
+
final class Path: GraphicsElement {
// segments[0] should always be a .move
- var segments: [Segment]
-
- init(x: Coordinate, y: Coordinate) {
+ package var segments: [Segment]
+
+ package init(x: Coordinate, y: Coordinate) {
let s = Segment.move(x: x, y: y, space: .absolute)
segments = [s]
super.init()
}
- enum Segment {
+ package enum Segment: Equatable {
case move(x: Coordinate, y: Coordinate, space: CoordinateSpace)
case line(x: Coordinate, y: Coordinate, space: CoordinateSpace)
case horizontal(x: Coordinate, space: CoordinateSpace)
@@ -62,13 +62,13 @@ extension DOM {
x: Coordinate, y: Coordinate, space: CoordinateSpace)
case close
- enum CoordinateSpace {
+ package enum CoordinateSpace {
case absolute
case relative
}
}
- enum Command: UnicodeScalar {
+ package enum Command: UnicodeScalar {
case move = "M"
case moveRelative = "m"
case line = "L"
@@ -90,7 +90,7 @@ extension DOM {
case close = "Z"
case closeAlias = "z"
- var coordinateSpace: Segment.CoordinateSpace {
+ package var coordinateSpace: Segment.CoordinateSpace {
switch self {
case .move, .line,
.horizontal, .vertical,
diff --git a/SwiftDraw/DOM.Pattern.swift b/DOM/Sources/DOM.Pattern.swift
similarity index 75%
rename from SwiftDraw/DOM.Pattern.swift
rename to DOM/Sources/DOM.Pattern.swift
index 0b9ca176..6f7e5f7f 100644
--- a/SwiftDraw/DOM.Pattern.swift
+++ b/DOM/Sources/DOM.Pattern.swift
@@ -31,22 +31,22 @@
import Foundation
-extension DOM {
-
+package extension DOM {
+
struct Pattern: ContainerElement {
- var id: String
- var x: Coordinate?
- var y: Coordinate?
- var width: Coordinate
- var height: Coordinate
-
- var patternUnits: Units?
- var patternContentUnits: Units?
-
- var childElements: [DOM.GraphicsElement] = []
-
- init(id: String, width: Coordinate, height: Coordinate) {
+ package var id: String
+ package var x: Coordinate?
+ package var y: Coordinate?
+ package var width: Coordinate
+ package var height: Coordinate
+
+ package var patternUnits: Units?
+ package var patternContentUnits: Units?
+
+ package var childElements: [DOM.GraphicsElement] = []
+
+ package init(id: String, width: Coordinate, height: Coordinate) {
self.id = id
self.width = width
self.height = height
@@ -54,7 +54,7 @@ extension DOM {
}
}
-extension DOM.Pattern {
+package extension DOM.Pattern {
enum Units: String {
case userSpaceOnUse
diff --git a/SwiftDraw/DOM.PresentationAttributes.swift b/DOM/Sources/DOM.PresentationAttributes.swift
similarity index 85%
rename from SwiftDraw/DOM.PresentationAttributes.swift
rename to DOM/Sources/DOM.PresentationAttributes.swift
index e81fe5bc..16a08b30 100644
--- a/SwiftDraw/DOM.PresentationAttributes.swift
+++ b/DOM/Sources/DOM.PresentationAttributes.swift
@@ -31,36 +31,36 @@
import Foundation
-extension DOM {
-
+package extension DOM {
+
// PresentationAttributes cascade;
// element.attributes --> .element() --> .class() ---> .id() ---> element.style ---> layerTree.state
struct PresentationAttributes {
- var opacity: DOM.Float?
- var display: DOM.DisplayMode?
- var color: DOM.Color?
-
- var stroke: DOM.Fill?
- var strokeWidth: DOM.Float?
- var strokeOpacity: DOM.Float?
- var strokeLineCap: DOM.LineCap?
- var strokeLineJoin: DOM.LineJoin?
- var strokeDashArray: [DOM.Float]?
-
- var fill: DOM.Fill?
- var fillOpacity: DOM.Float?
- var fillRule: DOM.FillRule?
-
- var fontFamily: String?
- var fontSize: Float?
- var textAnchor: TextAnchor?
-
- var transform: [DOM.Transform]?
- var clipPath: DOM.URL?
- var clipRule: DOM.FillRule?
- var mask: DOM.URL?
- var filter: DOM.URL?
+ package var opacity: DOM.Float?
+ package var display: DOM.DisplayMode?
+ package var color: DOM.Color?
+
+ package var stroke: DOM.Fill?
+ package var strokeWidth: DOM.Float?
+ package var strokeOpacity: DOM.Float?
+ package var strokeLineCap: DOM.LineCap?
+ package var strokeLineJoin: DOM.LineJoin?
+ package var strokeDashArray: [DOM.Float]?
+
+ package var fill: DOM.Fill?
+ package var fillOpacity: DOM.Float?
+ package var fillRule: DOM.FillRule?
+
+ package var fontFamily: String?
+ package var fontSize: Float?
+ package var textAnchor: TextAnchor?
+
+ package var transform: [DOM.Transform]?
+ package var clipPath: DOM.URL?
+ package var clipRule: DOM.FillRule?
+ package var mask: DOM.URL?
+ package var filter: DOM.URL?
}
static func presentationAttributes(for element: DOM.GraphicsElement,
diff --git a/SwiftDraw/DOM.RadialGradient.swift b/DOM/Sources/DOM.RadialGradient.swift
similarity index 65%
rename from SwiftDraw/DOM.RadialGradient.swift
rename to DOM/Sources/DOM.RadialGradient.swift
index 42fc2841..b30f04e4 100644
--- a/SwiftDraw/DOM.RadialGradient.swift
+++ b/DOM/Sources/DOM.RadialGradient.swift
@@ -29,38 +29,38 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
-
+package extension DOM {
+
final class RadialGradient: Element {
- typealias Units = LinearGradient.Units
-
- var id: String
- var r: Coordinate?
- var cx: Coordinate?
- var cy: Coordinate?
- var fr: Coordinate?
- var fx: Coordinate?
- var fy: Coordinate?
-
- var stops: [Stop]
- var gradientUnits: Units?
- var gradientTransform: [Transform]
+ package typealias Units = LinearGradient.Units
+ package var id: String
+ package var r: Coordinate?
+ package var cx: Coordinate?
+ package var cy: Coordinate?
+ package var fr: Coordinate?
+ package var fx: Coordinate?
+ package var fy: Coordinate?
+
+ package var stops: [Stop]
+ package var gradientUnits: Units?
+ package var gradientTransform: [Transform]
+
//references another RadialGradient element id within defs
- var href: URL?
-
- init(id: String) {
+ package var href: URL?
+
+ package init(id: String) {
self.id = id
self.stops = []
self.gradientTransform = []
}
- struct Stop: Equatable {
- var offset: Float
- var color: Color
- var opacity: Float
-
- init(offset: Float, color: Color, opacity: Opacity = 1.0) {
+ package struct Stop: Equatable {
+ package var offset: Float
+ package var color: Color
+ package var opacity: Float
+
+ package init(offset: Float, color: Color, opacity: Opacity = 1.0) {
self.offset = offset
self.color = color
self.opacity = opacity
@@ -70,7 +70,7 @@ extension DOM {
}
extension DOM.RadialGradient: Equatable {
- static func ==(lhs: DOM.RadialGradient, rhs: DOM.RadialGradient) -> Bool {
+ package static func ==(lhs: DOM.RadialGradient, rhs: DOM.RadialGradient) -> Bool {
return lhs.id == rhs.id && lhs.stops == rhs.stops
}
}
diff --git a/Examples/Sources/AppDelegate.swift b/DOM/Sources/DOM.SVG+Parse.swift
similarity index 59%
rename from Examples/Sources/AppDelegate.swift
rename to DOM/Sources/DOM.SVG+Parse.swift
index c5fde6d2..e13a8134 100644
--- a/Examples/Sources/AppDelegate.swift
+++ b/DOM/Sources/DOM.SVG+Parse.swift
@@ -1,9 +1,9 @@
//
-// AppDelegate.swift
+// DOM.SVG+Parse.swift
// SwiftDraw
//
-// Created by Simon Whitty on 10/2/19.
-// Copyright 2019 Simon Whitty
+// Created by Simon Whitty on 23/2/25.
+// Copyright 2025 Simon Whitty
//
// Distributed under the permissive zlib license
// Get the latest version from here:
@@ -29,24 +29,19 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import UIKit
+import Foundation
-@UIApplicationMain
-class AppDelegate: UIResponder, UIApplicationDelegate {
+package extension DOM.SVG {
- var window: UIWindow?
-
-
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-
- let window = UIWindow()
- window.rootViewController = UINavigationController(rootViewController: ViewController())
- window.makeKeyAndVisible()
- self.window = window
-
- return true
+ static func parse(fileURL url: URL, options: XMLParser.Options = .skipInvalidElements) throws -> DOM.SVG {
+ let element = try XML.SAXParser.parse(contentsOf: url)
+ let parser = XMLParser(options: options, filename: url.lastPathComponent)
+ return try parser.parseSVG(element)
}
-
+ static func parse(data: Data, options: XMLParser.Options = .skipInvalidElements) throws -> DOM.SVG {
+ let element = try XML.SAXParser.parse(data: data)
+ let parser = XMLParser(options: options)
+ return try parser.parseSVG(element)
+ }
}
-
diff --git a/DOM/Sources/DOM.SVG.swift b/DOM/Sources/DOM.SVG.swift
new file mode 100644
index 00000000..9fd3877e
--- /dev/null
+++ b/DOM/Sources/DOM.SVG.swift
@@ -0,0 +1,103 @@
+//
+// DOM.SVG.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 11/2/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+package extension DOM {
+ final class SVG: GraphicsElement, ContainerElement {
+ package var x: Coordinate?
+ package var y: Coordinate?
+ package var width: Length
+ package var height: Length
+ package var viewBox: ViewBox?
+
+ package var childElements = [GraphicsElement]()
+
+ package var styles = [StyleSheet]()
+ package var defs = Defs()
+
+ package init(x: Coordinate? = nil, y: Coordinate? = nil, width: Length, height: Length) {
+ self.x = x
+ self.y = y
+ self.width = width
+ self.height = height
+ }
+
+ package struct ViewBox: Equatable {
+ package var x: Coordinate
+ package var y: Coordinate
+ package var width: Coordinate
+ package var height: Coordinate
+
+ package init(x: Coordinate, y: Coordinate, width: Coordinate, height: Coordinate) {
+ self.x = x
+ self.y = y
+ self.width = width
+ self.height = height
+ }
+ }
+
+ package struct Defs {
+ package var clipPaths = [ClipPath]()
+ package var linearGradients = [LinearGradient]()
+ package var radialGradients = [RadialGradient]()
+ package var masks = [Mask]()
+ package var patterns = [Pattern]()
+ package var filters = [Filter]()
+
+ package var elements = [String: GraphicsElement]()
+ }
+ }
+
+ struct ClipPath: ContainerElement {
+ package var id: String
+ package var childElements = [GraphicsElement]()
+ }
+
+ final class Mask: GraphicsElement, ContainerElement {
+ package var childElements = [GraphicsElement]()
+
+ init(id: String, childElements: [GraphicsElement] = []) {
+ super.init()
+ self.id = id
+ self.childElements = childElements
+ }
+ }
+
+ struct StyleSheet {
+
+ package enum Selector: Hashable, Comparable {
+ case id(String)
+ case element(String)
+ case `class`(String)
+ }
+
+ package var attributes: [Selector: PresentationAttributes] = [:]
+ }
+}
diff --git a/SwiftDraw/DOM.Switch.swift b/DOM/Sources/DOM.Switch.swift
similarity index 93%
rename from SwiftDraw/DOM.Switch.swift
rename to DOM/Sources/DOM.Switch.swift
index 5f9b717e..862f3603 100644
--- a/SwiftDraw/DOM.Switch.swift
+++ b/DOM/Sources/DOM.Switch.swift
@@ -29,8 +29,8 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
+package extension DOM {
final class Switch: GraphicsElement, ContainerElement {
- var childElements = [DOM.GraphicsElement]()
+ package var childElements = [DOM.GraphicsElement]()
}
}
diff --git a/SwiftDraw/DOM.Text.swift b/DOM/Sources/DOM.Text.swift
similarity index 81%
rename from SwiftDraw/DOM.Text.swift
rename to DOM/Sources/DOM.Text.swift
index 26614bfa..a6aa5675 100644
--- a/SwiftDraw/DOM.Text.swift
+++ b/DOM/Sources/DOM.Text.swift
@@ -31,14 +31,14 @@
import Foundation
-extension DOM {
-
+package extension DOM {
+
final class Text: GraphicsElement {
- var x: Coordinate?
- var y: Coordinate?
- var value: String
-
- init(x: Coordinate? = nil, y: Coordinate? = nil, value: String) {
+ package var x: Coordinate?
+ package var y: Coordinate?
+ package var value: String
+
+ package init(x: Coordinate? = nil, y: Coordinate? = nil, value: String) {
self.x = x
self.y = y
self.value = value
@@ -46,7 +46,7 @@ extension DOM {
}
final class Anchor: GraphicsElement, ContainerElement {
- var href: URL?
- var childElements = [GraphicsElement]()
+ package var href: URL?
+ package var childElements = [GraphicsElement]()
}
}
diff --git a/DOM/Sources/DOM.Use.swift b/DOM/Sources/DOM.Use.swift
new file mode 100644
index 00000000..c116b0f4
--- /dev/null
+++ b/DOM/Sources/DOM.Use.swift
@@ -0,0 +1,70 @@
+//
+// DOM.Use.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 27/2/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+package extension DOM {
+ final class Use: GraphicsElement {
+ package var x: Coordinate?
+ package var y: Coordinate?
+
+ //references element ids within defs
+ package var href: URL
+
+ package init(href: URL) {
+ self.href = href
+ }
+ }
+}
+
+package extension DOM.SVG {
+
+ func firstGraphicsElement(with id: String) -> DOM.GraphicsElement? {
+ if let def = defs.elements[id] {
+ return def
+ }
+
+ return childElements.firstGraphicsElement(with: id)
+ }
+}
+
+package extension Array {
+
+ func firstGraphicsElement(with id: String) -> DOM.GraphicsElement? {
+ for element in self {
+ if element.id == id {
+ return element
+ }
+ if let container = element as? any ContainerElement {
+ return container.childElements.firstGraphicsElement(with: id)
+ }
+ }
+ return nil
+ }
+}
diff --git a/SwiftDraw/DOM.swift b/DOM/Sources/DOM.swift
similarity index 60%
rename from SwiftDraw/DOM.swift
rename to DOM/Sources/DOM.swift
index cc4764ab..bb9249f1 100644
--- a/SwiftDraw/DOM.swift
+++ b/DOM/Sources/DOM.swift
@@ -31,9 +31,9 @@
import Foundation
-public enum DOM { /* namespace */ }
+package enum DOM { /* namespace */ }
-public extension DOM {
+package extension DOM {
typealias Float = Swift.Float
typealias Coordinate = Swift.Float
typealias Length = Swift.Int
@@ -43,21 +43,21 @@ public extension DOM {
}
extension DOM {
- struct Point: Equatable {
- var x: Coordinate
- var y: Coordinate
-
- init(_ x: Coordinate, _ y: Coordinate) {
+ package struct Point: Equatable {
+ package var x: Coordinate
+ package var y: Coordinate
+
+ package init(_ x: Coordinate, _ y: Coordinate) {
self.x = x
self.y = y
}
}
- enum Fill: Equatable {
+ package enum Fill: Equatable {
case url(URL)
case color(DOM.Color)
- func getColor() throws -> DOM.Color {
+ package func getColor() throws -> DOM.Color {
switch self {
case .url:
throw Error.missing("Color")
@@ -67,35 +67,36 @@ extension DOM {
}
}
- enum FillRule: String {
+ package enum FillRule: String {
case nonzero
case evenodd
}
- enum DisplayMode: String {
+ package enum DisplayMode: String {
case none
case inline
+ case block
}
- enum LineCap: String {
+ package enum LineCap: String {
case butt
case round
case square
}
- enum LineJoin: String {
+ package enum LineJoin: String {
case miter
case round
case bevel
}
- enum TextAnchor: String {
+ package enum TextAnchor: String {
case start
case middle
case end
}
- enum Transform: Equatable {
+ package enum Transform: Equatable {
case matrix(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float)
case translate(tx: Float, ty: Float)
case scale(sx: Float, sy: Float)
@@ -104,8 +105,55 @@ extension DOM {
case skewX(angle: Float)
case skewY(angle: Float)
}
-
- enum Error: Swift.Error {
+
+ package enum Unit {
+ case pixel
+ case inch
+ case centimeter
+ case millimeter
+ case point
+ case pica
+ }
+
+ package enum Error: Swift.Error {
case missing(String)
}
}
+
+package extension DOM.Unit {
+ var rawValue: String {
+ switch self {
+ case .pixel:
+ return "px"
+ case .inch:
+ return "in"
+ case .centimeter:
+ return "cm"
+ case .millimeter:
+ return "mm"
+ case .point:
+ return "pt"
+ case .pica:
+ return "pc"
+ }
+ }
+}
+
+package extension Double {
+ func apply(unit: DOM.Unit) -> Double {
+ switch unit {
+ case .pixel:
+ return self
+ case .inch:
+ return self * 96
+ case .centimeter:
+ return self * 37.795
+ case .millimeter:
+ return self * 3.7795
+ case .point:
+ return self * 1.3333
+ case .pica:
+ return self * 16
+ }
+ }
+}
diff --git a/SwiftDraw/Parser.XML.Attributes.swift b/DOM/Sources/Parser.XML.Attributes.swift
similarity index 98%
rename from SwiftDraw/Parser.XML.Attributes.swift
rename to DOM/Sources/Parser.XML.Attributes.swift
index a383e328..f50f9686 100644
--- a/SwiftDraw/Parser.XML.Attributes.swift
+++ b/DOM/Sources/Parser.XML.Attributes.swift
@@ -39,13 +39,13 @@ extension XMLParser {
// attributes["fill"] == "red"
final class Attributes: AttributeParser {
- let parser: AttributeValueParser
+ let parser: any AttributeValueParser
let options: XMLParser.Options
let element: [String: String]
let style: [String: String]
- init(parser: AttributeValueParser,
+ init(parser: any AttributeValueParser,
options: XMLParser.Options = [],
element: [String: String],
style: [String: String]) {
diff --git a/SwiftDraw/Parser.XML.Color.swift b/DOM/Sources/Parser.XML.Color.swift
similarity index 78%
rename from SwiftDraw/Parser.XML.Color.swift
rename to DOM/Sources/Parser.XML.Color.swift
index cbc1e39a..317f2dc9 100644
--- a/SwiftDraw/Parser.XML.Color.swift
+++ b/DOM/Sources/Parser.XML.Color.swift
@@ -49,6 +49,8 @@ extension XMLParser {
return .color(c)
} else if let url = try parseURLSelector(data: data) {
return .url(url)
+ } else if let c = try parseColorRGBA(data: data) {
+ return .color(c)
}
throw Error.invalid
@@ -89,6 +91,17 @@ extension XMLParser {
return try parseColorRGBi(data: data)
}
+ private func parseColorRGBA(data: String) throws -> DOM.Color? {
+ var scanner = XMLParser.Scanner(text: data)
+ guard scanner.scanStringIfPossible("rgba(") else { return nil }
+
+ if let c = try? parseColorRGBAf(data: data) {
+ return c
+ }
+
+ return try parseColorRGBAi(data: data)
+ }
+
private func parseURLSelector(data: String) throws -> DOM.URL? {
var scanner = XMLParser.Scanner(text: data)
guard (try? scanner.scanString("url(")) == true else {
@@ -107,33 +120,66 @@ extension XMLParser {
return url
}
- private func parseColorRGBi(data: String) throws -> DOM.Color {
+ private func parseIntColor(data: String, requireAlpha: Bool) throws -> DOM.Color {
var scanner = XMLParser.Scanner(text: data)
- try scanner.scanString("rgb(")
-
+ try scanner.scanString(requireAlpha ? "rgba(" : "rgb(")
+
let r = try scanner.scanUInt8()
scanner.scanStringIfPossible(",")
let g = try scanner.scanUInt8()
scanner.scanStringIfPossible(",")
let b = try scanner.scanUInt8()
+ var a: Float = 1.0
+
+ if requireAlpha {
+ scanner.scanStringIfPossible(",")
+ a = try scanner.scanAlpha()
+ } else if scanner.scanStringIfPossible(",") {
+ a = try scanner.scanAlpha()
+ }
+
try scanner.scanString(")")
- return .rgbi(r, g, b)
+ return .rgbi(r, g, b, min(1, a))
}
- private func parseColorRGBf(data: String) throws -> DOM.Color {
+ private func parseColorRGBi(data: String) throws -> DOM.Color {
+ return try parseIntColor(data: data, requireAlpha: false)
+ }
+
+ private func parseColorRGBAi(data: String) throws -> DOM.Color {
+ return try parseIntColor(data: data, requireAlpha: true)
+ }
+
+ private func parsePercentageColor(data: String, withAlpha: Bool) throws -> DOM.Color {
var scanner = XMLParser.Scanner(text: data)
- try scanner.scanString("rgb(")
+ try scanner.scanString(withAlpha ? "rgba(" : "rgb(")
let r = try scanner.scanPercentage()
scanner.scanStringIfPossible(",")
let g = try scanner.scanPercentage()
scanner.scanStringIfPossible(",")
let b = try scanner.scanPercentage()
+
+ var a: Float = 1.0
+ if withAlpha {
+ scanner.scanStringIfPossible(",")
+ a = try scanner.scanFloat() // Opacity
+ }
+
try scanner.scanString(")")
- return .rgbf(r, g, b)
+ return .rgbf(r, g, b, a)
+ }
+
+ private func parseColorRGBf(data: String) throws -> DOM.Color {
+ return try parsePercentageColor(data: data, withAlpha: false)
}
+ private func parseColorRGBAf(data: String) throws -> DOM.Color {
+ return try parsePercentageColor(data: data, withAlpha: true)
+ }
+
+
private func parseColorP3(data: String) throws -> DOM.Color? {
var scanner = XMLParser.Scanner(text: data)
guard scanner.scanStringIfPossible("color(display-p3") else { return nil }
diff --git a/SwiftDraw/Parser.XML.Element.swift b/DOM/Sources/Parser.XML.Element.swift
similarity index 85%
rename from SwiftDraw/Parser.XML.Element.swift
rename to DOM/Sources/Parser.XML.Element.swift
index 45bd019c..8cdc40e5 100644
--- a/SwiftDraw/Parser.XML.Element.swift
+++ b/DOM/Sources/Parser.XML.Element.swift
@@ -31,7 +31,7 @@
extension XMLParser {
- func parseLine(_ att: AttributeParser) throws -> DOM.Line {
+ func parseLine(_ att: any AttributeParser) throws -> DOM.Line {
let x1: DOM.Coordinate = try att.parseCoordinate("x1")
let y1: DOM.Coordinate = try att.parseCoordinate("y1")
let x2: DOM.Coordinate = try att.parseCoordinate("x2")
@@ -39,14 +39,14 @@ extension XMLParser {
return DOM.Line(x1: x1, y1: y1, x2: x2, y2: y2)
}
- func parseCircle(_ att: AttributeParser) throws -> DOM.Circle {
+ func parseCircle(_ att: any AttributeParser) throws -> DOM.Circle {
let cx: DOM.Coordinate? = try att.parseCoordinate("cx")
let cy: DOM.Coordinate? = try att.parseCoordinate("cy")
let r: DOM.Coordinate = try att.parseCoordinate("r")
return DOM.Circle(cx: cx, cy: cy, r: r)
}
- func parseEllipse(_ att: AttributeParser) throws -> DOM.Ellipse {
+ func parseEllipse(_ att: any AttributeParser) throws -> DOM.Ellipse {
let cx: DOM.Coordinate? = try att.parseCoordinate("cx")
let cy: DOM.Coordinate? = try att.parseCoordinate("cy")
let rx: DOM.Coordinate = try att.parseCoordinate("rx")
@@ -54,7 +54,7 @@ extension XMLParser {
return DOM.Ellipse(cx: cx, cy: cy, rx: rx, ry: ry)
}
- func parseRect(_ att: AttributeParser) throws -> DOM.Rect {
+ func parseRect(_ att: any AttributeParser) throws -> DOM.Rect {
let width: DOM.Coordinate = try att.parseCoordinate("width")
let height: DOM.Coordinate = try att.parseCoordinate("height")
let rect = DOM.Rect(width: width, height: height)
@@ -67,11 +67,11 @@ extension XMLParser {
return rect
}
- func parsePolyline(_ att: AttributeParser) throws -> DOM.Polyline {
+ func parsePolyline(_ att: any AttributeParser) throws -> DOM.Polyline {
return DOM.Polyline(points: try att.parsePoints("points"))
}
- func parsePolygon(_ att: AttributeParser) throws -> DOM.Polygon {
+ func parsePolygon(_ att: any AttributeParser) throws -> DOM.Polygon {
return DOM.Polygon(points: try att.parsePoints("points"))
}
@@ -98,6 +98,7 @@ extension XMLParser {
case "use": ge = try parseUse(att)
case "switch": ge = try parseSwitch(e)
case "image": ge = try parseImage(att)
+ case "svg": ge = try parseSVG(e)
default: return nil
}
@@ -110,36 +111,33 @@ extension XMLParser {
return ge
}
- func parseContainerChildren(_ e: XML.Element) throws -> [DOM.GraphicsElement] {
- guard e.name == "svg" ||
- e.name == "clipPath" ||
- e.name == "pattern" ||
- e.name == "mask" ||
- e.name == "defs" ||
- e.name == "switch" ||
- e.name == "g" ||
- e.name == "a" else {
- throw Error.invalid
- }
+ func parseGraphicsElements(_ elements: [XML.Element]) throws -> [DOM.GraphicsElement] {
+ var result = [DOM.GraphicsElement]()
+ var stack: [(XML.Element, parent: (any ContainerElement)?)] = elements
+ .reversed()
+ .map { ($0, parent: nil) }
+
+ while let (element, parent) = stack.popLast() {
+ guard let ge = try parseGraphicsElement(element) else {
+ continue
+ }
- var children = [DOM.GraphicsElement]()
-
- for n in e.children {
- do {
- if let ge = try parseGraphicsElement(n) {
- children.append(ge)
- }
- } catch let error {
- if let parseError = parseError(for: error, parsing: n, with: options) {
- throw parseError
- }
+ if var parent {
+ parent.childElements.append(ge)
+ } else {
+ result.append(ge)
+ }
+
+ if let container = ge as? any ContainerElement {
+ stack.append(contentsOf: element.children.reversed().map { ($0, container) })
}
+
}
- return children
+ return result
}
- func parseError(for error: Swift.Error, parsing element: XML.Element, with options: Options) -> XMLParser.Error? {
+ func parseError(for error: any Swift.Error, parsing element: XML.Element, with options: Options) -> XMLParser.Error? {
guard options.contains(.skipInvalidElements) == false else {
Self.logParsingError(for: error, filename: filename, parsing: element)
return nil
@@ -164,9 +162,7 @@ extension XMLParser {
throw Error.invalid
}
- let group = DOM.Group()
- group.childElements = try parseContainerChildren(e)
- return group
+ return DOM.Group()
}
func parseSwitch(_ e: XML.Element) throws -> DOM.Switch {
@@ -174,9 +170,7 @@ extension XMLParser {
throw Error.invalid
}
- let node = DOM.Switch()
- node.childElements = try parseContainerChildren(e)
- return node
+ return DOM.Switch()
}
func parseAttributes(_ e: XML.Element) throws -> Attributes {
@@ -230,7 +224,7 @@ extension XMLParser {
value.trimmingCharacters(in: .whitespaces))
}
- func parsePresentationAttributes(_ att: AttributeParser) throws -> DOM.PresentationAttributes {
+ func parsePresentationAttributes(_ att: any AttributeParser) throws -> DOM.PresentationAttributes {
var el = DOM.PresentationAttributes()
el.opacity = try att.parsePercentage("opacity")
@@ -272,7 +266,7 @@ extension XMLParser {
return el
}
- func parseElementAttributes(_ att: AttributeParser) throws -> ElementAttributes {
+ func parseElementAttributes(_ att: any AttributeParser) throws -> any ElementAttributes {
var el = ElementAtt()
el.id = try? att.parseString("id")
el.class = try? att.parseString("class")
@@ -284,7 +278,7 @@ extension XMLParser {
var `class`: String?
}
- static func logParsingError(for error: Swift.Error, filename: String?, parsing element: XML.Element? = nil) {
+ package static func logParsingError(for error: any Swift.Error, filename: String?, parsing element: XML.Element? = nil) {
let elementName = element.map { "<\($0.name)>" } ?? ""
let filename = filename ?? ""
switch error {
diff --git a/SwiftDraw/Parser.XML.Filter.swift b/DOM/Sources/Parser.XML.Filter.swift
similarity index 94%
rename from SwiftDraw/Parser.XML.Filter.swift
rename to DOM/Sources/Parser.XML.Filter.swift
index 1b0c8eda..c0dc52ee 100644
--- a/SwiftDraw/Parser.XML.Filter.swift
+++ b/DOM/Sources/Parser.XML.Filter.swift
@@ -49,7 +49,7 @@ extension XMLParser {
throw Error.invalid
}
- let nodeAtt: AttributeParser = try parseAttributes(e)
+ let nodeAtt: any AttributeParser = try parseAttributes(e)
let node = DOM.Filter(id: try nodeAtt.parseString("id"))
for n in e.children {
@@ -64,7 +64,7 @@ extension XMLParser {
func parseEffect(_ e: XML.Element) throws -> DOM.Filter.Effect? {
switch e.name {
case "feGaussianBlur":
- let att: AttributeParser = try parseAttributes(e)
+ let att: any AttributeParser = try parseAttributes(e)
return try .gaussianBlur(stdDeviation: att.parseFloat("stdDeviation"))
default:
return nil
diff --git a/SwiftDraw/Parser.XML.Image.swift b/DOM/Sources/Parser.XML.Image.swift
similarity index 76%
rename from SwiftDraw/Parser.XML.Image.swift
rename to DOM/Sources/Parser.XML.Image.swift
index be78bbd7..756a2337 100644
--- a/SwiftDraw/Parser.XML.Image.swift
+++ b/DOM/Sources/Parser.XML.Image.swift
@@ -31,15 +31,15 @@
extension XMLParser {
- func parseImage(_ att: AttributeParser) throws -> DOM.Image {
+ func parseImage(_ att: any AttributeParser) throws -> DOM.Image {
let href: DOM.URL = try att.parseUrl("xlink:href")
- let width: DOM.Coordinate = try att.parseCoordinate("width")
- let height: DOM.Coordinate = try att.parseCoordinate("height")
-
- let use = DOM.Image(href: href, width: width, height: height)
- use.x = try att.parseCoordinate("x")
- use.y = try att.parseCoordinate("y")
-
- return use
+
+ let image = DOM.Image(href: href)
+ image.x = try att.parseCoordinate("x")
+ image.y = try att.parseCoordinate("y")
+ image.width = try att.parseCoordinate("width")
+ image.height = try att.parseCoordinate("height")
+
+ return image
}
}
diff --git a/SwiftDraw/Parser.XML.LinearGradient.swift b/DOM/Sources/Parser.XML.LinearGradient.swift
similarity index 92%
rename from SwiftDraw/Parser.XML.LinearGradient.swift
rename to DOM/Sources/Parser.XML.LinearGradient.swift
index bb29c84a..b0b8a5fa 100644
--- a/SwiftDraw/Parser.XML.LinearGradient.swift
+++ b/DOM/Sources/Parser.XML.LinearGradient.swift
@@ -49,7 +49,7 @@ extension XMLParser {
throw Error.invalid
}
- let nodeAtt: AttributeParser = try parseAttributes(e)
+ let nodeAtt: any AttributeParser = try parseAttributes(e)
let node = DOM.LinearGradient(id: try nodeAtt.parseString("id"))
node.x1 = try nodeAtt.parseCoordinate("x1")
node.y1 = try nodeAtt.parseCoordinate("y1")
@@ -57,7 +57,7 @@ extension XMLParser {
node.y2 = try nodeAtt.parseCoordinate("y2")
for n in e.children where n.name == "stop" {
- let att: AttributeParser = try parseAttributes(n)
+ let att: any AttributeParser = try parseAttributes(n)
node.stops.append(try parseLinearGradientStop(att))
}
@@ -71,7 +71,7 @@ extension XMLParser {
return node
}
- func parseLinearGradientStop(_ att: AttributeParser) throws -> DOM.LinearGradient.Stop {
+ func parseLinearGradientStop(_ att: any AttributeParser) throws -> DOM.LinearGradient.Stop {
let offset: DOM.Float? = try? att.parsePercentage("offset")
let color: DOM.Color? = try? att.parseFill("stop-color").getColor()
let opacity: DOM.Float? = try att.parsePercentage("stop-opacity")
diff --git a/DOM/Sources/Parser.XML.Path.swift b/DOM/Sources/Parser.XML.Path.swift
new file mode 100644
index 00000000..a0537876
--- /dev/null
+++ b/DOM/Sources/Parser.XML.Path.swift
@@ -0,0 +1,234 @@
+//
+// Parser.XML.Path.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+
+package extension XMLParser {
+
+ typealias PathScanner = Foundation.Scanner
+
+ typealias Segment = DOM.Path.Segment
+ typealias Command = DOM.Path.Command
+ typealias CoordinateSpace = DOM.Path.Segment.CoordinateSpace
+
+ func parsePath(_ att: AttributeParser) throws -> DOM.Path {
+ return try parsePath(from: att.parseString("d"))
+ }
+
+ func parsePath(from data: String) throws -> DOM.Path {
+ let path = DOM.Path(x: 0, y: 0)
+ path.segments = try parsePathSegments(data)
+ return path
+ }
+
+ func parsePathSegments(_ data: String) throws -> [Segment] {
+ var segments = Array()
+
+ var scanner = PathScanner(string: data)
+
+ scanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines
+
+ guard !scanner.isAtEnd else {
+ return []
+ }
+
+ var lastCommand: Command?
+
+ repeat {
+ guard let cmd = nextPathCommand(&scanner, lastCommand: lastCommand) else {
+ throw Error.invalid
+ }
+ lastCommand = cmd
+ segments.append(try parsePathSegment(for: cmd, with: &scanner))
+ } while !scanner.isAtEnd
+
+ return segments
+ }
+
+ func nextPathCommand(_ scanner: inout PathScanner, lastCommand: Command?) -> Command? {
+ if let cmd = parseCommand(&scanner) {
+ return cmd
+ }
+
+ switch lastCommand {
+ case .some(.move):
+ return .line
+ case .some(.moveRelative):
+ return .lineRelative
+ default:
+ return lastCommand
+ }
+ }
+
+
+ func parsePathSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ switch command {
+ case .move, .moveRelative:
+ return try parseMoveSegment(for: command, with: &scanner)
+ case .line, .lineRelative:
+ return try parseLineSegment(for: command, with: &scanner)
+ case .horizontal, .horizontalRelative:
+ return try parseHorizontalSegment(for: command, with: &scanner)
+ case .vertical, .verticalRelative:
+ return try parseVerticalSegment(for: command, with: &scanner)
+ case .cubic, .cubicRelative:
+ return try parseCubicSegment(for: command, with: &scanner)
+ case .cubicSmooth, .cubicSmoothRelative:
+ return try parseCubicSmoothSegment(for: command, with: &scanner)
+ case .quadratic, .quadraticRelative:
+ return try parseQuadraticSegment(for: command, with: &scanner)
+ case .quadraticSmooth, .quadraticSmoothRelative:
+ return try parseQuadraticSmoothSegment(for: command, with: &scanner)
+ case .arc, .arcRelative:
+ return try parseArcSegment(for: command, with: &scanner)
+ case .close, .closeAlias:
+ return .close
+ }
+ }
+
+ func parseCommand(_ scanner: inout PathScanner) -> Command? {
+ guard let char = scanner.scan(first: .commands),
+ let command = Command(rawValue: char) else {
+ return nil
+ }
+ return command
+ }
+
+ func parseMoveSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .move(x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseLineSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .line(x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseHorizontalSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .horizontal(x: x, space: command.coordinateSpace)
+ }
+
+ func parseVerticalSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .vertical(y: y, space: command.coordinateSpace)
+ }
+
+ func parseCubicSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x1 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y1 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let x2 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y2 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .cubic(x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseCubicSmoothSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x2 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y2 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .cubicSmooth(x2: x2, y2: y2, x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseQuadraticSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x1 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y1 = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .quadratic(x1: x1, y1: y1, x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseQuadraticSmoothSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .quadraticSmooth(x: x, y: y, space: command.coordinateSpace)
+ }
+
+ func parseArcSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
+ let rx = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let ry = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let rotate = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let large = try scanner.scanBool()
+ _ = scanner.scan(first: .delimeter)
+ let sweep = try scanner.scanBool()
+ _ = scanner.scan(first: .delimeter)
+ let x = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+ let y = try scanner.scanCoordinate()
+ _ = scanner.scan(first: .delimeter)
+
+ return .arc(rx: rx, ry: ry, rotate: rotate,
+ large: large, sweep: sweep,
+ x: x, y: y, space: command.coordinateSpace)
+ }
+}
+
+private extension CharacterSet {
+ static let delimeter = CharacterSet(charactersIn: ",;")
+ static let commands = CharacterSet(charactersIn: "MmLlHhVvCcSsQqTtAaZz")
+}
diff --git a/SwiftDraw/Parser.XML.Pattern.swift b/DOM/Sources/Parser.XML.Pattern.swift
similarity index 95%
rename from SwiftDraw/Parser.XML.Pattern.swift
rename to DOM/Sources/Parser.XML.Pattern.swift
index f8488a6a..1dfe4f20 100644
--- a/SwiftDraw/Parser.XML.Pattern.swift
+++ b/DOM/Sources/Parser.XML.Pattern.swift
@@ -34,7 +34,7 @@ import Foundation
extension XMLParser {
- func parsePattern(_ att: AttributeParser) throws -> DOM.Pattern {
+ func parsePattern(_ att: any AttributeParser) throws -> DOM.Pattern {
let id: String = try att.parseString("id")
let width: DOM.Coordinate = try att.parseCoordinate("width")
diff --git a/SwiftDraw/Parser.XML.RadialGradient.swift b/DOM/Sources/Parser.XML.RadialGradient.swift
similarity index 92%
rename from SwiftDraw/Parser.XML.RadialGradient.swift
rename to DOM/Sources/Parser.XML.RadialGradient.swift
index 371b3f05..92a370b8 100644
--- a/SwiftDraw/Parser.XML.RadialGradient.swift
+++ b/DOM/Sources/Parser.XML.RadialGradient.swift
@@ -49,7 +49,7 @@ extension XMLParser {
throw Error.invalid
}
- let nodeAtt: AttributeParser = try parseAttributes(e)
+ let nodeAtt: any AttributeParser = try parseAttributes(e)
let node = DOM.RadialGradient(id: try nodeAtt.parseString("id"))
node.r = try? nodeAtt.parseCoordinate("r")
node.cx = try? nodeAtt.parseCoordinate("cx")
@@ -59,7 +59,7 @@ extension XMLParser {
node.fy = try? nodeAtt.parseCoordinate("fy")
for n in e.children where n.name == "stop" {
- let att: AttributeParser = try parseAttributes(n)
+ let att: any AttributeParser = try parseAttributes(n)
node.stops.append(try parseRadialGradientStop(att))
}
@@ -73,7 +73,7 @@ extension XMLParser {
return node
}
- func parseRadialGradientStop(_ att: AttributeParser) throws -> DOM.RadialGradient.Stop {
+ func parseRadialGradientStop(_ att: any AttributeParser) throws -> DOM.RadialGradient.Stop {
let offset: DOM.Float? = try? att.parsePercentage("offset")
let color: DOM.Color? = try? att.parseFill("stop-color").getColor()
let opacity: DOM.Float? = try att.parsePercentage("stop-opacity")
diff --git a/SwiftDraw/Parser.XML.SVG.swift b/DOM/Sources/Parser.XML.SVG.swift
similarity index 90%
rename from SwiftDraw/Parser.XML.SVG.swift
rename to DOM/Sources/Parser.XML.SVG.swift
index f78046f1..2a57a364 100644
--- a/SwiftDraw/Parser.XML.SVG.swift
+++ b/DOM/Sources/Parser.XML.SVG.swift
@@ -29,7 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension XMLParser {
+package extension XMLParser {
func parseSVG(_ e: XML.Element) throws -> DOM.SVG {
guard e.name == "svg" else {
@@ -56,7 +56,9 @@ extension XMLParser {
guard let h = height else { throw XMLParser.Error.missingAttribute(name: "height") }
let svg = DOM.SVG(width: DOM.Length(w), height: DOM.Length(h))
- svg.childElements = try parseContainerChildren(e)
+ svg.x = try att.parseCoordinate("x")
+ svg.y = try att.parseCoordinate("y")
+ svg.childElements = try parseGraphicsElements(e.children)
svg.viewBox = try parseViewBox(try att.parseString("viewBox"))
svg.defs = try parseSVGDefs(e)
@@ -119,7 +121,7 @@ extension XMLParser {
}
var defs = Dictionary()
- let elements = try parseContainerChildren(e)
+ let elements = try parseGraphicsElements(e.children)
for e in elements {
guard let id = e.id else {
@@ -151,7 +153,7 @@ extension XMLParser {
let att = try parseAttributes(e)
let id: String = try att.parseString("id")
- let children = try parseContainerChildren(e)
+ let children = try parseGraphicsElements(e.children)
return DOM.ClipPath(id: id, childElements: children)
}
@@ -174,8 +176,12 @@ extension XMLParser {
let att = try parseAttributes(e)
let id: String = try att.parseString("id")
- let children = try parseContainerChildren(e)
- return DOM.Mask(id: id, childElements: children)
+ let mask = DOM.Mask(id: id)
+ mask.class = try att.parseString("class")
+ mask.attributes = try parsePresentationAttributes(e)
+ mask.style = try parseStyleAttributes(e)
+ mask.childElements = try parseGraphicsElements(e.children)
+ return mask
}
func parsePatterns(_ e: XML.Element) throws -> [DOM.Pattern] {
@@ -196,7 +202,7 @@ extension XMLParser {
let att = try parseAttributes(e)
var pattern = try parsePattern(att)
- pattern.childElements = try parseContainerChildren(e)
+ pattern.childElements = try parseGraphicsElements(e.children)
return pattern
}
}
diff --git a/SwiftDraw/Parser.XML.Scanner.swift b/DOM/Sources/Parser.XML.Scanner.swift
similarity index 70%
rename from SwiftDraw/Parser.XML.Scanner.swift
rename to DOM/Sources/Parser.XML.Scanner.swift
index caa56e94..0bebd023 100644
--- a/SwiftDraw/Parser.XML.Scanner.swift
+++ b/DOM/Sources/Parser.XML.Scanner.swift
@@ -31,32 +31,39 @@
import Foundation
-extension XMLParser {
-
+package extension XMLParser {
+
struct Scanner {
private let scanner: Foundation.Scanner
- var currentIndex: String.Index
+ package var currentIndex: String.Index
- init(text: String) {
+ package init(text: String) {
self.scanner = Foundation.Scanner(string: text)
self.currentIndex = self.scanner.currentIndex
self.scanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines
}
- var isEOF: Bool { return scanner.isAtEnd }
-
+ package var isEOF: Bool { return scanner.isAtEnd }
+
@discardableResult
- mutating func scanString(_ token: String) throws -> Bool {
+ package mutating func scanString(_ token: String) throws -> Bool {
return try self.scanString(matchingAny: [token]) == token
}
@discardableResult
- mutating func scanStringIfPossible(_ token: String) -> Bool {
+ package mutating func scanStringIfPossible(_ token: String) -> Bool {
return (try? self.scanString(token)) == true
}
-
- mutating func scanString(matchingAny tokens: Set) throws -> String {
+
+ @discardableResult
+ package mutating func nextScanString(_ token: String) -> Bool {
+ scanner.currentIndex = currentIndex
+ defer { scanner.currentIndex = currentIndex }
+ return scanStringIfPossible(token)
+ }
+
+ package mutating func scanString(matchingAny tokens: Set) throws -> String {
scanner.currentIndex = currentIndex
guard let match = tokens.first(where: { scanner.scanString($0) != nil }) else {
throw Error.invalid
@@ -65,7 +72,7 @@ extension XMLParser {
return match
}
- mutating func scanCase(from type: T.Type) throws -> T where T.RawValue == String {
+ package mutating func scanCase(from type: T.Type) throws -> T where T.RawValue == String {
scanner.currentIndex = currentIndex
guard let match = type.allCases.first(where: { scanner.scanString($0.rawValue) != nil }) else {
@@ -75,7 +82,7 @@ extension XMLParser {
return match
}
- mutating func scanString(matchingAny characters: Foundation.CharacterSet) throws -> String {
+ package mutating func scanString(matchingAny characters: Foundation.CharacterSet) throws -> String {
scanner.currentIndex = currentIndex
guard
let match = scanner.scanCharacters(from: characters),
@@ -87,7 +94,7 @@ extension XMLParser {
return match
}
- mutating func doScanString(_ string: String) -> Bool {
+ package mutating func doScanString(_ string: String) -> Bool {
scanner.currentIndex = currentIndex
guard scanner.scanString(string) != nil else {
return false
@@ -96,7 +103,7 @@ extension XMLParser {
return true
}
- mutating func scanString(upTo token: String) throws -> String {
+ package mutating func scanString(upTo token: String) throws -> String {
scanner.currentIndex = currentIndex
guard let match = scanner.scanUpToString(token) else {
throw Error.invalid
@@ -106,7 +113,7 @@ extension XMLParser {
return match
}
- mutating func scanString(upTo characters: Foundation.CharacterSet) throws -> String {
+ package mutating func scanString(upTo characters: Foundation.CharacterSet) throws -> String {
let location = currentIndex
guard let match = scanner.scanUpToCharacters(from: characters) else {
scanner.currentIndex = location
@@ -116,7 +123,7 @@ extension XMLParser {
return match
}
- mutating func scanCharacter(matchingAny characters: Foundation.CharacterSet) throws -> Character {
+ package mutating func scanCharacter(matchingAny characters: Foundation.CharacterSet) throws -> Character {
let location = currentIndex
guard let scalar = scanner.scan(first: characters) else {
scanner.currentIndex = location
@@ -126,7 +133,7 @@ extension XMLParser {
return Character(scalar)
}
- mutating func scanUInt8() throws -> UInt8 {
+ package mutating func scanUInt8() throws -> UInt8 {
scanner.currentIndex = currentIndex
var longVal: UInt64 = 0
guard
@@ -138,7 +145,7 @@ extension XMLParser {
return val
}
- mutating func scanFloat() throws -> Float {
+ package mutating func scanFloat() throws -> Float {
scanner.currentIndex = currentIndex
guard let val = scanner.scanFloat() else {
throw Error.invalid
@@ -147,7 +154,7 @@ extension XMLParser {
return val
}
- mutating func scanDouble() throws -> Double {
+ package mutating func scanDouble() throws -> Double {
scanner.currentIndex = currentIndex
guard let val = scanner.scanDouble() else {
throw Error.invalid
@@ -155,8 +162,35 @@ extension XMLParser {
currentIndex = scanner.currentIndex
return val
}
-
- mutating func scanLength() throws -> DOM.Length {
+
+ package mutating func scanUnit(_ unit: DOM.Unit) -> Bool {
+ scanner.currentIndex = currentIndex
+ guard scanner.scanString(unit.rawValue) != nil else {
+ return false
+ }
+ currentIndex = scanner.currentIndex
+ return true
+ }
+
+ package mutating func scanUnit() -> DOM.Unit? {
+ if scanUnit(.pixel) {
+ return .pixel
+ } else if scanUnit(.inch) {
+ return .inch
+ } else if scanUnit(.centimeter) {
+ return .centimeter
+ } else if scanUnit(.millimeter) {
+ return .millimeter
+ } else if scanUnit(.point) {
+ return .point
+ } else if scanUnit(.pica) {
+ return .pica
+ } else {
+ return nil
+ }
+ }
+
+ package mutating func scanLength() throws -> DOM.Length {
scanner.currentIndex = currentIndex
guard
let int64 = scanner.scanInt64(),
@@ -168,15 +202,17 @@ extension XMLParser {
return val
}
- mutating func scanBool() throws -> Bool {
+ package mutating func scanBool() throws -> Bool {
return try self.scanCase(from: Boolean.self).boolValue
}
- mutating func scanCoordinate() throws -> DOM.Coordinate {
- return DOM.Coordinate(try scanDouble())
+ package mutating func scanCoordinate() throws -> DOM.Coordinate {
+ let double = try scanDouble()
+ let unit = scanUnit() ?? .pixel
+ return DOM.Coordinate(double.apply(unit: unit))
}
- mutating func scanPercentageFloat() throws -> Float {
+ package mutating func scanPercentageFloat() throws -> Float {
scanner.currentIndex = currentIndex
let val = try scanFloat()
guard val >= 0.0, val <= 1.0 else {
@@ -185,8 +221,16 @@ extension XMLParser {
currentIndex = scanner.currentIndex
return val
}
-
- mutating func scanPercentage() throws -> Float {
+
+ package mutating func scanAlpha() throws -> Float {
+ if let pc = try? scanPercentage() {
+ return pc
+ } else {
+ return try scanFloat()
+ }
+ }
+
+ package mutating func scanPercentage() throws -> Float {
let initialLocation = currentIndex
scanner.currentIndex = currentIndex
@@ -224,7 +268,7 @@ private enum Boolean: String, CaseIterable {
}
}
-extension Scanner {
+package extension Scanner {
enum Error: Swift.Error {
case invalid
diff --git a/SwiftDraw/Parser.XML.StyleSheet.swift b/DOM/Sources/Parser.XML.StyleSheet.swift
similarity index 72%
rename from SwiftDraw/Parser.XML.StyleSheet.swift
rename to DOM/Sources/Parser.XML.StyleSheet.swift
index 092c4d6a..1a2b58e0 100644
--- a/SwiftDraw/Parser.XML.StyleSheet.swift
+++ b/DOM/Sources/Parser.XML.StyleSheet.swift
@@ -71,13 +71,15 @@ extension XMLParser {
var scanner = XMLParser.Scanner(text: removeCSSComments(from: text))
var entries = [DOM.StyleSheet.Selector: [String: String]]()
- var last: (DOM.StyleSheet.Selector, [String: String])?
- repeat {
- last = try scanner.scanNextSelector()
- if let last = last {
- entries[last.0] = last.1
+ while let (selectors, attributes) = try scanner.scanNextSelectorDecl() {
+ for selector in selectors {
+ var copy = entries[selector] ?? [:]
+ for (key, value) in attributes {
+ copy[key] = value
+ }
+ entries[selector] = copy
}
- } while last != nil
+ }
return entries
}
@@ -91,15 +93,10 @@ extension XMLParser {
extension XMLParser.Scanner {
- mutating func scanNextSelector() throws -> (DOM.StyleSheet.Selector, [String: String])? {
- if let c = try scanNextClass() {
- return (.class(c), try scanAtttributes())
- } else if let id = try scanNextID() {
- return (.id(id), try scanAtttributes())
- } else if let e = try scanNextElement() {
- return (.element(e), try scanAtttributes())
- }
- return nil
+ mutating func scanNextSelectorDecl() throws -> ([DOM.StyleSheet.Selector], [String: String])? {
+ let selectorTypes = try scanSelectorTypes()
+ guard !selectorTypes.isEmpty else { return nil }
+ return (selectorTypes, try scanAtttributes())
}
private mutating func scanNextClass() throws -> String? {
@@ -124,7 +121,30 @@ extension XMLParser.Scanner {
}
private mutating func scanSelectorName() throws -> String? {
- try scanString(upTo: "{").trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !nextScanString("{") else { return nil }
+ let name = try scanString(upTo: .init(charactersIn: "{,")).trimmingCharacters(in: .whitespacesAndNewlines)
+ scanStringIfPossible(",")
+ return name
+ }
+
+ mutating func scanSelectorTypes() throws -> [DOM.StyleSheet.Selector] {
+ var selectors: [DOM.StyleSheet.Selector] = []
+ while let next = try scanNextSelectorType() {
+ selectors.append(next)
+ }
+ return selectors
+ }
+
+ private mutating func scanNextSelectorType() throws -> DOM.StyleSheet.Selector? {
+ if let name = try scanNextClass() {
+ return .class(name)
+ } else if let name = try scanNextID() {
+ return .id(name)
+ } else if let name = try scanNextElement() {
+ return .element(name)
+ } else {
+ return nil
+ }
}
private mutating func scanAtttributes() throws -> [String: String] {
@@ -163,13 +183,13 @@ extension XMLParser.Scanner {
//Allow Dictionary to become an attribute parser
extension Dictionary: AttributeParser where Key == String, Value == String {
- var parser: AttributeValueParser { return XMLParser.ValueParser() }
- var options: XMLParser.Options { return [] }
+ package var parser: any AttributeValueParser { return XMLParser.ValueParser() }
+ package var options: XMLParser.Options { return [] }
- func parse(_ key: String, _ exp: (String) throws -> T) throws -> T {
- guard let value = self[key] else {
- throw XMLParser.Error.missingAttribute(name: key)
+ package func parse(_ key: String, _ exp: (String) throws -> T) throws -> T {
+ guard let value = self[key] else {
+ throw XMLParser.Error.missingAttribute(name: key)
+ }
+ return try exp(value)
}
- return try exp(value)
- }
}
diff --git a/SwiftDraw/Parser.XML.Text.swift b/DOM/Sources/Parser.XML.Text.swift
similarity index 83%
rename from SwiftDraw/Parser.XML.Text.swift
rename to DOM/Sources/Parser.XML.Text.swift
index 9546ab63..87e7c6c3 100644
--- a/SwiftDraw/Parser.XML.Text.swift
+++ b/DOM/Sources/Parser.XML.Text.swift
@@ -33,7 +33,7 @@ import Foundation
extension XMLParser {
- func parseText(_ att: AttributeParser, element: XML.Element) throws -> DOM.Text? {
+ func parseText(_ att: any AttributeParser, element: XML.Element) throws -> DOM.Text? {
guard
let text = element.innerText?.trimmingCharacters(in: .whitespacesAndNewlines),
!text.isEmpty else {
@@ -43,14 +43,13 @@ extension XMLParser {
return try parseText(att, value: text)
}
- func parseAnchor(_ att: AttributeParser, element: XML.Element) throws -> DOM.Anchor? {
+ func parseAnchor(_ att: any AttributeParser, element: XML.Element) throws -> DOM.Anchor? {
let anchor = DOM.Anchor()
anchor.href = try att.parseUrl("href")
- anchor.childElements = try parseContainerChildren(element)
return anchor
}
- func parseText(_ att: AttributeParser, value: String) throws -> DOM.Text {
+ func parseText(_ att: any AttributeParser, value: String) throws -> DOM.Text {
let element = DOM.Text(value: value)
element.x = try att.parseCoordinate("x")
element.y = try att.parseCoordinate("y")
diff --git a/SwiftDraw/Parser.XML.Transform.swift b/DOM/Sources/Parser.XML.Transform.swift
similarity index 99%
rename from SwiftDraw/Parser.XML.Transform.swift
rename to DOM/Sources/Parser.XML.Transform.swift
index 8eb47d65..ce7320e6 100644
--- a/SwiftDraw/Parser.XML.Transform.swift
+++ b/DOM/Sources/Parser.XML.Transform.swift
@@ -29,7 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension XMLParser {
+package extension XMLParser {
func parseTransform(_ data: String) throws -> [DOM.Transform] {
diff --git a/SwiftDraw/Parser.XML.Use.swift b/DOM/Sources/Parser.XML.Use.swift
similarity index 95%
rename from SwiftDraw/Parser.XML.Use.swift
rename to DOM/Sources/Parser.XML.Use.swift
index f70d025b..67f03803 100644
--- a/SwiftDraw/Parser.XML.Use.swift
+++ b/DOM/Sources/Parser.XML.Use.swift
@@ -31,7 +31,7 @@
extension XMLParser {
- func parseUse(_ att: AttributeParser) throws -> DOM.Use {
+ func parseUse(_ att: any AttributeParser) throws -> DOM.Use {
let use = DOM.Use(href: try att.parseUrl("xlink:href"))
use.x = try att.parseCoordinate("x")
use.y = try att.parseCoordinate("y")
diff --git a/DOM/Sources/Parser.XML.swift b/DOM/Sources/Parser.XML.swift
new file mode 100644
index 00000000..220e0cd5
--- /dev/null
+++ b/DOM/Sources/Parser.XML.swift
@@ -0,0 +1,208 @@
+//
+// Parser.XML.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+package typealias DOMXMLParser = XMLParser
+
+package struct XMLParser {
+ package enum Error: Swift.Error {
+ case invalid
+ case missingAttribute(name: String)
+ case invalidAttribute(name: String, value: any Sendable)
+ case invalidElement(name: String, error: Swift.Error, line: Int?, column: Int?)
+ case invalidDocument(error: Swift.Error?, element: String?, line: Int, column: Int)
+ }
+
+ package var options: Options = []
+ package var filename: String?
+
+ package struct Options: OptionSet {
+ package let rawValue: Int
+ package init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ package static let skipInvalidAttributes = Options(rawValue: 1 << 0)
+ package static let skipInvalidElements = Options(rawValue: 1 << 1)
+ }
+
+ package init(options: Options = [], filename: String? = nil) {
+ self.options = options
+ self.filename = filename
+ }
+}
+
+package protocol AttributeValueParser {
+ func parseFloat(_ value: String) throws -> DOM.Float
+ func parseFloats(_ value: String) throws -> [DOM.Float]
+ func parsePercentage(_ value: String) throws -> DOM.Float
+ func parseCoordinate(_ value: String) throws -> DOM.Coordinate
+ func parseLength(_ value: String) throws -> DOM.Length
+ func parseBool(_ value: String) throws -> DOM.Bool
+ func parseFill(_ value: String) throws -> DOM.Fill
+ func parseUrl(_ value: String) throws -> DOM.URL
+ func parseUrlSelector(_ value: String) throws -> DOM.URL
+ func parsePoints(_ value: String) throws -> [DOM.Point]
+
+ func parseRaw(_ value: String) throws -> T where T.RawValue == String
+}
+
+package protocol AttributeParser {
+ var parser: AttributeValueParser { get }
+ var options: XMLParser.Options { get }
+
+ // either parse and return T or
+ // throw Error.missingAttribute when key cannot resolve to a value
+ // throw Error.invalidAttribute when value cannot be parsed into T
+ func parse(_ key: String, _ exp: (String) throws -> T) throws -> T
+}
+
+package extension AttributeParser {
+
+ func parseString(_ key: String) throws -> String {
+ return try parse(key) { $0 }
+ }
+
+ func parseFloat(_ key: String) throws -> DOM.Float {
+ return try parse(key) { return try parser.parseFloat($0) }
+ }
+
+ func parseFloats(_ key: String) throws -> [DOM.Float] {
+ return try parse(key) { return try parser.parseFloats($0) }
+ }
+
+ func parsePercentage(_ key: String) throws -> DOM.Float {
+ return try parse(key) { return try parser.parsePercentage($0) }
+ }
+
+ func parseCoordinate(_ key: String) throws -> DOM.Coordinate {
+ return try parse(key) { return try parser.parseCoordinate($0) }
+ }
+
+ func parseLength(_ key: String) throws -> DOM.Length {
+ return try parse(key) { return try parser.parseLength($0) }
+ }
+
+ func parseBool(_ key: String) throws -> DOM.Bool {
+ return try parse(key) { return try parser.parseBool($0) }
+ }
+
+ func parseFill(_ key: String) throws -> DOM.Fill {
+ return try parse(key) { return try parser.parseFill($0) }
+ }
+
+ func parseColor(_ key: String) throws -> DOM.Color {
+ return try parseFill(key).getColor()
+ }
+
+ func parseUrl(_ key: String) throws -> DOM.URL {
+ return try parse(key) { return try parser.parseUrl($0) }
+ }
+
+ func parseUrlSelector(_ key: String) throws -> DOM.URL {
+ return try parse(key) { return try parser.parseUrlSelector($0) }
+ }
+
+ func parsePoints(_ key: String) throws -> [DOM.Point] {
+ return try parse(key) { return try parser.parsePoints($0) }
+ }
+
+ func parseRaw(_ key: String) throws -> T where T.RawValue == String {
+ return try parse(key) { return try parser.parseRaw($0) }
+ }
+}
+
+package extension AttributeParser {
+
+ typealias Options = XMLParser.Options
+
+ func parse(_ key: String, exp: (String) throws -> T) throws -> T? {
+ do {
+ return try parse(key, exp)
+ } catch XMLParser.Error.missingAttribute(_) {
+ return nil
+ } catch let error {
+ guard options.contains(.skipInvalidAttributes) else { throw error }
+ }
+ return nil
+ }
+
+ func parseString(_ key: String) throws -> String? {
+ return try parse(key) { $0 }
+ }
+
+ func parseFloat(_ key: String) throws -> DOM.Float? {
+ return try parse(key) { return try parser.parseFloat($0) }
+ }
+
+ func parseFloats(_ key: String) throws -> [DOM.Float]? {
+ return try parse(key) { return try parser.parseFloats($0) }
+ }
+
+ func parsePercentage(_ key: String) throws -> DOM.Float? {
+ return try parse(key) { return try parser.parsePercentage($0) }
+ }
+
+ func parseCoordinate(_ key: String) throws -> DOM.Coordinate? {
+ return try parse(key) { return try parser.parseCoordinate($0) }
+ }
+
+ func parseLength(_ key: String) throws -> DOM.Length? {
+ return try parse(key) { return try parser.parseLength($0) }
+ }
+
+ func parseBool(_ key: String) throws -> DOM.Bool? {
+ return try parse(key) { return try parser.parseBool($0) }
+ }
+
+ func parseFill(_ key: String) throws -> DOM.Fill? {
+ return try parse(key) { return try parser.parseFill($0) }
+ }
+
+ func parseColor(_ key: String) throws -> DOM.Color? {
+ return try parseFill(key)?.getColor()
+ }
+
+ func parseUrl(_ key: String) throws -> DOM.URL? {
+ return try parse(key) { return try parser.parseUrl($0) }
+ }
+
+ func parseUrlSelector(_ key: String) throws -> DOM.URL? {
+ return try parse(key) { return try parser.parseUrlSelector($0) }
+ }
+
+ func parsePoints(_ key: String) throws -> [DOM.Point]? {
+ return try parse(key) { return try parser.parsePoints($0) }
+ }
+
+ func parseRaw(_ key: String) throws -> T? where T.RawValue == String {
+ return try parse(key) { return try parser.parseRaw($0) }
+ }
+}
diff --git a/SwiftDraw/Utilities/TextOutputStream+StandardError.swift b/DOM/Sources/TextOutputStream+StandardError.swift
similarity index 85%
rename from SwiftDraw/Utilities/TextOutputStream+StandardError.swift
rename to DOM/Sources/TextOutputStream+StandardError.swift
index 918bd9d3..5e03ebfc 100644
--- a/SwiftDraw/Utilities/TextOutputStream+StandardError.swift
+++ b/DOM/Sources/TextOutputStream+StandardError.swift
@@ -31,7 +31,7 @@
import Foundation
-extension TextOutputStream where Self == StandardErrorStream {
+package extension TextOutputStream where Self == StandardErrorStream {
static var standardError: Self {
get {
StandardErrorStream.shared
@@ -42,12 +42,13 @@ extension TextOutputStream where Self == StandardErrorStream {
}
}
-struct StandardErrorStream: TextOutputStream {
+package struct StandardErrorStream: TextOutputStream {
+ nonisolated(unsafe)
fileprivate static var shared = StandardErrorStream()
- func write(_ string: String) {
- if #available(macOS 10.15.4, iOS 13.4, *) {
+ package func write(_ string: String) {
+ if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) {
try! FileHandle.standardError.write(contentsOf: string.data(using: .utf8)!)
} else {
FileHandle.standardError.write(string.data(using: .utf8)!)
diff --git a/SwiftDraw/URL+Data.swift b/DOM/Sources/URL+Data.swift
similarity index 98%
rename from SwiftDraw/URL+Data.swift
rename to DOM/Sources/URL+Data.swift
index deeabe17..bc7b9641 100644
--- a/SwiftDraw/URL+Data.swift
+++ b/DOM/Sources/URL+Data.swift
@@ -31,7 +31,7 @@
import Foundation
-extension URL {
+package extension URL {
init?(maybeData string: String) {
guard string.hasPrefix("data:") else {
diff --git a/SwiftDraw/SwiftDraw.h b/DOM/Sources/URL+Fragment.swift
similarity index 69%
rename from SwiftDraw/SwiftDraw.h
rename to DOM/Sources/URL+Fragment.swift
index d68de5ed..05b64bf3 100644
--- a/SwiftDraw/SwiftDraw.h
+++ b/DOM/Sources/URL+Fragment.swift
@@ -1,9 +1,9 @@
//
-// SwiftDraw.h
+// URL+Fragment.swift
// SwiftDraw
//
-// Created by Simon Whitty on 31/12/16.
-// Copyright 2020 Simon Whitty
+// Created by Simon Whitty on 12/10/24.
+// Copyright 2024 Simon Whitty
//
// Distributed under the permissive zlib license
// Get the latest version from here:
@@ -29,14 +29,20 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-#import
+import Foundation
-//! Project version number for SwiftDraw.
-FOUNDATION_EXPORT double SwiftDrawVersionNumber;
-
-//! Project version string for SwiftDraw.
-FOUNDATION_EXPORT const unsigned char SwiftDrawVersionString[];
-
-// In this header, you should import all the public headers of your framework using statements like #import
+package extension URL {
+ var fragmentID: String? {
+ #if canImport(Darwin)
+ if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) {
+ return fragment(percentEncoded: false)
+ } else {
+ return fragment
+ }
+ #else
+ return fragment
+ #endif
+ }
+}
diff --git a/SwiftDraw/SwiftDraw-macOS.h b/DOM/Sources/XML.Element.swift
similarity index 68%
rename from SwiftDraw/SwiftDraw-macOS.h
rename to DOM/Sources/XML.Element.swift
index 76897773..9e053c96 100644
--- a/SwiftDraw/SwiftDraw-macOS.h
+++ b/DOM/Sources/XML.Element.swift
@@ -1,5 +1,5 @@
//
-// SwiftDraw-macOS.h
+// XML.swift
// SwiftDraw
//
// Created by Simon Whitty on 31/12/16.
@@ -29,13 +29,22 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-#import
+package enum XML { /* namespace */ }
-//! Project version number for SwiftDraw-macOS.
-FOUNDATION_EXPORT double SwiftDraw_macOSVersionNumber;
+package extension XML {
+ final class Element {
-//! Project version string for SwiftDraw-macOS.
-FOUNDATION_EXPORT const unsigned char SwiftDraw_macOSVersionString[];
+ package let name: String
+ package var attributes: [String: String]
+ package var children = [Element]()
+ package var innerText: String?
-// In this header, you should import all the public headers of your framework using statements like #import
+ package var parsedLocation: (line: Int, column: Int)?
+ package init(name: String, attributes: [String: String] = [:]) {
+ self.name = name
+ self.attributes = attributes
+ self.innerText = nil
+ }
+ }
+}
diff --git a/DOM/Sources/XML.SAXParser.swift b/DOM/Sources/XML.SAXParser.swift
new file mode 100644
index 00000000..89d57d43
--- /dev/null
+++ b/DOM/Sources/XML.SAXParser.swift
@@ -0,0 +1,122 @@
+//
+// XML.SAXParser.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 28/1/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+#if canImport(FoundationXML)
+import FoundationXML
+#endif
+
+package extension XML {
+
+ final class SAXParser: NSObject, XMLParserDelegate {
+
+#if canImport(FoundationXML)
+ package typealias FoundationXMLParser = FoundationXML.XMLParser
+#else
+ package typealias FoundationXMLParser = Foundation.XMLParser
+#endif
+
+ private let parser: FoundationXMLParser
+ private let validNamespaces = Set(["http://www.w3.org/2000/svg", ""])
+
+ private var rootNode: Element?
+ private var elements: [Element]
+
+ private var currentElement: Element {
+ return elements.last!
+ }
+
+ private init(data: Data) {
+ self.parser = FoundationXMLParser(data: data)
+ elements = [Element]()
+ super.init()
+
+ self.parser.delegate = self
+ self.parser.shouldProcessNamespaces = true
+ }
+
+ package static func parse(data: Data) throws -> Element {
+ let parser = SAXParser(data: data)
+
+ guard
+ parser.parser.parse(),
+
+ let rootNode = parser.rootNode else {
+ throw XMLParser.Error.invalidDocument(error: parser.parser.parserError,
+ element: parser.elements.last?.name,
+ line: parser.parser.lineNumber,
+ column: parser.parser.columnNumber)
+ }
+
+ return rootNode
+ }
+
+ package static func parse(contentsOf url: URL) throws -> Element {
+ let data = try Data(contentsOf: url)
+ return try parse(data: data)
+ }
+
+ package func isValidNamespaceURI(_ uri: String?) -> Bool {
+ validNamespaces.contains(uri ?? "")
+ }
+
+ package func parser(_ parser: FoundationXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) {
+ guard
+ self.parser === parser, isValidNamespaceURI(namespaceURI) else {
+ return
+ }
+
+ let element = Element(name: elementName, attributes: attributeDict)
+ element.parsedLocation = (line: parser.lineNumber, column: parser.columnNumber)
+
+ elements.last?.children.append(element)
+ elements.append(element)
+
+ if rootNode == nil {
+ rootNode = element
+ }
+ }
+
+ package func parser(_ parser: FoundationXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName _: String?) {
+ guard isValidNamespaceURI(namespaceURI), currentElement.name == elementName else {
+ return
+ }
+
+ elements.removeLast()
+ }
+
+ package func parser(_ parser: FoundationXMLParser, foundCharacters string: String) {
+ guard let element = elements.last else { return }
+ let text = element.innerText.map { $0.appending(string) }
+ element.innerText = text ?? string
+ }
+ }
+}
diff --git a/SwiftDrawTests/Bundle+Extensions.swift b/DOM/Tests/Bundle+Extensions.swift
similarity index 100%
rename from SwiftDrawTests/Bundle+Extensions.swift
rename to DOM/Tests/Bundle+Extensions.swift
diff --git a/DOM/Tests/DOM+Extensions.swift b/DOM/Tests/DOM+Extensions.swift
new file mode 100644
index 00000000..ab2de762
--- /dev/null
+++ b/DOM/Tests/DOM+Extensions.swift
@@ -0,0 +1,91 @@
+//
+// DOM.Element.Equality.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+@testable import SwiftDrawDOM
+import Foundation
+
+extension DOM {
+
+ static func createLine() -> DOM.Line {
+ return DOM.Line(x1: 0, y1: 1, x2: 3, y2: 4)
+ }
+
+ static func createCircle() -> DOM.Circle {
+ return DOM.Circle(cx: 0, cy: 1, r: 2)
+ }
+
+ static func createEllipse() -> DOM.Ellipse {
+ return DOM.Ellipse(cx: 0, cy: 1, rx: 2, ry: 3)
+ }
+
+ static func createRect() -> DOM.Rect {
+ return DOM.Rect(x: 0, y: 1, width: 2, height: 3)
+ }
+
+ static func createPolygon() -> DOM.Polygon {
+ return DOM.Polygon(0, 1, 2, 3, 4, 5)
+ }
+
+ static func createPolyline() -> DOM.Polyline {
+ return DOM.Polyline(0, 1, 2, 3, 4, 5)
+ }
+
+ static func createText() -> DOM.Text {
+ return DOM.Text(y: 1, value: "The quick brown fox")
+ }
+
+ static func createPath() -> DOM.Path {
+ let path = DOM.Path(x: 0, y: 1)
+ path.segments.append(.move(x: 10, y: 10, space: .absolute))
+ path.segments.append(.horizontal(x: 10, space: .absolute))
+ return path
+ }
+
+ static func createGroup() -> DOM.Group {
+ let group = DOM.Group()
+ group.childElements.append(createLine())
+ group.childElements.append(createPolygon())
+ group.childElements.append(createCircle())
+ group.childElements.append(createPath())
+ group.childElements.append(createRect())
+ group.childElements.append(createEllipse())
+ return group
+ }
+}
+
+// Equatable just for tests
+
+extension DOM.GraphicsElement: Swift.Equatable {
+ static func ==(lhs: DOM.GraphicsElement, rhs: DOM.GraphicsElement) -> Bool {
+ let toString: (Any) -> String = { var text = ""; dump($0, to: &text); return text }
+ return toString(lhs) == toString(rhs)
+ }
+}
diff --git a/DOM/Tests/DOM.ElementTests.swift b/DOM/Tests/DOM.ElementTests.swift
new file mode 100644
index 00000000..6f52d960
--- /dev/null
+++ b/DOM/Tests/DOM.ElementTests.swift
@@ -0,0 +1,180 @@
+//
+// DOM.ElementTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+
+struct DOMElementTests {
+
+ @Test
+ func line() {
+ let element = DOM.createLine()
+ var another = DOM.createLine()
+
+ #expect(element == another)
+
+ another.x1 = 1
+ #expect(element != another)
+
+ another = DOM.createLine()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func circle() {
+ let element = DOM.createCircle()
+ var another = DOM.createCircle()
+
+ #expect(element == another)
+
+ another.cx = 1
+ #expect(element != another)
+
+ another = DOM.createCircle()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func ellipse() {
+ let element = DOM.createEllipse()
+ var another = DOM.createEllipse()
+
+ #expect(element == another)
+
+ another.cx = 1
+ #expect(element != another)
+
+ another = DOM.createEllipse()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func rect() {
+ let element = DOM.createRect()
+ var another = DOM.createRect()
+
+ #expect(element == another)
+
+ another.x = 1
+ #expect(element != another)
+
+ another = DOM.createRect()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func polygon() {
+ let element = DOM.createPolygon()
+ var another = DOM.createPolygon()
+
+ #expect(element == another)
+
+ another.points.append(DOM.Point(6, 7))
+ #expect(element != another)
+
+ another = DOM.createPolygon()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func polyline() {
+ let element = DOM.createPolyline()
+ var another = DOM.createPolyline()
+
+ #expect(element == another)
+
+ another.points.append(DOM.Point(6, 7))
+ #expect(element != another)
+
+ another = DOM.createPolyline()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func text() {
+ let element = DOM.createText()
+ var another = DOM.createText()
+
+ #expect(element == another)
+
+ another.value = "Simon"
+ #expect(element != another)
+
+ another = DOM.createText()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(element != another)
+
+ another.attributes.fill = nil
+ #expect(element == another)
+ }
+
+ @Test
+ func group() {
+ let group = DOM.createGroup()
+ var another = DOM.createGroup()
+
+ #expect(group == another)
+
+ another.childElements.append(DOM.createCircle())
+ #expect(group != another)
+
+ another = DOM.createGroup()
+ another.attributes.fill = .color(.keyword(.black))
+ #expect(group != another)
+
+ another.attributes.fill = nil
+ #expect(group == another)
+ }
+}
diff --git a/SwiftDrawTests/DOM.PresentationAttributesTests.swift b/DOM/Tests/DOM.PresentationAttributesTests.swift
similarity index 73%
rename from SwiftDrawTests/DOM.PresentationAttributesTests.swift
rename to DOM/Tests/DOM.PresentationAttributesTests.swift
index 0771e9b2..23e0dc38 100644
--- a/SwiftDrawTests/DOM.PresentationAttributesTests.swift
+++ b/DOM/Tests/DOM.PresentationAttributesTests.swift
@@ -29,100 +29,97 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import XCTest
-@testable import SwiftDraw
+import Testing
+@testable import SwiftDrawDOM
-final class PresentationAttributesTests: XCTestCase {
+struct PresentationAttributesTests {
typealias Attributes = DOM.PresentationAttributes
typealias StyleSheet = DOM.StyleSheet
- func testOpacityIsApplied() {
- XCTAssertNil(
+ @Test
+ func opacityIsApplied() {
+ #expect(
Attributes(opacity: nil)
.applyingAttributes(Attributes(opacity: nil))
- .opacity
+ .opacity == nil
)
- XCTAssertEqual(
+ #expect(
Attributes(opacity: 5)
.applyingAttributes(Attributes(opacity: nil))
- .opacity,
- 5
+ .opacity == 5
)
- XCTAssertEqual(
+ #expect(
Attributes(opacity: 5)
.applyingAttributes(Attributes(opacity: 10))
- .opacity,
- 10
+ .opacity == 10
)
}
- func testDisplayIsApplied() {
- XCTAssertNil(
+ @Test
+ func displayIsApplied() {
+ #expect(
Attributes(display: nil)
.applyingAttributes(Attributes(display: nil))
- .display
+ .display == nil
)
- XCTAssertEqual(
+ #expect(
Attributes(display: DOM.DisplayMode.none)
.applyingAttributes(Attributes(display: nil))
- .display,
- DOM.DisplayMode.none
+ .display == DOM.DisplayMode.none
)
- XCTAssertEqual(
+ #expect(
Attributes(display: DOM.DisplayMode.none)
.applyingAttributes(Attributes(display: .inline))
- .display,
- .inline
+ .display == .inline
)
}
- func testColorIsApplied() {
- XCTAssertNil(
+ @Test
+ func colorIsApplied() {
+ #expect(
Attributes(color: nil)
.applyingAttributes(Attributes(color: nil))
- .color
+ .color == nil
)
- XCTAssertEqual(
+ #expect(
Attributes(color: .keyword(.green))
.applyingAttributes(Attributes(color: nil))
- .color,
- .keyword(.green)
+ .color == .keyword(.green)
)
- XCTAssertEqual(
+ #expect(
Attributes(color: .keyword(.green))
.applyingAttributes(Attributes(color: .currentColor))
- .color,
- .currentColor
+ .color == .currentColor
)
}
- func testSelectors() {
- XCTAssertEqual(
- DOM.makeSelectors(for: .circle()),
- [.element("circle")]
+ @Test
+ func selectors() {
+ #expect(
+ DOM.makeSelectors(for: .circle()) == [.element("circle")]
)
- XCTAssertEqual(
- DOM.makeSelectors(for: .circle(id: "c1")),
+ #expect(
+ DOM.makeSelectors(for: .circle(id: "c1")) ==
[.element("circle"),
.id("c1")]
)
- XCTAssertEqual(
- DOM.makeSelectors(for: .circle(class: "c")),
+ #expect(
+ DOM.makeSelectors(for: .circle(class: "c")) ==
[.element("circle"),
.class("c")]
)
- XCTAssertEqual(
- DOM.makeSelectors(for: .circle(id: "c1 ", class: "a b c")),
+ #expect(
+ DOM.makeSelectors(for: .circle(id: "c1 ", class: "a b c")) ==
[.element("circle"),
.class("a"),
.class("b"),
@@ -131,7 +128,8 @@ final class PresentationAttributesTests: XCTestCase {
)
}
- func testLastSheetAttributesAreUsed() {
+ @Test
+ func lastSheetAttributesAreUsed() {
var sheet = StyleSheet()
sheet[.id("b")].opacity = 0
sheet[.id("a")].opacity = 1
@@ -140,45 +138,41 @@ final class PresentationAttributesTests: XCTestCase {
another[.id("b")].opacity = 0.1
another[.id("a")].opacity = 0.5
- XCTAssertEqual(
+ #expect(
DOM.makeAttributes(for: .id("a"), styles: [sheet])
- .opacity,
- 1
+ .opacity == 1
)
- XCTAssertEqual(
+ #expect(
DOM.makeAttributes(for: .id("a"), styles: [sheet, another])
- .opacity,
- 0.5
+ .opacity == 0.5
)
}
- func testSelectorPrecedence() {
+ @Test
+ func selectorPrecedence() {
var sheet = StyleSheet()
sheet[.element("circle")].opacity = 1
sheet[.id("c1")].opacity = 0.5
sheet[.class("b")].opacity = 0.1
sheet[.class("c")].opacity = 0.2
- XCTAssertEqual(
+ #expect(
DOM.presentationAttributes(for: .circle(id: "c1", class: "b c"),
styles: [sheet])
- .opacity,
- 0.5
+ .opacity == 0.5
)
- XCTAssertEqual(
+ #expect(
DOM.presentationAttributes(for: .circle(id: "c2", class: "b c"),
styles: [sheet])
- .opacity,
- 0.2
+ .opacity == 0.2
)
- XCTAssertEqual(
+ #expect(
DOM.presentationAttributes(for: .circle(id: "c2", class: "z"),
styles: [sheet])
- .opacity,
- 1
+ .opacity == 1
)
}
}
diff --git a/DOM/Tests/Parser.AttributesTests.swift b/DOM/Tests/Parser.AttributesTests.swift
new file mode 100644
index 00000000..65c73a28
--- /dev/null
+++ b/DOM/Tests/Parser.AttributesTests.swift
@@ -0,0 +1,190 @@
+//
+// AttributeParserTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 6/3/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+@testable import SwiftDrawDOM
+import Testing
+import Foundation
+
+struct AttributeParserTests {
+
+ @Test
+ func parserOrder() throws {
+ let parser = XMLParser.ValueParser()
+
+ let att = XMLParser.Attributes(parser: parser,
+ element: ["x": "10", "y": "20.0", "fill": "red"],
+ style: ["x": "d", "fill": "green"])
+
+ //parse from style
+ #expect(try att.parseColor("fill") == .keyword(.green))
+ #expect(throws: (any Error).self) {
+ try att.parseFloat("x")
+ }
+
+ //missing throws error
+ #expect(throws: (any Error).self) {
+ try att.parseFloat("other")
+ }
+ //missing returns optional
+ #expect(try att.parseFloat("other") as DOM.Float? == nil)
+
+ //fall through to element
+ #expect(try att.parseFloat("y") == 20)
+
+ //SkipInvalidAttributes
+ let another = XMLParser.Attributes(parser: parser,
+ options: [.skipInvalidAttributes],
+ element: att.element,
+ style: att.style)
+
+
+ #expect(try another.parseColor("fill") == .keyword(.green))
+ #expect(try another.parseFloat("x") == 10)
+ #expect(try another.parseFloat("y") == 20)
+
+ //missing throws error
+ #expect(throws: (any Error).self) {
+ try another.parseFloat("other")
+ }
+ //missing returns optional
+ #expect(try another.parseFloat("other") as DOM.Float? == nil)
+ //invalid returns optional
+ #expect(try another.parseColor("x") as DOM.Color? == nil)
+ }
+
+ @Test
+ func dictionary() throws {
+ let att = ["x": "20", "y": "30", "fill": "#a0a0a0", "display": "none", "some": "random"]
+
+ #expect(try att.parseCoordinate("x") == 20.0)
+ #expect(try att.parseCoordinate("y") == 30.0)
+ #expect(try att.parseColor("fill") == .hex(160, 160, 160))
+ #expect(try att.parseRaw("display") == DOM.DisplayMode.none)
+
+ #expect(throws: (any Error).self) {
+ try att.parseFloat("other")
+ }
+ #expect(throws: (any Error).self) {
+ try att.parseColor("some")
+ }
+
+ //missing returns optional
+ #expect(try att.parseFloat("other") as DOM.Float? == nil)
+ }
+
+ @Test
+ func parseString() throws {
+ let att = ["x": "20", "some": "random"]
+ #expect(try att.parseString("x") == "20")
+ #expect(throws: (any Error).self) {
+ try att.parseString("missing")
+ }
+ }
+
+ @Test
+ func parseFloat() throws {
+ let att = ["x": "20", "some": "random"]
+ #expect(try att.parseFloat("x") == 20.0)
+ #expect(try att.parseFloat("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parseFloat("some")
+ }
+ }
+
+ @Test
+ func parseFloats() throws {
+ let att = ["x": "20 30 40", "some": "random"]
+ #expect(try att.parseFloats("x") == [20.0, 30.0, 40.0])
+ #expect(throws: (any Error).self) {
+ try att.parseFloats("some")
+ }
+ }
+
+ @Test
+ func parsePoints() throws {
+ let att = ["x": "20 30 40 50", "some": "random"]
+ #expect(try att.parsePoints("x") == [DOM.Point(20, 30), DOM.Point(40, 50)])
+ #expect(try att.parsePoints("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parsePoints("some")
+ }
+ #expect(throws: (any Error).self) {
+ try att.parsePoints("some") as [DOM.Point]?
+ }
+ }
+
+ @Test
+ func parseLength() throws {
+ let att = ["x": "20", "y": "aa"]
+ #expect(try att.parseLength("x") == 20)
+ #expect(try att.parseLength("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parseLength("y")
+ }
+ #expect(throws: (any Error).self) {
+ try att.parseLength("y") as DOM.Length?
+ }
+ }
+
+ @Test
+ func parseBool() throws {
+ let att = ["x": "true", "y": "5"]
+ #expect(try att.parseBool("x") == true)
+ #expect(try att.parseBool("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parseBool("y")
+ }
+ #expect(throws: (any Error).self) {
+ try att.parseBool("y") as Bool?
+ }
+ }
+
+ @Test
+ func parseURL() throws {
+ let att = ["clip": "http://www.test.com", "mask": "20 twenty"]
+ #expect(try att.parseUrl("clip") == URL(string: "http://www.test.com"))
+ #expect(try att.parseUrl("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parseUrl(" ")
+ }
+ }
+
+ @Test
+ func parseURLSelector() throws {
+ let att = ["clip": "url(#shape)", "mask": "aa"]
+ #expect(try att.parseUrlSelector("clip") == URL(string: "#shape"))
+ #expect(try att.parseUrlSelector("missing") == nil)
+ #expect(throws: (any Error).self) {
+ try att.parseUrlSelector("mask")
+ }
+ }
+}
+
diff --git a/DOM/Tests/Parser.GraphicAttributeTests.swift b/DOM/Tests/Parser.GraphicAttributeTests.swift
new file mode 100644
index 00000000..93d2c8c9
--- /dev/null
+++ b/DOM/Tests/Parser.GraphicAttributeTests.swift
@@ -0,0 +1,136 @@
+//
+// Parser.GraphicAttributeTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 27/2/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+
+@testable import SwiftDrawDOM
+import Testing
+
+struct ParserGraphicAttributeTests {
+
+ @Test
+ func presentationAttributes() throws {
+ var parsed = try XMLParser().parsePresentationAttributes([:])
+ #expect(parsed.opacity == nil)
+ #expect(parsed.display == nil)
+ #expect(parsed.stroke == nil)
+ #expect(parsed.strokeWidth == nil)
+ #expect(parsed.strokeOpacity == nil)
+ #expect(parsed.strokeLineCap == nil)
+ #expect(parsed.strokeLineJoin == nil)
+ #expect(parsed.strokeDashArray == nil)
+ #expect(parsed.fill == nil)
+ #expect(parsed.fillOpacity == nil)
+ #expect(parsed.fillRule == nil)
+ #expect(parsed.transform == nil)
+ #expect(parsed.clipPath == nil)
+ #expect(parsed.mask == nil)
+
+ let att = ["opacity": "95%",
+ "display": "none",
+ "stroke": "green",
+ "stroke-width": "15.0",
+ "stroke-opacity": "75.6%",
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "miter",
+ "stroke-dasharray": "1 5 10",
+ "fill": "purple",
+ "fill-opacity": "25%",
+ "fill-rule": "evenodd",
+ "transform": "scale(15)",
+ "clip-path": "url(#circlePath)",
+ "mask": "url(#fancyMask)",
+ "filter": "url(#blur)"
+ ]
+
+ parsed = try XMLParser().parsePresentationAttributes(att)
+
+ #expect(parsed.opacity == 0.95)
+ #expect(parsed.display == DOM.DisplayMode.none)
+ #expect(parsed.stroke == .color(.keyword(.green)))
+ #expect(parsed.strokeWidth == 15)
+ #expect(parsed.strokeOpacity == 0.756)
+ #expect(parsed.strokeLineCap == .butt)
+ #expect(parsed.strokeLineJoin == .miter)
+ #expect(parsed.strokeDashArray == [1, 5, 10])
+ #expect(parsed.fill == .color(.keyword(.purple)))
+ #expect(parsed.fillOpacity == 0.25)
+ #expect(parsed.fillRule == .evenodd)
+ #expect(parsed.transform == [.scale(sx: 15, sy: 15)])
+ #expect(parsed.clipPath?.fragmentID == "circlePath")
+ #expect(parsed.mask?.fragmentID == "fancyMask")
+ #expect(parsed.filter?.fragmentID == "blur")
+ }
+
+ @Test
+ func circle() throws {
+ let el = XML.Element("circle", style: "clip-path: url(#cp1); cx:10;cy:10;r:10; fill:black; stroke-width:2")
+
+ let parsed = try XMLParser().parseGraphicsElement(el)
+ let circle = parsed as? DOM.Circle
+ #expect(circle != nil)
+ #expect(circle?.style.clipPath?.fragmentID == "cp1")
+ #expect(circle?.style.fill == .color(.keyword(.black)))
+ #expect(circle?.style.strokeWidth == 2)
+ }
+
+ @Test
+ func displayMode() throws {
+ let parser = XMLParser.ValueParser()
+
+ #expect(try parser.parseRaw("none") == DOM.DisplayMode.none)
+ #expect(try parser.parseRaw(" none ") == DOM.DisplayMode.none)
+ #expect(throws: (any Error).self) {
+ try parser.parseRaw("ds") as DOM.DisplayMode
+ }
+ }
+
+ @Test
+ func strokeLineCap() throws {
+ let parser = XMLParser.ValueParser()
+
+ #expect(try parser.parseRaw("butt") == DOM.LineCap.butt)
+ #expect(try parser.parseRaw(" round") == DOM.LineCap.round)
+ #expect(throws: (any Error).self) {
+ try parser.parseRaw("squdare") as DOM.LineCap
+ }
+ }
+
+ @Test
+ func strokeLineJoin() throws {
+ let parser = XMLParser.ValueParser()
+
+ #expect(try parser.parseRaw("miter") == DOM.LineJoin.miter)
+ #expect(try parser.parseRaw(" bevel") == DOM.LineJoin.bevel)
+ #expect(throws: (any Error).self) {
+ try parser.parseRaw("ds") as DOM.LineJoin
+ }
+ }
+}
diff --git a/DOM/Tests/Parser.SVGTests.swift b/DOM/Tests/Parser.SVGTests.swift
new file mode 100644
index 00000000..4abb0119
--- /dev/null
+++ b/DOM/Tests/Parser.SVGTests.swift
@@ -0,0 +1,191 @@
+//
+// Parser.SVGTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 3/2/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+
+@testable import SwiftDrawDOM
+import Testing
+
+struct ParserSVGTests {
+
+ @Test
+ func svg() throws {
+ let node = XML.Element(name: "svg", attributes: ["width": "100", "height": "200"])
+ let parser = DOMXMLParser()
+
+ var parsed = try parser.parseSVG(node)
+ let expected = DOM.SVG(width: 100, height: 200)
+ #expect(parsed == expected)
+
+ let expected2 = expected
+ expected2.viewBox = DOM.SVG.ViewBox(x: 10, y: 20, width: 100, height: 200)
+ #expect(parsed != expected2)
+
+ node.attributes["viewBox"] = "10 20 100 200"
+ parsed = try parser.parseSVG(node)
+ #expect(parsed == expected2)
+
+ expected2.attributes.fill = .color(.keyword(.red))
+ #expect(parsed != expected2)
+ }
+
+ @Test
+ func parseSVGInvalidNode() {
+ let node = XML.Element(name: "svg2", attributes: ["width": "100", "height": "200"])
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseSVG(node)
+ }
+ }
+
+ @Test
+ func parseSVGMissingHeightInvalidNode() {
+ let node = XML.Element(name: "svg", attributes: ["width": "100"])
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseSVG(node)
+ }
+ }
+
+ @Test
+ func parseSVGMissingWidthInvalidNode() {
+ let node = XML.Element(name: "svg", attributes: ["height": "100"])
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseSVG(node)
+ }
+ }
+
+ @Test
+ func viewBox() throws {
+ let parsed = try #require(try XMLParser().parseViewBox(" 10\t20 300.0 5e2"))
+ #expect(parsed.x == 10)
+ #expect(parsed.y == 20)
+ #expect(parsed.width == 300)
+ #expect(parsed.height == 500)
+
+ #expect(try XMLParser().parseViewBox("10 10 10 10") != nil)
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseViewBox("10 10 10 10a")
+ }
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseViewBox(" 10\t20 300")
+ }
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseViewBox("10 10 10 10a")
+ }
+ }
+
+ @Test
+ func clipPath() throws {
+ let node = XML.Element(name: "clipPath", attributes: ["id": "hello"])
+
+ var parsed = try XMLParser().parseClipPath(node)
+ #expect(parsed.id == "hello")
+
+ node.children.append(XML.Element("line", style: "x1:0;y1:0;x2:50;y2:60"))
+ node.children.append(XML.Element("circle", style: "cx:0;cy:10;r:20"))
+
+ parsed = try XMLParser().parseClipPath(node)
+ #expect(parsed.id == "hello")
+ #expect(parsed.childElements.count == 2)
+ }
+
+ @Test
+ func parseDefs() throws {
+ let svg = XML.Element(name: "svg")
+ let defs = XML.Element(name: "defs")
+ let g = XML.Element(name: "g")
+ svg.children.append(defs)
+ svg.children.append(g)
+
+ g.children.append(XML.Element("circle", id: "c2", style: "cx:0;cy:10;r:20"))
+ let defs1 = XML.Element(name: "defs")
+ g.children.append(defs1)
+ defs1.children.append(XML.Element("circle", id: "c3", style: "cx:0;cy:10;r:20"))
+
+ defs.children.append(XML.Element("circle", id: "c1", style: "cx:0;cy:10;r:20"))
+ svg.children.append(defs1)
+
+ let elements = try DOMXMLParser().parseSVGDefs(svg).elements
+ #expect(elements.count == 2)
+ }
+
+ @Test
+ func use() throws {
+ let svg = try DOM.SVG.parse(xml: #"""
+
+
+ """#)
+
+ let rect = try #require(svg.firstGraphicsElement(with: "a") as? DOM.Rect)
+ #expect(rect.id == "a")
+
+ let circle = try #require(svg.firstGraphicsElement(with: "b") as? DOM.Circle)
+ #expect(circle.id == "b")
+ }
+
+ @Test
+ func missingNamespce() throws {
+ let svg = try DOM.SVG.parse(xml: #"""
+
+
+ """#)
+
+ let rect = try #require(svg.firstGraphicsElement(with: "a") as? DOM.Rect)
+ #expect(rect.id == "a")
+
+ let circle = try #require(svg.firstGraphicsElement(with: "b") as? DOM.Circle)
+ #expect(circle.id == "b")
+ }
+
+ @Test
+ func deepNestingParses() throws {
+ let groupOpen = String(repeating: "", count: 500)
+ let groupClose = String(repeating: "", count: 500)
+
+ // If this throws, the test will fail automatically
+ _ = try DOM.SVG.parse(xml: #"""
+
+
+ """#)
+ }
+}
diff --git a/DOM/Tests/Parser.XML.ColorTests.swift b/DOM/Tests/Parser.XML.ColorTests.swift
new file mode 100644
index 00000000..f383d912
--- /dev/null
+++ b/DOM/Tests/Parser.XML.ColorTests.swift
@@ -0,0 +1,124 @@
+//
+// Parser.XML.ColorTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+@testable import SwiftDrawDOM
+import Testing
+
+struct ParserColorTests {
+
+ @Test
+ func colorNone() throws {
+ #expect(try XMLParser().parseColor("none") == .none)
+ #expect(try XMLParser().parseColor(" none") == .none)
+ #expect(try XMLParser().parseColor("\t none \t") == .none)
+ }
+
+ @Test
+ func colorTransparent() throws {
+ #expect(try XMLParser().parseColor("transparent") == .none)
+ #expect(try XMLParser().parseColor(" transparent") == .none)
+ #expect(try XMLParser().parseColor("\t transparent \t") == .none)
+ }
+
+ @Test
+ func colorCurrent() throws {
+ #expect(try XMLParser().parseColor("currentColor") == .currentColor)
+ #expect(try XMLParser().parseColor(" currentColor") == .currentColor)
+ #expect(try XMLParser().parseColor("\t currentColor \t") == .currentColor)
+ }
+
+ @Test
+ func colorKeyword() throws {
+ #expect(try XMLParser().parseColor("aliceblue") == .keyword(.aliceblue))
+ #expect(try XMLParser().parseColor("wheat") == .keyword(.wheat))
+ #expect(try XMLParser().parseColor("cornflowerblue") == .keyword(.cornflowerblue))
+ #expect(try XMLParser().parseColor(" magenta") == .keyword(.magenta))
+ #expect(try XMLParser().parseColor("black ") == .keyword(.black))
+ #expect(try XMLParser().parseColor("\t red \t") == .keyword(.red))
+ }
+
+ @Test
+ func colorRGBi() throws {
+ // integer 0-255
+ #expect(try XMLParser().parseColor("rgb(0,1,2)") == .rgbi(0, 1, 2, 1.0))
+ #expect(try XMLParser().parseColor(" rgb( 0 , 1 , 2) ") == .rgbi(0, 1, 2, 1.0))
+ #expect(try XMLParser().parseColor("rgb(255,100,78)") == .rgbi(255, 100, 78, 1.0))
+
+ #expect(try XMLParser().parseColor("rgb(0,1,2,255)") == .rgbi(0, 1, 2, 1.0))
+ #expect(try XMLParser().parseColor("rgb(0,1,2,25%)") == .rgbi(0, 1, 2, 0.25))
+ #expect(try XMLParser().parseColor(" rgb( 0 , 1 , 2, 0.5) ") == .rgbi(0, 1, 2, 0.5))
+ #expect(try XMLParser().parseColor("rgb(255,100, 78, 0)") == .rgbi(255, 100, 78, 0))
+ }
+
+ @Test
+ func colorRGBf() throws {
+ // percentage 0-100%
+ #expect(try XMLParser().parseColor("rgb(0,1%,99%)") == .rgbf(0.0, 0.01, 0.99, 1.0))
+ #expect(try XMLParser().parseColor("rgb( 0%, 52% , 100%) ") == .rgbf(0.0, 0.52, 1.0, 1.0))
+ #expect(try XMLParser().parseColor("rgb(75%,25%,7%)") == .rgbf(0.75, 0.25, 0.07, 1.0))
+ }
+
+ @Test
+ func colorRGBA() throws {
+ // integer 0-255
+ #expect(try XMLParser().parseColor("rgba(0,1,2,0.5)") == .rgbi(0, 1, 2, 0.5))
+ #expect(try XMLParser().parseColor(" rgba( 0 , 1 , 2, 0.6) ") == .rgbi(0, 1, 2, 0.6))
+ #expect(try XMLParser().parseColor("rgba(255,100,78,0.7)") == .rgbi(255, 100, 78, 0.7))
+ // percentage 0-100%
+ #expect(try XMLParser().parseColor("rgba(0,1%,99%,0.5)") == .rgbf(0.0, 0.01, 0.99, 0.5))
+ #expect(try XMLParser().parseColor("rgba( 0%, 52% , 100%, 0.6) ") == .rgbf(0.0, 0.52, 1.0, 0.6))
+ #expect(try XMLParser().parseColor("rgba(75%,25%,7%,0.7)") == .rgbf(0.75, 0.25, 0.07, 0.7))
+ }
+
+ @Test
+ func colorHex() throws {
+ #expect(try XMLParser().parseColor("#a06") == .hex(170, 0, 102))
+ #expect(try XMLParser().parseColor("#123456") == .hex(18, 52, 86))
+ #expect(try XMLParser().parseColor("#FF11DD") == .hex(255, 17, 221))
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseColor("#invalid")
+ }
+ }
+
+ @Test
+ func colorP3() throws {
+ // percentage 0-100%
+ #expect(try XMLParser().parseColor("color(display-p3 0 0.5 0.9)") == .p3(0, 0.5, 0.9))
+ #expect(try XMLParser().parseColor("color(display-p3 0.1, 0.2, 0)") == .p3(0.1, 0.2, 0))
+ #expect(try XMLParser().parseColor("color(display-p3 1,0.3,0.5)") == .p3(1, 0.3, 0.5))
+ }
+}
+
+private extension DOMXMLParser {
+ func parseColor(_ value: String) throws -> DOM.Color {
+ return try parseFill(value).getColor()
+ }
+}
diff --git a/DOM/Tests/Parser.XML.ElementTests.swift b/DOM/Tests/Parser.XML.ElementTests.swift
new file mode 100644
index 00000000..4dfe3b29
--- /dev/null
+++ b/DOM/Tests/Parser.XML.ElementTests.swift
@@ -0,0 +1,169 @@
+//
+// Parser.XML.ElementTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+@testable import SwiftDrawDOM
+import Testing
+
+struct XMLParserElementTests {
+
+ @Test
+ func line() {
+ let node = ["x1": "0",
+ "y1": "10",
+ "x2": "50",
+ "y2": "60"]
+
+ let parsed = try? XMLParser().parseLine(node)
+ #expect(DOM.Line(x1: 0, y1: 10, x2: 50, y2: 60) == parsed)
+ }
+
+ @Test
+ func circle() {
+ let node = ["cx": "0",
+ "cy": "10",
+ "r": "20"]
+
+ let parsed = try? XMLParser().parseCircle(node)
+ #expect(DOM.Circle(cx: 0, cy: 10, r: 20) == parsed)
+ }
+
+ @Test
+ func ellipse() {
+ let node = ["cx": "0",
+ "cy": "10",
+ "rx": "20",
+ "ry": "30"]
+
+ let parsed = try? XMLParser().parseEllipse(node)
+ #expect(DOM.Ellipse(cx: 0, cy: 10, rx: 20, ry: 30) == parsed)
+ }
+
+ @Test
+ func rect() throws {
+ var node = ["x": "0",
+ "y": "10",
+ "width": "20",
+ "height": "30"]
+
+ let rect = DOM.Rect(x: 0, y: 10, width: 20, height: 30)
+ #expect(try XMLParser().parseRect(node) == rect)
+
+ node["rx"] = "3"
+ node["ry"] = "2"
+ rect.rx = 3
+ rect.ry = 2
+ #expect(try XMLParser().parseRect(node) == rect)
+ }
+
+ @Test
+ func polyline() {
+ let node = ["points": "0,1 2 3; 4;5;6;7;8 9"]
+
+ let parsed = try? XMLParser().parsePolyline(node)
+ #expect(DOM.Polyline(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) == parsed)
+ }
+
+ @Test
+ func polygon() {
+ let att = ["points": "0, 1,2,3;4;5;6;7;8 9"]
+ let parsed = try? XMLParser().parsePolygon(att)
+ #expect(DOM.Polygon(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) == parsed)
+ }
+
+ @Test
+ func polygonFillRule() throws {
+ let att = ["points": "0,1,2,3;4;5;6;7;8 9"]
+ #expect((try XMLParser().parsePolygon(att)).attributes.fillRule == nil)
+
+ let node = XML.Element(name: "polygon")
+ node.attributes["points"] = "0,1,2,3"
+
+ node.attributes["fill-rule"] = "nonzero"
+ #expect(try XMLParser().parseGraphicsElement(node)!.attributes.fillRule == .nonzero)
+
+ node.attributes["fill-rule"] = "evenodd"
+ #expect(try XMLParser().parseGraphicsElement(node)!.attributes.fillRule == .evenodd)
+
+ node.attributes["fill-rule"] = "asdf"
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parseGraphicsElement(node)!.attributes.fillRule
+ }
+ }
+
+ @Test
+ func elementParserSkipsErrors() {
+ let error = XMLParser().parseError(for: XMLParser.Error.invalid,
+ parsing: XML.Element(name: "polygon"),
+ with: [.skipInvalidElements])
+
+ #expect(error == nil)
+ }
+
+ @Test
+ func elementParserErrorsPreserveLineNumbers() {
+ let invalidElement = XMLParser.Error.invalidElement(name: "polygon",
+ error: XMLParser.Error.invalid,
+ line: 100,
+ column: 50)
+
+ let parseError = XMLParser().parseError(for: invalidElement,
+ parsing: XML.Element(name: "polygon"),
+ with: [])
+
+ switch parseError! {
+ case let .invalidElement(_, _, line, column):
+ #expect(line == 100)
+ #expect(column == 50)
+ default:
+ Issue.record("not forwarderd")
+ #expect(Bool(false))
+ }
+ }
+
+ @Test
+ func elementParserErrorsPreserveLineNumbersFromElement() {
+ let element = XML.Element(name: "polygon")
+ element.parsedLocation = (line: 100, column: 50)
+
+ let parseError = XMLParser().parseError(for: XMLParser.Error.invalid,
+ parsing: element,
+ with: [])
+
+ switch parseError! {
+ case let .invalidElement(_, _, line, column):
+ #expect(line == 100)
+ #expect(column == 50)
+ default:
+ Issue.record("not forwarderd")
+ #expect(Bool(false))
+ }
+ }
+}
diff --git a/SwiftDrawTests/Parser.XML.FilterTests.swift b/DOM/Tests/Parser.XML.FilterTests.swift
similarity index 82%
rename from SwiftDrawTests/Parser.XML.FilterTests.swift
rename to DOM/Tests/Parser.XML.FilterTests.swift
index aba6414b..7ccbe41b 100644
--- a/SwiftDrawTests/Parser.XML.FilterTests.swift
+++ b/DOM/Tests/Parser.XML.FilterTests.swift
@@ -32,23 +32,25 @@
import Foundation
-import XCTest
-@testable import SwiftDraw
+@testable import SwiftDrawDOM
+import Testing
-final class ParserXMLFilterTests: XCTestCase {
+struct ParserXMLFilterTests {
- func testParseFilters() throws {
+ @Test
+ func parseFilters() throws {
let child = XML.Element(name: "child")
child.children = [XML.Element.makeMockFilter(), XML.Element.makeMockFilter()]
let parent = XML.Element(name: "parent")
parent.children = [XML.Element.makeMockFilter(), child]
- XCTAssertEqual(try XMLParser().parseFilters(child).count, 2)
- XCTAssertEqual(try XMLParser().parseFilters(parent).count, 3)
+ #expect(try XMLParser().parseFilters(child).count == 2)
+ #expect(try XMLParser().parseFilters(parent).count == 3)
}
- func testParseEffect() throws {
+ @Test
+ func parseEffect() throws {
let element = XML.Element.makeMockFilter(id: "blur")
element.children = [
@@ -57,8 +59,8 @@ final class ParserXMLFilterTests: XCTestCase {
]
let filter = try XMLParser().parseFilter(element)
- XCTAssertEqual(filter.id, "blur")
- XCTAssertEqual(filter.effects, [.gaussianBlur(stdDeviation: 0.5)])
+ #expect(filter.id == "blur")
+ #expect(filter.effects == [.gaussianBlur(stdDeviation: 0.5)])
}
}
diff --git a/DOM/Tests/Parser.XML.ImageTests.swift b/DOM/Tests/Parser.XML.ImageTests.swift
new file mode 100644
index 00000000..d8b7ef6f
--- /dev/null
+++ b/DOM/Tests/Parser.XML.ImageTests.swift
@@ -0,0 +1,104 @@
+//
+// Parser.XML.ImageTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 3/3/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+
+import Foundation
+
+@testable import SwiftDrawDOM
+import Testing
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+#if canImport(UIKit)
+import UIKit
+#elseif canImport(AppKit)
+import AppKit
+#endif
+
+extension CGImage {
+ static func from(data: Data) -> CGImage? {
+#if canImport(UIKit)
+ return UIImage(data: data)?.cgImage
+#elseif canImport(AppKit)
+ guard let image = NSImage(data: data) else { return nil }
+ var rect = NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
+ return image.cgImage(forProposedRect: &rect, context: nil, hints: nil)
+#endif
+ }
+}
+
+struct ParserXMLImageTests {
+
+ @Test
+ func image() throws {
+ var node = ["xlink:href": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="]
+ node["width"] = "10"
+ node["height"] = "10"
+
+ let image = try XMLParser().parseImage(node)
+
+ #expect(image.href.isDataURL)
+
+ let decode = try #require(image.href.decodedData)
+
+ #expect(decode.mimeType == "image/png")
+
+ let cgImage = CGImage.from(data: decode.data)
+
+ #expect(cgImage != nil)
+ #expect(cgImage?.width == 5)
+ #expect(cgImage?.height == 5)
+ }
+
+ @Test
+ func imageLineBreaks() throws {
+ let base64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblA " + "\n" +
+ " AAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="
+
+ let node = ["xlink:href": base64, "width": "10", "height": "10"]
+
+ let image = try XMLParser().parseImage(node)
+
+ #expect(image.href.isDataURL)
+
+ let decode = try #require(image.href.decodedData)
+
+ #expect(decode.mimeType == "image/png")
+
+ let cgImage = CGImage.from(data: decode.data)
+
+ #expect(cgImage != nil)
+ #expect(cgImage?.width == 5)
+ #expect(cgImage?.height == 5)
+ }
+}
+
+#endif
diff --git a/SwiftDrawTests/Parser.XML.LinearGradientTests.swift b/DOM/Tests/Parser.XML.LinearGradientTests.swift
similarity index 61%
rename from SwiftDrawTests/Parser.XML.LinearGradientTests.swift
rename to DOM/Tests/Parser.XML.LinearGradientTests.swift
index 1669bd4a..4ec14fe3 100644
--- a/SwiftDrawTests/Parser.XML.LinearGradientTests.swift
+++ b/DOM/Tests/Parser.XML.LinearGradientTests.swift
@@ -32,36 +32,37 @@
import Foundation
-import XCTest
-@testable import SwiftDraw
+@testable import SwiftDrawDOM
+import Testing
-final class ParserXMLLinearGradientTests: XCTestCase {
+struct ParserXMLLinearGradientTests {
- func testParseGradients() throws {
+ @Test
+ func parseGradients() throws {
let child = XML.Element(name: "child")
child.children = [XML.Element.makeMockGradient(), XML.Element.makeMockGradient()]
let parent = XML.Element(name: "parent")
parent.children = [XML.Element.makeMockGradient(), child]
- XCTAssertEqual(try XMLParser().parseLinearGradients(child).count, 2)
- XCTAssertEqual(try XMLParser().parseLinearGradients(parent).count, 3)
+ #expect(try XMLParser().parseLinearGradients(child).count == 2)
+ #expect(try XMLParser().parseLinearGradients(parent).count == 3)
}
- func testParseFile() throws {
+ @Test
+ func parseFile() throws {
+ let dom = try DOM.SVG.parse(fileNamed: "linearGradient.svg", in: .test)
- let dom = try DOM.SVG.parse(fileNamed: "linearGradient.svg")
+ #expect(dom.defs.linearGradients.count == 5)
+ #expect(dom.defs.linearGradients.first(where: { $0.id == "snow" }) != nil)
+ #expect(dom.defs.linearGradients.first(where: { $0.id == "blue" }) != nil)
+ #expect(dom.defs.linearGradients.first(where: { $0.id == "purple" }) != nil)
+ #expect(dom.defs.linearGradients.first(where: { $0.id == "salmon" }) != nil)
+ #expect(dom.defs.linearGradients.first(where: { $0.id == "green" }) != nil)
- XCTAssertEqual(dom.defs.linearGradients.count, 5)
- XCTAssertNotNil(dom.defs.linearGradients.first(where: { $0.id == "snow" }))
- XCTAssertNotNil(dom.defs.linearGradients.first(where: { $0.id == "blue" }))
- XCTAssertNotNil(dom.defs.linearGradients.first(where: { $0.id == "purple" }))
- XCTAssertNotNil(dom.defs.linearGradients.first(where: { $0.id == "salmon" }))
- XCTAssertNotNil(dom.defs.linearGradients.first(where: { $0.id == "green" }))
-
- XCTAssertGreaterThan(dom.childElements.count, 2)
- XCTAssertEqual(dom.childElements[0].attributes.fill, .url(URL(string: "#snow")!))
- XCTAssertEqual(dom.childElements[1].attributes.fill, .url(URL(string: "#blue")!))
+ #expect(dom.childElements.count > 2)
+ #expect(dom.childElements[0].attributes.fill == .url(URL(string: "#snow")!))
+ #expect(dom.childElements[1].attributes.fill == .url(URL(string: "#blue")!))
}
}
diff --git a/DOM/Tests/Parser.XML.PathTests.swift b/DOM/Tests/Parser.XML.PathTests.swift
new file mode 100644
index 00000000..09e68aa5
--- /dev/null
+++ b/DOM/Tests/Parser.XML.PathTests.swift
@@ -0,0 +1,268 @@
+//
+// Parser.XML.PathTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 8/3/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+import SwiftDrawDOM
+import Testing
+
+@testable import SwiftDrawDOM
+
+private typealias Coordinate = DOM.Coordinate
+private typealias Segment = DOM.Path.Segment
+private typealias CoordinateSpace = DOM.Path.Segment.CoordinateSpace
+
+@Suite("Parser XML Path Tests")
+struct ParserXMLPathTests {
+
+ @Test
+ func scanBool() throws {
+ let scanner = XMLParser.PathScanner(string: "true FALSE 0 1")
+
+ #expect(try scanner.scanBool() == true)
+ #expect(try scanner.scanBool() == false)
+ #expect(try scanner.scanBool() == false)
+ #expect(try scanner.scanBool() == true)
+ #expect(throws: (any Error).self) { _ = try scanner.scanBool() }
+ }
+
+ @Test
+ func scanCoordinate() throws {
+ let scanner = XMLParser.PathScanner(string: "10 20.0")
+
+ #expect(try scanner.scanCoordinate() == 10.0)
+ #expect(try scanner.scanCoordinate() == 20.0)
+ #expect(throws: (any Error).self) { _ = try scanner.scanCoordinate() }
+ }
+
+ @Test
+ func equality() {
+ #expect(Segment.move(x: 10, y: 20, space: .relative) == move(10, 20, .relative))
+
+ #expect(Segment.move(x: 20, y: 20, space: .absolute) != move(10, 20, .absolute))
+
+ #expect(Segment.move(x: 10, y: 20, space: .relative) != move(10, 20, .absolute))
+ }
+
+ @Test
+ func empty() throws {
+ let parser = SwiftDrawDOM.XMLParser()
+ #expect(try parser.parsePathSegments("") == [])
+ #expect(try parser.parsePathSegments(" ") == [])
+ }
+
+ @Test
+ func moveSegments() {
+ #expect(parseSegment("M 10 20") == move(10, 20, .absolute))
+ #expect(parseSegment("m 10 20") == move(10, 20, .relative))
+ #expect(parseSegment("M10,20") == move(10, 20, .absolute))
+ #expect(parseSegment("M10;20") == move(10, 20, .absolute))
+ #expect(parseSegment("M 10; 20 ") == move(10, 20, .absolute))
+ #expect(parseSegment("M10-20") == move(10, -20, .absolute))
+
+ #expect(parseSegments("M10-20 5 1") == [move(10, -20, .absolute),
+ line(5, 1, .absolute)])
+
+ #expect(parseSegments("m10-20 5 1") == [move(10, -20, .relative),
+ line(5, 1, .relative)])
+ }
+
+ @Test
+ func lineSegments() {
+ #expect(parseSegment("L 10 20") == line(10, 20, .absolute))
+ #expect(parseSegment("l 10 20") == line(10, 20, .relative))
+ #expect(parseSegment("L10,20") == line(10, 20, .absolute))
+ #expect(parseSegment("L10;20") == line(10, 20, .absolute))
+ #expect(parseSegment(" L 10;20 ") == line(10, 20, .absolute))
+ #expect(parseSegment("L10-20 ") == line(10, -20, .absolute))
+
+ #expect(parseSegments("L10-20 5 1") == [line(10, -20, .absolute),
+ line(5, 1, .absolute)])
+ }
+
+ @Test
+ func horizontalSegments() {
+ #expect(parseSegment("H 10") == horizontal(10, .absolute))
+ #expect(parseSegment("h 10") == horizontal(10, .relative))
+ #expect(parseSegment("H10") == horizontal(10, .absolute))
+ #expect(parseSegment("H10;") == horizontal(10, .absolute))
+ #expect(parseSegment(" H10 ") == horizontal(10, .absolute))
+
+ #expect(parseSegments("h10 5") == [horizontal(10, .relative),
+ horizontal(5, .relative)])
+ }
+
+ @Test
+ func vericalSegments() {
+ #expect(parseSegment("V 10") == vertical(10, .absolute))
+ #expect(parseSegment("v 10") == vertical(10, .relative))
+ #expect(parseSegment("V10") == vertical(10, .absolute))
+ #expect(parseSegment("V10;") == vertical(10, .absolute))
+ #expect(parseSegment(" V10 ") == vertical(10, .absolute))
+ }
+
+ @Test
+ func cubicSmoothSegments() {
+ #expect(parseSegment("S 10 20 50 60") == cubicSmooth(10, 20, 50, 60, .absolute))
+ #expect(parseSegment("s 10 20 50 60") == cubicSmooth(10, 20, 50, 60, .relative))
+ #expect(parseSegment("S10,20,50,60") == cubicSmooth(10, 20, 50, 60, .absolute))
+ #expect(parseSegment("S10;20;50;60") == cubicSmooth(10, 20, 50, 60, .absolute))
+ #expect(parseSegment(" S10; 20; 50; 60") == cubicSmooth(10, 20, 50, 60, .absolute))
+ }
+
+ @Test
+ func quadraticSegments() {
+ #expect(parseSegment("Q 10 20 50 60") == quadratic(10, 20, 50, 60, .absolute))
+ #expect(parseSegment("q 10 20 50 60") == quadratic(10, 20, 50, 60, .relative))
+ #expect(parseSegment("Q10,20,50,60") == quadratic(10, 20, 50, 60, .absolute))
+ #expect(parseSegment("Q10;20;50;60") == quadratic(10, 20, 50, 60, .absolute))
+ #expect(parseSegment(" Q10; 20; 50; 60") == quadratic(10, 20, 50, 60, .absolute))
+ }
+
+ @Test
+ func quadraticSmoothSegments() {
+ #expect(parseSegment("T 10 20") == quadraticSmooth(10, 20, .absolute))
+ #expect(parseSegment("t 10 20") == quadraticSmooth(10, 20, .relative))
+ #expect(parseSegment("T10,20") == quadraticSmooth(10, 20, .absolute))
+ #expect(parseSegment("T10;20") == quadraticSmooth(10, 20, .absolute))
+ #expect(parseSegment(" T10; 20;") == quadraticSmooth(10, 20, .absolute))
+ }
+
+ @Test
+ func arcSegments() {
+ #expect(parseSegment(" A10; 20; 30; 1 0;40 50") == arc(10, 20, 30, true, false, 40, 50, .absolute))
+ }
+
+ @Test
+ func closeSegments() {
+ #expect(parseSegment("Z") == .close)
+ #expect(parseSegment("z") == .close)
+ #expect(parseSegment(" z ") == .close)
+ }
+
+ @Test
+ func path() throws {
+ let node = ["d": "M 10 10 h 10 v 10 h -10 v -10"]
+ let parser = DOMXMLParser()
+
+ let path = try parser.parsePath(node)
+
+ #expect(path.segments.count == 5)
+
+ #expect(path.segments[0] == .move(x: 10, y: 10, space: .absolute))
+ #expect(path.segments[1] == .horizontal(x: 10, space: .relative))
+ #expect(path.segments[2] == .vertical(y: 10, space: .relative))
+ #expect(path.segments[3] == .horizontal(x: -10, space: .relative))
+ #expect(path.segments[4] == .vertical(y: -10, space: .relative))
+ }
+
+ @Test
+ func pathLineBreak() throws {
+ let node = ["d": "M230 520\n \t\t A 45 45, 0, 1, 0, 275 565 \n \t\t L 275 520 Z"]
+ let parser = DOMXMLParser()
+
+ let path = try? parser.parsePath(node)
+
+ #expect(path?.segments.count == 4)
+ }
+
+ @Test
+ func pathLong() throws {
+
+ let node = ["d": "m10,2h-30v-40zm50,60"]
+ let parser = DOMXMLParser()
+
+ let path = try parser.parsePath(node)
+
+ #expect(path.segments.count == 5)
+
+ #expect(path.segments[0] == .move(x: 10, y: 2.0, space: .relative))
+ #expect(path.segments[1] == .horizontal(x: -30, space: .relative))
+ #expect(path.segments[2] == .vertical(y: -40, space: .relative))
+ #expect(path.segments[3] == .close)
+ #expect(path.segments[4] == .move(x: 50, y: 60, space: .relative))
+ }
+}
+
+private func parseSegment(_ text: String) -> Segment? {
+ let parsed = try? XMLParser().parsePathSegments(text)
+ return parsed?[0]
+}
+
+private func parseSegments(_ text: String) -> [Segment] {
+ let parsed = try? XMLParser().parsePathSegments(text)
+ return parsed ?? []
+}
+
+// helpers to create Segments without labels
+// splatting of tuple is no longer supported
+private func move(_ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .move(x: x, y: y, space: space)
+}
+
+private func line(_ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .line(x: x, y: y, space: space)
+}
+
+private func horizontal(_ x: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .horizontal(x: x, space: space)
+}
+
+private func vertical(_ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .vertical(y: y, space: space)
+}
+
+private func cubic(_ x1: Coordinate, _ y1: Coordinate,
+ _ x2: Coordinate, _ y2: Coordinate,
+ _ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .cubic(x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y, space: space)
+}
+
+private func cubicSmooth(_ x2: Coordinate, _ y2: Coordinate,
+ _ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .cubicSmooth(x2: x2, y2: y2, x: x, y: y, space: space)
+}
+
+private func quadratic(_ x1: Coordinate, _ y1: Coordinate,
+ _ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .quadratic(x1: x1, y1: y1, x: x, y: y, space: space)
+}
+
+private func quadraticSmooth(_ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .quadraticSmooth(x: x, y: y, space: space)
+}
+
+private func arc(_ rx: Coordinate, _ ry: Coordinate, _ rotate: Coordinate,
+ _ large: Bool, _ sweep: Bool,
+ _ x: Coordinate, _ y: Coordinate, _ space: CoordinateSpace) -> Segment {
+ return .arc(rx: rx, ry: ry, rotate: rotate,
+ large: large, sweep: sweep,
+ x: x, y: y, space: space)
+}
diff --git a/DOM/Tests/Parser.XML.PatternTests.swift b/DOM/Tests/Parser.XML.PatternTests.swift
new file mode 100644
index 00000000..bd2e5e36
--- /dev/null
+++ b/DOM/Tests/Parser.XML.PatternTests.swift
@@ -0,0 +1,100 @@
+//
+// Parser.XML.PatternTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 26/3/19.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+
+private typealias Coordinate = DOM.Coordinate
+
+@Suite("Parser XML Pattern Tests")
+struct ParserXMLPatternTests {
+
+ @Test
+ func pattern() throws {
+ let pattern = try XMLParser().parsePattern(["id": "p1", "width": "10", "height": "20"])
+
+ #expect(pattern.id == "p1")
+ #expect(pattern.width == 10)
+ #expect(pattern.height == 20)
+
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parsePattern(["width": "10", "height": "20"])
+ }
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parsePattern(["id": "p1", "height": "20"])
+ }
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parsePattern(["id": "p1", "width": "10"])
+ }
+ }
+
+ @Test
+ func patternUnits() throws {
+ var node = ["id": "p1", "width": "10", "height": "20"]
+
+ var pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternUnits == nil)
+
+ node["patternUnits"] = "userSpaceOnUse"
+ pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternUnits == .userSpaceOnUse)
+
+ node["patternUnits"] = "objectBoundingBox"
+ pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternUnits == .objectBoundingBox)
+
+ node["patternUnits"] = "invalid"
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parsePattern(node)
+ }
+ }
+
+ @Test
+ func patternContentUnits() throws {
+ var node = ["id": "p1", "width": "10", "height": "20"]
+
+ var pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternContentUnits == nil)
+
+ node["patternContentUnits"] = "userSpaceOnUse"
+ pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternContentUnits == .userSpaceOnUse)
+
+ node["patternContentUnits"] = "objectBoundingBox"
+ pattern = try XMLParser().parsePattern(node)
+ #expect(pattern.patternContentUnits == .objectBoundingBox)
+
+ node["patternContentUnits"] = "invalid"
+ #expect(throws: (any Error).self) {
+ _ = try XMLParser().parsePattern(node)
+ }
+ }
+}
diff --git a/SwiftDrawTests/Parser.XML.RadialGradientTests.swift b/DOM/Tests/Parser.XML.RadialGradientTests.swift
similarity index 60%
rename from SwiftDrawTests/Parser.XML.RadialGradientTests.swift
rename to DOM/Tests/Parser.XML.RadialGradientTests.swift
index 550becd9..47005885 100644
--- a/SwiftDrawTests/Parser.XML.RadialGradientTests.swift
+++ b/DOM/Tests/Parser.XML.RadialGradientTests.swift
@@ -32,23 +32,26 @@
import Foundation
-import XCTest
-@testable import SwiftDraw
+import Testing
+@testable import SwiftDrawDOM
-final class ParserXMLRadialGradientTests: XCTestCase {
+@Suite("Parser XML Radial Gradient Tests")
+struct ParserXMLRadialGradientTests {
- func testParseGradients() throws {
+ @Test
+ func parseGradients() throws {
let child = XML.Element(name: "child")
child.children = [XML.Element.makeMockGradient(), XML.Element.makeMockGradient()]
let parent = XML.Element(name: "parent")
parent.children = [XML.Element.makeMockGradient(), child]
- XCTAssertEqual(try XMLParser().parseRadialGradients(child).count, 2)
- XCTAssertEqual(try XMLParser().parseRadialGradients(parent).count, 3)
+ #expect(try XMLParser().parseRadialGradients(child).count == 2)
+ #expect(try XMLParser().parseRadialGradients(parent).count == 3)
}
- func testParseCoordinates() throws {
+ @Test
+ func parseCoordinates() throws {
let element = XML.Element.makeMockGradient()
element.attributes["r"] = "0.1"
element.attributes["cx"] = "0.2"
@@ -58,28 +61,29 @@ final class ParserXMLRadialGradientTests: XCTestCase {
element.attributes["fy"] = "0.6"
let gradient = try XMLParser().parseRadialGradient(element)
- XCTAssertEqual(gradient.r, 0.1)
- XCTAssertEqual(gradient.cx, 0.2)
- XCTAssertEqual(gradient.cy, 0.3)
- XCTAssertEqual(gradient.fr, 0.4)
- XCTAssertEqual(gradient.fx, 0.5)
- XCTAssertEqual(gradient.fy, 0.6)
+ #expect(gradient.r == 0.1)
+ #expect(gradient.cx == 0.2)
+ #expect(gradient.cy == 0.3)
+ #expect(gradient.fr == 0.4)
+ #expect(gradient.fx == 0.5)
+ #expect(gradient.fy == 0.6)
}
- func testParseFile() throws {
+ @Test
+ func parseFile() throws {
- let dom = try DOM.SVG.parse(fileNamed: "radialGradient.svg")
+ let dom = try DOM.SVG.parse(fileNamed: "radialGradient.svg", in: .test)
- XCTAssertEqual(dom.defs.radialGradients.count, 5)
- XCTAssertNotNil(dom.defs.radialGradients.first(where: { $0.id == "snow" }))
- XCTAssertNotNil(dom.defs.radialGradients.first(where: { $0.id == "blue" }))
- XCTAssertNotNil(dom.defs.radialGradients.first(where: { $0.id == "purple" }))
- XCTAssertNotNil(dom.defs.radialGradients.first(where: { $0.id == "salmon" }))
- XCTAssertNotNil(dom.defs.radialGradients.first(where: { $0.id == "green" }))
+ #expect(dom.defs.radialGradients.count == 5)
+ #expect(dom.defs.radialGradients.first(where: { $0.id == "snow" }) != nil)
+ #expect(dom.defs.radialGradients.first(where: { $0.id == "blue" }) != nil)
+ #expect(dom.defs.radialGradients.first(where: { $0.id == "purple" }) != nil)
+ #expect(dom.defs.radialGradients.first(where: { $0.id == "salmon" }) != nil)
+ #expect(dom.defs.radialGradients.first(where: { $0.id == "green" }) != nil)
- XCTAssertGreaterThan(dom.childElements.count, 2)
- XCTAssertEqual(dom.childElements[0].attributes.fill, .url(URL(string: "#snow")!))
- XCTAssertEqual(dom.childElements[1].attributes.fill, .url(URL(string: "#blue")!))
+ #expect(dom.childElements.count > 2)
+ #expect(dom.childElements[0].attributes.fill == .url(URL(string: "#snow")!))
+ #expect(dom.childElements[1].attributes.fill == .url(URL(string: "#blue")!))
}
}
diff --git a/DOM/Tests/Parser.XML.StyleSheetTests.swift b/DOM/Tests/Parser.XML.StyleSheetTests.swift
new file mode 100644
index 00000000..7c78189c
--- /dev/null
+++ b/DOM/Tests/Parser.XML.StyleSheetTests.swift
@@ -0,0 +1,157 @@
+//
+// Parser.XML.StyleSheetTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 18/8/22.
+// Copyright 2022 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("Parser XML StyleSheet Tests")
+struct ParserXMLStyleSheetTests {
+
+ @Test
+ func parsesStyleSheetsSelectors() throws {
+ let dom = try DOM.SVG.parse(fileNamed: "stylesheet.svg", in: .test)
+
+ let keys = Set(dom.styles.flatMap(\.attributes.keys))
+ #expect(
+ keys == [
+ .class("s"),
+ .class("b"),
+ .element("rect"),
+ .class("o"),
+ .class("g"),
+ .element("circle"),
+ .element("g"),
+ .id("a")
+ ]
+ )
+ }
+
+ @Test
+ func parsesSelectors() throws {
+ let entries = try XMLParser.parseEntries(
+ """
+ .s {
+ stroke: darkgray;
+ stroke-width: 5 /* asd */;
+ fill-opacity: 0.3
+ }
+
+ /* comment */
+ /* another */
+
+ .b {
+ fill: blue;
+ }
+
+ rect {
+ fill: pink;
+ }
+ /* comment */
+ """
+ )
+
+ #expect(
+ entries == [
+ .class("s"): ["stroke": "darkgray", "stroke-width": "5", "fill-opacity": "0.3"],
+ .class("b"): ["fill": "blue"],
+ .element("rect"): ["fill": "pink"]
+ ]
+ )
+ }
+
+ @Test
+ func parsesStyleSheet() throws {
+ let sheet = try XMLParser().parseStyleSheetElement(
+ """
+ .s {
+ stroke: darkgray;
+ stroke-width: 5 /* asd */;
+ fill-opacity: 30%
+ }
+
+ /* comment */
+ /* another */
+
+ .b {
+ fill: blue;
+ }
+
+ rect {
+ fill: pink;
+ }
+ /* comment */
+ """
+ ).attributes
+
+ #expect(sheet[.class("s")]?.stroke == .color(.keyword(.darkgray)))
+ #expect(sheet[.class("s")]?.strokeWidth == 5)
+ #expect(sheet[.class("s")]?.fillOpacity == 0.3)
+ #expect(sheet[.class("b")]?.fill == .color(.keyword(.blue)))
+ #expect(sheet[.element("rect")]?.fill == .color(.keyword(.pink)))
+ }
+
+ @Test
+ func mergesSelectors() throws {
+ let entries = try XMLParser.parseEntries(
+ """
+ .a {
+ fill: red;
+ }
+ .a {
+ stroke: blue;
+ }
+ .a {
+ fill: purple;
+ }
+ """
+ )
+
+ #expect(entries == [.class("a"): ["fill": "purple", "stroke": "blue"]])
+ }
+
+ @Test
+ func mutlipleSelectors() throws {
+ let entries = try XMLParser.parseEntries(
+ """
+ .a, .b {
+ fill: red;
+ }
+ """
+ )
+
+ #expect(
+ entries == [
+ .class("a"): ["fill": "red"],
+ .class("b"): ["fill": "red"]
+ ]
+ )
+ }
+}
diff --git a/SwiftDrawTests/Parser.XML.TextTests.swift b/DOM/Tests/Parser.XML.TextTests.swift
similarity index 65%
rename from SwiftDrawTests/Parser.XML.TextTests.swift
rename to DOM/Tests/Parser.XML.TextTests.swift
index 7e9e13f5..2dc2aa26 100644
--- a/SwiftDrawTests/Parser.XML.TextTests.swift
+++ b/DOM/Tests/Parser.XML.TextTests.swift
@@ -29,12 +29,15 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import XCTest
-@testable import SwiftDraw
-final class ParserXMLTextTests: XCTestCase {
+import Testing
+@testable import SwiftDrawDOM
- func testTextNodeParses() throws {
+@Suite("Parser XML Text Tests")
+struct ParserXMLTextTests {
+
+ @Test
+ func textNodeParses() throws {
let el = XML.Element(name: "text", attributes: [:])
el.innerText = "Simon"
el.attributes["x"] = "1"
@@ -43,21 +46,25 @@ final class ParserXMLTextTests: XCTestCase {
el.attributes["font-size"] = "12.5"
el.attributes["text-anchor"] = "end"
- let text = try XCTUnwrap(XMLParser().parseGraphicsElement(el) as? DOM.Text)
- XCTAssertEqual(text.x, 1)
- XCTAssertEqual(text.y, 2)
- XCTAssertEqual(text.value, "Simon")
- XCTAssertEqual(text.attributes.fontFamily, "Futura")
- XCTAssertEqual(text.attributes.fontSize, 12.5)
- XCTAssertEqual(text.attributes.textAnchor, .end)
+ let parsed = try XMLParser().parseGraphicsElement(el) as? DOM.Text
+ let text = try #require(parsed)
+
+ #expect(text.x == 1)
+ #expect(text.y == 2)
+ #expect(text.value == "Simon")
+ #expect(text.attributes.fontFamily == "Futura")
+ #expect(text.attributes.fontSize == 12.5)
+ #expect(text.attributes.textAnchor == .end)
}
- func testEmptyTextNodeReturnsNil() {
+ @Test
+ func emptyTextNodeReturnsNil() throws {
let el = XML.Element(name: "text", attributes: [:])
- XCTAssertNil(try XMLParser().parseText(["x": "1", "y": "1"], element: el))
+ let first = try XMLParser().parseText(["x": "1", "y": "1"], element: el)
+ #expect(first == nil)
+
el.innerText = " "
- XCTAssertNil(try XMLParser().parseText(["x": "1", "y": "1"], element: el))
+ let second = try XMLParser().parseText(["x": "1", "y": "1"], element: el)
+ #expect(second == nil)
}
}
-
-
diff --git a/DOM/Tests/Parser.XML.TransformTests.swift b/DOM/Tests/Parser.XML.TransformTests.swift
new file mode 100644
index 00000000..2828ebae
--- /dev/null
+++ b/DOM/Tests/Parser.XML.TransformTests.swift
@@ -0,0 +1,148 @@
+//
+// Parser.XML.TransformTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("Parser Transform Tests")
+struct ParserTransformTests {
+
+ @Test
+ func matrix() throws {
+ #expect(try XMLParser().parseTransform("matrix(0 1 2 3 4 5)") ==
+ [.matrix(a: 0, b: 1, c: 2, d: 3, e: 4, f: 5)])
+ #expect(try XMLParser().parseTransform("matrix(0,1,2,3,4,5)") ==
+ [.matrix(a: 0, b: 1, c: 2, d: 3, e: 4, f: 5)])
+ #expect(try XMLParser().parseTransform("matrix(1.1,1.2,1.3,1.4,1.5,1.6)") ==
+ [.matrix(a: 1.1, b: 1.2, c: 1.3, d: 1.4, e: 1.5, f: 1.6)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("matrix(0 1 a b 4 5)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("matrix(0 1 2)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("matrix(0 1 2 3 4 5") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("matrix 0 1 2 3 4 5)") }
+ }
+
+ @Test
+ func translate() throws {
+ #expect(try XMLParser().parseTransform("translate(5)") ==
+ [.translate(tx: 5, ty: 0)])
+ #expect(try XMLParser().parseTransform("translate(5, 6)") ==
+ [.translate(tx: 5, ty: 6)])
+ #expect(try XMLParser().parseTransform("translate(5 6)") ==
+ [.translate(tx: 5, ty: 6)])
+ #expect(try XMLParser().parseTransform("translate(1.3, 4.5)") ==
+ [.translate(tx: 1.3, ty: 4.5)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("translate(5 a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("translate(0 1 2)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("translate(0 1") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("translate 0 1)") }
+ }
+
+ @Test
+ func scale() throws {
+ #expect(try XMLParser().parseTransform("scale(5)") ==
+ [.scale(sx: 5, sy: 5)])
+ #expect(try XMLParser().parseTransform("scale(5, 6)") ==
+ [.scale(sx: 5, sy: 6)])
+ #expect(try XMLParser().parseTransform("scale(5 6)") ==
+ [.scale(sx: 5, sy: 6)])
+ #expect(try XMLParser().parseTransform("scale(1.3, 4.5)") ==
+ [.scale(sx: 1.3, sy: 4.5)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("scale(5 a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("scale(0 1 2)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("scale(0 1") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("scale 0 1)") }
+ }
+
+ @Test
+ func rotate() throws {
+ #expect(try XMLParser().parseTransform("rotate(5)") ==
+ [.rotate(angle: 5)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate(a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate()") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate(1") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate 1)") }
+ }
+
+ @Test
+ func rotatePoint() throws {
+ #expect(try XMLParser().parseTransform("rotate(5, 10, 20)") ==
+ [.rotatePoint(angle: 5, cx: 10, cy: 20)])
+ #expect(try XMLParser().parseTransform("rotate(5 10 20)") ==
+ [.rotatePoint(angle: 5, cx: 10, cy: 20)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate(5 10 a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate(5 10)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate(5 10 20") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("rotate 5 10 20)") }
+ }
+
+ @Test
+ func skewX() throws {
+ #expect(try XMLParser().parseTransform("skewX(5)") ==
+ [.skewX(angle: 5)])
+ #expect(try XMLParser().parseTransform("skewX(6.7)") ==
+ [.skewX(angle: 6.7)])
+ #expect(try XMLParser().parseTransform("skewX(0)") ==
+ [.skewX(angle: 0)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewX(a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewX()") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewX(1") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewX 1)") }
+ }
+
+ @Test
+ func skewY() throws {
+ #expect(try XMLParser().parseTransform("skewY(5)") ==
+ [.skewY(angle: 5)])
+ #expect(try XMLParser().parseTransform("skewY(6.7)") ==
+ [.skewY(angle: 6.7)])
+ #expect(try XMLParser().parseTransform("skewY(0)") ==
+ [.skewY(angle: 0)])
+
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewY(a)") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewY()") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewY(1") }
+ #expect(throws: (any Error).self) { _ = try XMLParser().parseTransform("skewY 1)") }
+ }
+
+ @Test
+ func transform() throws {
+ #expect(try XMLParser().parseTransform("scale(2) translate(4) scale(5, 5) ") ==
+ [.scale(sx: 2, sy: 2),
+ .translate(tx: 4, ty: 0),
+ .scale(sx: 5, sy: 5)])
+ }
+}
diff --git a/DOM/Tests/ParserSVGImageTests.swift b/DOM/Tests/ParserSVGImageTests.swift
new file mode 100644
index 00000000..4ab1b283
--- /dev/null
+++ b/DOM/Tests/ParserSVGImageTests.swift
@@ -0,0 +1,125 @@
+//
+// Parser.ImageTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 28/1/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+import Foundation
+
+@Suite("Parser SVG Image Tests")
+struct ParserSVGImageTests {
+
+ @Test
+ func shapes() throws {
+ let svg = try DOM.SVG.parse(fileNamed: "shapes.svg", in: .test)
+
+ #expect(svg.width == 500)
+ #expect(svg.height == 700)
+ #expect(svg.viewBox?.width == 500)
+ #expect(svg.viewBox?.height == 700)
+ #expect(svg.defs.clipPaths.count == 2)
+ #expect(svg.defs.linearGradients.count == 1)
+ #expect(svg.defs.radialGradients.count == 1)
+ #expect(svg.defs.elements["star"] != nil)
+ #expect(svg.defs.elements.count == 2)
+
+ var c = svg.childElements.enumerated().makeIterator()
+
+ #expect(c.next()!.element is DOM.Ellipse)
+ #expect(c.next()!.element is DOM.Group)
+ #expect(c.next()!.element is DOM.Circle)
+ #expect(c.next()!.element is DOM.Group)
+ #expect(c.next()!.element is DOM.Line)
+ #expect(c.next()!.element is DOM.Path)
+ #expect(c.next()!.element is DOM.Path)
+ #expect(c.next()!.element is DOM.Path)
+ #expect(c.next()!.element is DOM.Path)
+ #expect(c.next()!.element is DOM.Polyline)
+ #expect(c.next()!.element is DOM.Polyline)
+ #expect(c.next()!.element is DOM.Polygon)
+ #expect(c.next()!.element is DOM.Group)
+ #expect(c.next()!.element is DOM.Circle)
+ #expect(c.next()!.element is DOM.Switch)
+ #expect(c.next()!.element is DOM.Rect)
+ #expect(c.next()!.element is DOM.Text)
+ #expect(c.next()!.element is DOM.Text)
+ #expect(c.next()!.element is DOM.Line)
+ #expect(c.next()!.element is DOM.Use)
+ #expect(c.next()!.element is DOM.Use)
+ #expect(c.next()!.element is DOM.Rect)
+ #expect(c.next() == nil)
+ }
+
+ @Test
+ func starry() throws {
+ let svg = try DOM.SVG.parse(fileNamed: "starry.svg", in: .test)
+ guard let g = svg.childElements.first as? DOM.Group,
+ let g1 = g.childElements.first as? DOM.Group else {
+ Issue.record("missing group")
+ return
+ }
+
+ #expect(svg.width == 500)
+ #expect(svg.height == 500)
+
+ #expect(g1.childElements.count == 9323)
+
+ var counter = [String: Int]()
+
+ for e in g1.childElements {
+ let key = String(describing: type(of: e))
+ counter[key] = (counter[key] ?? 0) + 1
+ }
+
+ #expect(counter["Path"] == 9314)
+ #expect(counter["Polygon"] == 9)
+ }
+
+ @Test
+ func quad() throws {
+ let svg = try DOM.SVG.parse(fileNamed: "quad.svg", in: .test)
+ #expect(svg.width == 1000)
+ #expect(svg.height == 500)
+ }
+
+ @Test
+ func curves() throws {
+ let svg = try DOM.SVG.parse(fileNamed: "curves.svg", in: .test)
+ #expect(svg.width == 550)
+ #expect(svg.height == 350)
+ }
+
+ @Test
+ func nested() throws {
+ let svg = try DOM.SVG.parse(fileNamed: "nested-svg.svg", in: .test)
+ #expect(svg.width == 360)
+ #expect(svg.height == 450)
+ }
+}
diff --git a/DOM/Tests/StyleTests.swift b/DOM/Tests/StyleTests.swift
new file mode 100644
index 00000000..8ac76ee0
--- /dev/null
+++ b/DOM/Tests/StyleTests.swift
@@ -0,0 +1,69 @@
+//
+// StyleTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 27/2/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("Style Tests")
+struct StyleTests {
+
+ @Test
+ func style() throws {
+ #expect(try XMLParser().parseStyleAttributes("selector: hi;") == ["selector": "hi"])
+ #expect(try XMLParser().parseStyleAttributes("selector: hi") == ["selector": "hi"])
+ #expect(try XMLParser().parseStyleAttributes("selector: hi ") == ["selector": "hi"])
+ #expect(try XMLParser().parseStyleAttributes(" trans-form : rotate(4)") == ["trans-form": "rotate(4)"])
+
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseStyleAttributes("selector")
+ }
+ #expect(throws: (any Error).self) {
+ try XMLParser().parseStyleAttributes(": hmm")
+ }
+ }
+
+ @Test
+ func styles() throws {
+ let e = XML.Element(name: "line")
+ e.attributes["x"] = "5"
+ e.attributes["y"] = "5"
+ e.attributes["stroke-color"] = "black"
+ e.attributes["style"] = "fill: red; x: 20"
+
+ // Style attributes should override any XML.Element attribute
+ let att = try XMLParser().parseAttributes(e)
+
+ #expect(try att.parseCoordinate("x") == 20.0)
+ #expect(try att.parseCoordinate("y") == 5.0)
+ #expect(try att.parseColor("stroke-color") == .keyword(.black))
+ #expect(try att.parseColor("fill") == .keyword(.red))
+ }
+}
diff --git a/SwiftDrawTests/Test.bundle/curves.svg b/DOM/Tests/Test.bundle/curves.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/curves.svg
rename to DOM/Tests/Test.bundle/curves.svg
diff --git a/SwiftDrawTests/Test.bundle/linearGradient.svg b/DOM/Tests/Test.bundle/linearGradient.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/linearGradient.svg
rename to DOM/Tests/Test.bundle/linearGradient.svg
diff --git a/DOM/Tests/Test.bundle/nested-svg.svg b/DOM/Tests/Test.bundle/nested-svg.svg
new file mode 100644
index 00000000..ae0b0807
--- /dev/null
+++ b/DOM/Tests/Test.bundle/nested-svg.svg
@@ -0,0 +1,38 @@
+
+
diff --git a/SwiftDrawTests/Test.bundle/quad.svg b/DOM/Tests/Test.bundle/quad.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/quad.svg
rename to DOM/Tests/Test.bundle/quad.svg
diff --git a/SwiftDrawTests/Test.bundle/radialGradient.svg b/DOM/Tests/Test.bundle/radialGradient.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/radialGradient.svg
rename to DOM/Tests/Test.bundle/radialGradient.svg
diff --git a/SwiftDrawTests/Test.bundle/shapes.svg b/DOM/Tests/Test.bundle/shapes.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/shapes.svg
rename to DOM/Tests/Test.bundle/shapes.svg
diff --git a/SwiftDrawTests/Test.bundle/starry.svg b/DOM/Tests/Test.bundle/starry.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/starry.svg
rename to DOM/Tests/Test.bundle/starry.svg
diff --git a/SwiftDrawTests/Test.bundle/stylesheet.svg b/DOM/Tests/Test.bundle/stylesheet.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/stylesheet.svg
rename to DOM/Tests/Test.bundle/stylesheet.svg
diff --git a/SwiftDrawTests/UseTests.swift b/DOM/Tests/UseTests.swift
similarity index 66%
rename from SwiftDrawTests/UseTests.swift
rename to DOM/Tests/UseTests.swift
index f77d9508..1732d773 100644
--- a/SwiftDrawTests/UseTests.swift
+++ b/DOM/Tests/UseTests.swift
@@ -29,25 +29,27 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import XCTest
-@testable import SwiftDraw
+import Testing
+@testable import SwiftDrawDOM
-final class UseTests: XCTestCase {
-
- func testUse() throws {
- var node = ["xlink:href": "#line2", "href": "#line1"]
-
- var parsed = try XMLParser().parseUse(node)
- XCTAssertEqual(parsed.href.fragment, "line2")
- XCTAssertNil(parsed.x)
- XCTAssertNil(parsed.y)
-
- node["x"] = "20"
- node["y"] = "30"
-
- parsed = try XMLParser().parseUse(node)
- XCTAssertEqual(parsed.href.fragment, "line2")
- XCTAssertEqual(parsed.x, 20)
- XCTAssertEqual(parsed.y, 30)
- }
+@Suite("Use Tests")
+struct UseTests {
+
+ @Test
+ func use() throws {
+ var node = ["xlink:href": "#line2", "href": "#line1"]
+
+ var parsed = try XMLParser().parseUse(node)
+ #expect(parsed.href.fragmentID == "line2")
+ #expect(parsed.x == nil)
+ #expect(parsed.y == nil)
+
+ node["x"] = "20"
+ node["y"] = "30"
+
+ parsed = try XMLParser().parseUse(node)
+ #expect(parsed.href.fragmentID == "line2")
+ #expect(parsed.x == 20)
+ #expect(parsed.y == 30)
+ }
}
diff --git a/DOM/Tests/ValueParserTests.swift b/DOM/Tests/ValueParserTests.swift
new file mode 100644
index 00000000..68ccca63
--- /dev/null
+++ b/DOM/Tests/ValueParserTests.swift
@@ -0,0 +1,172 @@
+//
+// ValueParserTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 6/3/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("Value Parser Tests")
+struct ValueParserTests {
+
+ var parser = XMLParser.ValueParser()
+
+ @Test
+ func float() throws {
+ #expect(try parser.parseFloat("10") == 10)
+ #expect(try parser.parseFloat("10.0") == 10.0)
+
+ #expect(throws: (any Error).self) { _ = try parser.parseFloat("") }
+ // #expect(throws: (any Error).self) { _ = try parser.parseFloat("10a") }
+ }
+
+ @Test
+ func floats() throws {
+ #expect(try parser.parseFloats("10 20 30.5") == [10, 20, 30.5])
+ #expect(try parser.parseFloats("10.0") == [10.0])
+ #expect(try parser.parseFloats("5 10 1 5") == [5, 10, 1, 5])
+ #expect(try parser.parseFloats(" 1, 2.5, 3.5 ") == [1, 2.5, 3.5])
+ #expect(try parser.parseFloats(" ") == [])
+ #expect(try parser.parseFloats("") == [])
+
+ // #expect(throws: (any Error).self) { _ = try parser.parseFloats("") }
+ // #expect(throws: (any Error).self) { _ = try parser.parseFloat("10a") }
+ }
+
+ @Test
+ func percentage() throws {
+ #expect(try parser.parsePercentage("0") == 0)
+ #expect(try parser.parsePercentage("1") == 1)
+ #expect(try parser.parsePercentage("0.45") == 0.45)
+ #expect(try parser.parsePercentage("0.0%") == 0)
+ #expect(try parser.parsePercentage("100%") == 1)
+ #expect(try parser.parsePercentage("55%") == 0.55)
+ #expect(try parser.parsePercentage("10.25%") == 0.1025)
+
+ #expect(throws: (any Error).self) { _ = try parser.parsePercentage("100") }
+ #expect(throws: (any Error).self) { _ = try parser.parsePercentage("asd") }
+ #expect(throws: (any Error).self) { _ = try parser.parsePercentage(" ") }
+ // #expect(throws: (any Error).self) { _ = try parser.parseFloat("10a") }
+ }
+
+ @Test
+ func coordinate() throws {
+ #expect(try parser.parseCoordinate("0") == 0)
+ #expect(try parser.parseCoordinate("0.0") == 0)
+ #expect(try parser.parseCoordinate("100") == 100)
+ #expect(try parser.parseCoordinate("25.0") == 25.0)
+ #expect(try parser.parseCoordinate("-25.0") == -25.0)
+
+ #expect(throws: (any Error).self) { _ = try parser.parseCoordinate("asd") }
+ #expect(throws: (any Error).self) { _ = try parser.parseCoordinate(" ") }
+ }
+
+ @Test
+ func length() throws {
+ #expect(try parser.parseLength("0") == 0)
+ #expect(try parser.parseLength("100") == 100)
+ #expect(try parser.parseLength("25") == 25)
+ #expect(try parser.parseLength("1.3") == 1) // should error?
+
+ #expect(throws: (any Error).self) { _ = try parser.parseLength("asd") }
+ #expect(throws: (any Error).self) { _ = try parser.parseLength(" ") }
+ #expect(throws: (any Error).self) { _ = try parser.parseLength("-25") }
+ }
+
+ @Test
+ func bools() throws {
+ #expect(try parser.parseBool("false") == false)
+ #expect(try parser.parseBool("FALSE") == false)
+ #expect(try parser.parseBool("true") == true)
+ #expect(try parser.parseBool("TRUE") == true)
+ #expect(try parser.parseBool("1") == true)
+ #expect(try parser.parseBool("0") == false)
+
+ #expect(throws: (any Error).self) { _ = try parser.parseBool("asd") }
+ #expect(throws: (any Error).self) { _ = try parser.parseBool("yes") }
+ }
+
+ @Test
+ func fill() throws {
+ #expect(try parser.parseFill("none") == .color(.none))
+ #expect(try parser.parseFill("black") == .color(.keyword(.black)))
+ #expect(try parser.parseFill("red") == .color(.keyword(.red)))
+
+ #expect(try parser.parseFill("rgb(10,20,30)") == .color(.rgbi(10, 20, 30, 1.0)))
+ #expect(try parser.parseFill("rgb(10%,20%,100%)") == .color(.rgbf(0.1, 0.2, 1.0, 1.0)))
+ #expect(try parser.parseFill("rgba(10, 20, 30, 0.5)") == .color(.rgbi(10, 20, 30, 0.5)))
+ #expect(try parser.parseFill("rgba(10%,20%,100%,0.6)") == .color(.rgbf(0.1, 0.2, 1.0, 0.6)))
+ #expect(try parser.parseFill("#AAFF00") == .color(.hex(170, 255, 0)))
+
+ #expect(try parser.parseFill("url(#test)") == .url(URL(string: "#test")!))
+
+ #expect(throws: (any Error).self) { _ = try parser.parseFill("Ns ") }
+ #expect(throws: (any Error).self) { _ = try parser.parseFill("d") }
+ #expect(throws: (any Error).self) { _ = try parser.parseFill("url()") }
+ // #expect(throws: (any Error).self) { _ = try parser.parseFill("url(asdf") }
+ }
+
+ @Test
+ func url() throws {
+ #if canImport(Darwin)
+ #expect(try parser.parseUrl("#testingš").fragmentID == "testingš")
+ #else
+ #expect(try parser.parseUrl("#testing").fragmentID == "testing")
+ #endif
+ #expect(try parser.parseUrl("http://www.google.com").host == "www.google.com")
+ }
+
+ @Test
+ func urlSelector() throws {
+ #expect(try parser.parseUrlSelector("url(#testingId)").fragmentID == "testingId")
+ #expect(try parser.parseUrlSelector("url(http://www.google.com)").host == "www.google.com")
+
+ #expect(throws: (any Error).self) { _ = try parser.parseUrlSelector("url(#testingId) other") }
+ }
+
+ @Test
+ func points() throws {
+ #expect(try parser.parsePoints("0 1 2 3") == [DOM.Point(0, 1), DOM.Point(2, 3)])
+ #expect(try parser.parsePoints("0,1 2,3") == [DOM.Point(0, 1), DOM.Point(2, 3)])
+ #expect(try parser.parsePoints("0 1.5 1e4 2.4") == [DOM.Point(0, 1.5), DOM.Point(1e4, 2.4)])
+ // #expect(try parser.parsePoints("0 1 2 3 5.0 6.5") == [0, 1 ,2])
+ }
+
+ @Test
+ func raw() throws {
+ #expect(try parser.parseRaw("evenodd") as DOM.FillRule == .evenodd)
+ #expect(try parser.parseRaw("round") as DOM.LineCap == .round)
+ #expect(try parser.parseRaw("miter") as DOM.LineJoin == .miter)
+
+ #expect(throws: (any Error).self) {
+ let _: DOM.LineJoin = try parser.parseRaw("sd")
+ }
+ }
+}
diff --git a/DOM/Tests/XML.Parser.ScannerTests.swift b/DOM/Tests/XML.Parser.ScannerTests.swift
new file mode 100644
index 00000000..c25b20cb
--- /dev/null
+++ b/DOM/Tests/XML.Parser.ScannerTests.swift
@@ -0,0 +1,271 @@
+//
+// ScannerTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/12/16.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+public import Foundation
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("Scanner Tests")
+struct ScannerTests {
+
+ @Test
+ func isEOF() throws {
+ var scanner = XMLParser.Scanner(text: "Hi")
+ #expect(scanner.isEOF == false)
+ try scanner.scanString("Hi")
+ #expect(scanner.isEOF == true)
+ }
+
+ @Test
+ func scanCharsetHex() throws {
+ var scanner = XMLParser.Scanner(text: " \t 8badf00d \t \t 007")
+ #expect(try scanner.scanString(matchingAny: .hexadecimal) == "8badf00d")
+ #expect(try scanner.scanString(matchingAny: .hexadecimal) == "007")
+ #expect(throws: (any Error).self) { _ = try scanner.scanString(matchingAny: .hexadecimal) }
+ }
+
+ @Test
+ func scanCharsetEmoji() throws {
+ var scanner = XMLParser.Scanner(text: " \t 8badf00d \tš¶ \tšš¦šŗ 007")
+ let emoji: Foundation.CharacterSet = "š¤ ššš¶\u{1f1e6}\u{1f1fa}"
+
+ #expect(throws: (any Error).self) { _ = try scanner.scanString(matchingAny: emoji) }
+ #expect(try scanner.scanString(matchingAny: .hexadecimal) == "8badf00d")
+ #expect(throws: (any Error).self) { _ = try scanner.scanString(matchingAny: .hexadecimal) }
+ #expect(try scanner.scanString(matchingAny: emoji) == "š¶")
+ #expect(throws: (any Error).self) { _ = try scanner.scanString(matchingAny: .hexadecimal) }
+ #expect(try scanner.scanString(matchingAny: emoji) == "šš¦šŗ")
+ #expect(throws: (any Error).self) { _ = try scanner.scanString(matchingAny: emoji) }
+ #expect(try scanner.scanString(matchingAny: .hexadecimal) == "007")
+ }
+
+ @Test
+ func scanString() throws {
+ var scanner = XMLParser.Scanner(text: " \t The quick brown fox")
+
+ #expect(throws: (any Error).self) { _ = try scanner.scanString("fox") }
+ #expect(throws: Never.self) { try scanner.scanString("The") }
+ #expect(throws: (any Error).self) { _ = try scanner.scanString("quick fox") }
+
+ #expect(throws: Never.self) { try scanner.scanString("quick brown") }
+ #expect(throws: Never.self) { try scanner.scanString("fox") }
+ #expect(throws: (any Error).self) { _ = try scanner.scanString("fox") }
+ }
+
+ @Test
+ func scanCase() throws {
+ var scanner = XMLParser.Scanner(text: "NOT OK")
+ #expect(try scanner.scanCase(from: Token.self) == .nok)
+ #expect(try scanner.scanCase(from: Token.self) == .ok)
+ #expect(throws: (any Error).self) { _ = try scanner.scanCase(from: Token.self) }
+ }
+
+ @Test
+ func scanCharacter() throws {
+ var scanner = XMLParser.Scanner(text: " \t The fox 8badf00d ")
+
+ #expect(throws: (any Error).self) { _ = try scanner.scanCharacter(matchingAny: "qfxh") }
+ #expect(try scanner.scanCharacter(matchingAny: "fxT") == "T")
+ #expect(throws: (any Error).self) { _ = try scanner.scanCharacter(matchingAny: "fxT") }
+ #expect(try scanner.scanCharacter(matchingAny: "qfxh") == "h")
+ #expect(throws: Never.self) { try scanner.scanString("e fox") }
+
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "8")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "b")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "a")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "d")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "f")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "0")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "0")
+ #expect(try scanner.scanCharacter(matchingAny: .hexadecimal) == "d")
+ }
+
+ @Test
+ func scan_UInt8() {
+ #expect(scanUInt8("0") == 0)
+ #expect(scanUInt8("124") == 124)
+ #expect(scanUInt8(" 045") == 45)
+ #if canImport(Darwin)
+ #expect(scanUInt8("-29") == nil)
+ #endif
+ #expect(scanUInt8("ab24") == nil)
+ }
+
+ @Test
+ func scan_Float() {
+ #expect(scanFloat("0") == 0)
+ #expect(scanFloat("124") == 124)
+ #expect(scanFloat(" 045") == 45)
+ #expect(scanFloat("-29") == -29.0)
+ #expect(scanFloat("ab24") == nil)
+ }
+
+ @Test
+ func scan_Double() {
+ #expect(scanDouble("0") == 0)
+ #expect(scanDouble("124") == 124)
+ #expect(scanDouble(" 045") == 45)
+ #expect(scanDouble("-29") == -29)
+ #expect(scanDouble("ab24") == nil)
+ }
+
+ @Test
+ func scan_Length() {
+ #expect(scanLength("0") == 0)
+ #expect(scanLength("124") == 124)
+ #expect(scanLength(" 045") == 45)
+ #expect(scanLength("-29") == nil)
+ #expect(scanLength("ab24") == nil)
+ }
+
+ @Test
+ func scan_Bool() throws {
+ #expect(scanBool("0") == false)
+ #expect(scanBool("1") == true)
+ #expect(scanBool("true") == true)
+ #expect(scanBool("false") == false)
+ #expect(scanBool("false") == false)
+
+ var scanner = XMLParser.Scanner(text: "-29")
+ #expect(throws: (any Error).self) { _ = try scanner.scanBool() }
+ #expect(scanner.currentIndex == "".startIndex)
+ }
+
+ @Test
+ func scan_PercentageFloat() {
+ #expect(scanPercentageFloat("0") == 0)
+ #expect(scanPercentageFloat("0.5") == 0.5)
+ #expect(scanPercentageFloat("0.75") == 0.75)
+ #expect(scanPercentageFloat("1.0") == 1.0)
+ #expect(scanPercentageFloat("-0.5") == nil)
+ #expect(scanPercentageFloat("1.5") == nil)
+ #expect(scanPercentageFloat("as") == nil)
+ #expect(scanPercentageFloat("29") == nil)
+ #expect(scanPercentageFloat("24") == nil)
+ }
+
+ @Test
+ func scan_Percentage() {
+ #expect(scanPercentage("0") == 0)
+ #expect(scanPercentage("0%") == 0)
+ #expect(scanPercentage("100%") == 1.0)
+ #expect(scanPercentage("100 %") == 1.0)
+ #expect(scanPercentage("45.5 %") == 0.455)
+ #expect(scanPercentage("0.5 %") == 0.005)
+ #expect(scanPercentage("as") == nil)
+ #expect(scanPercentage("29") == nil)
+ #expect(scanPercentage("24") == nil)
+ }
+
+ @Test
+ func scanCoordinate() throws {
+ var scanner = XMLParser.Scanner(text: "10.05,12.04-49.05,30.02-10")
+
+ #expect(try scanner.scanCoordinate() == 10.05)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 12.04)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == -49.05)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 30.02)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == -10)
+ }
+
+ @Test
+ func scanCoordinate_Units() throws {
+ var scanner = XMLParser.Scanner(text: "1, 2px, 1cm, 2mm, 1pt, 5pc")
+
+ #expect(try scanner.scanCoordinate() == 1)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 2)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 37.795)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 2 * 3.7795)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 1 * 1.3333)
+ _ = try? scanner.scanString(",")
+ #expect(try scanner.scanCoordinate() == 5 * 16)
+ }
+}
+
+private func scanUInt8(_ text: String) -> UInt8? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanUInt8()
+}
+
+private func scanFloat(_ text: String) -> Float? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanFloat()
+}
+
+private func scanDouble(_ text: String) -> Double? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanDouble()
+}
+
+private func scanLength(_ text: String) -> DOM.Length? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanLength()
+}
+
+private func scanBool(_ text: String) -> Bool? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanBool()
+}
+
+private func scanPercentage(_ text: String) -> Float? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanPercentage()
+}
+
+private func scanPercentageFloat(_ text: String) -> Float? {
+ var scanner = XMLParser.Scanner(text: text)
+ return try? scanner.scanPercentageFloat()
+}
+
+extension CharacterSet: @retroactive ExpressibleByExtendedGraphemeClusterLiteral {}
+extension CharacterSet: @retroactive ExpressibleByUnicodeScalarLiteral {}
+extension CharacterSet: @retroactive ExpressibleByStringLiteral {
+
+ static let hexadecimal: Foundation.CharacterSet = "0123456789ABCDEFabcdef"
+
+ public init(stringLiteral value: String) {
+ self.init(charactersIn: value)
+ }
+
+}
+
+enum Token: String, CaseIterable {
+ case ok = "OK"
+ case nok = "NOT"
+}
diff --git a/DOM/Tests/XML.SAXParserTests.swift b/DOM/Tests/XML.SAXParserTests.swift
new file mode 100644
index 00000000..53db9a1a
--- /dev/null
+++ b/DOM/Tests/XML.SAXParserTests.swift
@@ -0,0 +1,95 @@
+//
+// XML.SAXParserTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 16/11/18.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import Foundation
+import Testing
+@testable import SwiftDrawDOM
+
+@Suite("SAX Parser Tests")
+struct SAXParserTests {
+
+ @Test
+ func missingFileThrows() {
+ let missingFile = URL(fileURLWithPath: "/user/tmp/SWIFTDraw/SwiftDraw/missing")
+ #expect(throws: (any Error).self) {
+ _ = try XML.SAXParser.parse(contentsOf: missingFile)
+ }
+ }
+
+ @Test
+ func invalidXMLThrows() {
+ let xml = "hi"
+ #expect(throws: (any Error).self) {
+ _ = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ }
+ }
+
+ @Test
+ func validSVGParses() throws {
+ let xml = """
+
+
+ """
+
+ let root = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ #expect(root.name == "svg")
+ #expect(root.children.isEmpty)
+ }
+
+ #if canImport(Darwin)
+ @Test
+ func unexpectedElementsThrows() {
+ let xml = """
+
+
+
+ """
+
+ #expect(throws: (any Error).self) {
+ _ = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ }
+ }
+ #endif
+
+ @Test
+ func unexpectedNamespaceElementsSkipped() throws {
+ let xml = """
+
+
+
+
+ """
+ let root = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ #expect(root.name == "svg")
+ #expect(root.children.count == 1)
+ #expect(root.children[0].name == "b")
+ }
+}
diff --git a/Examples/Basic.xcodeproj/project.pbxproj b/Examples/Basic.xcodeproj/project.pbxproj
index 3c5a33a6..a9c969eb 100644
--- a/Examples/Basic.xcodeproj/project.pbxproj
+++ b/Examples/Basic.xcodeproj/project.pbxproj
@@ -3,15 +3,14 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 52;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
- D9618466220FDD1100C59D9B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9618465220FDD1100C59D9B /* AppDelegate.swift */; };
- D9618468220FDD1100C59D9B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9618467220FDD1100C59D9B /* ViewController.swift */; };
+ D9618466220FDD1100C59D9B /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9618465220FDD1100C59D9B /* ExampleApp.swift */; };
D961846D220FDD1200C59D9B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D961846C220FDD1200C59D9B /* Assets.xcassets */; };
- D9618470220FDD1200C59D9B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D961846E220FDD1200C59D9B /* LaunchScreen.storyboard */; };
D9742E1E27D4877300E02FFD /* SwiftDraw in Frameworks */ = {isa = PBXBuildFile; productRef = D9742E1D27D4877300E02FFD /* SwiftDraw */; };
+ D9AC57BF2D65E86A005ACBFF /* GalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AC57BE2D65E86A005ACBFF /* GalleryView.swift */; };
D9EE86AF2A4EC94E00C7CAE1 /* Samples.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D94D5BE22A4EC906001DCD83 /* Samples.bundle */; };
/* End PBXBuildFile section */
@@ -31,12 +30,11 @@
014F853028F0BC3100B4BE96 /* Basic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Basic.entitlements; path = Basic/Basic.entitlements; sourceTree = ""; };
D94D5BE22A4EC906001DCD83 /* Samples.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = Samples.bundle; path = ../Samples.bundle; sourceTree = ""; };
D9618462220FDD1100C59D9B /* Basic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Basic.app; sourceTree = BUILT_PRODUCTS_DIR; };
- D9618465220FDD1100C59D9B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
- D9618467220FDD1100C59D9B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
+ D9618465220FDD1100C59D9B /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
D961846C220FDD1200C59D9B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- D961846F220FDD1200C59D9B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
D9618471220FDD1200C59D9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
D9742E1C27D4875100E02FFD /* SwiftDraw */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SwiftDraw; path = ..; sourceTree = ""; };
+ D9AC57BE2D65E86A005ACBFF /* GalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryView.swift; sourceTree = ""; };
D9ACD7A0220FDE04009717CF /* SwiftDraw.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftDraw.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@@ -75,10 +73,9 @@
D9618464220FDD1100C59D9B /* Sources */ = {
isa = PBXGroup;
children = (
- D9618465220FDD1100C59D9B /* AppDelegate.swift */,
- D9618467220FDD1100C59D9B /* ViewController.swift */,
+ D9AC57BE2D65E86A005ACBFF /* GalleryView.swift */,
+ D9618465220FDD1100C59D9B /* ExampleApp.swift */,
D961846C220FDD1200C59D9B /* Assets.xcassets */,
- D961846E220FDD1200C59D9B /* LaunchScreen.storyboard */,
D9618471220FDD1200C59D9B /* Info.plist */,
);
path = Sources;
@@ -154,7 +151,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- D9618470220FDD1200C59D9B /* LaunchScreen.storyboard in Resources */,
D9EE86AF2A4EC94E00C7CAE1 /* Samples.bundle in Resources */,
D961846D220FDD1200C59D9B /* Assets.xcassets in Resources */,
);
@@ -167,24 +163,13 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- D9618468220FDD1100C59D9B /* ViewController.swift in Sources */,
- D9618466220FDD1100C59D9B /* AppDelegate.swift in Sources */,
+ D9618466220FDD1100C59D9B /* ExampleApp.swift in Sources */,
+ D9AC57BF2D65E86A005ACBFF /* GalleryView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
-/* Begin PBXVariantGroup section */
- D961846E220FDD1200C59D9B /* LaunchScreen.storyboard */ = {
- isa = PBXVariantGroup;
- children = (
- D961846F220FDD1200C59D9B /* Base */,
- );
- name = LaunchScreen.storyboard;
- sourceTree = "";
- };
-/* End PBXVariantGroup section */
-
/* Begin XCBuildConfiguration section */
D9618472220FDD1200C59D9B /* Debug */ = {
isa = XCBuildConfiguration;
@@ -312,13 +297,16 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = C8TWBM2E6Q;
INFOPLIST_FILE = Sources/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 14.6;
PRODUCT_BUNDLE_IDENTIFIER = com.whiloop.Basic;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -331,13 +319,16 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = C8TWBM2E6Q;
INFOPLIST_FILE = Sources/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 14.6;
PRODUCT_BUNDLE_IDENTIFIER = com.whiloop.Basic;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
+ SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
diff --git a/Examples/Sources/Base.lproj/LaunchScreen.storyboard b/Examples/Sources/Base.lproj/LaunchScreen.storyboard
deleted file mode 100644
index bfa36129..00000000
--- a/Examples/Sources/Base.lproj/LaunchScreen.storyboard
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/SwiftDraw/DOM.Use.swift b/Examples/Sources/ExampleApp.swift
similarity index 75%
rename from SwiftDraw/DOM.Use.swift
rename to Examples/Sources/ExampleApp.swift
index 5776914e..dfa24b83 100644
--- a/SwiftDraw/DOM.Use.swift
+++ b/Examples/Sources/ExampleApp.swift
@@ -1,9 +1,9 @@
//
-// DOM.Use.swift
+// AppDelegate.swift
// SwiftDraw
//
-// Created by Simon Whitty on 27/2/17.
-// Copyright 2020 Simon Whitty
+// Created by Simon Whitty on 10/2/19.
+// Copyright 2019 Simon Whitty
//
// Distributed under the permissive zlib license
// Get the latest version from here:
@@ -29,16 +29,15 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-extension DOM {
- final class Use: GraphicsElement {
- var x: Coordinate?
- var y: Coordinate?
-
- //references element ids within defs
- var href: URL
-
- init(href: URL) {
- self.href = href
+import SwiftUI
+
+@main
+struct ExampleApp: App {
+ var body: some Scene {
+ WindowGroup {
+ NavigationStack {
+ GalleryView()
+ }
}
}
}
diff --git a/Examples/Sources/GalleryView.swift b/Examples/Sources/GalleryView.swift
new file mode 100644
index 00000000..9894189d
--- /dev/null
+++ b/Examples/Sources/GalleryView.swift
@@ -0,0 +1,79 @@
+//
+// GalleryView.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 19/2/25.
+// Copyright 2019 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import SwiftDraw
+import SwiftUI
+
+struct GalleryView: View {
+
+ var images = [
+ "thats-no-moon.svg",
+ "avocado.svg",
+ "angry.svg",
+ "ogre.svg",
+ "monkey.svg",
+ "fuji.svg",
+ "dish.svg",
+ "mouth-open.svg",
+ "sleepy.svg",
+ "smile.svg",
+ "snake.svg",
+ "spider.svg",
+ "star-struck.svg",
+ "worried.svg",
+ "yawning.svg",
+ "thats-no-moon.svg",
+ "alert.svg",
+ "effigy.svg",
+ "stylesheet-multiple.svg"
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVStack(spacing: 20) {
+ ForEach(images, id: \.self) { image in
+ SVGView(image, bundle: .samples)
+ .resizable()
+ .scaledToFit()
+ .padding([.leading, .trailing], 10)
+ }
+ }
+ .background(Color.white)
+ }
+ }
+}
+
+extension Bundle {
+ static let samples: Bundle = {
+ let url = Bundle.main.url(forResource: "Samples.bundle", withExtension: nil)!
+ return Bundle(url: url)!
+ }()
+}
diff --git a/Examples/Sources/Info.plist b/Examples/Sources/Info.plist
index 4222ac2d..64ae6618 100644
--- a/Examples/Sources/Info.plist
+++ b/Examples/Sources/Info.plist
@@ -18,10 +18,13 @@
1.0
CFBundleVersion
1
+ UILaunchScreen
+
+ UILaunchScreen
+
+
LSRequiresIPhoneOS
- UILaunchStoryboardName
- LaunchScreen
UIRequiredDeviceCapabilities
armv7
diff --git a/Examples/Sources/ViewController.swift b/Examples/Sources/ViewController.swift
deleted file mode 100644
index 83cc1302..00000000
--- a/Examples/Sources/ViewController.swift
+++ /dev/null
@@ -1,99 +0,0 @@
-//
-// ViewController.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 10/2/19.
-// Copyright 2019 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-import SwiftDraw
-import UIKit
-
-class ViewController: UIViewController {
-
- init() {
- super.init(nibName: nil, bundle: nil)
- self.title = "SVG"
- navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Mode", style: .plain, target: self, action: #selector(didTap))
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- @objc
- func didTap() {
- guard let contentMode = imageViewIfLoaded?.contentMode else { return }
- switch contentMode {
- case .center:
- imageViewIfLoaded?.contentMode = .scaleAspectFit
- case .scaleAspectFit:
- imageViewIfLoaded?.contentMode = .scaleAspectFill
- case .scaleAspectFill:
- imageViewIfLoaded?.contentMode = .center
- default:
- imageViewIfLoaded?.contentMode = .center
- }
- }
-
- var imageViewIfLoaded: UIImageView? {
- return viewIfLoaded as? UIImageView
- }
-
- override func loadView() {
- let imageView = UIImageView(frame: UIScreen.main.bounds)
- imageView.image = SVG(named: "gradient-stroke.svg", in: .samples)?.rasterize()
- imageView.contentMode = .scaleAspectFit
- imageView.backgroundColor = .white
- self.view = imageView
- }
-}
-
-private extension SVG {
-
- // UIImage backed with PDF preserves vector data.
-
- func pdfImage() -> UIImage? {
- guard
- let data = try? pdfData(),
- let provider = CGDataProvider(data: data as CFData),
- let pdf = CGPDFDocument(provider),
- let page = pdf.page(at: 1) else {
- return nil
- }
-
- return UIImage
- .perform(NSSelectorFromString("_imageWithCGPDFPage:"), with: page)?
- .takeUnretainedValue() as? UIImage
- }
-}
-
-extension Bundle {
- static let samples: Bundle = {
- let url = Bundle.main.url(forResource: "Samples.bundle", withExtension: nil)!
- return Bundle(url: url)!
- }()
-}
diff --git a/Package.swift b/Package.swift
index 92d5c22b..dea8269d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,16 +1,15 @@
-// swift-tools-version:5.7
+// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "SwiftDraw",
- platforms: [
- .iOS(.v13), .macOS(.v10_15)
+ platforms: [
+ .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)
],
products: [
- // Products define the executables and libraries produced by a package, and make them visible to other packages.
- .executable(name: "swiftdraw", targets: ["CommandLine"]),
- .library(
+ .executable(name: "swiftdrawcli", targets: ["CommandLine"]),
+ .library(
name: "SwiftDraw",
targets: ["SwiftDraw"]),
],
@@ -18,21 +17,50 @@ let package = Package(
targets: [
.target(
name: "SwiftDraw",
+ dependencies: ["SwiftDrawDOM"],
+ path: "SwiftDraw/Sources",
+ swiftSettings: .upcomingFeatures
+ ),
+ .target(
+ name: "SwiftDrawDOM",
dependencies: [],
- path: "SwiftDraw"
- ),
+ path: "DOM/Sources"
+ ),
.executableTarget(
name: "CommandLine",
dependencies: ["SwiftDraw"],
- path: "CommandLine"
- ),
+ path: "CommandLine",
+ swiftSettings: .upcomingFeatures
+ ),
+ .testTarget(
+ name: "SwiftDrawDOMTests",
+ dependencies: ["SwiftDrawDOM"],
+ path: "DOM/Tests",
+ resources: [
+ .copy("Test.bundle")
+ ],
+ swiftSettings: .upcomingFeatures
+ ),
.testTarget(
name: "SwiftDrawTests",
dependencies: ["SwiftDraw"],
- path: "SwiftDrawTests",
+ path: "SwiftDraw/Tests",
resources: [
.copy("Test.bundle")
- ]
- )
+ ],
+ swiftSettings: .upcomingFeatures
+ )
]
)
+
+extension Array where Element == SwiftSetting {
+
+ static var upcomingFeatures: [SwiftSetting] {
+ [
+ .enableUpcomingFeature("ExistentialAny"),
+ .enableUpcomingFeature("InternalImportsByDefault"),
+ .enableUpcomingFeature("MemberImportVisibility"),
+ .swiftLanguageMode(.v6)
+ ]
+ }
+}
diff --git a/README.md b/README.md
index d44be273..3c518a45 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
[](https://github.com/swhitty/SwiftDraw/actions/workflows/build.yml)
[](https://codecov.io/gh/swhitty/SwiftDraw)
-[](https://github.com/swhitty/SwiftDraw/blob/main/Package.swift)
-[](https://developer.apple.com/swift)
-[](https://opensource.org/licenses/Zlib)
-[](http://twitter.com/simonwhitty)
+[](https://swiftpackageindex.com/swhitty/SwiftDraw)
+[](https://swiftpackageindex.com/swhitty/SwiftDraw)
# Introduction
**SwiftDraw** is Swift library for parsing and drawing SVG images and includes a command line tool to convert SVGs into SFSymbol, PNG, PDF and Swift source code.
- [Usage](#usage)
- - [iOS](#ios)
- - [macOS](#macos)
+ - [SwiftUI](#swiftui)
+ - [UIKit](#uikit)
+ - [AppKit](#appkit)
- [Command Line Tool](#command-line-tool)
- [Installation](#installation)
- [SF Symbol](#sf-symbol)
@@ -27,28 +26,50 @@ let svg = SVG(named: "sample.svg", in: .main)!
imageView.image = svg.rasterize()
```
-Rasterize to any size:
+Transformations can be added before rasterizing:
```swift
-let svg = SVG(named: "sample.svg", in: .main)!
-imageView.image = svg.rasterize(with: CGSize(width: 640, height: 480))
+let svg = SVG(named: "fish.svg")! // 100x100
+ .expanded(left: 10, right: 10) // 120x100
+ .scaled(2) // 240x200
+
+imageView.image = svg.rasterize() // 240x200
```
-Crop the image using insets:
+### SwiftUI
+
+`SVGView` works much like SwiftUIās built-in `Image` view:
```swift
-let svg = SVG(named: "sample.svg", in: .main)!
-imageView.image = svg.rasterize(insets: .init(top: 10, left: 0, bottom: 10, bottom: 0))
+SVGView("sample.svg")
```
-Add padding using negative insets:
+By default, the SVG is rendered at its intrinsic size. To make it flexible within layouts, mark it as resizable:
```swift
-let svg = SVG(named: "sample.svg", in: .main)!
-imageView.image = svg.rasterize(insets: .init(top: -10, left: -10, bottom: -10, bottom: -10))
+SVGView("sample.svg")
+ .resizable()
+ .scaledToFit()
+```
+
+`SVGView` works just like `Image`:
+
+- `.scaledToFit()` to scale proportionally so the SVG fits inside its container.
+- `.scaledToFill()` to fill the entire container, cropping if necessary.
+- `.resizable(resizingMode: .tile)` to repeat the SVG as tiles across the available space.
+
+When loading by name, `SVGView` maintains an internal cache for efficient repeated lookups.
+For more predictable performance (avoiding cache lookups or parsing), you can pass in an already-constructed `SVG` instance:
+
+```swift
+var image: SVG
+
+var body: some View {
+ SVGView(svg: image)
+}
```
-### iOS
+### UIKit
Create a `UIImage` directly from an SVG within a bundle, `Data` or file `URL`:
@@ -57,7 +78,7 @@ import SwiftDraw
let image = UIImage(svgNamed: "sample.svg")
```
-### macOS
+### AppKit
Create an `NSImage` directly from an SVG within a bundle, `Data` or file `URL`:
@@ -71,7 +92,7 @@ let image = NSImage(svgNamed: "sample.svg")
The command line tool converts SVGs to other formats: PNG, JPEG, SFSymbol and Swift source code.
```
-copyright (c) 2023 Simon Whitty
+copyright (c) 2025 Simon Whitty
usage: swiftdraw [--format png | pdf | jpeg | swift | sfsymbol] [--size wxh] [--scale 1x | 2x | 3x]
@@ -85,17 +106,19 @@ Options:
--precision maximum number of decimal places
--output optional path of output file
- --hideUnsupportedFilters hide elements with unsupported filters.
+ --hide-unsupported-filters hide elements with unsupported filters.
Available keys for --format swift:
--api api of generated code: appkit | uikit
Available keys for --format sfsymbol:
--insets alignment of regular variant: top,left,bottom,right | auto
+ --size size category to generate: small, medium large. (default is small)
--ultralight svg file of ultralight variant
- --ultralightInsets alignment of ultralight variant: top,left,bottom,right | auto
+ --ultralight-insets alignment of ultralight variant: top,left,bottom,right | auto
--black svg file of black variant
- --blackInsets alignment of black variant: top,left,bottom,right | auto
+ --black-insets alignment of black variant: top,left,bottom,right | auto
+ --legacy use the original, less precise alignment logic from earlier swiftdraw versions.
```
```bash
diff --git a/Samples.bundle/abc.svg b/Samples.bundle/abc.svg
new file mode 100644
index 00000000..df04890c
--- /dev/null
+++ b/Samples.bundle/abc.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Samples.bundle/alert.svg b/Samples.bundle/alert.svg
new file mode 100644
index 00000000..115a1da6
--- /dev/null
+++ b/Samples.bundle/alert.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/amex.svg b/Samples.bundle/amex.svg
new file mode 100644
index 00000000..18bd8445
--- /dev/null
+++ b/Samples.bundle/amex.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Samples.bundle/base64-image.svg b/Samples.bundle/base64-image.svg
new file mode 100644
index 00000000..f3da110e
--- /dev/null
+++ b/Samples.bundle/base64-image.svg
@@ -0,0 +1,8 @@
+
+
+ Plugin Icon
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples.bundle/base64.svg b/Samples.bundle/base64.svg
index 69e29584..d3e325f4 100644
--- a/Samples.bundle/base64.svg
+++ b/Samples.bundle/base64.svg
@@ -2,6 +2,7 @@
-
+ width="100" height="46">
+
+
\ No newline at end of file
diff --git a/Samples.bundle/bike.svg b/Samples.bundle/bike.svg
new file mode 100644
index 00000000..558d92e7
--- /dev/null
+++ b/Samples.bundle/bike.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Samples.bundle/effigy.svg b/Samples.bundle/effigy.svg
new file mode 100644
index 00000000..fd70993d
--- /dev/null
+++ b/Samples.bundle/effigy.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Samples.bundle/fuji.svg b/Samples.bundle/fuji.svg
new file mode 100644
index 00000000..6ec44726
--- /dev/null
+++ b/Samples.bundle/fuji.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/monkey.svg b/Samples.bundle/monkey.svg
new file mode 100644
index 00000000..ab3dc87a
--- /dev/null
+++ b/Samples.bundle/monkey.svg
@@ -0,0 +1,247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/nested-svg.svg b/Samples.bundle/nested-svg.svg
new file mode 100644
index 00000000..ae0b0807
--- /dev/null
+++ b/Samples.bundle/nested-svg.svg
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/ogre.svg b/Samples.bundle/ogre.svg
new file mode 100644
index 00000000..b8c3ea24
--- /dev/null
+++ b/Samples.bundle/ogre.svg
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/rgba.svg b/Samples.bundle/rgba.svg
new file mode 100644
index 00000000..7b3c286a
--- /dev/null
+++ b/Samples.bundle/rgba.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Samples.bundle/robin.svg b/Samples.bundle/robin.svg
new file mode 100644
index 00000000..b767088e
--- /dev/null
+++ b/Samples.bundle/robin.svg
@@ -0,0 +1,29 @@
+
+
+ Bank Logos/robinhood
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples.bundle/stars.svg b/Samples.bundle/stars.svg
new file mode 100644
index 00000000..d2e08a59
--- /dev/null
+++ b/Samples.bundle/stars.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/stylesheet-multiple.svg b/Samples.bundle/stylesheet-multiple.svg
new file mode 100644
index 00000000..9fb95ae6
--- /dev/null
+++ b/Samples.bundle/stylesheet-multiple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Samples.bundle/units-cm.svg b/Samples.bundle/units-cm.svg
new file mode 100644
index 00000000..791ec24e
--- /dev/null
+++ b/Samples.bundle/units-cm.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples.bundle/units.svg b/Samples.bundle/units.svg
new file mode 100644
index 00000000..29ae9e79
--- /dev/null
+++ b/Samples.bundle/units.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples.bundle/usa.svg b/Samples.bundle/usa.svg
new file mode 100644
index 00000000..0baeeb20
--- /dev/null
+++ b/Samples.bundle/usa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Samples.bundle/whileloop.svg b/Samples.bundle/whileloop.svg
new file mode 100644
index 00000000..165f4a49
--- /dev/null
+++ b/Samples.bundle/whileloop.svg
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SwiftDraw.podspec.json b/SwiftDraw.podspec.json
index f7e13317..6148d862 100644
--- a/SwiftDraw.podspec.json
+++ b/SwiftDraw.podspec.json
@@ -1,6 +1,6 @@
{
"name": "SwiftDraw",
- "version": "0.16.1",
+ "version": "0.25.0",
"summary": "A Swift library that adds support for SVG files to UIImage and NSImage.",
"homepage": "https://github.com/swhitty/SwiftDraw",
"authors": "Simon Whitty",
@@ -10,20 +10,37 @@
},
"source": {
"git": "https://github.com/swhitty/SwiftDraw.git",
- "tag": "0.16.1"
+ "tag": "0.25.0"
},
"platforms": {
- "ios": "12.0",
- "osx": "10.14"
+ "ios": "13.0",
+ "osx": "10.15",
+ "tvos": "13.0",
+ "watchos": "6.0",
+ "visionos": "1.0"
},
- "source_files": "SwiftDraw/**/*.swift",
+ "source_files": "SwiftDraw/Sources/**/*.swift",
"ios": {
- "exclude_files": "SwiftDraw/NSImage+Image.swift",
- "frameworks": ["UIKit", "Foundation"]
+ "exclude_files": "SwiftDraw/Sources/NSImage+Image.swift",
+ "frameworks": ["UIKit", "Foundation"]
},
"osx": {
- "exclude_files": "SwiftDraw/UIImage+Image.swift",
- "frameworks": ["AppKit", "Foundation"]
+ "exclude_files": "SwiftDraw/Sources/UIImage+Image.swift",
+ "frameworks": ["AppKit", "Foundation"]
},
- "swift_version": "5.0"
+ "tvos": {
+ "exclude_files": "SwiftDraw/Sources/NSImage+Image.swift",
+ "frameworks": ["UIKit", "Foundation"]
+ },
+ "watchos": {
+ "exclude_files": "SwiftDraw/Sources/NSImage+Image.swift",
+ "frameworks": ["UIKit", "WatchKit", "Foundation"]
+ },
+ "swift_version": "6.0",
+ "pod_target_xcconfig": {
+ "OTHER_SWIFT_FLAGS": "-package-name SwiftDraw"
+ },
+ "dependencies": {
+ "SwiftDrawDOM": "~> 0.25.0"
+ }
}
diff --git a/SwiftDraw/DOM.SVG.swift b/SwiftDraw/DOM.SVG.swift
deleted file mode 100644
index 0b43301e..00000000
--- a/SwiftDraw/DOM.SVG.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-//
-// DOM.SVG.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 11/2/17.
-// Copyright 2020 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-extension DOM {
- final class SVG: GraphicsElement, ContainerElement {
- var width: Length
- var height: Length
- var viewBox: ViewBox?
-
- var childElements = [GraphicsElement]()
-
- var styles = [StyleSheet]()
- var defs = Defs()
-
- init(width: Length, height: Length) {
- self.width = width
- self.height = height
- }
-
- struct ViewBox: Equatable {
- var x: Coordinate
- var y: Coordinate
- var width: Coordinate
- var height: Coordinate
- }
-
- struct Defs {
- var clipPaths = [ClipPath]()
- var linearGradients = [LinearGradient]()
- var radialGradients = [RadialGradient]()
- var masks = [Mask]()
- var patterns = [Pattern]()
- var filters = [Filter]()
-
- var elements = [String: GraphicsElement]()
- }
- }
-
- struct ClipPath: ContainerElement {
- var id: String
- var childElements = [GraphicsElement]()
- }
-
- struct Mask: ContainerElement {
- var id: String
- var childElements = [GraphicsElement]()
- }
-
- struct StyleSheet {
-
- enum Selector: Hashable, Comparable {
- case id(String)
- case element(String)
- case `class`(String)
- }
-
- var attributes: [Selector: PresentationAttributes] = [:]
- }
-}
diff --git a/SwiftDraw/Image+CoreGraphics.swift b/SwiftDraw/Image+CoreGraphics.swift
deleted file mode 100644
index a0b53a90..00000000
--- a/SwiftDraw/Image+CoreGraphics.swift
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// Image.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 24/5/17.
-// Copyright 2020 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-#if canImport(CoreGraphics)
-import CoreGraphics
-import Foundation
-
-public extension CGContext {
-
- func draw(_ image: SVG, in rect: CGRect? = nil) {
- let defaultRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
- let renderer = CGRenderer(context: self)
-
- guard let rect = rect, rect != defaultRect else {
- renderer.perform(image.commands)
- return
- }
-
- let scale = CGSize(width: rect.width / image.size.width,
- height: rect.height / image.size.height)
- draw(image.commands, in: rect, scale: scale)
- }
-
- fileprivate func draw(_ commands: [RendererCommand], in rect: CGRect, scale: CGSize = CGSize(width: 1.0, height: 1.0)) {
- let renderer = CGRenderer(context: self)
- saveGState()
- translateBy(x: rect.origin.x, y: rect.origin.y)
- scaleBy(x: scale.width, y: scale.height)
- renderer.perform(commands)
- restoreGState()
- }
-}
-
-public extension SVG {
-
- func pdfData(size: CGSize? = nil, insets: Insets = .zero) throws -> Data {
- let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: 1, insets: insets)
- var mediaBox = CGRect(x: 0.0, y: 0.0, width: CGFloat(pixelsWide), height: CGFloat(pixelsHigh))
-
- let data = NSMutableData()
- guard let consumer = CGDataConsumer(data: data as CFMutableData),
- let ctx = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
- throw Error("Failed to create CGContext")
- }
-
- ctx.beginPage(mediaBox: &mediaBox)
- let flip = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: mediaBox.size.height)
- ctx.concatenate(flip)
- ctx.draw(self, in: bounds)
- ctx.endPage()
- ctx.closePDF()
-
- return data as Data
- }
-
- private struct Error: LocalizedError {
- var errorDescription: String?
-
- init(_ message: String) {
- self.errorDescription = message
- }
- }
-}
-
-extension SVG {
-
- static func makeBounds(size: CGSize?,
- defaultSize: CGSize,
- scale: CGFloat,
- insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) {
- let viewport = CGSize(
- width: defaultSize.width - (insets.left + insets.right),
- height: defaultSize.height - (insets.top + insets.bottom)
- )
-
- let size = size ?? viewport
-
- let sx = size.width / viewport.width
- let sy = size.height / viewport.height
-
- let width = size.width * scale
- let height = size.height * scale
- let insets = insets.applying(sx: sx * scale, sy: sy * scale)
- let bounds = CGRect(x: -insets.left,
- y: -insets.top,
- width: width + insets.left + insets.right,
- height: height + insets.top + insets.bottom)
- return (
- bounds: bounds,
- pixelsWide: Int(width),
- pixelsHigh: Int(height)
- )
- }
-}
-
-private extension SVG.Insets {
- func applying(sx: CGFloat, sy: CGFloat) -> Self {
- Self(
- top: top * sy,
- left: left * sx,
- bottom: bottom * sy,
- right: right * sx
- )
- }
-}
-
-private extension LayerTree.Size {
- init(_ size: CGSize) {
- self.width = LayerTree.Float(size.width)
- self.height = LayerTree.Float(size.height)
- }
-}
-
-#endif
diff --git a/SwiftDraw/Image.swift b/SwiftDraw/Image.swift
deleted file mode 100644
index 5539317d..00000000
--- a/SwiftDraw/Image.swift
+++ /dev/null
@@ -1,172 +0,0 @@
-//
-// Image.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 24/5/17.
-// Copyright 2020 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-import Foundation
-
-#if canImport(CoreGraphics)
-import CoreGraphics
-
-@objc(SVGImage)
-public final class SVG: NSObject {
- public let size: CGSize
-
- //An Image is simply an array of CoreGraphics draw commands
- //see: Renderer.swift
- let commands: [RendererCommand]
-
- init(dom: DOM.SVG, options: Options) {
- self.size = CGSize(width: dom.width, height: dom.height)
-
- //To create the draw commands;
- // - XML is parsed into DOM.SVG
- // - DOM.SVG is converted into a LayerTree
- // - LayerTree is converted into RenderCommands
- // - RenderCommands are performed by Renderer (drawn to CGContext)
- let layer = LayerTree.Builder(svg: dom).makeLayer()
- let generator = LayerTree.CommandGenerator(provider: CGProvider(),
- size: LayerTree.Size(dom.width, dom.height),
- options: options)
-
- let optimizer = LayerTree.CommandOptimizer()
- commands = optimizer.optimizeCommands(
- generator.renderCommands(for: layer)
- )
- }
-
- public struct Options: OptionSet {
- public let rawValue: Int
- public init(rawValue: Int) {
- self.rawValue = rawValue
- }
-
- public static let hideUnsupportedFilters = Options(rawValue: 1 << 0)
-
- public static let `default`: Options = []
- }
-}
-
-@available(*, deprecated, renamed: "SVG")
-public typealias Image = SVG
-
-#else
-
-public final class SVG: NSObject {
- public let size: CGSize
-
- init(dom: DOM.SVG, options: Options) {
- size = CGSize(width: dom.width, height: dom.height)
- }
-
- public struct Options: OptionSet {
- public let rawValue: Int
- public init(rawValue: Int) {
- self.rawValue = rawValue
- }
-
- public static let hideUnsupportedFilters = Options(rawValue: 1 << 0)
-
- public static let `default`: Options = []
- }
-}
-
-public extension SVG {
-
- func pngData(size: CGSize? = nil, scale: CGFloat = 1) -> Data? {
- return nil
- }
-
- func jpegData(size: CGSize? = nil, scale: CGFloat = 1, compressionQuality quality: CGFloat = 1) -> Data? {
- return nil
- }
-
- func pdfData(size: CGSize? = nil) -> Data? {
- return nil
- }
-
- static func pdfData(fileURL url: URL, size: CGSize? = nil) throws -> Data {
- throw DOM.Error.missing("not implemented")
- }
-}
-#endif
-
-extension DOM.SVG {
-
- static func parse(fileURL url: URL, options: XMLParser.Options = .skipInvalidElements) throws -> DOM.SVG {
- let element = try XML.SAXParser.parse(contentsOf: url)
- let parser = XMLParser(options: options, filename: url.lastPathComponent)
- return try parser.parseSVG(element)
- }
-
- static func parse(data: Data, options: XMLParser.Options = .skipInvalidElements) throws -> DOM.SVG {
- let element = try XML.SAXParser.parse(data: data)
- let parser = XMLParser(options: options)
- return try parser.parseSVG(element)
- }
-}
-
-public extension SVG {
-
- convenience init?(fileURL url: URL, options: SVG.Options = .default) {
- do {
- let svg = try DOM.SVG.parse(fileURL: url)
- self.init(dom: svg, options: options)
- } catch {
- XMLParser.logParsingError(for: error, filename: url.lastPathComponent, parsing: nil)
- return nil
- }
- }
-
- convenience init?(named name: String, in bundle: Bundle = Bundle.main, options: SVG.Options = .default) {
- guard let url = bundle.url(forResource: name, withExtension: nil) else {
- return nil
- }
-
- self.init(fileURL: url, options: options)
- }
-
- convenience init?(data: Data, options: SVG.Options = .default) {
- guard let svg = try? DOM.SVG.parse(data: data) else {
- return nil
- }
-
- self.init(dom: svg, options: options)
- }
-
-
- struct Insets: Equatable {
- public var top: CGFloat
- public var left: CGFloat
- public var bottom: CGFloat
- public var right: CGFloat
-
- public static let zero = Insets(top: 0, left: 0, bottom: 0, right: 0)
- }
-}
diff --git a/SwiftDraw/Parser.XML.Path.swift b/SwiftDraw/Parser.XML.Path.swift
deleted file mode 100644
index 99d5a7ef..00000000
--- a/SwiftDraw/Parser.XML.Path.swift
+++ /dev/null
@@ -1,231 +0,0 @@
-//
-// Parser.XML.Path.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 31/12/16.
-// Copyright 2020 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-import Foundation
-
-extension XMLParser {
-
- typealias PathScanner = Foundation.Scanner
-
- typealias Segment = DOM.Path.Segment
- typealias Command = DOM.Path.Command
- typealias CoordinateSpace = DOM.Path.Segment.CoordinateSpace
-
- func parsePath(_ att: AttributeParser) throws -> DOM.Path {
- return try parsePath(from: att.parseString("d"))
- }
-
- func parsePath(from data: String) throws -> DOM.Path {
- let path = DOM.Path(x: 0, y: 0)
- path.segments = try parsePathSegments(data)
- return path
- }
-
- func parsePathSegments(_ data: String) throws -> [Segment] {
-
- var segments = Array()
-
- var scanner = PathScanner(string: data)
-
- scanner.charactersToBeSkipped = Foundation.CharacterSet.whitespacesAndNewlines
-
- var lastCommand: Command?
-
- repeat {
- guard let cmd = nextPathCommand(&scanner, lastCommand: lastCommand) else {
- throw Error.invalid
- }
- lastCommand = cmd
- segments.append(try parsePathSegment(for: cmd, with: &scanner))
- } while !scanner.isAtEnd
-
- return segments
- }
-
- func nextPathCommand(_ scanner: inout PathScanner, lastCommand: Command?) -> Command? {
- if let cmd = parseCommand(&scanner) {
- return cmd
- }
-
- switch lastCommand {
- case .some(.move):
- return .line
- case .some(.moveRelative):
- return .lineRelative
- default:
- return lastCommand
- }
- }
-
-
- func parsePathSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- switch command {
- case .move, .moveRelative:
- return try parseMoveSegment(for: command, with: &scanner)
- case .line, .lineRelative:
- return try parseLineSegment(for: command, with: &scanner)
- case .horizontal, .horizontalRelative:
- return try parseHorizontalSegment(for: command, with: &scanner)
- case .vertical, .verticalRelative:
- return try parseVerticalSegment(for: command, with: &scanner)
- case .cubic, .cubicRelative:
- return try parseCubicSegment(for: command, with: &scanner)
- case .cubicSmooth, .cubicSmoothRelative:
- return try parseCubicSmoothSegment(for: command, with: &scanner)
- case .quadratic, .quadraticRelative:
- return try parseQuadraticSegment(for: command, with: &scanner)
- case .quadraticSmooth, .quadraticSmoothRelative:
- return try parseQuadraticSmoothSegment(for: command, with: &scanner)
- case .arc, .arcRelative:
- return try parseArcSegment(for: command, with: &scanner)
- case .close, .closeAlias:
- return .close
- }
- }
-
- func parseCommand(_ scanner: inout PathScanner) -> Command? {
- guard let char = scanner.scan(first: .commands),
- let command = Command(rawValue: char) else {
- return nil
- }
- return command
- }
-
- func parseMoveSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .move(x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseLineSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .line(x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseHorizontalSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .horizontal(x: x, space: command.coordinateSpace)
- }
-
- func parseVerticalSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .vertical(y: y, space: command.coordinateSpace)
- }
-
- func parseCubicSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x1 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y1 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let x2 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y2 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .cubic(x1: x1, y1: y1, x2: x2, y2: y2, x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseCubicSmoothSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x2 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y2 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .cubicSmooth(x2: x2, y2: y2, x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseQuadraticSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x1 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y1 = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .quadratic(x1: x1, y1: y1, x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseQuadraticSmoothSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .quadraticSmooth(x: x, y: y, space: command.coordinateSpace)
- }
-
- func parseArcSegment(for command: Command, with scanner: inout PathScanner) throws -> Segment {
- let rx = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let ry = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let rotate = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let large = try scanner.scanBool()
- _ = scanner.scan(first: .delimeter)
- let sweep = try scanner.scanBool()
- _ = scanner.scan(first: .delimeter)
- let x = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
- let y = try scanner.scanCoordinate()
- _ = scanner.scan(first: .delimeter)
-
- return .arc(rx: rx, ry: ry, rotate: rotate,
- large: large, sweep: sweep,
- x: x, y: y, space: command.coordinateSpace)
- }
-}
-
-private extension CharacterSet {
- static let delimeter = CharacterSet(charactersIn: ",;")
- static let commands = CharacterSet(charactersIn: "MmLlHhVvCcSsQqTtAaZz")
-}
diff --git a/SwiftDraw/Parser.XML.swift b/SwiftDraw/Parser.XML.swift
deleted file mode 100644
index 9df4105e..00000000
--- a/SwiftDraw/Parser.XML.swift
+++ /dev/null
@@ -1,201 +0,0 @@
-//
-// Parser.XML.swift
-// SwiftDraw
-//
-// Created by Simon Whitty on 31/12/16.
-// Copyright 2020 Simon Whitty
-//
-// Distributed under the permissive zlib license
-// Get the latest version from here:
-//
-// https://github.com/swhitty/SwiftDraw
-//
-// This software is provided 'as-is', without any express or implied
-// warranty. In no event will the authors be held liable for any damages
-// arising from the use of this software.
-//
-// Permission is granted to anyone to use this software for any purpose,
-// including commercial applications, and to alter it and redistribute it
-// freely, subject to the following restrictions:
-//
-// 1. The origin of this software must not be misrepresented; you must not
-// claim that you wrote the original software. If you use this software
-// in a product, an acknowledgment in the product documentation would be
-// appreciated but is not required.
-//
-// 2. Altered source versions must be plainly marked as such, and must not be
-// misrepresented as being the original software.
-//
-// 3. This notice may not be removed or altered from any source distribution.
-//
-
-struct XMLParser {
- enum Error: Swift.Error {
- case invalid
- case missingAttribute(name: String)
- case invalidAttribute(name: String, value: Any)
- case invalidElement(name: String, error: Swift.Error, line: Int?, column: Int?)
- case invalidDocument(error: Swift.Error?, element: String?, line: Int, column: Int)
- }
-
- var options: Options = []
- var filename: String?
-
- struct Options: OptionSet {
- let rawValue: Int
- init(rawValue: Int) {
- self.rawValue = rawValue
- }
-
- static let skipInvalidAttributes = Options(rawValue: 1)
- static let skipInvalidElements = Options(rawValue: 2)
- }
-}
-
-protocol AttributeValueParser {
- func parseFloat(_ value: String) throws -> DOM.Float
- func parseFloats(_ value: String) throws -> [DOM.Float]
- func parsePercentage(_ value: String) throws -> DOM.Float
- func parseCoordinate(_ value: String) throws -> DOM.Coordinate
- func parseLength(_ value: String) throws -> DOM.Length
- func parseBool(_ value: String) throws -> DOM.Bool
- func parseFill(_ value: String) throws -> DOM.Fill
- func parseUrl(_ value: String) throws -> DOM.URL
- func parseUrlSelector(_ value: String) throws -> DOM.URL
- func parsePoints(_ value: String) throws -> [DOM.Point]
-
- func parseRaw(_ value: String) throws -> T where T.RawValue == String
-}
-
-protocol AttributeParser {
- var parser: AttributeValueParser { get }
- var options: XMLParser.Options { get }
-
- // either parse and return T or
- // throw Error.missingAttribute when key cannot resolve to a value
- // throw Error.invalidAttribute when value cannot be parsed into T
- func parse(_ key: String, _ exp: (String) throws -> T) throws -> T
-}
-
-extension AttributeParser {
-
- func parseString(_ key: String) throws -> String {
- return try parse(key) { $0 }
- }
-
- func parseFloat(_ key: String) throws -> DOM.Float {
- return try parse(key) { return try parser.parseFloat($0) }
- }
-
- func parseFloats(_ key: String) throws -> [DOM.Float] {
- return try parse(key) { return try parser.parseFloats($0) }
- }
-
- func parsePercentage(_ key: String) throws -> DOM.Float {
- return try parse(key) { return try parser.parsePercentage($0) }
- }
-
- func parseCoordinate(_ key: String) throws -> DOM.Coordinate {
- return try parse(key) { return try parser.parseCoordinate($0) }
- }
-
- func parseLength(_ key: String) throws -> DOM.Length {
- return try parse(key) { return try parser.parseLength($0) }
- }
-
- func parseBool(_ key: String) throws -> DOM.Bool {
- return try parse(key) { return try parser.parseBool($0) }
- }
-
- func parseFill(_ key: String) throws -> DOM.Fill {
- return try parse(key) { return try parser.parseFill($0) }
- }
-
- func parseColor(_ key: String) throws -> DOM.Color {
- return try parseFill(key).getColor()
- }
-
- func parseUrl(_ key: String) throws -> DOM.URL {
- return try parse(key) { return try parser.parseUrl($0) }
- }
-
- func parseUrlSelector(_ key: String) throws -> DOM.URL {
- return try parse(key) { return try parser.parseUrlSelector($0) }
- }
-
- func parsePoints(_ key: String) throws -> [DOM.Point] {
- return try parse(key) { return try parser.parsePoints($0) }
- }
-
- func parseRaw(_ key: String) throws -> T where T.RawValue == String {
- return try parse(key) { return try parser.parseRaw($0) }
- }
-}
-
-extension AttributeParser {
-
- typealias Options = XMLParser.Options
-
- func parse(_ key: String, exp: (String) throws -> T) throws -> T? {
- do {
- return try parse(key, exp)
- } catch XMLParser.Error.missingAttribute(_) {
- return nil
- } catch let error {
- guard options.contains(.skipInvalidAttributes) else { throw error }
- }
- return nil
- }
-
- func parseString(_ key: String) throws -> String? {
- return try parse(key) { $0 }
- }
-
- func parseFloat(_ key: String) throws -> DOM.Float? {
- return try parse(key) { return try parser.parseFloat($0) }
- }
-
- func parseFloats(_ key: String) throws -> [DOM.Float]? {
- return try parse(key) { return try parser.parseFloats($0) }
- }
-
- func parsePercentage(_ key: String) throws -> DOM.Float? {
- return try parse(key) { return try parser.parsePercentage($0) }
- }
-
- func parseCoordinate(_ key: String) throws -> DOM.Coordinate? {
- return try parse(key) { return try parser.parseCoordinate($0) }
- }
-
- func parseLength(_ key: String) throws -> DOM.Length? {
- return try parse(key) { return try parser.parseLength($0) }
- }
-
- func parseBool(_ key: String) throws -> DOM.Bool? {
- return try parse(key) { return try parser.parseBool($0) }
- }
-
- func parseFill(_ key: String) throws -> DOM.Fill? {
- return try parse(key) { return try parser.parseFill($0) }
- }
-
- func parseColor(_ key: String) throws -> DOM.Color? {
- return try parseFill(key)?.getColor()
- }
-
- func parseUrl(_ key: String) throws -> DOM.URL? {
- return try parse(key) { return try parser.parseUrl($0) }
- }
-
- func parseUrlSelector(_ key: String) throws -> DOM.URL? {
- return try parse(key) { return try parser.parseUrlSelector($0) }
- }
-
- func parsePoints(_ key: String) throws -> [DOM.Point]? {
- return try parse(key) { return try parser.parsePoints($0) }
- }
-
- func parseRaw(_ key: String) throws -> T? where T.RawValue == String {
- return try parse(key) { return try parser.parseRaw($0) }
- }
-}
diff --git a/SwiftDraw/Sources/CanvasNSView.swift b/SwiftDraw/Sources/CanvasNSView.swift
new file mode 100644
index 00000000..897ee2bc
--- /dev/null
+++ b/SwiftDraw/Sources/CanvasNSView.swift
@@ -0,0 +1,80 @@
+//
+// CanvasNSView.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 07/9/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(AppKit)
+import AppKit
+import SwiftUI
+
+@available(macOS, deprecated: 12.0, message: "use SwiftUI.Canvas")
+struct CanvasFallbackView: NSViewRepresentable {
+
+ var svg: SVG
+ var capInsets: EdgeInsets
+ var resizingMode: SVGView.ResizingMode
+
+ func makeNSView(context: Context) -> CanvasNSView {
+ let nsView = CanvasNSView()
+ nsView.wantsLayer = true
+ nsView.layerContentsRedrawPolicy = .duringViewResize
+ nsView.layer?.needsDisplayOnBoundsChange = true
+ return nsView
+ }
+
+ func updateNSView(_ nsView: CanvasNSView, context: Context) {
+ nsView.svg = svg
+ nsView.resizeMode = resizingMode
+ nsView.capInsets = (capInsets.top, capInsets.leading, capInsets.bottom, capInsets.trailing)
+ nsView.needsDisplay = true
+ }
+}
+
+final class CanvasNSView: NSView {
+
+ var svg: SVG?
+ var resizeMode: SVGView.ResizingMode = .stretch
+ var capInsets: (top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) = (0, 0, 0, 0)
+
+ override var isFlipped: Bool { true }
+
+ override func draw(_ dirtyRect: NSRect) {
+ guard let svg,
+ let ctx = NSGraphicsContext.current?.cgContext else { return }
+
+ ctx.draw(
+ svg,
+ in: bounds,
+ capInsets: capInsets,
+ byTiling: resizeMode == .tile
+ )
+ }
+}
+
+#endif
diff --git a/SwiftDraw/Sources/CanvasUIView.swift b/SwiftDraw/Sources/CanvasUIView.swift
new file mode 100644
index 00000000..c3ac6c8b
--- /dev/null
+++ b/SwiftDraw/Sources/CanvasUIView.swift
@@ -0,0 +1,77 @@
+//
+// CanvasUIView.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 07/9/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(UIKit) && !os(watchOS)
+import UIKit
+import SwiftUI
+
+@available(iOS, deprecated: 15.0, message: "use SwiftUI.Canvas")
+struct CanvasFallbackView: UIViewRepresentable {
+
+ var svg: SVG
+ var capInsets: EdgeInsets
+ var resizingMode: SVGView.ResizingMode
+
+ func makeUIView(context: Context) -> CanvasUIView {
+ let uiView = CanvasUIView()
+ uiView.isOpaque = false
+ uiView.contentMode = .redraw
+ return uiView
+ }
+
+ func updateUIView(_ uiView: CanvasUIView, context: Context) {
+ uiView.svg = svg
+ uiView.resizeMode = resizingMode
+ uiView.capInsets = (capInsets.top, capInsets.leading, capInsets.bottom, capInsets.trailing)
+ uiView.setNeedsDisplay()
+ }
+}
+
+final class CanvasUIView: UIView {
+
+ var svg: SVG?
+ var resizeMode: SVGView.ResizingMode = .stretch
+ var capInsets: (top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) = (0, 0, 0, 0)
+
+ override func draw(_ rect: CGRect) {
+ guard let svg,
+ let ctx = UIGraphicsGetCurrentContext() else { return }
+
+ ctx.draw(
+ svg,
+ in: rect,
+ capInsets: capInsets,
+ byTiling: resizeMode == .tile
+ )
+ }
+}
+
+#endif
diff --git a/SwiftDraw/CommandLine+Process.swift b/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift
similarity index 75%
rename from SwiftDraw/CommandLine+Process.swift
rename to SwiftDraw/Sources/CommandLine/CommandLine+Process.swift
index 478dcea7..2a470e22 100644
--- a/SwiftDraw/CommandLine+Process.swift
+++ b/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift
@@ -29,7 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import Foundation
+public import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
#endif
@@ -51,14 +51,20 @@ public extension CommandLine {
precision: config.precision ?? 2)
return code.data(using: .utf8)!
case .sfsymbol:
- let renderer = SFSymbolRenderer(options: config.options,
- insets: config.insets,
- insetsUltralight: config.insetsUltralight ?? config.insets,
- insetsBlack: config.insetsBlack ?? config.insets,
- precision: config.precision ?? 3)
- let svg = try renderer.render(regular: config.input,
- ultralight: config.inputUltralight,
- black: config.inputBlack)
+ let renderer = SFSymbolRenderer(
+ size: config.symbolSize ?? .small,
+ options: config.options,
+ insets: config.insets,
+ insetsUltralight: config.insetsUltralight ?? config.insets,
+ insetsBlack: config.insetsBlack ?? config.insets,
+ precision: config.precision ?? 3,
+ isLegacyInsets: config.isLegacyInsetsEnabled
+ )
+ let svg = try renderer.render(
+ regular: config.input,
+ ultralight: config.inputUltralight,
+ black: config.inputBlack
+ )
return svg.data(using: .utf8)!
case .jpeg, .pdf, .png:
#if canImport(CoreGraphics)
@@ -95,14 +101,20 @@ public extension CommandLine {
#if canImport(CoreGraphics)
switch config.format {
case .jpeg:
- let insets = try makeImageInsets(for: config.insets)
- return try image.jpegData(size: config.size.cgValue, scale: config.scale.cgValue, insets: insets)
+ return try image
+ .inset(makeImageInsets(for: config.insets))
+ .size(config.size.cgValue)
+ .jpegData(scale: config.scale.cgValue)
case .pdf:
- let insets = try makeImageInsets(for: config.insets)
- return try image.pdfData(size: config.size.cgValue, insets: insets)
+ return try image
+ .inset(makeImageInsets(for: config.insets))
+ .size(config.size.cgValue)
+ .pdfData()
case .png:
- let insets = try makeImageInsets(for: config.insets)
- return try image.pngData(size: config.size.cgValue, scale: config.scale.cgValue, insets: insets)
+ return try image
+ .inset(makeImageInsets(for: config.insets))
+ .size(config.size.cgValue)
+ .pngData(scale: config.scale.cgValue)
case .swift, .sfsymbol:
throw Error.unsupported
}
@@ -129,6 +141,18 @@ public extension CommandLine {
}
}
+private extension SVG {
+
+ func size(_ s: CGSize?) -> SVG {
+ guard let s else { return self }
+ return sized(s)
+ }
+
+ func inset(_ insets: Insets) -> SVG {
+ expanded(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right)
+ }
+}
+
#if canImport(CoreGraphics)
private extension CommandLine.Scale {
var cgValue: CGFloat {
diff --git a/SwiftDraw/CommandLine.Arguments.swift b/SwiftDraw/Sources/CommandLine/CommandLine.Arguments.swift
similarity index 77%
rename from SwiftDraw/CommandLine.Arguments.swift
rename to SwiftDraw/Sources/CommandLine/CommandLine.Arguments.swift
index e6f17e76..18b953b5 100644
--- a/SwiftDraw/CommandLine.Arguments.swift
+++ b/SwiftDraw/Sources/CommandLine/CommandLine.Arguments.swift
@@ -46,9 +46,32 @@ extension CommandLine {
case black
case blackInsets
case hideUnsupportedFilters
+ case legacy
var hasValue: Bool {
- self != .hideUnsupportedFilters
+ switch self {
+ case .hideUnsupportedFilters, .legacy:
+ return false
+ default:
+ return true
+ }
+ }
+
+ static func make(from text: String) -> Self? {
+ if let modifier = Modifier(rawValue: text) {
+ return modifier
+ }
+
+ switch text {
+ case "ultralight-insets":
+ return .ultralightInsets
+ case "black-insets":
+ return .blackInsets
+ case "hide-unsupported-filters":
+ return .hideUnsupportedFilters
+ default:
+ return nil
+ }
}
}
@@ -85,7 +108,7 @@ private extension Array where Element == String {
}
guard self[0].hasPrefix("--"),
- let modifier = CommandLine.Modifier(rawValue: String(self[0].dropFirst(2))) else {
+ let modifier = CommandLine.Modifier.make(from: String(self[0].dropFirst(2))) else {
throw CommandLine.Error.invalid
}
diff --git a/SwiftDraw/CommandLine.Configuration.swift b/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift
similarity index 82%
rename from SwiftDraw/CommandLine.Configuration.swift
rename to SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift
index f7581c6b..df279136 100644
--- a/SwiftDraw/CommandLine.Configuration.swift
+++ b/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift
@@ -29,7 +29,8 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import Foundation
+import SwiftDrawDOM
+public import Foundation
extension CommandLine {
@@ -47,6 +48,8 @@ extension CommandLine {
public var scale: Scale
public var options: SVG.Options
public var precision: Int?
+ public var symbolSize: SFSymbolRenderer.SizeCategory?
+ public var isLegacyInsetsEnabled: Bool
}
public enum Format: String {
@@ -105,32 +108,37 @@ extension CommandLine {
throw Error.invalid
}
- let size = try parseSize(from: modifiers[.size])
+ let size = try parseSize(from: modifiers[.size], format: format)
let scale = try parseScale(from: modifiers[.scale])
let precision = try parsePrecision(from: modifiers[.precision])
- let insets = try parseInsets(from: modifiers[.insets])
+ let insets = try parseInsets(from: modifiers[.insets]) ?? Insets()
let api = try parseAPI(from: modifiers[.api])
let ultralight = try parseFileURL(file: modifiers[.ultralight], within: baseDirectory)
let ultralightInsets = try parseInsets(from: modifiers[.ultralightInsets])
let black = try parseFileURL(file: modifiers[.black], within: baseDirectory)
let blackInsets = try parseInsets(from: modifiers[.blackInsets])
let output = try parseFileURL(file: modifiers[.output], within: baseDirectory)
+ let symbolSize = try parseSymbolSize(from: modifiers[.size], format: format)
let options = try parseOptions(from: modifiers)
let result = source.newURL(for: format, scale: scale)
- return Configuration(input: source,
- inputUltralight: ultralight,
- inputBlack: black,
- output: output ?? result,
- format: format,
- size: size,
- api: api,
- insets: insets,
- insetsUltralight: ultralightInsets,
- insetsBlack: blackInsets,
- scale: scale,
- options: options,
- precision: precision)
+ return Configuration(
+ input: source,
+ inputUltralight: ultralight,
+ inputBlack: black,
+ output: output ?? result,
+ format: format,
+ size: size,
+ api: api,
+ insets: insets,
+ insetsUltralight: ultralightInsets,
+ insetsBlack: blackInsets,
+ scale: scale,
+ options: options,
+ precision: precision,
+ symbolSize: symbolSize,
+ isLegacyInsetsEnabled: modifiers.keys.contains(.legacy)
+ )
}
static func parseFileURL(file: String, within directory: URL) throws -> URL {
@@ -173,8 +181,9 @@ extension CommandLine {
return precision
}
- static func parseSize(from value: String??) throws -> Size {
- guard let value = value,
+ static func parseSize(from value: String??, format: Format) throws -> Size {
+ guard format != .sfsymbol,
+ let value = value,
let value = value else {
return .default
}
@@ -191,6 +200,26 @@ extension CommandLine {
return .custom(width: Int(width), height: Int(height))
}
+ static func parseSymbolSize(from value: String??, format: Format) throws -> SFSymbolRenderer.SizeCategory? {
+ guard format == .sfsymbol,
+ let value = value,
+ let value = value else {
+ return nil
+ }
+
+ switch value {
+ case "small":
+ return .small
+ case "medium":
+ return .medium
+ case "large":
+ return .large
+ default:
+ throw Error.invalid
+
+ }
+ }
+
static func parseAPI(from value: String??) throws -> API? {
guard let value = value,
let value = value else {
@@ -203,10 +232,13 @@ extension CommandLine {
return api
}
- static func parseInsets(from value: String??) throws -> Insets {
+ static func parseInsets(from value: String??) throws -> Insets? {
guard let value = value,
- let value = value,
- value != "auto" else {
+ let value = value else {
+ return nil
+ }
+
+ guard value != "auto" else {
return Insets()
}
@@ -237,7 +269,7 @@ extension CommandLine {
}
}
-private extension XMLParser.Scanner {
+private extension DOMXMLParser.Scanner {
mutating func scanInset() throws -> Double? {
guard !scanStringIfPossible("auto") else {
@@ -248,8 +280,8 @@ private extension XMLParser.Scanner {
}
extension SVG.Options {
- static let disableTransparencyLayers = Self(rawValue: 1 << 8)
- static let commandLine = Self(rawValue: 1 << 9)
+ static var disableTransparencyLayers: SVG.Options { Self(rawValue: 1 << 8) }
+ static var commandLine: SVG.Options { Self(rawValue: 1 << 9) }
}
extension URL {
diff --git a/SwiftDraw/CommandLine.swift b/SwiftDraw/Sources/CommandLine/CommandLine.swift
similarity index 100%
rename from SwiftDraw/CommandLine.swift
rename to SwiftDraw/Sources/CommandLine/CommandLine.swift
diff --git a/SwiftDraw/CoordinateFormatter.swift b/SwiftDraw/Sources/Formatter/CoordinateFormatter.swift
similarity index 99%
rename from SwiftDraw/CoordinateFormatter.swift
rename to SwiftDraw/Sources/Formatter/CoordinateFormatter.swift
index d3f15d9e..ddd02052 100644
--- a/SwiftDraw/CoordinateFormatter.swift
+++ b/SwiftDraw/Sources/Formatter/CoordinateFormatter.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
struct CoordinateFormatter {
diff --git a/SwiftDraw/XML.Formatter.SVG.swift b/SwiftDraw/Sources/Formatter/XML.Formatter.SVG.swift
similarity index 93%
rename from SwiftDraw/XML.Formatter.SVG.swift
rename to SwiftDraw/Sources/Formatter/XML.Formatter.SVG.swift
index 0a8273bf..2c1e49f4 100644
--- a/SwiftDraw/XML.Formatter.SVG.swift
+++ b/SwiftDraw/Sources/Formatter/XML.Formatter.SVG.swift
@@ -29,12 +29,13 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
extension XML.Formatter {
enum Error: Swift.Error {
- case unsupportedGraphicsElement(DOM.GraphicsElement)
+ case unsupportedGraphicsElement(name: String)
}
struct SVG {
@@ -197,10 +198,10 @@ extension XML.Formatter {
} else if let path = graphic as? DOM.Path {
element = makeElement(from: path)
} else {
- throw Error.unsupportedGraphicsElement(graphic)
+ throw Error.unsupportedGraphicsElement(name: String(describing: type(of: graphic)))
}
- if let container = graphic as? ContainerElement {
+ if let container = graphic as? any ContainerElement {
try element.children.append(
contentsOf: makeElements(from: container.childElements)
)
@@ -295,13 +296,23 @@ extension XML.Formatter {
return "currentColor"
case let .keyword(k):
return k.rawValue
- case let .rgbi(r, g, b):
- return "rgb(\(r), \(g), \(b))"
- case let .rgbf(r, g, b):
+ case let .rgbi(r, g, b, a):
+ if a == 1.0 {
+ return "rgb(\(r), \(g), \(b))"
+ } else {
+ let aa = String(format: "%.2f", a)
+ return "rgba(\(r), \(g), \(b), \(aa))"
+ }
+ case let .rgbf(r, g, b, a):
let rr = String(format: "%.0f", r * 100)
let gg = String(format: "%.0f", g * 100)
let bb = String(format: "%.0f", b * 100)
- return "rgb(\(rr)%, \(gg)%, \(bb)%)"
+ if a == 1.0 {
+ return "rgb(\(rr)%, \(gg)%, \(bb)%)"
+ } else {
+ let aa = String(format: "%.2f", a)
+ return "rgba(\(rr)%, \(gg)%, \(bb)%, \(aa))"
+ }
case let .p3(r, g, b):
return "color(display-p3 \(r), \(g), \(b))"
case let .hex(r, g, b):
@@ -317,17 +328,17 @@ extension XML.Formatter {
case let .matrix(a: a, b: b, c: c, d: d, e: e, f: f):
return "matrix(\(formatter.format(a,b,c,d,e,f)))"
case let .translate(tx: tx, ty: ty):
- return "translate(\(formatter.format(tx, ty))"
+ return "translate(\(formatter.format(tx, ty)))"
case let .scale(sx: sx, sy: sy):
- return "scale(\(formatter.format(sx, sy))"
+ return "scale(\(formatter.format(sx, sy)))"
case let .rotate(angle: angle):
- return "rotate(\(formatter.format(angle))"
+ return "rotate(\(formatter.format(angle)))"
case let .rotatePoint(angle: angle, cx: cx, cy: cy):
- return "rotate(\(formatter.format(angle, cx, cy))"
+ return "rotate(\(formatter.format(angle, cx, cy)))"
case let .skewX(angle: angle):
- return "skewX(\(formatter.format(angle))"
+ return "skewX(\(formatter.format(angle)))"
case let .skewY(angle: angle):
- return "skewY(\(formatter.format(angle))"
+ return "skewY(\(formatter.format(angle)))"
}
}
diff --git a/SwiftDraw/XML.Formatter.swift b/SwiftDraw/Sources/Formatter/XML.Formatter.swift
similarity index 99%
rename from SwiftDraw/XML.Formatter.swift
rename to SwiftDraw/Sources/Formatter/XML.Formatter.swift
index 29a3de5b..a1b54d85 100644
--- a/SwiftDraw/XML.Formatter.swift
+++ b/SwiftDraw/Sources/Formatter/XML.Formatter.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
extension XML {
diff --git a/SwiftDraw/LayerTree.Builder.Layer.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Layer.swift
similarity index 86%
rename from SwiftDraw/LayerTree.Builder.Layer.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.Layer.swift
index 5f3cbb23..0ff7d240 100644
--- a/SwiftDraw/LayerTree.Builder.Layer.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Layer.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
extension LayerTree.Builder {
@@ -41,8 +42,8 @@ extension LayerTree.Builder {
func makeUseLayerContents(from use: DOM.Use, with state: State) throws -> LayerTree.Layer.Contents {
guard
- let id = use.href.fragment,
- let element = svg.defs.elements[id] else {
+ let id = use.href.fragmentID,
+ let element = svg.firstGraphicsElement(with: id) else {
throw LayerTree.Error.invalid("missing referenced element: \(use.href)")
}
@@ -55,7 +56,6 @@ extension LayerTree.Builder {
}
return .layer(l)
-
}
static func makeTextContents(from text: DOM.Text, with state: State) -> LayerTree.Layer.Contents {
@@ -71,9 +71,15 @@ extension LayerTree.Builder {
static func makeImageContents(from image: DOM.Image) throws -> LayerTree.Layer.Contents {
guard
let decoded = image.href.decodedData,
- let im = LayerTree.Image(mimeType: decoded.mimeType, data: decoded.data) else {
+ var im = LayerTree.Image(mimeType: decoded.mimeType, data: decoded.data) else {
throw LayerTree.Error.invalid("Cannot decode image")
}
+
+ im.origin.x = LayerTree.Float(image.x ?? 0)
+ im.origin.y = LayerTree.Float(image.y ?? 0)
+ im.width = image.width.map { LayerTree.Float($0) }
+ im.height = image.height.map { LayerTree.Float($0) }
+
return .image(im)
}
}
diff --git a/SwiftDraw/LayerTree.Builder.Path.Arc.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.Arc.swift
similarity index 94%
rename from SwiftDraw/LayerTree.Builder.Path.Arc.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.Arc.swift
index aeb95c8c..0cf58c19 100644
--- a/SwiftDraw/LayerTree.Builder.Path.Arc.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.Arc.swift
@@ -33,9 +33,9 @@ import Foundation
//converts DOM.Path.Arc -> LayerTree.Path.Cubic
-private extension Comparable {
+extension Comparable {
func clamped(to limits: ClosedRange) -> Self {
- return min(max(self, limits.lowerBound), limits.upperBound)
+ return min(limits.upperBound, max(limits.lowerBound, self))
}
}
@@ -43,7 +43,7 @@ private func almostEqual(_ a: T, _ b: T) -> Bool {
return a >= b.nextDown && a <= b.nextUp
}
-private func vectorAngle(ux: LayerTree.Float, uy: LayerTree.Float, vx: LayerTree.Float, vy: LayerTree.Float) -> LayerTree.Float {
+func vectorAngle(ux: LayerTree.Float, uy: LayerTree.Float, vx: LayerTree.Float, vy: LayerTree.Float) -> LayerTree.Float {
let sign: LayerTree.Float = (ux * vy - uy * vx) < 0.0 ? -1.0 : 1.0
let dot = (ux * vx + uy * vy).clamped(to: -1.0...1.0)
return sign * acos(dot)
@@ -163,7 +163,10 @@ func makeCubic(from origin: LayerTree.Point, to destination: LayerTree.Point,
var result = [[LayerTree.Float]]()
- let segments = max(Int(ceil(abs(cc.deltaT) / (LayerTree.Float.tau / 4.0))), 1)
+ guard let totalSegments = Int(exactly: ceil(abs(cc.deltaT) / (LayerTree.Float.tau / 4.0))) else {
+ return []
+ }
+ let segments = max(totalSegments, 1)
let deltaT = cc.deltaT / LayerTree.Float(segments)
var theta1 = cc.theta
diff --git a/SwiftDraw/LayerTree.Builder.Path.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.swift
similarity index 97%
rename from SwiftDraw/LayerTree.Builder.Path.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.swift
index 2031d95a..9a496f04 100644
--- a/SwiftDraw/LayerTree.Builder.Path.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Path.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
//converts DOM.Path -> LayerTree.Path
@@ -182,10 +183,8 @@ extension LayerTree.Builder {
let cp1 = Point(origin.x + (controlPoint.x - origin.x) * ratio,
origin.y + (controlPoint.y - origin.y) * ratio)
- let cpX = (final.x - origin.x)*Float(1.0/3.0)
-
- let cp2 = Point(cp1.x + cpX,
- cp1.y)
+ let cp2 = Point(final.x + (controlPoint.x - final.x) * ratio,
+ final.y + (controlPoint.y - final.y) * ratio)
return .cubic(to: final, control1: cp1, control2: cp2)
}
@@ -201,8 +200,10 @@ extension LayerTree.Builder {
let final = space == .absolute ? Point(x, y) : Point(x, y).absolute(from: point)
let cpX = (final.x - point.x)*Float(1.0/3.0)
+ let cpY = (final.y - point.y)*Float(1.0/3.0)
+
let cp2 = Point(cp1.x + cpX,
- cp1.y)
+ cp1.y + cpY)
return .cubic(to: final, control1: cp1, control2: cp2)
}
diff --git a/SwiftDraw/LayerTree.Builder.Shape.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Shape.swift
similarity index 99%
rename from SwiftDraw/LayerTree.Builder.Shape.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.Shape.swift
index 45a939e8..5168457a 100644
--- a/SwiftDraw/LayerTree.Builder.Shape.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Shape.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
extension LayerTree.Builder {
diff --git a/SwiftDraw/LayerTree.Builder.Text.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Text.swift
similarity index 99%
rename from SwiftDraw/LayerTree.Builder.Text.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.Text.swift
index d12a839c..95fac603 100644
--- a/SwiftDraw/LayerTree.Builder.Text.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.Text.swift
@@ -31,6 +31,7 @@
// Convert a DOM.SVG into a layer tree
+import SwiftDrawDOM
import Foundation
#if canImport(CoreText)
import CoreText
diff --git a/SwiftDraw/LayerTree.Builder.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.swift
similarity index 78%
rename from SwiftDraw/LayerTree.Builder.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Builder.swift
index 013cfa7f..ad8c852d 100644
--- a/SwiftDraw/LayerTree.Builder.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Builder.swift
@@ -31,6 +31,7 @@
// Convert a DOM.SVG into a layer tree
+import SwiftDrawDOM
import Foundation
extension LayerTree {
@@ -44,24 +45,41 @@ extension LayerTree {
}
func makeLayer() -> Layer {
- let l = makeLayer(from: svg, inheriting: State())
- l.transform = Builder.makeTransform(for: svg.viewBox,
- width: svg.width,
- height: svg.height)
+ makeLayer(svg: svg, inheriting: State())
+ }
+
+ func makeLayer(svg: DOM.SVG, inheriting previousState: State) -> Layer {
+ let l = makeLayer(from: svg, inheriting: previousState)
+ l.transform = Builder.makeTransform(
+ x: svg.x,
+ y: svg.y,
+ viewBox: svg.viewBox,
+ width: svg.width,
+ height: svg.height
+ )
return l
}
- static func makeTransform(for viewBox: DOM.SVG.ViewBox?, width: DOM.Length, height: DOM.Length) -> [LayerTree.Transform] {
- guard let viewBox = viewBox else {
- return []
- }
+ static func makeTransform(
+ x: DOM.Coordinate?,
+ y: DOM.Coordinate?,
+ viewBox: DOM.SVG.ViewBox?,
+ width: DOM.Length,
+ height: DOM.Length
+ ) -> [LayerTree.Transform] {
+ let position = LayerTree.Transform.translate(tx: x ?? 0, ty: y ?? 0)
+ let viewBox = viewBox ?? DOM.SVG.ViewBox(x: 0, y: 0, width: .init(width), height: .init(height))
let sx = LayerTree.Float(width) / viewBox.width
let sy = LayerTree.Float(height) / viewBox.height
let scale = LayerTree.Transform.scale(sx: sx, sy: sy)
let translate = LayerTree.Transform.translate(tx: -viewBox.x, ty: -viewBox.y)
- var transform = [LayerTree.Transform]()
+ var transform: [LayerTree.Transform] = []
+
+ if position != .translate(tx: 0, ty: 0) {
+ transform.append(position)
+ }
if scale != .scale(sx: 1, sy: 1) {
transform.append(scale)
@@ -74,35 +92,60 @@ extension LayerTree {
return transform
}
- func makeLayer(from element: DOM.GraphicsElement, inheriting previousState: State) -> Layer {
+ func makeLayer(from root: DOM.GraphicsElement, inheriting previousState: State) -> Layer {
+ var stack: [(DOM.GraphicsElement, State, Layer?)] = [(root, previousState, nil)]
+ var resultLayer: Layer? = nil
+
+ while let (currentElement, currentState, parentLayer) = stack.popLast() {
+ let (layer, newState) = makeBaseLayer(from: currentElement, inheriting: currentState)
+
+ if let contents = makeContents(from: currentElement, with: newState) {
+ layer.appendContents(contents)
+ } else if let container = currentElement as? any ContainerElement {
+ // Push children in reverse so they are processed in the original order
+ for child in container.childElements.reversed() {
+ stack.append((child, newState, layer))
+ }
+ }
+
+ if let parent = parentLayer {
+ parent.appendContents(.layer(layer))
+
+ if let svg = currentElement as? DOM.SVG {
+ let viewBox = svg.viewBox ?? DOM.SVG.ViewBox(x: 0, y: 0, width: .init(svg.width), height: .init(svg.height))
+ let bounds = LayerTree.Rect(x: viewBox.x, y: viewBox.y, width: viewBox.width, height: viewBox.height)
+ layer.clip = [ClipShape(shape: .rect(within: bounds, radii: .zero), transform: .identity)]
+ layer.transform = Builder.makeTransform(
+ x: svg.x,
+ y: svg.y,
+ viewBox: svg.viewBox,
+ width: svg.width,
+ height: svg.height
+ )
+ }
+ } else {
+ // This must be the top-level root layer
+ resultLayer = layer
+ }
+ }
+
+ return resultLayer!
+ }
+
+ func makeBaseLayer(from element: DOM.GraphicsElement, inheriting previousState: State) -> (Layer, State) {
let state = createState(for: element, inheriting: previousState)
let attributes = element.attributes
let l = Layer()
l.class = element.class
- guard state.display == .inline else { return l }
+ guard state.display != .none else { return (l, state) }
l.transform = Builder.createTransforms(from: attributes.transform ?? [])
- l.clip = createClipShapes(for: element)
+ l.clip = makeClipShapes(for: element)
l.clipRule = attributes.clipRule
l.mask = createMaskLayer(for: element)
l.opacity = state.opacity
- l.contents = makeAllContents(from: element, with: state)
l.filters = makeFilters(for: state)
- return l
- }
-
- func makeAllContents(from element: DOM.GraphicsElement, with state: State) -> [Layer.Contents] {
- var all = [Layer.Contents]()
- if let contents = makeContents(from: element, with: state) {
- all.append(contents)
- }
- else if let container = element as? ContainerElement {
- container.childElements.forEach{
- let contents = Layer.Contents.layer(makeLayer(from: $0, inheriting: state))
- all.append(contents)
- }
- }
- return all
+ return (l, state)
}
func makeContents(from element: DOM.GraphicsElement, with state: State) -> Layer.Contents? {
@@ -123,21 +166,33 @@ extension LayerTree {
return nil
}
- func createClipShapes(for element: DOM.GraphicsElement) -> [Shape] {
- guard let clipId = element.attributes.clipPath?.fragment,
- let clip = svg.defs.clipPaths.first(where: { $0.id == clipId }) else { return [] }
+ func makeClipShapes(for element: DOM.GraphicsElement) -> [ClipShape] {
+ let attributes = DOM.presentationAttributes(for: element, styles: svg.styles)
+ guard let clipID = attributes.clipPath?.fragmentID,
+ let clip = svg.defs.clipPaths.first(where: { $0.id == clipID }) else { return [] }
+ return clip.childElements.compactMap(makeClipShape)
+ }
+
+ func makeClipShape(for element: DOM.GraphicsElement) -> ClipShape? {
+ guard let shape = Builder.makeShape(from: element) else {
+ return nil
+ }
+
+ let transform = Self.createTransforms(from: element.attributes.transform ?? [])
+ .toMatrix()
- return clip.childElements.compactMap{ Builder.makeShape(from: $0) }
+ return ClipShape(shape: shape, transform: transform)
}
func createMaskLayer(for element: DOM.GraphicsElement) -> Layer? {
- guard let maskId = element.attributes.mask?.fragment,
+ guard let maskId = element.attributes.mask?.fragmentID,
let mask = svg.defs.masks.first(where: { $0.id == maskId }) else { return nil }
let l = Layer()
+ let maskState = createState(for: mask, inheriting: State())
mask.childElements.forEach {
- let contents = Layer.Contents.layer(makeLayer(from: $0, inheriting: State()))
+ let contents = Layer.Contents.layer(makeLayer(from: $0, inheriting: maskState))
l.appendContents(contents)
}
@@ -145,7 +200,7 @@ extension LayerTree {
}
func makeFilters(for state: State) -> [Filter] {
- guard let filterId = state.filter?.fragment,
+ guard let filterId = state.filter?.fragmentID,
let filter = svg.defs.filters.first(where: { $0.id == filterId }) else { return [] }
return filter.effects
}
@@ -191,15 +246,15 @@ extension LayerTree.Builder {
.withAlpha(state.fillOpacity).maybeNone()
if case .url(let patternId) = state.fill,
- let element = svg.defs.patterns.first(where: { $0.id == patternId.fragment }) {
+ let element = svg.defs.patterns.first(where: { $0.id == patternId.fragmentID }) {
let pattern = makePattern(for: element)
return LayerTree.FillAttributes(pattern: pattern, rule: state.fillRule, opacity: state.fillOpacity)
} else if case .url(let gradientId) = state.fill,
- let element = svg.defs.linearGradients.first(where: { $0.id == gradientId.fragment }),
+ let element = svg.defs.linearGradients.first(where: { $0.id == gradientId.fragmentID }),
let gradient = makeGradient(for: element) {
return LayerTree.FillAttributes(linear: gradient, rule: state.fillRule, opacity: state.fillOpacity)
} else if case .url(let gradientId) = state.fill,
- let element = svg.defs.radialGradients.first(where: { $0.id == gradientId.fragment }),
+ let element = svg.defs.radialGradients.first(where: { $0.id == gradientId.fragmentID }),
let gradient = makeGradient(for: element) {
return LayerTree.FillAttributes(radial: gradient, rule: state.fillRule, opacity: state.fillOpacity)
} else {
@@ -208,7 +263,7 @@ extension LayerTree.Builder {
}
func makeLinearGradient(for gradientId: URL) -> LayerTree.LinearGradient? {
- guard let element = svg.defs.linearGradients.first(where: { $0.id == gradientId.fragment }),
+ guard let element = svg.defs.linearGradients.first(where: { $0.id == gradientId.fragmentID }),
let gradient = makeGradient(for: element) else {
return nil
}
@@ -216,7 +271,7 @@ extension LayerTree.Builder {
}
func makeRadialGradient(for gradientId: URL) -> LayerTree.RadialGradient? {
- guard let element = svg.defs.radialGradients.first(where: { $0.id == gradientId.fragment }),
+ guard let element = svg.defs.radialGradients.first(where: { $0.id == gradientId.fragmentID }),
let gradient = makeGradient(for: element) else {
return nil
}
@@ -249,7 +304,7 @@ extension LayerTree.Builder {
let y2 = element.y2 ?? 0
var stops = [LayerTree.Gradient.Stop]()
- if let id = element.href?.fragment,
+ if let id = element.href?.fragmentID,
let reference = svg.defs.linearGradients.first(where: { $0.id == id }) {
stops = makeGradientStops(for: reference)
} else {
@@ -272,7 +327,7 @@ extension LayerTree.Builder {
func makeGradient(for element: DOM.RadialGradient) -> LayerTree.RadialGradient? {
var stops = [LayerTree.Gradient.Stop]()
- if let id = element.href?.fragment,
+ if let id = element.href?.fragmentID,
let reference = svg.defs.radialGradients.first(where: { $0.id == id }) {
stops = makeGradientStops(for: reference)
} else {
diff --git a/SwiftDraw/LayerTree.Color.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Color.swift
similarity index 83%
rename from SwiftDraw/LayerTree.Color.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Color.swift
index 289c89c5..7b23045d 100644
--- a/SwiftDraw/LayerTree.Color.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Color.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
extension LayerTree {
enum Color: Hashable {
@@ -58,15 +59,15 @@ extension LayerTree.Color {
case let .keyword(c):
let rgbi = c.rgbi
return LayerTree.Color(rgbi.0, rgbi.1, rgbi.2)
- case let .rgbi(r, g, b):
- return LayerTree.Color(r, g, b)
+ case let .rgbi(r, g, b, a):
+ return LayerTree.Color(r, g, b, Float(a))
case let .hex(r, g, b):
return LayerTree.Color(r, g, b)
- case let .rgbf(r, g, b):
+ case let .rgbf(r, g, b, a):
return .rgba(r: Float(r),
g: Float(g),
b: Float(b),
- a: 1.0,
+ a: Float(a),
space: .srgb)
case let .p3(r, g, b):
return .rgba(r: Float(r),
@@ -84,6 +85,14 @@ extension LayerTree.Color {
a: 1.0,
space: .srgb)
}
+
+ init(_ r: UInt8, _ g: UInt8, _ b: UInt8, _ a: DOM.Float) {
+ self = .rgba(r: Float(r)/255.0,
+ g: Float(g)/255.0,
+ b: Float(b)/255.0,
+ a: a,
+ space: .srgb)
+ }
var isOpaque: Bool {
switch self {
@@ -100,14 +109,14 @@ extension LayerTree.Color {
switch self {
case .none:
return .none
- case let .rgba(r: r, g: g, b: b, a: _, space):
+ case let .rgba(r: r, g: g, b: b, a: a, space):
return .rgba(r: r,
g: g,
b: b,
- a: alpha,
+ a: alpha * a,
space: space)
- case .gray(white: let w, a: _):
- return .gray(white: w, a: alpha)
+ case .gray(white: let w, a: let a):
+ return .gray(white: w, a: alpha * a)
}
}
@@ -150,6 +159,10 @@ struct DefaultColorConverter: ColorConverter {
}
}
+extension ColorConverter where Self == DefaultColorConverter {
+ static var `default`: DefaultColorConverter { DefaultColorConverter() }
+}
+
struct LuminanceColorConverter: ColorConverter {
func createColor(from color: LayerTree.Color) -> LayerTree.Color {
switch color {
@@ -162,3 +175,7 @@ struct LuminanceColorConverter: ColorConverter {
}
}
}
+
+extension ColorConverter where Self == LuminanceColorConverter {
+ static var luminance: LuminanceColorConverter { LuminanceColorConverter() }
+}
diff --git a/SwiftDraw/LayerTree.CommandGenerator.swift b/SwiftDraw/Sources/LayerTree/LayerTree.CommandGenerator.swift
similarity index 70%
rename from SwiftDraw/LayerTree.CommandGenerator.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.CommandGenerator.swift
index 5bda2105..f46494ce 100644
--- a/SwiftDraw/LayerTree.CommandGenerator.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.CommandGenerator.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
import Foundation
+import SwiftDrawDOM
// Convert a LayerTree into RenderCommands
@@ -45,6 +46,9 @@ extension LayerTree {
private var hasLoggedGradientWarning = false
private var hasLoggedMaskWarning = false
+ private var paths: [LayerTree.Shape: P.Types.Path] = [:]
+ private var images: [LayerTree.Image: P.Types.Image] = [:]
+
init(provider: P, size: LayerTree.Size, scale: LayerTree.Float = 3.0, options: SVG.Options) {
self.provider = provider
self.size = size
@@ -52,86 +56,146 @@ extension LayerTree {
self.options = options
}
- func renderCommands(for layer: Layer, colorConverter: ColorConverter = DefaultColorConverter()) -> [RendererCommand] {
- guard layer.opacity > 0.0 else { return [] }
+ func renderCommands(for l: Layer, colorConverter c: any ColorConverter) -> [RendererCommand] {
+ var commands = [RendererCommand]()
- if !layer.filters.isEmpty {
- guard !options.contains(.hideUnsupportedFilters) else {
- return []
+ var stack: [RenderStep] = [
+ .beginLayer(l, c)
+ ]
+
+ while let step = stack.popLast() {
+ switch step {
+ case let .beginLayer(layer, colorConverter):
+ let state = makeCommandState(for: layer, colorConverter: colorConverter)
+
+ //guard state.hasContents else { continue }
+ stack.append(.endLayer(layer, state))
+
+ if state.hasFilters {
+ logUnsupportedFilters(layer.filters)
+ }
+
+ if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
+ commands.append(.pushState)
+ }
+
+ commands.append(contentsOf: renderCommands(forTransforms: layer.transform))
+ commands.append(contentsOf: renderCommands(forOpacity: layer.opacity))
+ commands.append(contentsOf: renderCommands(forClip: layer.clip, using: layer.clipRule))
+
+ if state.hasMask {
+ commands.append(.pushTransparencyLayer)
+ }
+
+ //push render of all of the layer contents in reverse order
+ for contents in layer.contents.reversed() {
+ switch makeRenderContents(for: contents, colorConverter: colorConverter) {
+ case let .simple(cmd):
+ stack.append(.contents(cmd))
+ case let .layer(layer):
+ stack.append(.beginLayer(layer, colorConverter))
+ }
+ }
+
+ case let .contents(cmd):
+ commands.append(contentsOf: cmd)
+
+ case let .endLayer(layer, state):
+ //render apply mask
+ if state.hasMask {
+ commands.append(contentsOf: renderCommands(forMask: layer.mask))
+ commands.append(.popTransparencyLayer)
+ }
+
+ if state.hasOpacity {
+ commands.append(.popTransparencyLayer)
+ }
+
+ if state.hasOpacity || state.hasTransform || state.hasClip || state.hasMask {
+ commands.append(.popState)
+ }
}
- logUnsupportedFilters(layer.filters)
}
- let opacityCommands = renderCommands(forOpacity: layer.opacity)
- let transformCommands = renderCommands(forTransforms: layer.transform)
- let clipCommands = renderCommands(forClip: layer.clip, using: layer.clipRule)
- let maskCommands = renderCommands(forMask: layer.mask)
-
- guard canRenderMask(maskCommands) else {
- return []
- }
+ return commands
+ }
- var commands = [RendererCommand]()
+ enum RenderStep {
+ case beginLayer(LayerTree.Layer, any ColorConverter)
+ case contents([RendererCommand])
+ case endLayer(LayerTree.Layer, CommandState)
+ }
- if !opacityCommands.isEmpty ||
- !transformCommands.isEmpty ||
- !clipCommands.isEmpty ||
- !maskCommands.isEmpty {
- commands.append(.pushState)
- }
+ struct CommandState {
+ var hasOpacity: Bool
+ var hasTransform: Bool
+ var hasClip: Bool
+ var hasContents: Bool
+ var hasMask: Bool
+ var hasFilters: Bool
+ var colorConverter: any ColorConverter
+ }
- commands.append(contentsOf: transformCommands)
- commands.append(contentsOf: opacityCommands)
- commands.append(contentsOf: clipCommands)
+ func makeCommandState(for layer: Layer, colorConverter: any ColorConverter) -> CommandState {
+ var hasContents = !layer.contents.isEmpty && layer.opacity > 0.0
+ let hasMask = layer.mask != nil
+ let hasFilters = !layer.filters.isEmpty
- if !maskCommands.isEmpty {
- commands.append(.pushTransparencyLayer)
+ if hasMask && options.contains(.disableTransparencyLayers) {
+ hasContents = false
}
- //render all of the layer contents
- for contents in layer.contents {
- commands.append(contentsOf: renderCommands(for: contents, colorConverter: colorConverter))
+ if hasFilters && options.contains(.hideUnsupportedFilters) {
+ hasContents = false
}
- //render apply mask
- if !maskCommands.isEmpty {
- commands.append(contentsOf: maskCommands)
- commands.append(.popTransparencyLayer)
- }
+ return CommandState(
+ hasOpacity: layer.opacity < 1.0,
+ hasTransform: !layer.transform.isEmpty,
+ hasClip: !layer.clip.isEmpty,
+ hasContents: hasContents,
+ hasMask: hasMask,
+ hasFilters: hasFilters,
+ colorConverter: colorConverter
+ )
+ }
- if !opacityCommands.isEmpty {
- commands.append(.popTransparencyLayer)
+ func renderCommands(for contents: Layer.Contents, colorConverter: any ColorConverter) -> [RendererCommand] {
+ switch makeRenderContents(for: contents, colorConverter: colorConverter) {
+ case .simple(let commands):
+ return commands
+ case .layer(let layer):
+ return renderCommands(for: layer, colorConverter: colorConverter)
}
+ }
- if !opacityCommands.isEmpty ||
- !transformCommands.isEmpty ||
- !clipCommands.isEmpty ||
- !maskCommands.isEmpty {
- commands.append(.popState)
- }
+ enum RenderContents {
+ // simple contents create array of commands
+ case simple([RendererCommand])
- return commands
+ // layer contents requires recursion
+ case layer(LayerTree.Layer)
}
- func renderCommands(for contents: Layer.Contents, colorConverter: ColorConverter) -> [RendererCommand] {
+ func makeRenderContents(for contents: Layer.Contents, colorConverter: any ColorConverter) -> RenderContents {
switch contents {
case .shape(let shape, let stroke, let fill):
- return renderCommands(for: shape, stroke: stroke, fill: fill, colorConverter: colorConverter)
+ return .simple(renderCommands(for: shape, stroke: stroke, fill: fill, colorConverter: colorConverter))
case .image(let image):
- return renderCommands(for: image)
+ return .simple(renderCommands(for: image))
case .text(let text, let point, let att):
- return renderCommands(for: text, at: point, attributes: att, colorConverter: colorConverter)
+ return .simple(renderCommands(for: text, at: point, attributes: att, colorConverter: colorConverter))
case .layer(let layer):
- return renderCommands(for: layer, colorConverter: colorConverter)
+ return .layer(layer)
}
}
func renderCommands(for shape: Shape,
stroke: StrokeAttributes,
fill: FillAttributes,
- colorConverter: ColorConverter) -> [RendererCommand] {
+ colorConverter: any ColorConverter) -> [RendererCommand] {
var commands = [RendererCommand]()
- let path = provider.createPath(from: shape)
+ let path = makeCachedPath(from: shape)
switch fill.fill {
case .color(let color):
@@ -242,11 +306,55 @@ extension LayerTree {
}
func renderCommands(for image: Image) -> [RendererCommand] {
- guard let renderImage = provider.createImage(from: image) else { return [] }
- return [.draw(image: renderImage)]
+ guard let renderImage = makeCachedImage(from: image) else { return [] }
+ let size = provider.createSize(from: renderImage)
+ guard size.width > 0 && size.height > 0 else { return [] }
+
+ let frame = makeImageFrame(for: image, bitmapSize: size)
+ let rect = provider.createRect(from: frame)
+ return [.draw(image: renderImage, in: rect)]
}
- func renderCommands(for text: String, at point: Point, attributes: TextAttributes, colorConverter: ColorConverter = DefaultColorConverter()) -> [RendererCommand] {
+ private func makeCachedPath(from shape: LayerTree.Shape) -> P.Types.Path {
+ if let existing = paths[shape] {
+ return existing
+ }
+ let new = provider.createPath(from: shape)
+ paths[shape] = new
+ return new
+ }
+
+ private func makeCachedImage(from image: Image) -> P.Types.Image? {
+ if let existing = images[image] {
+ return existing
+ }
+ guard let new = provider.createImage(from: image) else {
+ return nil
+ }
+ images[image] = new
+ return new
+ }
+
+ func makeImageFrame(for image: Image, bitmapSize: LayerTree.Size) -> LayerTree.Rect {
+ var frame = LayerTree.Rect(
+ x: image.origin.x,
+ y: image.origin.y,
+ width: image.width ?? bitmapSize.width,
+ height: image.height ?? bitmapSize.height
+ )
+
+ let aspectRatio = bitmapSize.width / bitmapSize.height
+
+ if let height = image.height, image.width == nil {
+ frame.size.width = height * aspectRatio
+ }
+ if let width = image.width, image.height == nil {
+ frame.size.height = width / aspectRatio
+ }
+ return frame
+ }
+
+ func renderCommands(for text: String, at point: Point, attributes: TextAttributes, colorConverter: any ColorConverter = .default) -> [RendererCommand] {
guard let path = provider.createPath(from: text, at: point, with: attributes) else { return [] }
let converted = colorConverter.createColor(from: attributes.color)
@@ -287,15 +395,27 @@ extension LayerTree {
}
}
- func renderCommands(forClip shapes: [Shape], using rule: FillRule?) -> [RendererCommand] {
+ func renderCommands(forClip shapes: [ClipShape], using rule: FillRule?) -> [RendererCommand] {
guard !shapes.isEmpty else { return [] }
+ let paths = shapes.map { clip in
+ if clip.transform == .identity {
+ return makeCachedPath(from: clip.shape)
+ } else {
+ return makeCachedPath(from: .path(clip.shape.path.applying(matrix: clip.transform)))
+ }
+ }
- let paths = shapes.map { provider.createPath(from: $0) }
- let clipPath = provider.createPath(from: paths)
let rule = provider.createFillRule(from: rule ?? .nonzero)
- return [.setClip(path: clipPath, rule: rule)]
+
+ if paths.count == 1 {
+ return [.setClip(path: paths[0], rule: rule)]
+ } else {
+ let clipPath = provider.createPath(from: paths)
+ return [.setClip(path: clipPath, rule: rule)]
+ }
}
+
func renderCommands(forMask layer: Layer?) -> [RendererCommand] {
guard let layer = layer else { return [] }
@@ -308,7 +428,7 @@ extension LayerTree {
commands.append(.setBlend(mode: copy))
//commands.append(contentsOf: renderCommands(forClip: layer.clip))
let drawMask = layer.contents.flatMap{
- renderCommands(for: $0, colorConverter: LuminanceColorConverter())
+ renderCommands(for: $0, colorConverter: .luminance)
}
commands.append(contentsOf: drawMask)
commands.append(.popTransparencyLayer)
@@ -340,7 +460,7 @@ extension LayerTree {
func renderCommands(forLinear gradient: LayerTree.LinearGradient,
endpoints: (start: LayerTree.Point, end: LayerTree.Point),
opacity: LayerTree.Float,
- colorConverter: ColorConverter) -> [RendererCommand] {
+ colorConverter: any ColorConverter) -> [RendererCommand] {
let pathStart: LayerTree.Point
let pathEnd: LayerTree.Point
switch gradient.units {
@@ -374,7 +494,7 @@ extension LayerTree {
func renderCommands(forRadial gradient: RadialGradient,
in bounds: LayerTree.Rect,
opacity: LayerTree.Float,
- colorConverter: ColorConverter) -> [RendererCommand] {
+ colorConverter: any ColorConverter) -> [RendererCommand] {
let startCenter: LayerTree.Point
let startRadius: LayerTree.Float
let endCenter: LayerTree.Point
@@ -470,7 +590,7 @@ private extension LayerTree.Rect {
private extension LayerTree.Gradient {
- func convertColor(using converter: ColorConverter) -> LayerTree.Gradient {
+ func convertColor(using converter: any ColorConverter) -> LayerTree.Gradient {
let stops: [LayerTree.Gradient.Stop] = stops.map { stop in
var stop = stop
stop.color = converter.createColor(from: stop.color).withMultiplyingAlpha(stop.opacity)
diff --git a/SwiftDraw/LayerTree.CommandOptimizer.swift b/SwiftDraw/Sources/LayerTree/LayerTree.CommandOptimizer.swift
similarity index 99%
rename from SwiftDraw/LayerTree.CommandOptimizer.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.CommandOptimizer.swift
index 9ae1e928..51670c6d 100644
--- a/SwiftDraw/LayerTree.CommandOptimizer.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.CommandOptimizer.swift
@@ -144,8 +144,8 @@ struct OptimizerOptions: OptionSet {
self.rawValue = rawValue
}
- static let skipRedundantState = OptimizerOptions(rawValue: 1)
- static let skipInitialSaveState = OptimizerOptions(rawValue: 2)
+ static let skipRedundantState = OptimizerOptions(rawValue: 1 << 0)
+ static let skipInitialSaveState = OptimizerOptions(rawValue: 1 << 1)
}
extension RendererCommand {
diff --git a/SwiftDraw/LayerTree.Gradient.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Gradient.swift
similarity index 100%
rename from SwiftDraw/LayerTree.Gradient.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Gradient.swift
diff --git a/SwiftDraw/LayerTree.Image.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Image.swift
similarity index 61%
rename from SwiftDraw/LayerTree.Image.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Image.swift
index a98e1ce2..db0a9c72 100644
--- a/SwiftDraw/LayerTree.Image.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Image.swift
@@ -32,23 +32,35 @@
import Foundation
extension LayerTree {
- enum Image: Equatable {
- case jpeg(data: Data)
- case png(data: Data)
-
- init?(mimeType: String, data: Data) {
- guard data.count > 0 else { return nil }
-
- switch mimeType {
- case "image/png":
- self = .png(data: data)
- case "image/jpeg":
- self = .jpeg(data: data)
- case "image/jpg":
- self = .jpeg(data: data)
- default:
- return nil
- }
+ struct Image: Hashable {
+
+ var bitmap: Bitmap
+ var origin: Point = .zero
+ var width: LayerTree.Float?
+ var height: LayerTree.Float?
+
+ enum Bitmap: Hashable {
+ case jpeg(Data)
+ case png(Data)
+ }
+
+ init(bitmap: Bitmap) {
+ self.bitmap = bitmap
+ }
+
+ init?(mimeType: String, data: Data) {
+ guard data.count > 0 else { return nil }
+
+ switch mimeType {
+ case "image/png":
+ self.bitmap = .png(data)
+ case "image/jpeg":
+ self.bitmap = .jpeg(data)
+ case "image/jpg":
+ self.bitmap = .jpeg(data)
+ default:
+ return nil
+ }
+ }
}
- }
}
diff --git a/SwiftDraw/LayerTree.Layer.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Layer.swift
similarity index 71%
rename from SwiftDraw/LayerTree.Layer.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Layer.swift
index ee3edcf0..216e6509 100644
--- a/SwiftDraw/LayerTree.Layer.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Layer.swift
@@ -26,18 +26,20 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
+
extension LayerTree {
- final class Layer: Equatable {
+ final class Layer: Hashable {
var `class`: String? = nil
var contents: [Contents] = []
var opacity: Float = 1.0
var transform: [Transform] = []
- var clip: [Shape] = []
+ var clip: [ClipShape] = []
var clipRule: FillRule?
var mask: Layer?
var filters: [Filter] = []
- enum Contents: Equatable {
+ enum Contents: Hashable {
case shape(Shape, StrokeAttributes, FillAttributes)
case image(Image)
case text(String, Point, TextAttributes)
@@ -47,8 +49,6 @@ extension LayerTree {
func appendContents(_ contents: Contents) {
switch contents {
case .layer(let l):
- guard l.contents.isEmpty == false else { return }
-
//if layer is simple, we can ignore all other properties
if let simple = l.simpleContents {
self.contents.append(simple)
@@ -63,6 +63,7 @@ extension LayerTree {
var simpleContents: Contents? {
guard self.contents.count == 1,
let first = self.contents.first,
+ `class` == nil,
opacity == 1.0,
transform == [],
clip == [],
@@ -72,24 +73,35 @@ extension LayerTree {
return first
}
+ func hash(into hasher: inout Hasher) {
+ `class`.hash(into: &hasher)
+ opacity.hash(into: &hasher)
+ transform.hash(into: &hasher)
+ clip.hash(into: &hasher)
+ mask.hash(into: &hasher)
+ filters.hash(into: &hasher)
+ }
+
static func ==(lhs: Layer, rhs: Layer) -> Bool {
- return lhs.contents == rhs.contents &&
+ return lhs.class == rhs.class &&
+ lhs.contents == rhs.contents &&
lhs.opacity == rhs.opacity &&
lhs.transform == rhs.transform &&
lhs.clip == rhs.clip &&
+ lhs.clipRule == rhs.clipRule &&
lhs.mask == rhs.mask &&
lhs.filters == rhs.filters
}
}
- struct StrokeAttributes: Equatable {
+ struct StrokeAttributes: Hashable {
var color: Stroke
var width: Float
var cap: LineCap
var join: LineJoin
var miterLimit: Float
- enum Stroke: Equatable {
+ enum Stroke: Hashable {
case color(Color)
case linearGradient(LinearGradient)
case radialGradient(RadialGradient)
@@ -98,7 +110,7 @@ extension LayerTree {
}
}
- struct FillAttributes: Equatable {
+ struct FillAttributes: Hashable {
var fill: Fill = .color(.none)
var opacity: Float = 1.0
var rule: FillRule
@@ -126,20 +138,49 @@ extension LayerTree {
self.opacity = opacity
}
- enum Fill: Equatable {
+ enum Fill: Hashable {
case color(Color)
case pattern(Pattern)
case linearGradient(LinearGradient)
case radialGradient(RadialGradient)
- static let none = Fill.color(.none)
+ static var none: Fill { .color(.none) }
}
}
- struct TextAttributes: Equatable {
+ struct TextAttributes: Hashable {
var color: Color
var fontName: String
var size: Float
var anchor: DOM.TextAnchor
}
}
+
+extension LayerTree.Layer.Contents: CustomDebugStringConvertible {
+
+ var debugDescription: String {
+ switch self {
+ case .image:
+ return "image"
+ case .layer(let l):
+ return "layer-\(l.contents.map(\.debugDescription).joined(separator: ", "))"
+ case .shape(let s, _, _):
+ return "shape-\(s.debugDescription)"
+ case .text:
+ return "text"
+ }
+ }
+}
+
+extension LayerTree.Shape: CustomDebugStringConvertible {
+
+ var debugDescription: String {
+ switch self {
+ case .ellipse: return "ellipse"
+ case .rect: return "rect"
+ case .line: return "line"
+ case .path: return "path"
+ case .polygon: return "polygon"
+ }
+ }
+}
diff --git a/SwiftDraw/LayerTree.Path+Bounds.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Path+Bounds.swift
similarity index 100%
rename from SwiftDraw/LayerTree.Path+Bounds.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Path+Bounds.swift
diff --git a/SwiftDraw/LayerTree.Path+Reversed.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Path+Reversed.swift
similarity index 100%
rename from SwiftDraw/LayerTree.Path+Reversed.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Path+Reversed.swift
diff --git a/SwiftDraw/LayerTree.Path+Subpath.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Path+Subpath.swift
similarity index 100%
rename from SwiftDraw/LayerTree.Path+Subpath.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Path+Subpath.swift
diff --git a/SwiftDraw/LayerTree.Path.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Path.swift
similarity index 100%
rename from SwiftDraw/LayerTree.Path.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Path.swift
diff --git a/SwiftDraw/LayerTree.Pattern.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Pattern.swift
similarity index 68%
rename from SwiftDraw/LayerTree.Pattern.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Pattern.swift
index af1e97ac..bc48ba9c 100644
--- a/SwiftDraw/LayerTree.Pattern.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Pattern.swift
@@ -30,19 +30,24 @@
//
extension LayerTree {
-
- final class Pattern: Equatable {
-
- var frame: LayerTree.Rect
- var contents: [LayerTree.Layer.Contents]
-
- init(frame: LayerTree.Rect) {
- self.frame = frame
- self.contents = []
- }
-
- static func == (lhs: LayerTree.Pattern, rhs: LayerTree.Pattern) -> Bool {
- return lhs.contents == rhs.contents
+
+ final class Pattern: Hashable {
+
+ var frame: LayerTree.Rect
+ var contents: [LayerTree.Layer.Contents]
+
+ init(frame: LayerTree.Rect) {
+ self.frame = frame
+ self.contents = []
+ }
+
+ func hash(into hasher: inout Hasher) {
+ frame.hash(into: &hasher)
+ contents.hash(into: &hasher)
+ }
+
+ static func == (lhs: LayerTree.Pattern, rhs: LayerTree.Pattern) -> Bool {
+ return lhs.frame == rhs.frame && lhs.contents == rhs.contents
+ }
}
- }
}
diff --git a/SwiftDraw/LayerTree.Shape.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Shape.swift
similarity index 97%
rename from SwiftDraw/LayerTree.Shape.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Shape.swift
index b1c6b803..8f6fafad 100644
--- a/SwiftDraw/LayerTree.Shape.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Shape.swift
@@ -38,6 +38,11 @@ extension LayerTree {
case polygon(between: [Point])
case path(Path)
}
+
+ struct ClipShape: Hashable {
+ var shape: Shape
+ var transform: Transform.Matrix
+ }
}
extension LayerTree.Shape {
diff --git a/SwiftDraw/LayerTree.Transform.swift b/SwiftDraw/Sources/LayerTree/LayerTree.Transform.swift
similarity index 88%
rename from SwiftDraw/LayerTree.Transform.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.Transform.swift
index 51cc0403..2c157fa1 100644
--- a/SwiftDraw/LayerTree.Transform.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.Transform.swift
@@ -31,6 +31,8 @@
#if canImport(Darwin)
import Darwin
+#elseif canImport(Android)
+import Android
#else
import Glibc
#endif
@@ -114,3 +116,15 @@ extension Array where Element == LayerTree.Transform {
}
}
}
+
+#if os(Android)
+// The Android module does not have Float overloads for the various math functions
+func tan(_ value: Float) -> Float { tanf(value) }
+func atan(_ value: Float) -> Float { atanf(value) }
+func cos(_ value: Float) -> Float { cosf(value) }
+func acos(_ value: Float) -> Float { acosf(value) }
+func sin(_ value: Float) -> Float { sinf(value) }
+func asin(_ value: Float) -> Float { asinf(value) }
+func ceil(_ value: Float) -> Float { ceilf(value) }
+#endif
+
diff --git a/SwiftDraw/LayerTree.swift b/SwiftDraw/Sources/LayerTree/LayerTree.swift
similarity index 98%
rename from SwiftDraw/LayerTree.swift
rename to SwiftDraw/Sources/LayerTree/LayerTree.swift
index 42dd9d3d..55d4086f 100644
--- a/SwiftDraw/LayerTree.swift
+++ b/SwiftDraw/Sources/LayerTree/LayerTree.swift
@@ -29,6 +29,8 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
+
enum LayerTree { /* namespace */ }
extension LayerTree {
@@ -40,7 +42,7 @@ extension LayerTree {
typealias Filter = DOM.Filter.Effect
enum Error: Swift.Error {
- case unsupported(Any)
+ case unsupported(any Sendable)
case invalid(String)
}
diff --git a/SwiftDraw/NSImage+Image.swift b/SwiftDraw/Sources/NSImage+SVG.swift
similarity index 86%
rename from SwiftDraw/NSImage+Image.swift
rename to SwiftDraw/Sources/NSImage+SVG.swift
index 84184e01..c6e39545 100644
--- a/SwiftDraw/NSImage+Image.swift
+++ b/SwiftDraw/Sources/NSImage+SVG.swift
@@ -1,5 +1,5 @@
//
-// NSImage+Image.swift
+// NSImage+SVG.swift
// SwiftDraw
//
// Created by Simon Whitty on 24/5/17.
@@ -30,8 +30,7 @@
//
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
-import AppKit
-import CoreGraphics
+public import AppKit
public extension NSImage {
@@ -72,6 +71,7 @@ public extension NSImage {
}
public extension SVG {
+
func rasterize() -> NSImage {
return rasterize(with: size)
}
@@ -91,8 +91,9 @@ public extension SVG {
return image
}
- func pngData(size: CGSize? = nil, scale: CGFloat = 0, insets: Insets = .zero) throws -> Data {
- let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: scale, insets: insets)
+ func pngData(scale: CGFloat = 0) throws -> Data {
+ let scale = scale == 0 ? SVG.defaultScale : scale
+ let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: scale)
guard let bitmap = makeBitmap(width: pixelsWide, height: pixelsHigh, isOpaque: false),
let ctx = NSGraphicsContext(bitmapImageRep: bitmap)?.cgContext else {
throw Error("Failed to create CGContext")
@@ -108,8 +109,9 @@ public extension SVG {
return data
}
- func jpegData(size: CGSize? = nil, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: Insets = .zero) throws -> Data {
- let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: scale, insets: insets)
+ func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data {
+ let scale = scale == 0 ? SVG.defaultScale : scale
+ let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: scale)
guard let bitmap = makeBitmap(width: pixelsWide, height: pixelsHigh, isOpaque: true),
let ctx = NSGraphicsContext(bitmapImageRep: bitmap)?.cgContext else {
throw Error("Failed to create CGContext")
@@ -127,6 +129,10 @@ public extension SVG {
return data
}
+ internal static var defaultScale: CGFloat {
+ NSScreen.main?.backingScaleFactor ?? 1.0
+ }
+
private struct Error: LocalizedError {
var errorDescription: String?
@@ -138,11 +144,6 @@ public extension SVG {
extension SVG {
- func makeBounds(size: CGSize?, scale: CGFloat, insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) {
- let scale = scale == 0 ? (NSScreen.main?.backingScaleFactor ?? 1.0) : scale
- return Self.makeBounds(size: size, defaultSize: self.size, scale: scale, insets: insets)
- }
-
func makeBitmap(width: Int, height: Int, isOpaque: Bool) -> NSBitmapImageRep? {
guard width > 0 && height > 0 else { return nil }
return NSBitmapImageRep(
diff --git a/SwiftDraw/CGTextRenderer+Code.swift b/SwiftDraw/Sources/Renderer/CGTextRenderer+Code.swift
similarity index 97%
rename from SwiftDraw/CGTextRenderer+Code.swift
rename to SwiftDraw/Sources/Renderer/CGTextRenderer+Code.swift
index 0fd595b9..5911783b 100644
--- a/SwiftDraw/CGTextRenderer+Code.swift
+++ b/SwiftDraw/Sources/Renderer/CGTextRenderer+Code.swift
@@ -29,7 +29,8 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import Foundation
+import SwiftDrawDOM
+public import Foundation
public extension CGTextRenderer {
@@ -108,7 +109,7 @@ public extension CGTextRenderer {
let optimizer = LayerTree.CommandOptimizer(options: [.skipRedundantState, .skipInitialSaveState])
let commands = optimizer.optimizeCommands(
- generator.renderCommands(for: layer)
+ generator.renderCommands(for: layer, colorConverter: .default)
)
let renderer = CGTextRenderer(api: api,
diff --git a/SwiftDraw/Renderer.CGText+Path.swift b/SwiftDraw/Sources/Renderer/Renderer.CGText+Path.swift
similarity index 100%
rename from SwiftDraw/Renderer.CGText+Path.swift
rename to SwiftDraw/Sources/Renderer/Renderer.CGText+Path.swift
diff --git a/SwiftDraw/Renderer.CGText.swift b/SwiftDraw/Sources/Renderer/Renderer.CGText.swift
similarity index 98%
rename from SwiftDraw/Renderer.CGText.swift
rename to SwiftDraw/Sources/Renderer/Renderer.CGText.swift
index 209f83be..cfbeacad 100644
--- a/SwiftDraw/Renderer.CGText.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.CGText.swift
@@ -28,6 +28,7 @@
//
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import Foundation
struct CGTextTypes: RendererTypes {
@@ -37,7 +38,7 @@ struct CGTextTypes: RendererTypes {
typealias Rect = String
typealias Color = String
typealias Gradient = LayerTree.Gradient
- typealias Mask = [Any]
+ typealias Mask = [AnyHashable]
typealias Path = [LayerTree.Shape]
typealias Pattern = String
typealias Transform = String
@@ -209,7 +210,11 @@ struct CGTextProvider: RendererTypeProvider {
func createImage(from image: LayerTree.Image) -> LayerTree.Image? {
return image
}
-
+
+ func createSize(from image: LayerTree.Image) -> LayerTree.Size {
+ LayerTree.Size(image.width ?? 0, image.height ?? 0)
+ }
+
func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect {
#if canImport(CoreGraphics)
return CGProvider().getBounds(from: shape)
@@ -509,7 +514,7 @@ public final class CGTextRenderer: Renderer {
}
}
- func setClip(mask: [Any], frame: String) {
+ func setClip(mask: [AnyHashable], frame: String) {
lines.append("ctx.clip(to: \(frame), mask: \(mask))")
}
@@ -556,7 +561,7 @@ public final class CGTextRenderer: Renderer {
}
}
- func draw(image: LayerTree.Image) {
+ func draw(image: LayerTree.Image, in rect: String) {
lines.append("ctx.saveGState()")
lines.append("ctx.translateBy(x: 0, y: image.height)")
lines.append("ctx.scaleBy(x: 1, y: -1)")
diff --git a/SwiftDraw/Sources/Renderer/Renderer.CoreGraphics+Cost.swift b/SwiftDraw/Sources/Renderer/Renderer.CoreGraphics+Cost.swift
new file mode 100644
index 00000000..5630d2e4
--- /dev/null
+++ b/SwiftDraw/Sources/Renderer/Renderer.CoreGraphics+Cost.swift
@@ -0,0 +1,119 @@
+//
+// Renderer.CoreGraphics+Cost.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/8/25.
+// Copyright 2025 WhileLoop Pty Ltd. All rights reserved.
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+extension [RendererCommand] {
+
+ var estimatedCost: Int {
+ let commandCost = MemoryLayout.stride * count
+ let pathCost = Set(allPaths).reduce(0) { $0 + $1.estimatedCost }
+ let imageCost = Set(allImages).reduce(0) { $0 + $1.estimatedCost }
+ return commandCost + pathCost + imageCost
+ }
+}
+
+extension CGPath {
+
+ var estimatedCost: Int {
+ var total = 0
+ applyWithBlock { element in
+ switch element.pointee.type {
+ case .moveToPoint, .addLineToPoint, .closeSubpath:
+ total += MemoryLayout.stride + MemoryLayout.stride
+ case .addQuadCurveToPoint:
+ total += MemoryLayout.stride + 2 * MemoryLayout.stride
+ case .addCurveToPoint:
+ total += MemoryLayout.stride + 3 * MemoryLayout.stride
+ @unknown default:
+ break
+ }
+ }
+ return MemoryLayout.size + total
+ }
+}
+
+extension CGImage {
+ var estimatedCost: Int { bytesPerRow * height }
+}
+
+extension RendererCommand {
+
+ var allPaths: [CGPath] {
+ switch self {
+ case .setClip(path: let p, rule: _):
+ return [p]
+ case .setFillPattern(let p):
+ return p.contents.allPaths
+ case .stroke(let p):
+ return [p]
+ case .clipStrokeOutline(let p):
+ return [p]
+ case .fill(let p, rule: _):
+ return [p]
+ default:
+ return []
+ }
+ }
+
+ var allImages: [CGImage] {
+ switch self {
+ case .setFillPattern(let p):
+ return p.contents.allImages
+ case .draw(image: let i, in: _):
+ return [i]
+ default:
+ return []
+ }
+ }
+}
+
+extension [RendererCommand] {
+
+ var allPaths: [CGPath] {
+ var paths = [CGPath]()
+ for command in self {
+ paths.append(contentsOf: command.allPaths)
+ }
+ return paths
+ }
+
+ var allImages: [CGImage] {
+ var images = [CGImage]()
+ for command in self {
+ images.append(contentsOf: command.allImages)
+ }
+ return images
+ }
+}
+
+#endif
diff --git a/SwiftDraw/Renderer.CoreGraphics.swift b/SwiftDraw/Sources/Renderer/Renderer.CoreGraphics.swift
similarity index 93%
rename from SwiftDraw/Renderer.CoreGraphics.swift
rename to SwiftDraw/Sources/Renderer/Renderer.CoreGraphics.swift
index e09ca19c..05b993d4 100644
--- a/SwiftDraw/Renderer.CoreGraphics.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.CoreGraphics.swift
@@ -32,20 +32,21 @@
#if canImport(CoreGraphics)
import Foundation
import CoreText
-#if os(macOS)
-import AppKit
-#elseif os(iOS)
+#if canImport(UIKit)
import UIKit
+#elseif canImport(AppKit)
+import AppKit
#endif
-struct CGTypes: RendererTypes {
+import SwiftDrawDOM
+
+struct CGTypes: RendererTypes, Sendable {
typealias Float = CGFloat
typealias Point = CGPoint
typealias Size = CGSize
typealias Rect = CGRect
typealias Color = CGColor
typealias Gradient = CGGradient
- typealias Mask = CGImage
typealias Path = CGPath
typealias Pattern = CGTransformingPattern
typealias Transform = CGAffineTransform
@@ -56,10 +57,10 @@ struct CGTypes: RendererTypes {
typealias Image = CGImage
}
-final class CGTransformingPattern: Equatable {
+struct CGTransformingPattern: Hashable {
- let bounds: CGRect
- let contents: [RendererCommand]
+ var bounds: CGRect
+ var contents: [RendererCommand]
init(bounds: CGRect, contents: [RendererCommand]) {
self.bounds = bounds
@@ -70,10 +71,6 @@ final class CGTransformingPattern: Equatable {
let renderer = CGRenderer(context: ctx)
renderer.perform(contents)
}
-
- static func == (lhs: CGTransformingPattern, rhs: CGTransformingPattern) -> Bool {
- lhs === rhs
- }
}
struct CGProvider: RendererTypeProvider {
@@ -150,14 +147,6 @@ struct CGProvider: RendererTypeProvider {
components: [w, a])!
}
- func createMask(from contents: [RendererCommand], size: LayerTree.Size) -> CGImage {
-
- return CGImage.makeMask(size: createSize(from: size)) { ctx in
- let renderer = CGRenderer(context: ctx)
- renderer.perform(contents)
- }
- }
-
func createBlendMode(from mode: LayerTree.BlendMode) -> CGBlendMode {
switch mode {
case .normal: return .normal
@@ -268,14 +257,21 @@ struct CGProvider: RendererTypeProvider {
}
func createImage(from image: LayerTree.Image) -> CGImage? {
- switch image {
- case .jpeg(data: let d):
+ switch image.bitmap {
+ case .jpeg(let d):
return CGImage.from(data: d)
- case .png(data: let d):
+ case .png(let d):
return CGImage.from(data: d)
}
}
+ func createSize(from image: CGImage) -> LayerTree.Size {
+ LayerTree.Size(
+ LayerTree.Float(image.width),
+ LayerTree.Float(image.height)
+ )
+ }
+
func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect {
let bounds = createPath(from: shape).boundingBoxOfPath
return LayerTree.Rect(x: LayerTree.Float(bounds.origin.x),
@@ -288,9 +284,9 @@ struct CGProvider: RendererTypeProvider {
//TODO: replace with CG implementation
private extension CGImage {
static func from(data: Data) -> CGImage? {
-#if os(iOS)
+#if canImport(UIKit)
return UIImage(data: data)?.cgImage
-#elseif os(macOS)
+#elseif canImport(AppKit)
guard let image = NSImage(data: data) else { return nil }
var rect = NSRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
return image.cgImage(forProposedRect: &rect, context: nil, hints: nil)
@@ -384,10 +380,6 @@ struct CGRenderer: Renderer {
ctx.clip(using: rule)
}
- func setClip(mask: CGImage, frame: CGRect) {
- ctx.clip(to: frame, mask: mask)
- }
-
func setAlpha(_ alpha: CGFloat) {
ctx.setAlpha(alpha)
}
@@ -412,12 +404,13 @@ struct CGRenderer: Renderer {
ctx.fillPath(using: rule)
}
- func draw(image: CGImage) {
- let rect = CGRect(x: 0, y: 0, width: image.width, height: image.height)
+ func draw(image: CGImage, in rect: CGRect) {
pushState()
- translate(tx: 0, ty: rect.height)
+ translate(tx: rect.minX, ty: rect.maxY)
scale(sx: 1, sy: -1)
- ctx.draw(image, in: rect)
+ pushState()
+ ctx.draw(image, in: CGRect(origin: .zero, size: rect.size))
+ popState()
popState()
}
diff --git a/SwiftDraw/Renderer.LayerTree.swift b/SwiftDraw/Sources/Renderer/Renderer.LayerTree.swift
similarity index 95%
rename from SwiftDraw/Renderer.LayerTree.swift
rename to SwiftDraw/Sources/Renderer/Renderer.LayerTree.swift
index 7a8af89e..62a5c3fe 100644
--- a/SwiftDraw/Renderer.LayerTree.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.LayerTree.swift
@@ -37,7 +37,7 @@ struct LayerTreeTypes: RendererTypes {
typealias Rect = LayerTree.Rect
typealias Color = LayerTree.Color
typealias Gradient = LayerTree.Gradient
- typealias Mask = [Any]
+ typealias Mask = [AnyHashable]
typealias Path = [LayerTree.Shape]
typealias Pattern = LayerTree.Pattern
typealias Transform = LayerTree.Transform
@@ -115,7 +115,14 @@ struct LayerTreeProvider: RendererTypeProvider {
func createImage(from image: LayerTree.Image) -> LayerTree.Image? {
return image
}
-
+
+ func createSize(from image: LayerTree.Image) -> LayerTree.Size {
+ LayerTree.Size(
+ image.width ?? 0,
+ image.height ?? 0
+ )
+ }
+
func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect {
return LayerTree.Rect(x: 0, y: 0, width: 0, height: 0)
}
diff --git a/SwiftDraw/Renderer.SFSymbol+CGPath.swift b/SwiftDraw/Sources/Renderer/Renderer.SFSymbol+CGPath.swift
similarity index 100%
rename from SwiftDraw/Renderer.SFSymbol+CGPath.swift
rename to SwiftDraw/Sources/Renderer/Renderer.SFSymbol+CGPath.swift
diff --git a/SwiftDraw/Renderer.SFSymbol.swift b/SwiftDraw/Sources/Renderer/Renderer.SFSymbol.swift
similarity index 80%
rename from SwiftDraw/Renderer.SFSymbol.swift
rename to SwiftDraw/Sources/Renderer/Renderer.SFSymbol.swift
index 227c1cb0..a7314081 100644
--- a/SwiftDraw/Renderer.SFSymbol.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.SFSymbol.swift
@@ -29,27 +29,44 @@
// 3. This notice may not be removed or altered from any source distribution.
//
-import Foundation
+import SwiftDrawDOM
+public import Foundation
public struct SFSymbolRenderer {
+ private let size: SizeCategory
private let options: SVG.Options
private let insets: CommandLine.Insets
private let insetsUltralight: CommandLine.Insets
private let insetsBlack: CommandLine.Insets
private let formatter: CoordinateFormatter
+ private let isLegacyInsets: Bool
- public init(options: SVG.Options,
- insets: CommandLine.Insets,
- insetsUltralight: CommandLine.Insets,
- insetsBlack: CommandLine.Insets,
- precision: Int) {
+ public enum SizeCategory {
+ case small
+ case medium
+ case large
+ }
+
+ public init(
+ size: SizeCategory,
+ options: SVG.Options,
+ insets: CommandLine.Insets,
+ insetsUltralight: CommandLine.Insets,
+ insetsBlack: CommandLine.Insets,
+ precision: Int,
+ isLegacyInsets: Bool
+ ) {
+ self.size = size
self.options = options
self.insets = insets
self.insetsUltralight = insetsUltralight
self.insetsBlack = insetsBlack
- self.formatter = CoordinateFormatter(delimeter: .comma,
- precision: .capped(max: precision))
+ self.formatter = CoordinateFormatter(
+ delimeter: .comma,
+ precision: .capped(max: precision)
+ )
+ self.isLegacyInsets = isLegacyInsets
}
public func render(regular: URL, ultralight: URL?, black: URL?) throws -> String {
@@ -67,27 +84,29 @@ public struct SFSymbolRenderer {
template.svg.styles = image.styles.map(makeSymbolStyleSheet)
- let boundsRegular = try makeBounds(svg: image, auto: Self.makeBounds(for: pathsRegular), for: .regular)
- template.regular.appendPaths(pathsRegular, from: boundsRegular)
+ let boundsRegular = try makeBounds(svg: image, auto: Self.makeAutoBounds(for: pathsRegular, isLegacy: isLegacyInsets), for: .regular)
+ template.regular.appendPaths(pathsRegular, from: boundsRegular, isLegacy: isLegacyInsets)
if let ultralight = ultralight,
let paths = Self.getPaths(for: ultralight) {
- let bounds = try makeBounds(svg: ultralight, auto: Self.makeBounds(for: paths), for: .ultralight)
- template.ultralight.appendPaths(paths, from: bounds)
+ let bounds = try makeBounds(svg: ultralight, isRegularSVG: false, auto: Self.makeAutoBounds(for: paths, isLegacy: isLegacyInsets), for: .ultralight)
+ template.ultralight.appendPaths(paths, from: bounds, isLegacy: isLegacyInsets)
} else {
- let bounds = try makeBounds(svg: image, auto: Self.makeBounds(for: pathsRegular), for: .ultralight)
- template.ultralight.appendPaths(pathsRegular, from: bounds)
+ let bounds = try makeBounds(svg: image, auto: Self.makeAutoBounds(for: pathsRegular, isLegacy: isLegacyInsets), for: .ultralight)
+ template.ultralight.appendPaths(pathsRegular, from: bounds, isLegacy: isLegacyInsets)
}
if let black = black,
let paths = Self.getPaths(for: black) {
- let bounds = try makeBounds(svg: black, auto: Self.makeBounds(for: paths), for: .black)
- template.black.appendPaths(paths, from: bounds)
+ let bounds = try makeBounds(svg: black, isRegularSVG: false, auto: Self.makeAutoBounds(for: paths, isLegacy: isLegacyInsets), for: .black)
+ template.black.appendPaths(paths, from: bounds, isLegacy: isLegacyInsets)
} else {
- let bounds = try makeBounds(svg: image, auto: Self.makeBounds(for: pathsRegular), for: .black)
- template.black.appendPaths(pathsRegular, from: bounds)
+ let bounds = try makeBounds(svg: image, auto: Self.makeAutoBounds(for: pathsRegular, isLegacy: isLegacyInsets), for: .black)
+ template.black.appendPaths(pathsRegular, from: bounds, isLegacy: isLegacyInsets)
}
+ template.setSize(size)
+
let element = try XML.Formatter.SVG(formatter: formatter).makeElement(from: template.svg)
let formatter = XML.Formatter(spaces: 4)
let result = formatter.encodeRootElement(element)
@@ -137,7 +156,7 @@ extension SFSymbolRenderer {
}
}
- func makeBounds(svg: DOM.SVG, auto: LayerTree.Rect, for variant: Variant) throws -> LayerTree.Rect {
+ func makeBounds(svg: DOM.SVG, isRegularSVG: Bool = true, auto: LayerTree.Rect, for variant: Variant) throws -> LayerTree.Rect {
let insets = getInsets(for: variant)
let width = LayerTree.Float(svg.width)
let height = LayerTree.Float(svg.height)
@@ -256,7 +275,7 @@ extension SFSymbolRenderer {
#endif
}
- static func makeBounds(for paths: [SymbolPath]) -> LayerTree.Rect {
+ static func makeAutoBounds(for paths: [SymbolPath], isLegacy: Bool = false) -> LayerTree.Rect {
var min = LayerTree.Point.maximum
var max = LayerTree.Point.minimum
for p in paths {
@@ -264,6 +283,12 @@ extension SFSymbolRenderer {
min = min.minimum(combining: .init(bounds.minX, bounds.minY))
max = max.maximum(combining: .init(bounds.maxX, bounds.maxY))
}
+
+ if !isLegacy {
+ min.x -= 10
+ max.x += 10
+ }
+
return LayerTree.Rect(
x: min.x,
y: min.y,
@@ -325,9 +350,9 @@ extension SFSymbolRenderer {
case .regular:
print("Alignment: --insets \(top),\(left),\(bottom),\(right)")
case .ultralight:
- print("Alignment: --ultralightInsets \(top),\(left),\(bottom),\(right)")
+ print("Alignment: --ultralight-insets \(top),\(left),\(bottom),\(right)")
case .black:
- print("Alignment: --blackInsets \(top),\(left),\(bottom),\(right)")
+ print("Alignment: --black-insets \(top),\(left),\(bottom),\(right)")
}
}
@@ -344,25 +369,36 @@ struct SFSymbolTemplate {
let svg: DOM.SVG
+ var typeReference: DOM.Path
var ultralight: Variant
var regular: Variant
var black: Variant
init(svg: DOM.SVG) throws {
self.svg = svg
+ self.typeReference = try svg.group(id: "Guides").path(id: "H-reference")
self.ultralight = try Variant(svg: svg, kind: "Ultralight")
self.regular = try Variant(svg: svg, kind: "Regular")
self.black = try Variant(svg: svg, kind: "Black")
}
+ mutating func setSize(_ size: SFSymbolRenderer.SizeCategory) {
+ typeReference.attributes.transform = [.translate(tx: 0, ty: size.yOffset)]
+ ultralight.setSize(size)
+ regular.setSize(size)
+ black.setSize(size)
+ }
+
struct Variant {
var left: Guide
var contents: Contents
var right: Guide
+ private var kind: String
init(svg: DOM.SVG, kind: String) throws {
let guides = try svg.group(id: "Guides")
let symbols = try svg.group(id: "Symbols")
+ self.kind = kind
self.left = try Guide(guides.path(id: "left-margin-\(kind)-S"))
self.contents = try Contents(symbols.group(id: "\(kind)-S"))
self.right = try Guide(guides.path(id: "right-margin-\(kind)-S"))
@@ -373,6 +409,15 @@ struct SFSymbolTemplate {
let maxX = right.x
return .init(x: minX, y: 76, width: maxX - minX, height: 70)
}
+
+ mutating func setSize(_ size: SFSymbolRenderer.SizeCategory) {
+ left.setID("left-margin-\(kind)-\(size.name)")
+ left.y += size.yOffset
+ contents.setID("\(kind)-\(size.name)")
+ contents.setTransform(.translate(tx: 0, ty: size.yOffset))
+ right.setID("right-margin-\(kind)-\(size.name)")
+ right.y += size.yOffset
+ }
}
struct Guide {
@@ -382,6 +427,10 @@ struct SFSymbolTemplate {
self.path = path
}
+ func setID(_ id: String) {
+ path.id = id
+ }
+
var x: DOM.Float {
get {
guard case let .move(x, _, _) = path.segments[0] else {
@@ -396,6 +445,21 @@ struct SFSymbolTemplate {
path.segments[0] = .move(x: newValue, y: y, space: space)
}
}
+
+ var y: DOM.Float {
+ get {
+ guard case let .move(_, y, _) = path.segments[0] else {
+ fatalError()
+ }
+ return y
+ }
+ set {
+ guard case let .move(x, _, space) = path.segments[0] else {
+ fatalError()
+ }
+ path.segments[0] = .move(x: x, y: newValue, space: space)
+ }
+ }
}
struct Contents {
@@ -405,6 +469,10 @@ struct SFSymbolTemplate {
self.group = group
}
+ func setID(_ id: String) {
+ group.id = id
+ }
+
var paths: [DOM.Path] {
get {
group.childElements as! [DOM.Path]
@@ -413,6 +481,35 @@ struct SFSymbolTemplate {
group.childElements = newValue
}
}
+
+ func setTransform(_ transform: DOM.Transform) {
+ group.attributes.transform = [transform]
+ }
+ }
+}
+
+extension SFSymbolRenderer.SizeCategory {
+
+ var name: String {
+ switch self {
+ case .small:
+ return "S"
+ case .medium:
+ return "M"
+ case .large:
+ return "L"
+ }
+ }
+
+ var yOffset: Float {
+ switch self {
+ case .small:
+ return 0
+ case .medium:
+ return 200
+ case .large:
+ return 400
+ }
}
}
@@ -511,7 +608,7 @@ private extension ContainerElement {
private extension SFSymbolTemplate.Variant {
- mutating func appendPaths(_ paths: [SFSymbolRenderer.SymbolPath], from source: LayerTree.Rect) {
+ mutating func appendPaths(_ paths: [SFSymbolRenderer.SymbolPath], from source: LayerTree.Rect, isLegacy: Bool = false) {
let matrix = SFSymbolRenderer.makeTransformation(from: source, to: bounds)
contents.paths = paths
.map {
@@ -522,9 +619,16 @@ private extension SFSymbolTemplate.Variant {
}
let midX = bounds.midX
- let newWidth = ((source.width * matrix.a) / 2) + 10
- left.x = min(left.x, midX - newWidth)
- right.x = max(right.x, midX + newWidth)
+ if isLegacy {
+ // preserve behaviour from earlier SwiftDraw versions with --legacy option
+ let newWidth = ((source.width * matrix.a) / 2) + 10
+ left.x = min(left.x, midX - newWidth)
+ right.x = max(right.x, midX + newWidth)
+ } else {
+ let newWidth = ((source.width * matrix.a) / 2)
+ left.x = midX - newWidth
+ right.x = midX + newWidth
+ }
}
}
diff --git a/SwiftDraw/Renderer.SVG.swift b/SwiftDraw/Sources/Renderer/Renderer.SVG.swift
similarity index 99%
rename from SwiftDraw/Renderer.SVG.swift
rename to SwiftDraw/Sources/Renderer/Renderer.SVG.swift
index a9a110e8..3b393113 100644
--- a/SwiftDraw/Renderer.SVG.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.SVG.swift
@@ -29,6 +29,8 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
+
public struct SVGRenderer {
/// Expand a Path by applying the supplied transformation
diff --git a/SwiftDraw/Renderer.swift b/SwiftDraw/Sources/Renderer/Renderer.swift
similarity index 93%
rename from SwiftDraw/Renderer.swift
rename to SwiftDraw/Sources/Renderer/Renderer.swift
index ff54a0ae..c0adf0eb 100644
--- a/SwiftDraw/Renderer.swift
+++ b/SwiftDraw/Sources/Renderer/Renderer.swift
@@ -37,7 +37,6 @@ protocol RendererTypes {
associatedtype Rect: Equatable
associatedtype Color: Equatable
associatedtype Gradient: Equatable
- associatedtype Mask
associatedtype Path: Equatable
associatedtype Pattern: Equatable
associatedtype Transform: Equatable
@@ -67,6 +66,7 @@ protocol RendererTypeProvider {
func createLineCap(from cap: LayerTree.LineCap) -> Types.LineCap
func createLineJoin(from join: LayerTree.LineJoin) -> Types.LineJoin
func createImage(from image: LayerTree.Image) -> Types.Image?
+ func createSize(from image: Types.Image) -> LayerTree.Size
func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect
}
@@ -92,14 +92,13 @@ protocol Renderer {
func setLine(join: Types.LineJoin)
func setLine(miterLimit: Types.Float)
func setClip(path: Types.Path, rule: Types.FillRule)
- func setClip(mask: Types.Mask, frame: Types.Rect)
func setAlpha(_ alpha: Types.Float)
func setBlend(mode: Types.BlendMode)
func stroke(path: Types.Path)
func clipStrokeOutline(path: Types.Path)
func fill(path: Types.Path, rule: Types.FillRule)
- func draw(image: Types.Image)
+ func draw(image: Types.Image, in rect: Types.Rect)
func draw(linear gradient: Types.Gradient, from start: Types.Point, to end: Types.Point)
func draw(radial gradient: Types.Gradient, startCenter: Types.Point, startRadius: Types.Float, endCenter: Types.Point, endRadius: Types.Float)
}
@@ -139,8 +138,6 @@ extension Renderer {
setLine(miterLimit: l)
case .setClip(path: let p, rule: let r):
setClip(path: p, rule: r)
- case .setClipMask(let m, frame: let f):
- setClip(mask: m, frame: f)
case .setAlpha(let a):
setAlpha(a)
case .setBlend(mode: let m):
@@ -151,8 +148,8 @@ extension Renderer {
clipStrokeOutline(path: p)
case .fill(let p, let r):
fill(path: p, rule: r)
- case .draw(image: let i):
- draw(image: i)
+ case .draw(image: let i, in: let r):
+ draw(image: i, in: r)
case .drawLinearGradient(let g, let start, let end):
draw(linear: g, from: start, to: end)
case let .drawRadialGradient(g, startCenter, startRadius, endCenter, endRadius):
@@ -167,7 +164,7 @@ extension Renderer {
}
}
-enum RendererCommand {
+enum RendererCommand: @unchecked Sendable {
case pushState
case popState
@@ -184,7 +181,6 @@ enum RendererCommand {
case setLineJoin(Types.LineJoin)
case setLineMiter(limit: Types.Float)
case setClip(path: Types.Path, rule: Types.FillRule)
- case setClipMask(Types.Mask, frame: Types.Rect)
case setAlpha(Types.Float)
case setBlend(mode: Types.BlendMode)
@@ -192,10 +188,18 @@ enum RendererCommand {
case clipStrokeOutline(Types.Path)
case fill(Types.Path, rule: Types.FillRule)
- case draw(image: Types.Image)
+ case draw(image: Types.Image, in: Types.Rect)
case drawLinearGradient(Types.Gradient, from: Types.Point, to: Types.Point)
case drawRadialGradient(Types.Gradient, startCenter: Types.Point, startRadius: Types.Float, endCenter: Types.Point, endRadius: Types.Float)
case pushTransparencyLayer
case popTransparencyLayer
}
+
+
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+extension RendererCommand: Equatable { }
+extension RendererCommand: Hashable { }
+#endif
diff --git a/SwiftDraw/Sources/SVG+CoreGraphics.swift b/SwiftDraw/Sources/SVG+CoreGraphics.swift
new file mode 100644
index 00000000..02de904c
--- /dev/null
+++ b/SwiftDraw/Sources/SVG+CoreGraphics.swift
@@ -0,0 +1,243 @@
+//
+// SVG+CoreGraphics.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 24/5/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+public import CoreGraphics
+public import Foundation
+
+public extension CGContext {
+
+ func draw(_ svg: SVG, in rect: CGRect? = nil) {
+ let defaultRect = CGRect(x: 0, y: 0, width: svg.size.width, height: svg.size.height)
+ let renderer = CGRenderer(context: self)
+ saveGState()
+
+ if let rect = rect, rect != defaultRect {
+ translateBy(x: rect.origin.x, y: rect.origin.y)
+ scaleBy(
+ x: rect.width / svg.size.width,
+ y: rect.height / svg.size.height
+ )
+ }
+ renderer.perform(svg.commands)
+
+ restoreGState()
+ }
+
+ func draw(_ svg: SVG, in rect: CGRect, byTiling: Bool) {
+ guard byTiling else {
+ draw(svg, in: rect)
+ return
+ }
+
+ let cols = Int(ceil(rect.size.width / svg.size.width))
+ let rows = Int(ceil(rect.size.height / svg.size.height))
+
+ for r in 0.. Data {
+ let (bounds, pixelsWide, pixelsHigh) = Self.makeBounds(size: size, scale: 1)
+ var mediaBox = CGRect(x: 0.0, y: 0.0, width: CGFloat(pixelsWide), height: CGFloat(pixelsHigh))
+
+ let data = NSMutableData()
+ guard let consumer = CGDataConsumer(data: data as CFMutableData),
+ let ctx = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
+ throw Error("Failed to create CGContext")
+ }
+
+ ctx.beginPage(mediaBox: &mediaBox)
+ let flip = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: mediaBox.size.height)
+ ctx.concatenate(flip)
+ ctx.draw(self, in: bounds)
+ ctx.endPage()
+ ctx.closePDF()
+
+ return data as Data
+ }
+
+ private struct Error: LocalizedError {
+ var errorDescription: String?
+
+ init(_ message: String) {
+ self.errorDescription = message
+ }
+ }
+}
+
+extension SVG {
+
+ static func makeBounds(size: CGSize, scale: CGFloat) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) {
+ let bounds = CGRect(
+ x: 0,
+ y: 0,
+ width: size.width * scale,
+ height: size.height * scale
+ )
+
+ return (
+ bounds: bounds,
+ pixelsWide: Int(exactly: ceil(bounds.width)) ?? 0,
+ pixelsHigh: Int(exactly: ceil(bounds.height)) ?? 0
+ )
+ }
+}
+
+private extension SVG.Insets {
+ func applying(sx: CGFloat, sy: CGFloat) -> Self {
+ Self(
+ top: top * sy,
+ left: left * sx,
+ bottom: bottom * sy,
+ right: right * sx
+ )
+ }
+}
+
+private extension LayerTree.Size {
+ init(_ size: CGSize) {
+ self.width = LayerTree.Float(size.width)
+ self.height = LayerTree.Float(size.height)
+ }
+}
+
+struct Slice9 {
+ var source: CGRect
+ var capInsets: (top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat)
+
+ var topLeft: CGRect {
+ CGRect(x: source.minX, y: source.minY, width: capInsets.left, height: capInsets.top)
+ }
+
+ var bottomLeft: CGRect {
+ CGRect(x: source.minX, y: source.height - capInsets.bottom, width: capInsets.left, height: capInsets.bottom)
+ }
+
+ var topRight: CGRect {
+ CGRect(x: source.maxX - capInsets.right, y: source.minY, width: capInsets.right, height: capInsets.top)
+ }
+
+ var bottomRight: CGRect {
+ CGRect(x: source.maxX - capInsets.right, y: source.maxY - capInsets.bottom, width: capInsets.right, height: capInsets.bottom)
+ }
+
+ var midLeft: CGRect {
+ CGRect(x: source.minX, y: capInsets.top, width: capInsets.left, height: source.maxY - capInsets.top - capInsets.bottom)
+ }
+
+ var midRight: CGRect {
+ CGRect(x: source.maxX - capInsets.right, y: capInsets.top, width: capInsets.right, height: source.maxY - capInsets.top - capInsets.bottom)
+ }
+
+ var topMid: CGRect {
+ CGRect(x: capInsets.left, y: source.minY, width: source.maxX - capInsets.left - capInsets.right, height: capInsets.top)
+ }
+
+ var bottomMid: CGRect {
+ CGRect(x: capInsets.left, y: source.maxY - capInsets.bottom, width: source.maxX - capInsets.left - capInsets.right, height: capInsets.bottom)
+ }
+
+ var center: CGRect {
+ CGRect(x: capInsets.left, y: capInsets.top, width: source.maxX - capInsets.left - capInsets.right, height: source.maxY - capInsets.top - capInsets.bottom)
+ }
+}
+
+#endif
diff --git a/SwiftDraw/Sources/SVG+Deprecated.swift b/SwiftDraw/Sources/SVG+Deprecated.swift
new file mode 100644
index 00000000..bc2542c4
--- /dev/null
+++ b/SwiftDraw/Sources/SVG+Deprecated.swift
@@ -0,0 +1,88 @@
+//
+// SVG+Deprecated.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 23/2/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+public import CoreGraphics
+public import Foundation
+
+#if canImport(UIKit)
+public import UIKit
+#endif
+
+public extension SVG {
+
+ @available(*, deprecated, message: "add insets via SVG.expand() before pngData")
+ func pngData(scale: CGFloat = 0, insets: Insets) throws -> Data {
+ try inset(insets).pngData(scale: scale)
+ }
+
+ @available(*, deprecated, message: "set size via SVG.size() before pngData")
+ func pngData(size: CGSize, scale: CGFloat = 0) throws -> Data {
+ try self.sized(size).pngData(scale: scale)
+ }
+
+ @available(*, deprecated, message: "add insets via SVG.expand() before jpegData")
+ func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: Insets) throws -> Data {
+ try inset(insets).jpegData(scale: scale, compressionQuality: quality)
+ }
+
+ @available(*, deprecated, message: "set size via SVG.size() before jpegData")
+ func jpegData(size: CGSize, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data {
+ try self.sized(size).jpegData(scale: scale, compressionQuality: quality)
+ }
+
+ private func inset(_ insets: Insets) -> SVG {
+ expanded(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right)
+ }
+
+#if canImport(UIKit)
+ @available(*, deprecated, message: "add insets via SVG.expand() before rasterize()")
+ func rasterize(scale: CGFloat = 0, insets: UIEdgeInsets) -> UIImage {
+ inset(insets).rasterize(scale: scale)
+ }
+
+ @available(*, deprecated, message: "add insets via SVG.expand() before pngData()")
+ func pngData(scale: CGFloat = 0, insets: UIEdgeInsets) throws -> Data {
+ try inset(insets).pngData(scale: scale)
+ }
+
+ @available(*, deprecated, message: "add insets via SVG.expand() before jpegData()")
+ func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: UIEdgeInsets) throws -> Data {
+ try inset(insets).jpegData(scale: scale, compressionQuality: quality)
+ }
+
+ private func inset(_ insets: UIEdgeInsets) -> SVG {
+ expanded(top: -insets.top, left: -insets.left, bottom: -insets.bottom, right: -insets.right)
+ }
+#endif
+
+}
+#endif
diff --git a/SwiftDraw/Sources/SVG+Insets.swift b/SwiftDraw/Sources/SVG+Insets.swift
new file mode 100644
index 00000000..7833874b
--- /dev/null
+++ b/SwiftDraw/Sources/SVG+Insets.swift
@@ -0,0 +1,59 @@
+//
+// SVG+Insets.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 23/2/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+public import struct CoreGraphics.CGFloat
+#else
+public import typealias Foundation.CGFloat
+#endif
+
+public extension SVG {
+ struct Insets: Equatable {
+ public var top: CGFloat
+ public var left: CGFloat
+ public var bottom: CGFloat
+ public var right: CGFloat
+
+ public init(
+ top: CGFloat = 0,
+ left: CGFloat = 0,
+ bottom: CGFloat = 0,
+ right: CGFloat = 0
+ ) {
+ self.top = top
+ self.left = left
+ self.bottom = bottom
+ self.right = right
+ }
+
+ public static var zero: Insets { Insets(top: 0, left: 0, bottom: 0, right: 0) }
+ }
+}
diff --git a/SwiftDraw/Sources/SVG.swift b/SwiftDraw/Sources/SVG.swift
new file mode 100644
index 00000000..97a1085a
--- /dev/null
+++ b/SwiftDraw/Sources/SVG.swift
@@ -0,0 +1,251 @@
+//
+// SVG.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 24/5/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import SwiftDrawDOM
+public import Foundation
+
+#if compiler(<6.0)
+// #warning("SwiftDraw will soon remove support for Swift 6.0")
+#endif
+
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public struct SVG: Hashable, Sendable {
+ public private(set) var size: CGSize
+
+ // Array of commands that render the image
+ // see: Renderer.swift
+ var commands: [RendererCommand]
+
+ public init?(fileURL url: URL, options: SVG.Options = .default) {
+ if let svg = SVGGCache.shared.svg(fileURL: url) {
+ self = svg
+ } else {
+ do {
+ let svg = try DOM.SVG.parse(fileURL: url)
+ self.init(dom: svg, options: options)
+ SVGGCache.shared.setSVG(self, for: url)
+ } catch {
+ XMLParser.logParsingError(for: error, filename: url.lastPathComponent, parsing: nil)
+ return nil
+ }
+ }
+ }
+
+ public init?(named name: String, in bundle: Bundle = Bundle.main, options: SVG.Options = .default) {
+ guard let url = bundle.url(forResource: name, withExtension: nil) else { return nil }
+ self.init(fileURL: url, options: options)
+ }
+
+ public init?(xml: String, options: SVG.Options = .default) {
+ guard let data = xml.data(using: .utf8) else { return nil }
+ self.init(data: data)
+ }
+
+ public init?(data: Data, options: SVG.Options = .default) {
+ guard let svg = try? DOM.SVG.parse(data: data) else { return nil }
+ self.init(dom: svg, options: options)
+ }
+
+ public struct Options: OptionSet {
+ public let rawValue: Int
+ public init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ public static var hideUnsupportedFilters: Options { Options(rawValue: 1 << 0) }
+
+ public static var `default`: Options { [] }
+ }
+}
+
+public extension SVG {
+
+ func sized(_ s: CGSize) -> SVG {
+ guard size != s else { return self }
+
+ let sx = s.width / size.width
+ let sy = s.height / size.height
+
+ var copy = self
+ copy.commands.insert(.scale(sx: sx, sy: sy), at: 0)
+ copy.size = s
+ return copy
+ }
+
+ func scaled(_ factor: CGFloat) -> SVG {
+ scaled(x: factor, y: factor)
+ }
+
+ func scaled(x: CGFloat, y: CGFloat) -> SVG {
+ var copy = self
+
+ copy.commands.insert(.scale(sx: x, sy: y), at: 0)
+ copy.size = CGSize(
+ width: size.width * x,
+ height: size.height * y
+ )
+ return copy
+ }
+
+ func translated(tx: CGFloat, ty: CGFloat) -> SVG {
+ var copy = self
+ copy.commands.insert(.translate(tx: tx, ty: ty), at: 0)
+ return copy
+ }
+
+ func expanded(_ padding: CGFloat) -> SVG {
+ expanded(top: padding, left: padding, bottom: padding, right: padding)
+ }
+
+ func expanded(top: CGFloat = 0,
+ left: CGFloat = 0,
+ bottom: CGFloat = 0,
+ right: CGFloat = 0) -> SVG {
+ var copy = self
+ copy.commands.insert(.translate(tx: left, ty: top), at: 0)
+ copy.size.width += left + right
+ copy.size.height += top + bottom
+ return copy
+ }
+}
+
+extension SVG {
+
+ init(dom: DOM.SVG, options: Options) {
+ self.size = CGSize(width: dom.width, height: dom.height)
+
+ //To create the draw commands;
+ // - XML is parsed into DOM.SVG
+ // - DOM.SVG is converted into a LayerTree
+ // - LayerTree is converted into RenderCommands
+ // - RenderCommands are performed by Renderer (drawn to CGContext)
+ let layer = LayerTree.Builder(svg: dom).makeLayer()
+ let generator = LayerTree.CommandGenerator(provider: CGProvider(),
+ size: LayerTree.Size(dom.width, dom.height),
+ options: options)
+
+ let optimizer = LayerTree.CommandOptimizer()
+ commands = optimizer.optimizeCommands(
+ generator.renderCommands(for: layer, colorConverter: .default)
+ )
+ }
+}
+
+@available(*, unavailable, renamed: "SVG")
+public enum Image { }
+
+#else
+
+public struct SVG: Sendable {
+ public let size: CGSize
+
+ init(dom: DOM.SVG, options: Options) {
+ size = CGSize(width: dom.width, height: dom.height)
+ }
+
+ public struct Options: OptionSet {
+ public let rawValue: Int
+ public init(rawValue: Int) {
+ self.rawValue = rawValue
+ }
+
+ public static var hideUnsupportedFilters: Options { Options(rawValue: 1 << 0) }
+
+ public static var `default`: Options { [] }
+ }
+}
+
+public extension SVG {
+
+ func pngData(size: CGSize? = nil, scale: CGFloat = 1) -> Data? {
+ return nil
+ }
+
+ func jpegData(size: CGSize? = nil, scale: CGFloat = 1, compressionQuality quality: CGFloat = 1) -> Data? {
+ return nil
+ }
+
+ func pdfData(size: CGSize? = nil) -> Data? {
+ return nil
+ }
+
+ static func pdfData(fileURL url: URL, size: CGSize? = nil) throws -> Data {
+ throw DOM.Error.missing("not implemented")
+ }
+
+ func sized(_ s: CGSize) -> SVG { self }
+
+ func scaled(_ factor: CGFloat) -> SVG { self }
+
+ func scaled(x: CGFloat, y: CGFloat) -> SVG { self }
+
+ func translated(tx: CGFloat, ty: CGFloat) -> SVG { self }
+
+ func expanded(_ padding: CGFloat) -> SVG { self }
+
+ func expanded(top: CGFloat = 0,
+ left: CGFloat = 0,
+ bottom: CGFloat = 0,
+ right: CGFloat = 0) -> SVG { self }
+}
+#endif
+
+public extension SVG {
+
+ mutating func size(_ s: CGSize) {
+ self = sized(s)
+ }
+
+ mutating func scale(_ factor: CGFloat) {
+ self = scaled(factor)
+ }
+
+ mutating func scale(x: CGFloat, y: CGFloat) {
+ self = scaled(x: x, y: y)
+ }
+
+ mutating func translate(tx: CGFloat, ty: CGFloat) {
+ self = translated(tx: tx, ty: ty)
+ }
+
+ mutating func expand(_ padding: CGFloat) {
+ self = expanded(padding)
+ }
+
+ mutating func expand(top: CGFloat = 0,
+ left: CGFloat = 0,
+ bottom: CGFloat = 0,
+ right: CGFloat = 0) {
+ self = expanded(top: top, left: left, bottom: bottom, right: right)
+ }
+}
diff --git a/SwiftDraw/Sources/SVGCache.swift b/SwiftDraw/Sources/SVGCache.swift
new file mode 100644
index 00000000..46de4f53
--- /dev/null
+++ b/SwiftDraw/Sources/SVGCache.swift
@@ -0,0 +1,75 @@
+//
+// SVG.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 24/5/17.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import SwiftDrawDOM
+public import Foundation
+
+#if canImport(CoreGraphics)
+import CoreGraphics
+
+public final class SVGGCache: Sendable {
+
+ public static let shared = SVGGCache()
+
+ nonisolated(unsafe)private let cache: NSCache>
+
+ public init(totalCostLimit: Int = defaultTotalCostLimit) {
+ self.cache = NSCache()
+ self.cache.totalCostLimit = totalCostLimit
+ }
+
+ public func svg(fileURL: URL) -> SVG? {
+ cache.object(forKey: fileURL as NSURL)?.value
+ }
+
+ public func setSVG(_ svg: SVG, for fileURL: URL) {
+ cache.setObject(Box(svg), forKey: fileURL as NSURL, cost: svg.commands.estimatedCost)
+ }
+
+ final class Box: NSObject {
+ let value: T
+ init(_ value: T) { self.value = value }
+ }
+
+ public static var defaultTotalCostLimit: Int {
+ #if canImport(WatchKit)
+ // 2 MB
+ return 2 * 1024 * 1024
+ #elseif canImport(AppKit)
+ // 200 MB
+ return 200 * 1024 * 1024
+ #else
+ // 50 MB
+ return 50 * 1024 * 1024
+ #endif
+ }
+}
+#endif
diff --git a/SwiftDraw/Sources/SVGView.swift b/SwiftDraw/Sources/SVGView.swift
new file mode 100644
index 00000000..160c10de
--- /dev/null
+++ b/SwiftDraw/Sources/SVGView.swift
@@ -0,0 +1,158 @@
+//
+// SVGView.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 19/2/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(SwiftUI)
+public import SwiftUI
+
+public struct SVGView: View {
+
+ public init(_ name: String, bundle: Bundle = .main) {
+ self.svg = SVG(named: name, in: bundle)
+ }
+
+ public init(svg: SVG) {
+ self.svg = svg
+ }
+
+ private let svg: SVG?
+ private var resizable: (capInsets: EdgeInsets, mode: ResizingMode)?
+
+ public var body: some View {
+ if let svg {
+ if let resizable {
+ SVGView.makeCanvas(svg: svg, capInsets: resizable.capInsets, resizingMode: resizable.mode)
+ .frame(idealWidth: svg.size.width, idealHeight: svg.size.height)
+ } else {
+ SVGView.makeCanvas(svg: svg, resizingMode: .stretch)
+ .frame(width: svg.size.width, height: svg.size.height)
+ }
+ }
+ }
+
+ public enum ResizingMode: Sendable, Hashable {
+ /// A mode to repeat the image at its original size, as many
+ /// times as necessary to fill the available space.
+ case tile
+
+ /// A mode to enlarge or reduce the size of an image so that it
+ /// fills the available space.
+ case stretch
+ }
+
+ /// Sets the mode by which SwiftUI resizes an SVG to fit its space.
+ /// - Parameters:
+ /// - capInsets: Inset values that indicate a portion of the image that
+ /// SwiftUI doesn't resize.
+ /// - resizingMode: The mode by which SwiftUI resizes the image.
+ /// - Returns: An SVGView, with the new resizing behavior set.
+ public func resizable(
+ capInsets: EdgeInsets = EdgeInsets(),
+ resizingMode: ResizingMode = .stretch
+ ) -> Self {
+ var copy = self
+ copy.resizable = (capInsets, resizingMode)
+ return copy
+ }
+
+ @ViewBuilder
+ private static func makeCanvas(svg: SVG, capInsets: EdgeInsets = .init(), resizingMode: ResizingMode) -> some View {
+ if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
+ Canvas(
+ opaque: false,
+ colorMode: .linear,
+ rendersAsynchronously: false
+ ) { ctx, size in
+ ctx.draw(
+ svg,
+ in: CGRect(origin: .zero, size: size),
+ capInsets: capInsets,
+ byTiling: resizingMode == .tile
+ )
+ }
+ } else {
+ #if !os(watchOS)
+ CanvasFallbackView(
+ svg: svg,
+ capInsets: capInsets,
+ resizingMode: resizingMode
+ )
+ #endif
+ }
+ }
+}
+
+@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
+public extension GraphicsContext {
+
+ func draw(_ svg: SVG, in rect: CGRect? = nil) {
+ withCGContext {
+ $0.draw(svg, in: rect)
+ }
+ }
+
+ func draw(_ svg: SVG, in rect: CGRect, capInsets: EdgeInsets, byTiling: Bool = false) {
+ withCGContext {
+ $0.draw(
+ svg,
+ in: rect,
+ capInsets: (capInsets.top, capInsets.leading, capInsets.bottom, capInsets.trailing),
+ byTiling: byTiling
+ )
+ }
+ }
+}
+
+#if DEBUG
+
+@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
+#Preview {
+ SVGView(svg: .circle)
+
+ SVGView(svg: .circle)
+ .resizable(resizingMode: .stretch)
+
+ SVGView(svg: .circle)
+ .resizable(resizingMode: .tile)
+}
+
+private extension SVG {
+
+ static var circle: SVG {
+ SVG(xml: """
+
+
+
+ """)!
+ }
+}
+#endif
+
+#endif
diff --git a/SwiftDraw/UIImage+Image.swift b/SwiftDraw/Sources/UIImage+SVG.swift
similarity index 61%
rename from SwiftDraw/UIImage+Image.swift
rename to SwiftDraw/Sources/UIImage+SVG.swift
index 4e701d34..fc9586be 100644
--- a/SwiftDraw/UIImage+Image.swift
+++ b/SwiftDraw/Sources/UIImage+SVG.swift
@@ -1,5 +1,5 @@
//
-// UIImage+Image.swift
+// UIImage+SVG.swift
// SwiftDraw
//
// Created by Simon Whitty on 24/5/17.
@@ -29,8 +29,12 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import Foundation
#if canImport(UIKit)
-import UIKit
+public import UIKit
+#if canImport(WatchKit)
+public import WatchKit
+#endif
public extension UIImage {
@@ -68,43 +72,48 @@ public extension UIImage {
}
public extension SVG {
- func rasterize() -> UIImage {
- return rasterize(with: size)
- }
- private func makeFormat() -> UIGraphicsImageRendererFormat {
- guard #available(iOS 12.0, *) else {
- let f = UIGraphicsImageRendererFormat.default()
- f.prefersExtendedRange = true
- return f
+#if os(watchOS)
+ func rasterize(scale: CGFloat = 0) -> UIImage {
+ let (bounds, pixelsWide, pixelsHigh) = SVG.makeBounds(size: size, scale: 1)
+ let actualScale = scale <= 0 ? SVG.defaultScale : scale
+ UIGraphicsBeginImageContextWithOptions(CGSize(width: pixelsWide, height: pixelsHigh), false, actualScale)
+ defer { UIGraphicsEndImageContext() }
+
+ if let context = UIGraphicsGetCurrentContext() {
+ context.draw(self, in: bounds)
}
+
+ return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
+ }
+#else
+ func rasterize(scale: CGFloat = 0) -> UIImage {
+ let (bounds, pixelsWide, pixelsHigh) = SVG.makeBounds(size: size, scale: 1)
let f = UIGraphicsImageRendererFormat.preferred()
f.preferredRange = .automatic
- return f
- }
-
- func rasterize(with size: CGSize? = nil, scale: CGFloat = 0, insets: UIEdgeInsets = .zero) -> UIImage {
- let insets = Insets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right)
- let (bounds, pixelsWide, pixelsHigh) = makeBounds(size: size, scale: 1, insets: insets)
- let f = makeFormat()
- f.scale = scale
+ f.scale = scale <= 0 ? SVG.defaultScale : scale
f.opaque = false
let r = UIGraphicsImageRenderer(size: CGSize(width: pixelsWide, height: pixelsHigh), format: f)
- return r.image{
+ return r.image {
$0.cgContext.draw(self, in: bounds)
}
}
+#endif
+
+ func rasterize(size: CGSize, scale: CGFloat = 0) -> UIImage {
+ self.sized(size).rasterize(scale: scale)
+ }
- func pngData(size: CGSize? = nil, scale: CGFloat = 0, insets: UIEdgeInsets = .zero) throws -> Data {
- let image = rasterize(with: size, scale: scale, insets: insets)
+ func pngData(scale: CGFloat = 0) throws -> Data {
+ let image = rasterize(scale: scale)
guard let data = image.pngData() else {
throw Error("Failed to create png data")
}
return data
}
- func jpegData(size: CGSize? = nil, scale: CGFloat = 0, compressionQuality quality: CGFloat = 1, insets: UIEdgeInsets = .zero) throws -> Data {
- let image = rasterize(with: size, scale: scale, insets: insets)
+ func jpegData(scale: CGFloat = 0, compressionQuality quality: CGFloat = 1) throws -> Data {
+ let image = rasterize(scale: scale)
guard let data = image.jpegData(compressionQuality: quality) else {
throw Error("Failed to create jpeg data")
}
@@ -114,19 +123,14 @@ public extension SVG {
extension SVG {
- func jpegData(size: CGSize?, scale: CGFloat, insets: Insets) throws -> Data {
- let insets = UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right)
- return try jpegData(size: size, scale: scale, insets: insets)
- }
-
- func pngData(size: CGSize?, scale: CGFloat, insets: Insets) throws -> Data {
- let insets = UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right)
- return try pngData(size: size, scale: scale, insets: insets)
- }
-
- func makeBounds(size: CGSize?, scale: CGFloat, insets: Insets) -> (bounds: CGRect, pixelsWide: Int, pixelsHigh: Int) {
- let scale = scale == 0 ? UIScreen.main.scale : scale
- return Self.makeBounds(size: size, defaultSize: self.size, scale: scale, insets: insets)
+ static var defaultScale: CGFloat {
+#if os(watchOS)
+ WKInterfaceDevice.current().screenScale
+#elseif os(visionOS)
+ 1.0
+#else
+ MainActor.syncIsolated { UIScreen.main.scale }
+#endif
}
private struct Error: LocalizedError {
@@ -139,3 +143,14 @@ extension SVG {
}
#endif
+
+private extension MainActor {
+
+ static func syncIsolated(operation: @MainActor () -> T) -> T {
+ if Thread.isMainThread {
+ return MainActor.assumeIsolated { operation() }
+ } else {
+ return DispatchQueue.main.sync { operation() }
+ }
+ }
+}
diff --git a/SwiftDraw/CGImage+Mask.swift b/SwiftDraw/Sources/Utilities/CGImage+Mask.swift
similarity index 100%
rename from SwiftDraw/CGImage+Mask.swift
rename to SwiftDraw/Sources/Utilities/CGImage+Mask.swift
diff --git a/SwiftDraw/CGPath+Segment.swift b/SwiftDraw/Sources/Utilities/CGPath+Segment.swift
similarity index 100%
rename from SwiftDraw/CGPath+Segment.swift
rename to SwiftDraw/Sources/Utilities/CGPath+Segment.swift
diff --git a/SwiftDraw/CGPattern+Closure.swift b/SwiftDraw/Sources/Utilities/CGPattern+Closure.swift
similarity index 100%
rename from SwiftDraw/CGPattern+Closure.swift
rename to SwiftDraw/Sources/Utilities/CGPattern+Closure.swift
diff --git a/SwiftDraw/Utilities/PairedSequence.swift b/SwiftDraw/Sources/Utilities/PairedSequence.swift
similarity index 100%
rename from SwiftDraw/Utilities/PairedSequence.swift
rename to SwiftDraw/Sources/Utilities/PairedSequence.swift
diff --git a/SwiftDraw/Stack.swift b/SwiftDraw/Sources/Utilities/Stack.swift
similarity index 100%
rename from SwiftDraw/Stack.swift
rename to SwiftDraw/Sources/Utilities/Stack.swift
diff --git a/SwiftDraw/Tests/CommandLine/CommandLine.ArgumentsTests.swift b/SwiftDraw/Tests/CommandLine/CommandLine.ArgumentsTests.swift
new file mode 100644
index 00000000..756e66f0
--- /dev/null
+++ b/SwiftDraw/Tests/CommandLine/CommandLine.ArgumentsTests.swift
@@ -0,0 +1,69 @@
+//
+// CommandLine.ArgumentsTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 7/12/18.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import XCTest
+@testable import SwiftDraw
+
+final class CommandLineArgumentsTests: XCTestCase {
+
+ func testParseModifiers() throws {
+ var modifiers = try CommandLine.parseModifiers(from: ["--format", "some", "--output", "more", "--scale", "magnify", "--size", "huge"])
+ XCTAssertEqual(modifiers, [.format: "some", .output: "more", .scale: "magnify", .size: "huge"])
+
+ modifiers = try CommandLine.parseModifiers(from: ["--ultralightInsets", "a", "--blackInsets", "b", "--hideUnsupportedFilters", "--legacy"])
+ XCTAssertEqual(modifiers, [.ultralightInsets: "a", .blackInsets: "b", .hideUnsupportedFilters: nil, .legacy: nil])
+
+ modifiers = try CommandLine.parseModifiers(from: ["--ultralight-insets", "a", "--black-insets", "b", "--hide-unsupported-filters", "--legacy"])
+ XCTAssertEqual(modifiers, [.ultralightInsets: "a", .blackInsets: "b", .hideUnsupportedFilters: nil, .legacy: nil])
+
+ }
+
+ func testParseModifiersThrowsForOddPairs() {
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format"]))
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format", "png", "--output"]))
+ }
+
+ func testParseModifiersThrowsForDuplicateModifiers() {
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format", "png", "--format", "jpg"]))
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format", "png", "--output", "more", "--output", "evenmore"]))
+ }
+
+ func testParseModifiersThrowsForUnknownModifiers() {
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--unknown", "png"]))
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format", "png", "--unknown", "more"]))
+ }
+
+ func testParseModifiersThrowsForMissingPrefix() {
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["format", "png"]))
+ XCTAssertThrowsError(try CommandLine.parseModifiers(from: ["--format", "png", "output", "more"]))
+ }
+
+}
diff --git a/SwiftDrawTests/CommandLine.ConfigurationTests.swift b/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift
similarity index 98%
rename from SwiftDrawTests/CommandLine.ConfigurationTests.swift
rename to SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift
index 9393122e..b2698443 100644
--- a/SwiftDrawTests/CommandLine.ConfigurationTests.swift
+++ b/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift
@@ -90,9 +90,8 @@ final class CommandLineConfigurationTests: XCTestCase {
}
func testParseInsets() throws {
- XCTAssertEqual(
- try CommandLine.parseInsets(from: nil),
- .init()
+ XCTAssertNil(
+ try CommandLine.parseInsets(from: nil)
)
XCTAssertEqual(
try CommandLine.parseInsets(from: "auto"),
diff --git a/SwiftDrawTests/CoordinateTests.swift b/SwiftDraw/Tests/Formatter/CoordinateTests.swift
similarity index 100%
rename from SwiftDrawTests/CoordinateTests.swift
rename to SwiftDraw/Tests/Formatter/CoordinateTests.swift
diff --git a/SwiftDrawTests/XML.FormatterTests.swift b/SwiftDraw/Tests/Formatter/XML.FormatterTests.swift
similarity index 99%
rename from SwiftDrawTests/XML.FormatterTests.swift
rename to SwiftDraw/Tests/Formatter/XML.FormatterTests.swift
index cdfa861c..f91193c0 100644
--- a/SwiftDrawTests/XML.FormatterTests.swift
+++ b/SwiftDraw/Tests/Formatter/XML.FormatterTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
@testable import SwiftDraw
import XCTest
diff --git a/SwiftDrawTests/LayerTree.Builder.LayerTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.LayerTests.swift
similarity index 91%
rename from SwiftDrawTests/LayerTree.Builder.LayerTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.Builder.LayerTests.swift
index 2358e946..274d2b74 100644
--- a/SwiftDrawTests/LayerTree.Builder.LayerTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.LayerTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
@@ -43,14 +44,12 @@ final class LayerTreeBuilderLayerTests: XCTestCase {
}
func testMakeImageContentsFromDOM() throws {
- let image = DOM.Image(href: URL(maybeData: "data:image/png;base64,f00d")!,
- width: 50,
- height: 50)
+ let image = DOM.Image(href: URL(maybeData: "data:image/png;base64,f00d")!)
let contents = try LayerTree.Builder.makeImageContents(from: image)
XCTAssertEqual(contents, .image(.png(data: Data(base64Encoded: "f00d")!)))
- let invalid = DOM.Image(href: URL(string: "aa")!, width: 10, height: 20)
+ let invalid = DOM.Image(href: URL(string: "aa")!)
XCTAssertThrowsError(try LayerTree.Builder.makeImageContents(from: invalid))
}
@@ -86,3 +85,14 @@ final class LayerTreeBuilderLayerTests: XCTestCase {
XCTAssertEqual(l2.transform, [.translate(tx: 0, ty: 20)])
}
}
+
+extension LayerTree.Image {
+
+ static func png(data: Data) -> Self {
+ Self(bitmap: .png(data))
+ }
+
+ static func jpeg(data: Data) -> Self {
+ Self(bitmap: .jpeg(data))
+ }
+}
diff --git a/SwiftDraw/Tests/LayerTree/LayerTree.Builder.Path.ArcTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.Path.ArcTests.swift
new file mode 100644
index 00000000..4c7dd6d8
--- /dev/null
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.Path.ArcTests.swift
@@ -0,0 +1,53 @@
+//
+// LayerTree.Builder.Path.ArcTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 14/2/25.
+// Copyright 2025 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+
+import XCTest
+@testable import SwiftDraw
+
+final class LayerTreeBuilderPathArcTests: XCTestCase {
+
+ func testClamped() {
+ XCTAssertEqual(5.clamped(to: 0...10), 5)
+ XCTAssertEqual((10.1).clamped(to: 0...10), 10)
+ XCTAssertEqual((-1).clamped(to: 0...10), 0)
+ XCTAssertEqual(Double.infinity.clamped(to: 0...10), 10)
+ XCTAssertEqual((-Double.infinity).clamped(to: 0...10), 0)
+ XCTAssertEqual(Double.nan.clamped(to: 0...10), 0)
+ XCTAssertEqual((-Double.nan).clamped(to: 0...10), 0)
+ }
+
+ func testVectorAngle() {
+ XCTAssertEqual(vectorAngle(ux: 1, uy: 1, vx: 1, vy: 1), 0)
+ XCTAssertEqual(vectorAngle(ux: 1, uy: 1, vx: -1, vy: 1), 1.5707964, accuracy: 0.001)
+ XCTAssertEqual(vectorAngle(ux: 1, uy: 1, vx: .nan, vy: 1), 3.1415925, accuracy: 0.001)
+ }
+}
diff --git a/SwiftDrawTests/LayerTree.Builder.ShapeTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.ShapeTests.swift
similarity index 98%
rename from SwiftDrawTests/LayerTree.Builder.ShapeTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.Builder.ShapeTests.swift
index 582bd5a3..caca7ba3 100644
--- a/SwiftDrawTests/LayerTree.Builder.ShapeTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.Builder.ShapeTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
diff --git a/SwiftDrawTests/LayerTree.BuilderTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.BuilderTests.swift
similarity index 64%
rename from SwiftDrawTests/LayerTree.BuilderTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.BuilderTests.swift
index 4721e5a3..6c64ec60 100644
--- a/SwiftDrawTests/LayerTree.BuilderTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.BuilderTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+@testable import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
@@ -38,47 +39,64 @@ final class LayerTreeBuilderTests: XCTestCase {
typealias Contents = LayerTree.Layer.Contents
func testMakeViewBoxTransform() {
- var transform = LayerTree.Builder.makeTransform(for: nil, width: 100, height: 200)
+ var transform = LayerTree.Builder.makeTransform(viewBox: nil, width: 100, height: 200)
XCTAssertEqual(transform, [])
let viewbox = DOM.SVG.ViewBox(x: 0, y: 0, width: 200, height: 200)
- transform = LayerTree.Builder.makeTransform(for: viewbox, width: 100, height: 100)
+ transform = LayerTree.Builder.makeTransform(viewBox: viewbox, width: 100, height: 100)
XCTAssertEqual(transform, [.scale(sx: 0.5, sy: 0.5)])
let viewbox1 = DOM.SVG.ViewBox(x: 10, y: -10, width: 100, height: 100)
- transform = LayerTree.Builder.makeTransform(for: viewbox1, width: 100, height: 100)
+ transform = LayerTree.Builder.makeTransform(viewBox: viewbox1, width: 100, height: 100)
XCTAssertEqual(transform, [.translate(tx: -10, ty: 10)])
}
func testDOMMaskMakesLayer() {
let circle = DOM.Circle(cx: 5, cy: 5, r: 5)
let line = DOM.Line(x1: 0, y1: 0, x2: 10, y2: 0)
+ let mask = DOM.Mask(id: "mask1", childElements: [circle, line])
+ mask.attributes.fill = .color(.keyword(.white))
+
let svg = DOM.SVG(width: 10, height: 10)
- svg.defs.masks.append(DOM.Mask(id: "mask1", childElements: [circle, line]))
-
+ svg.defs.masks.append(mask)
+
let builder = LayerTree.Builder(svg: svg)
let element = DOM.GraphicsElement()
element.attributes.mask = URL(string: "#mask1")
let layer = builder.createMaskLayer(for: element)
-
XCTAssertEqual(layer?.contents.count, 2)
+ XCTAssertEqual(layer?.contents[0].shape?.fill.fill, .color(.white))
}
func testDOMClipMakesShape() {
let circle = DOM.Circle(cx: 5, cy: 5, r: 5)
let svg = DOM.SVG(width: 10, height: 10)
svg.defs.clipPaths.append(DOM.ClipPath(id: "clip1", childElements: [circle]))
+
+ var attributes = DOM.PresentationAttributes()
+ attributes.clipPath = URL(string: "#clip1")
+ svg.styles = [DOM.StyleSheet(attributes: [.class("a"): attributes])]
+
let builder = LayerTree.Builder(svg: svg)
- let element = DOM.GraphicsElement()
+ var element = DOM.GraphicsElement()
element.attributes.clipPath = URL(string: "#clip1")
- let shapes = builder.createClipShapes(for: element)
- XCTAssertEqual(shapes, [.ellipse(within: LayerTree.Rect(x: 0, y: 0, width: 10, height: 10))])
+ XCTAssertEqual(
+ builder.createClipShapes(for: element),
+ [.ellipse(within: LayerTree.Rect(x: 0, y: 0, width: 10, height: 10))]
+ )
+
+ element = DOM.GraphicsElement()
+ element.class = "a"
+ XCTAssertEqual(
+ builder.createClipShapes(for: element),
+ [.ellipse(within: LayerTree.Rect(x: 0, y: 0, width: 10, height: 10))]
+ )
}
-
+
func testDOMGroupMakesChildContents() {
let builder = LayerTree.Builder(svg: DOM.SVG(width: 10, height: 10))
@@ -106,7 +124,7 @@ final class LayerTreeBuilderTests: XCTestCase {
func testStrokeAttributes() {
var state = LayerTree.Builder.State()
- state.stroke = .color(.rgbf(1.0, 0.0, 0.0))
+ state.stroke = .color(.rgbf(1.0, 0.0, 0.0, 1.0))
state.strokeOpacity = 0.5
state.strokeWidth = 5.0
state.strokeLineCap = .square
@@ -124,6 +142,14 @@ final class LayerTreeBuilderTests: XCTestCase {
let att2 = LayerTree.Builder.makeStrokeAttributes(with: state)
XCTAssertEqual(att2.color, .none)
}
+
+ func testDeepNestedSVGBuildLayers() async {
+ let circle = DOM.Circle(cx: 50, cy: 50, r: 10)
+ let svg = DOM.SVG(width: 50, height: 50)
+ svg.childElements.append(DOM.Group.make(child: circle, nestedLevels: 500))
+
+ let _ = LayerTree.Builder(svg: svg).makeLayer()
+ }
}
private extension LayerTree.StrokeAttributes {
@@ -141,10 +167,51 @@ private extension LayerTree.FillAttributes {
}
}
-private extension LayerTree.Builder {
+extension LayerTree.Builder {
static func makeStrokeAttributes(with state: State) -> LayerTree.StrokeAttributes {
let builder = LayerTree.Builder(svg: DOM.SVG(width: 10, height: 10))
return builder.makeStrokeAttributes(with: state)
}
+
+ func createClipShapes(for element: DOM.GraphicsElement) -> [LayerTree.Shape] {
+ makeClipShapes(for: element).map(\.shape)
+ }
+
+ static func makeTransform(
+ viewBox: DOM.SVG.ViewBox?,
+ width: DOM.Length,
+ height: DOM.Length
+ ) -> [LayerTree.Transform] {
+ makeTransform(
+ x: nil,
+ y: nil,
+ viewBox: viewBox,
+ width: width,
+ height: height
+ )
+ }
+}
+
+extension DOM.Group {
+
+ static func make(child: DOM.GraphicsElement, nestedLevels: Int) -> DOM.Group {
+ var group = DOM.Group()
+ group.childElements.append(child)
+
+ for _ in 0.. [RendererCommand] {
+ renderCommands(forClip: shapes.map { LayerTree.ClipShape(shape: $0, transform: .identity) }, using: rule)
+ }
+}
+
+extension DOM.SVG {
+
+ static func parse(fileNamed name: String, in bundle: Bundle = .test) throws -> DOM.SVG {
+ guard let url = bundle.url(forResource: name, withExtension: nil) else {
+ throw Error.missing
+ }
+
+ let parser = XMLParser(options: [.skipInvalidElements], filename: url.lastPathComponent)
+ let element = try XML.SAXParser.parse(contentsOf: url)
+ return try parser.parseSVG(element)
+ }
+
+ static func parse(
+ xml: String,
+ options: DOMXMLParser.Options = [.skipInvalidElements]
+ ) throws -> DOM.SVG {
+ let element = try XML.SAXParser.parse(data: xml.data(using: .utf8)!)
+ let parser = XMLParser(options: options)
+ return try parser.parseSVG(element)
+ }
+
+ enum Error: Swift.Error {
+ case missing
+ }
+}
diff --git a/SwiftDrawTests/LayerTree.ImageTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.ImageTests.swift
similarity index 100%
rename from SwiftDrawTests/LayerTree.ImageTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.ImageTests.swift
diff --git a/SwiftDrawTests/LayerTree.LayerTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.LayerTests.swift
similarity index 99%
rename from SwiftDrawTests/LayerTree.LayerTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.LayerTests.swift
index 316687c2..ef365e50 100644
--- a/SwiftDrawTests/LayerTree.LayerTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.LayerTests.swift
@@ -31,6 +31,7 @@
import XCTest
@testable import SwiftDraw
+import SwiftDrawDOM
final class LayerTreeLayerTests: XCTestCase {
diff --git a/SwiftDrawTests/LayerTree.Path+BoundsTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Path+BoundsTests.swift
similarity index 100%
rename from SwiftDrawTests/LayerTree.Path+BoundsTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.Path+BoundsTests.swift
diff --git a/SwiftDrawTests/LayerTree.Path+ReversedTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Path+ReversedTests.swift
similarity index 99%
rename from SwiftDrawTests/LayerTree.Path+ReversedTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.Path+ReversedTests.swift
index 65dbee94..034cb7f9 100644
--- a/SwiftDrawTests/LayerTree.Path+ReversedTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.Path+ReversedTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
@testable import SwiftDraw
import XCTest
diff --git a/SwiftDrawTests/LayerTree.Path+SubpathTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.Path+SubpathTests.swift
similarity index 100%
rename from SwiftDrawTests/LayerTree.Path+SubpathTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.Path+SubpathTests.swift
diff --git a/SwiftDrawTests/LayerTree.PathTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.PathTests.swift
similarity index 97%
rename from SwiftDrawTests/LayerTree.PathTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.PathTests.swift
index 7ce6eb5d..321bae43 100644
--- a/SwiftDrawTests/LayerTree.PathTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.PathTests.swift
@@ -26,6 +26,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
@@ -181,13 +182,13 @@ final class LayerTreePathTests: XCTestCase {
x: 300, y: 50, space: .absolute)
var c = LayerTree.Builder.createQuadratic(from: quad, last: Point(0, 50))
- XCTAssertEqual(c, cubic(300, 50, 100*twoThirds, 16.6666641, 100*twoThirds+150*twoThirds, 16.6666641))
+ XCTAssertEqual(c, cubic(300, 50, 66.66667, 16.666664, 166.66666, 16.666664))
//quad with control point to the right
quad = .quadratic(x1: 200, y1: 0,
x: 300, y: 50, space: .absolute)
c = LayerTree.Builder.createQuadratic(from: quad, last: Point(0, 50))
- XCTAssertEqual(c, cubic(300, 50, 200*twoThirds, 16.6666641, 200*twoThirds+150*twoThirds, 16.6666641))
+ XCTAssertEqual(c, cubic(300, 50, 133.33334, 16.666664, 233.33333, 16.666664))
}
func testQuadraticSmoothAbsolute() {
@@ -206,7 +207,8 @@ final class LayerTreePathTests: XCTestCase {
let domSegment = DOM.Path.Segment.quadraticSmooth(x: 10.0, y: 10.0, space: .relative)
let segment = LayerTree.Builder.makeSegment(from: domSegment, last: .init(10, 10), previous: nil)
- XCTAssertEqual(segment, .cubic(to: .init(20.0, 20.0), control1: .init(10, 10), control2: .init(13.333334, 10)))
+
+ XCTAssertEqual(segment, .cubic(to: .init(20.0, 20.0), control1: .init(10, 10), control2: .init(13.333334, 13.333334)))
}
func testDOMCubicSmooth() {
diff --git a/SwiftDrawTests/LayerTree.ShapeTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.ShapeTests.swift
similarity index 99%
rename from SwiftDrawTests/LayerTree.ShapeTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.ShapeTests.swift
index 244efc32..749339a3 100644
--- a/SwiftDrawTests/LayerTree.ShapeTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.ShapeTests.swift
@@ -26,6 +26,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
diff --git a/SwiftDrawTests/LayerTree.TransformTests.swift b/SwiftDraw/Tests/LayerTree/LayerTree.TransformTests.swift
similarity index 99%
rename from SwiftDrawTests/LayerTree.TransformTests.swift
rename to SwiftDraw/Tests/LayerTree/LayerTree.TransformTests.swift
index 8b7e907f..1248ae4f 100644
--- a/SwiftDrawTests/LayerTree.TransformTests.swift
+++ b/SwiftDraw/Tests/LayerTree/LayerTree.TransformTests.swift
@@ -27,6 +27,7 @@
//
import XCTest
+import SwiftDrawDOM
@testable import SwiftDraw
final class LayerTreeTransformTests: XCTestCase {
diff --git a/SwiftDraw/Tests/NSImage+SVGTests.swift b/SwiftDraw/Tests/NSImage+SVGTests.swift
new file mode 100644
index 00000000..7f03751c
--- /dev/null
+++ b/SwiftDraw/Tests/NSImage+SVGTests.swift
@@ -0,0 +1,79 @@
+//
+// NSImage+SVGTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 27/11/18.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import SwiftDrawDOM
+import Testing
+
+@testable import SwiftDraw
+#if canImport(AppKit) && !targetEnvironment(macCatalyst)
+import AppKit
+
+struct NSImageSVGTests {
+
+ @Test
+ func imageLoads() {
+ let image = NSImage(svgNamed: "lines.svg", in: .test)
+ #expect(image != nil)
+ }
+
+ @Test
+ func missingImageDoesNotLoad() {
+ let image = NSImage(svgNamed: "missing.svg", in: .test)
+ #expect(image == nil)
+ }
+
+ @Test
+ func imageDraws() {
+ let canvas = NSBitmapImageRep(pixelsWide: 2, pixelsHigh: 2)
+ let image = SVG.makeQuad().rasterize(with: CGSize(width: 2, height: 2))
+
+ canvas.lockFocus()
+ image.draw(in: NSRect(x: 0, y: 0, width: 2, height: 2))
+ canvas.unlockFocus()
+
+ #expect(canvas.colorAt(x: 0, y: 0) == NSColor(deviceRed: 1.0, green: 0, blue: 0, alpha: 1.0))
+ #expect(canvas.colorAt(x: 1, y: 1) == NSColor(deviceRed: 0.0, green: 0, blue: 1.0, alpha: 1.0))
+ }
+}
+
+private extension SVG {
+
+ static func makeQuad() -> SVG {
+ let svg = DOM.SVG(width: 2, height: 2)
+ svg.childElements.append(DOM.Rect(x: 0, y: 0, width: 1, height: 1))
+ svg.childElements.append(DOM.Rect(x: 1, y: 1, width: 1, height: 1))
+ svg.childElements[0].attributes.fill = .color(DOM.Color.rgbi(255, 0, 0, 1.0))
+ svg.childElements[1].attributes.fill = .color(DOM.Color.rgbi(0, 0, 255, 1.0))
+ return SVG(dom: svg, options: .default)
+ }
+}
+
+#endif
diff --git a/SwiftDrawTests/CGRendererTests.swift b/SwiftDraw/Tests/Renderer/CGRendererTests.swift
similarity index 100%
rename from SwiftDrawTests/CGRendererTests.swift
rename to SwiftDraw/Tests/Renderer/CGRendererTests.swift
diff --git a/SwiftDrawTests/GradientTests.swift b/SwiftDraw/Tests/Renderer/GradientTests.swift
similarity index 86%
rename from SwiftDrawTests/GradientTests.swift
rename to SwiftDraw/Tests/Renderer/GradientTests.swift
index 08f30912..7dac74b6 100644
--- a/SwiftDrawTests/GradientTests.swift
+++ b/SwiftDraw/Tests/Renderer/GradientTests.swift
@@ -30,6 +30,7 @@
//
import XCTest
+@testable import SwiftDrawDOM
@testable import SwiftDraw
final class GradientTests: XCTestCase {
@@ -48,7 +49,7 @@ final class GradientTests: XCTestCase {
expected.stops.append(DOM.LinearGradient.Stop(offset: 0.5, color: .keyword(.black)))
expected.stops.append(DOM.LinearGradient.Stop(offset: 1, color: .keyword(.red)))
- let parsed = try? XMLParser().parseLinearGradient(node)
+ let parsed = try? DOMXMLParser().parseLinearGradient(node)
XCTAssertEqual(expected, parsed)
XCTAssertEqual(expected.stops.count, parsed?.stops.count)
}
@@ -57,31 +58,31 @@ final class GradientTests: XCTestCase {
var node = ["offset": "25.5%", "stop-color": "black"]
- var parsed = try? XMLParser().parseLinearGradientStop(node)
+ var parsed = try? DOMXMLParser().parseLinearGradientStop(node)
XCTAssertEqual(parsed?.offset, 0.255)
XCTAssertEqual(parsed?.color, .keyword(.black))
XCTAssertEqual(parsed?.opacity, 1.0)
node["stop-opacity"] = "99%"
- parsed = try? XMLParser().parseLinearGradientStop(node)
+ parsed = try? DOMXMLParser().parseLinearGradientStop(node)
XCTAssertEqual(parsed?.opacity, 0.99)
}
func testGradientUnits() throws {
let node = XML.Element(name: "linearGradient", attributes: ["id": "abc"])
- var gradient = try XMLParser().parseLinearGradient(node)
+ var gradient = try DOMXMLParser().parseLinearGradient(node)
XCTAssertNil(gradient.gradientUnits)
node.attributes["gradientUnits"] = "userSpaceOnUse"
- gradient = try XMLParser().parseLinearGradient(node)
+ gradient = try DOMXMLParser().parseLinearGradient(node)
XCTAssertEqual(gradient.gradientUnits, .userSpaceOnUse)
node.attributes["gradientUnits"] = "objectBoundingBox"
- gradient = try XMLParser().parseLinearGradient(node)
+ gradient = try DOMXMLParser().parseLinearGradient(node)
XCTAssertEqual(gradient.gradientUnits, .objectBoundingBox)
node.attributes["gradientUnits"] = "invalid"
- XCTAssertThrowsError(try XMLParser().parseLinearGradient(node))
+ XCTAssertThrowsError(try DOMXMLParser().parseLinearGradient(node))
}
}
diff --git a/SwiftDraw/Tests/Renderer/Image+CoreGraphicsTests.swift b/SwiftDraw/Tests/Renderer/Image+CoreGraphicsTests.swift
new file mode 100644
index 00000000..8027946a
--- /dev/null
+++ b/SwiftDraw/Tests/Renderer/Image+CoreGraphicsTests.swift
@@ -0,0 +1,96 @@
+//
+// Image+CoreGraphicsTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 24/8/22.
+// Copyright 2022 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+import XCTest
+@testable import SwiftDraw
+import CoreGraphics
+
+final class ImageCoreGraphicsTests: XCTestCase {
+
+ func testPixelWide() {
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100, height: 50),
+ scale: 1).pixelsWide,
+ 100
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100.5, height: 50),
+ scale: 1).pixelsWide,
+ 101
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100, height: 50),
+ scale: 2).pixelsWide,
+ 200
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 300, height: 200),
+ scale: 1).pixelsWide,
+ 300
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 300, height: 200),
+ scale: 2).pixelsWide,
+ 600
+ )
+ }
+
+ func testPixelHigh() {
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100, height: 50),
+ scale: 1).pixelsHigh,
+ 50
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100, height: 50.5),
+ scale: 1).pixelsHigh,
+ 51
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 100, height: 50),
+ scale: 2).pixelsHigh,
+ 100
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 300, height: 200),
+ scale: 1).pixelsHigh,
+ 200
+ )
+ XCTAssertEqual(
+ SVG.makeBounds(size: CGSize(width: 300, height: 200),
+ scale: 2).pixelsHigh,
+ 400
+ )
+ }
+}
+
+#endif
diff --git a/SwiftDrawTests/MockRenderer.swift b/SwiftDraw/Tests/Renderer/MockRenderer.swift
similarity index 96%
rename from SwiftDrawTests/MockRenderer.swift
rename to SwiftDraw/Tests/Renderer/MockRenderer.swift
index 03a039e6..91e8a88a 100644
--- a/SwiftDrawTests/MockRenderer.swift
+++ b/SwiftDraw/Tests/Renderer/MockRenderer.swift
@@ -102,7 +102,7 @@ final class MockRenderer: Renderer {
operations.append("setClip")
}
- func setClip(mask: [Any], frame: LayerTree.Rect) {
+ func setClip(mask: [AnyHashable], frame: LayerTree.Rect) {
operations.append("setClipMask")
}
@@ -126,10 +126,10 @@ final class MockRenderer: Renderer {
operations.append("fillPath")
}
- func draw(image: LayerTree.Image) {
+ func draw(image: LayerTree.Image, in rect: LayerTree.Rect) {
operations.append("drawImage")
}
-
+
func draw(linear gradient: LayerTree.Gradient, from start: LayerTree.Point, to end: LayerTree.Point) {
operations.append("drawLinearGradient")
}
diff --git a/SwiftDrawTests/Renderer.CGTextTests.swift b/SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift
similarity index 100%
rename from SwiftDrawTests/Renderer.CGTextTests.swift
rename to SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift
diff --git a/SwiftDraw/Tests/Renderer/Renderer.CoreGraphics+CostTests.swift b/SwiftDraw/Tests/Renderer/Renderer.CoreGraphics+CostTests.swift
new file mode 100644
index 00000000..df9dd425
--- /dev/null
+++ b/SwiftDraw/Tests/Renderer/Renderer.CoreGraphics+CostTests.swift
@@ -0,0 +1,108 @@
+//
+// Renderer.CoreGraphics+CostTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 31/8/25.
+// Copyright 2025 WhileLoop Pty Ltd. All rights reserved.
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#if canImport(CoreGraphics)
+import CoreGraphics
+import Foundation
+@testable import SwiftDraw
+import SwiftDrawDOM
+import Testing
+
+struct RendererCoreGraphicsCostTests {
+
+ @Test
+ func duplicatePathInstancesRemoved() throws {
+ let source = try SVG.fromXML(#"""
+
+
+
+
+
+
+
+
+ """#)
+
+ let uniquePaths = Set(source.commands.allPaths.map(ObjectIdentifier.init))
+ #expect(uniquePaths.count == 1)
+ }
+
+ @Test
+ func duplicateImageInstancesRemoved() throws {
+ let source = try SVG.fromXML(#"""
+
+
+
+
+
+ """#)
+
+ let uniqueImages = Set(source.commands.allImages.map(ObjectIdentifier.init))
+ #expect(uniqueImages.count == 1)
+ }
+
+ @Test
+ func pathEstimatedCost() throws {
+ let source = try SVG.fromXML(#"""
+
+
+
+
+ """#)
+
+ #expect(source.commands.allPaths[0].estimatedCost == 168)
+ #expect(source.commands.estimatedCost == 232)
+ }
+
+ @Test
+ func imageEstimatedCost() throws {
+ let source = try SVG.fromXML(#"""
+
+
+
+
+ """#)
+
+ #expect(source.commands.allImages[0].estimatedCost == 100)
+ #expect(source.commands.estimatedCost == 108)
+ }
+
+ @Test
+ func shapesEstimatedCost() throws {
+ let image = try #require(SVG(named: "shapes.svg", in: .test))
+ #expect(image.commands.estimatedCost == 19220)
+ }
+}
+
+extension SVG {
+ static func fromXML(_ text: String, filename: String = #file) throws -> SVG {
+ let dom = try DOM.SVG.parse(data: text.data(using: .utf8)!)
+ return SVG(dom: dom, options: .default)
+ }
+}
+#endif
diff --git a/SwiftDrawTests/Renderer.CoreGraphicsTypesTests.swift b/SwiftDraw/Tests/Renderer/Renderer.CoreGraphicsTypesTests.swift
similarity index 99%
rename from SwiftDrawTests/Renderer.CoreGraphicsTypesTests.swift
rename to SwiftDraw/Tests/Renderer/Renderer.CoreGraphicsTypesTests.swift
index 8cff736a..606ec1c0 100644
--- a/SwiftDrawTests/Renderer.CoreGraphicsTypesTests.swift
+++ b/SwiftDraw/Tests/Renderer/Renderer.CoreGraphicsTypesTests.swift
@@ -30,6 +30,7 @@ import XCTest
#if canImport(CoreGraphics)
import CoreGraphics
@testable import SwiftDraw
+import SwiftDrawDOM
final class RendererCoreGraphicsTypesTests: XCTestCase {
@@ -109,7 +110,8 @@ final class RendererCoreGraphicsTypesTests: XCTestCase {
let segments: [CGPath.Segment] = [.move(CGPoint(0, 0)), .line(CGPoint(10, 20)), .line(CGPoint(30, 40))]
XCTAssertEqual(path.segments(), segments)
}
-
+
+ @MainActor
func testShapeRect() {
let path = CGProvider().createPath(from: .rect(within: Rect(x: 10, y: 20, width: 30, height: 40),
radii: Size(2, 4)))
diff --git a/SwiftDrawTests/Renderer.LayerTreeProviderTests.swift b/SwiftDraw/Tests/Renderer/Renderer.LayerTreeProviderTests.swift
similarity index 99%
rename from SwiftDrawTests/Renderer.LayerTreeProviderTests.swift
rename to SwiftDraw/Tests/Renderer/Renderer.LayerTreeProviderTests.swift
index 03e8920d..c01cb1c4 100644
--- a/SwiftDrawTests/Renderer.LayerTreeProviderTests.swift
+++ b/SwiftDraw/Tests/Renderer/Renderer.LayerTreeProviderTests.swift
@@ -28,6 +28,7 @@
import XCTest
@testable import SwiftDraw
+import SwiftDrawDOM
final class RendererLayerTreeProviderTests: XCTestCase {
diff --git a/SwiftDrawTests/Renderer.SFSymbolTests.swift b/SwiftDraw/Tests/Renderer/Renderer.SFSymbolTests.swift
similarity index 91%
rename from SwiftDrawTests/Renderer.SFSymbolTests.swift
rename to SwiftDraw/Tests/Renderer/Renderer.SFSymbolTests.swift
index 7ee254ff..7256f3e4 100644
--- a/SwiftDrawTests/Renderer.SFSymbolTests.swift
+++ b/SwiftDraw/Tests/Renderer/Renderer.SFSymbolTests.swift
@@ -29,6 +29,7 @@
// 3. This notice may not be removed or altered from any source distribution.
//
+import SwiftDrawDOM
import XCTest
@testable import SwiftDraw
@@ -64,7 +65,7 @@ final class RendererSFSymbolTests: XCTestCase {
)
XCTAssertTrue(
- template.regular.bounds.size.width == 108.0
+ template.regular.bounds.size.width == 88.0
)
XCTAssertTrue(
template.regular.bounds.size.height == 70.0
@@ -186,18 +187,28 @@ private extension DOM.SVG {
private extension SFSymbolRenderer {
static func render(fileURL: URL) throws -> String {
- let renderer = SFSymbolRenderer(options: [], insets: .init(),
- insetsUltralight: .init(),
- insetsBlack: .init(),
- precision: 3)
+ let renderer = SFSymbolRenderer(
+ size: .small,
+ options: [],
+ insets: .init(),
+ insetsUltralight: .init(),
+ insetsBlack: .init(),
+ precision: 3,
+ isLegacyInsets: false
+ )
return try renderer.render(regular: fileURL, ultralight: nil, black: nil)
}
static func render(svg: DOM.SVG) throws -> String {
- let renderer = SFSymbolRenderer(options: [], insets: .init(),
- insetsUltralight: .init(),
- insetsBlack: .init(),
- precision: 3)
+ let renderer = SFSymbolRenderer(
+ size: .small,
+ options: [],
+ insets: .init(),
+ insetsUltralight: .init(),
+ insetsBlack: .init(),
+ precision: 3,
+ isLegacyInsets: false
+ )
return try renderer.render(default: svg, ultralight: nil, black: nil)
}
}
diff --git a/SwiftDrawTests/Renderer.SVGTests.swift b/SwiftDraw/Tests/Renderer/Renderer.SVGTests.swift
similarity index 100%
rename from SwiftDrawTests/Renderer.SVGTests.swift
rename to SwiftDraw/Tests/Renderer/Renderer.SVGTests.swift
diff --git a/SwiftDrawTests/RendererTests.swift b/SwiftDraw/Tests/Renderer/RendererTests.swift
similarity index 97%
rename from SwiftDrawTests/RendererTests.swift
rename to SwiftDraw/Tests/Renderer/RendererTests.swift
index fdbe45b2..4ed96771 100644
--- a/SwiftDrawTests/RendererTests.swift
+++ b/SwiftDraw/Tests/Renderer/RendererTests.swift
@@ -31,6 +31,7 @@
import XCTest
@testable import SwiftDraw
+import SwiftDrawDOM
final class RendererTests: XCTestCase {
@@ -53,13 +54,12 @@ final class RendererTests: XCTestCase {
.setLineJoin(.bevel),
.setLineMiter(limit: 10),
.setClip(path: .mock, rule: .nonzero),
- .setClipMask([], frame: .zero),
.fill(.mock, rule: .nonzero),
.stroke(.mock),
.clipStrokeOutline(.mock),
.setAlpha(0.5),
.setBlend(mode: .sourceIn),
- .draw(image: .mock),
+ .draw(image: .mock, in: .zero),
.drawLinearGradient(.mock, from: .zero, to: .zero),
.drawRadialGradient(.mock, startCenter: .zero, startRadius: 0, endCenter: .zero, endRadius: 0)
])
@@ -81,7 +81,6 @@ final class RendererTests: XCTestCase {
"setLineJoin",
"setLineMiterLimit",
"setClip",
- "setClipMask",
"fillPath",
"strokePath",
"clipStrokeOutline",
diff --git a/SwiftDraw/Tests/SVGTests.swift b/SwiftDraw/Tests/SVGTests.swift
new file mode 100644
index 00000000..d36d4872
--- /dev/null
+++ b/SwiftDraw/Tests/SVGTests.swift
@@ -0,0 +1,222 @@
+//
+// SVGTests.swift
+// SwiftDraw
+//
+// Created by Simon Whitty on 19/11/18.
+// Copyright 2020 Simon Whitty
+//
+// Distributed under the permissive zlib license
+// Get the latest version from here:
+//
+// https://github.com/swhitty/SwiftDraw
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+import SwiftDrawDOM
+@testable import SwiftDraw
+import Testing
+
+#if canImport(AppKit)
+import AppKit
+#endif
+
+#if canImport(UIKit)
+import UIKit
+#endif
+
+#if canImport(AppKit) || canImport(UIKit)
+struct SVGTests {
+
+ @Test
+ func validSVGLoads() {
+ #expect(SVG(named: "lines.svg", in: .test) != nil)
+ }
+
+ @Test
+ func invalidSVGReturnsNil() {
+ #expect(SVG(named: "invalids.svg", in: .test) == nil)
+ }
+
+ @Test
+ func missingSVGReturnsNil() {
+ #expect(SVG(named: "missing.svg", in: .test) == nil)
+ }
+
+ @Test
+ func imageRasterizes() {
+ let image = SVG.makeLines()
+ let rendered = image.rasterize(scale: 1)
+ #expect(rendered.size == image.size)
+ #expect(throws: Never.self) {
+ try image.pngData()
+ }
+ #expect(throws: Never.self) {
+ try image.jpegData()
+ }
+ #expect(throws: Never.self) {
+ try image.pdfData()
+ }
+ }
+
+ @Test
+ func shapesImageRasterizes() throws {
+ let image = try #require(SVG(named: "shapes.svg", in: .test))
+ #expect(throws: Never.self) {
+ try image.pngData()
+ }
+ #expect(throws: Never.self) {
+ try image.jpegData()
+ }
+ #expect(throws: Never.self) {
+ try image.pdfData()
+ }
+ }
+
+#if canImport(UIKit)
+ @Test
+ func rasterize() {
+ let svg = SVG(named: "gradient-apple.svg", in: .test)!
+ .sized(CGSize(width: 100, height: 100))
+ let image = svg.rasterize(scale: 3)
+ #expect(image.size == CGSize(width: 100, height: 100))
+ #expect(image.scale == 3)
+
+ let data = image.pngData()!
+ let reloaded = UIImage(data: data)!
+ #expect(reloaded.size == CGSize(width: 300, height: 300))
+ #expect(reloaded.scale == 1)
+ }
+#endif
+
+ @Test
+ func size() {
+ let image = SVG.makeLines()
+
+ #expect(
+ image.size == CGSize(width: 100, height: 100)
+ )
+ #expect(
+ image.sized(CGSize(width: 200, height: 200)).size == CGSize(width: 200, height: 200)
+ )
+
+ var copy = image
+ copy.size(CGSize(width: 20, height: 20))
+ #expect(
+ copy.size == CGSize(width: 20, height: 20)
+ )
+ }
+
+ @Test
+ func scale() {
+ let image = SVG.makeLines()
+
+ #expect(
+ image.size == CGSize(width: 100, height: 100)
+ )
+ #expect(
+ image.scaled(2).size == CGSize(width: 200, height: 200)
+ )
+ #expect(
+ image.scaled(0.5).size == CGSize(width: 50, height: 50)
+ )
+ #expect(
+ image.scaled(x: 2, y: 3).size == CGSize(width: 200, height: 300)
+ )
+
+ var copy = image
+ copy.scale(5)
+ #expect(
+ copy.size == CGSize(width: 500, height: 500)
+ )
+ }
+
+ @Test
+ func translate() {
+ let image = SVG.makeLines()
+
+ #expect(
+ image.size == CGSize(width: 100, height: 100)
+ )
+ #expect(
+ image.translated(tx: 10, ty: 10).size == CGSize(width: 100, height: 100)
+ )
+
+ var copy = image
+ copy.translate(tx: 50, ty: 50)
+ #expect(
+ copy.size == CGSize(width: 100, height: 100)
+ )
+ }
+
+ @Test
+ func expand() {
+ let image = SVG.makeLines()
+
+ #expect(
+ image.size == CGSize(width: 100, height: 100)
+ )
+ #expect(
+ image.expanded(top: 50, right: 30).size == CGSize(width: 130, height: 150)
+ )
+
+ var copy = image
+ copy.expand(-10)
+ #expect(
+ copy.size == CGSize(width: 80, height: 80)
+ )
+ }
+
+ @Test
+ func hashable() {
+ var images = Set()
+ let lines = SVG.makeLines()
+
+ #expect(!images.contains(lines))
+
+ images.insert(SVG.makeLines())
+ #expect(images.contains(lines))
+
+ let linesResized = lines.sized(CGSize(width: 10, height: 10))
+ #expect(!images.contains(linesResized))
+
+ images.remove(lines)
+ #expect(!images.contains(SVG.makeLines()))
+ }
+
+ @Test
+ func deepNestedSVG() async {
+ let circle = DOM.Circle(cx: 50, cy: 50, r: 10)
+ let dom = DOM.SVG(width: 50, height: 50)
+ dom.childElements.append(DOM.Group.make(child: circle, nestedLevels: 500))
+
+ _ = SVG(dom: dom, options: .default)
+ }
+}
+
+private extension SVG {
+
+ static func makeLines() -> SVG {
+ let svg = DOM.SVG(width: 100, height: 100)
+ svg.childElements.append(DOM.Line(x1: 0, y1: 0, x2: 100, y2: 100))
+ svg.childElements.append(DOM.Line(x1: 100, y1: 0, x2: 0, y2: 100))
+ return SVG(dom: svg, options: .default)
+ }
+}
+#endif
diff --git a/SwiftDrawTests/Test.bundle/chart.svg b/SwiftDraw/Tests/Test.bundle/chart.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/chart.svg
rename to SwiftDraw/Tests/Test.bundle/chart.svg
diff --git a/SwiftDraw/Tests/Test.bundle/curves.svg b/SwiftDraw/Tests/Test.bundle/curves.svg
new file mode 100644
index 00000000..7efbba1f
--- /dev/null
+++ b/SwiftDraw/Tests/Test.bundle/curves.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SwiftDrawTests/Test.bundle/dash.svg b/SwiftDraw/Tests/Test.bundle/dash.svg
similarity index 100%
rename from SwiftDrawTests/Test.bundle/dash.svg
rename to SwiftDraw/Tests/Test.bundle/dash.svg
diff --git a/SwiftDraw/Tests/Test.bundle/empire.svg b/SwiftDraw/Tests/Test.bundle/empire.svg
new file mode 100644
index 00000000..5b36131f
--- /dev/null
+++ b/SwiftDraw/Tests/Test.bundle/empire.svg
@@ -0,0 +1,3857 @@
+
+
+
+image/svg+xml2008 Jun 12 03:54:25
OMC - Martin Weinelt
km
2008 Jun 12 04:45:24
OMC - Martin Weinelt
km