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: #""" + + + \#(groupOpen) + + \#(groupClose) + + """#) + } +} 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 @@ [![Build](https://github.com/swhitty/SwiftDraw/actions/workflows/build.yml/badge.svg)](https://github.com/swhitty/SwiftDraw/actions/workflows/build.yml) [![CodeCov](https://codecov.io/gh/swhitty/SwiftDraw/graphs/badge.svg)](https://codecov.io/gh/swhitty/SwiftDraw) -[![Platforms](https://img.shields.io/badge/platforms-iOS%20|%20Mac%20|%20Linux-lightgray.svg)](https://github.com/swhitty/SwiftDraw/blob/main/Package.swift) -[![Swift 5.7 — 5.9](https://img.shields.io/badge/swift-5.7-red.svg?style=flat)](https://developer.apple.com/swift) -[![License](https://img.shields.io/badge/license-zlib-lightgrey.svg)](https://opensource.org/licenses/Zlib) -[![Twitter](https://img.shields.io/badge/twitter-@simonwhitty-blue.svg)](http://twitter.com/simonwhitty) +[![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2FSwiftDraw%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/swhitty/SwiftDraw) +[![Swift 6.0](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fswhitty%2FSwiftDraw%2Fbadge%3Ftype%3Dswift-versions)](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 2008 Jun 12 03:54:25 OMC - Martin Weinelt km 2008 Jun 12 04:45:24 OMC - Martin Weinelt km diff --git a/SwiftDrawTests/Test.bundle/gradient-apple.svg b/SwiftDraw/Tests/Test.bundle/gradient-apple.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/gradient-apple.svg rename to SwiftDraw/Tests/Test.bundle/gradient-apple.svg diff --git a/SwiftDrawTests/Test.bundle/gradient-gratification.svg b/SwiftDraw/Tests/Test.bundle/gradient-gratification.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/gradient-gratification.svg rename to SwiftDraw/Tests/Test.bundle/gradient-gratification.svg diff --git a/SwiftDrawTests/Test.bundle/invalid.svg b/SwiftDraw/Tests/Test.bundle/invalid.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/invalid.svg rename to SwiftDraw/Tests/Test.bundle/invalid.svg diff --git a/SwiftDrawTests/Test.bundle/key.svg b/SwiftDraw/Tests/Test.bundle/key.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/key.svg rename to SwiftDraw/Tests/Test.bundle/key.svg diff --git a/SwiftDraw/Tests/Test.bundle/linearGradient.svg b/SwiftDraw/Tests/Test.bundle/linearGradient.svg new file mode 100644 index 00000000..4655ff41 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/linearGradient.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SwiftDrawTests/Test.bundle/lines.svg b/SwiftDraw/Tests/Test.bundle/lines.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/lines.svg rename to SwiftDraw/Tests/Test.bundle/lines.svg diff --git a/SwiftDraw/Tests/Test.bundle/nested-svg.svg b/SwiftDraw/Tests/Test.bundle/nested-svg.svg new file mode 100644 index 00000000..ae0b0807 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/nested-svg.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDraw/Tests/Test.bundle/quad.svg b/SwiftDraw/Tests/Test.bundle/quad.svg new file mode 100644 index 00000000..e8dfea35 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/quad.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDraw/Tests/Test.bundle/radialGradient.svg b/SwiftDraw/Tests/Test.bundle/radialGradient.svg new file mode 100644 index 00000000..855976e9 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/radialGradient.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SwiftDraw/Tests/Test.bundle/shapes.svg b/SwiftDraw/Tests/Test.bundle/shapes.svg new file mode 100644 index 00000000..5f78f352 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/shapes.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Madeleine Whitty šŸ˜€ + + + Test + + + + + + + + + + + \ No newline at end of file diff --git a/SwiftDraw/Tests/Test.bundle/starry.svg b/SwiftDraw/Tests/Test.bundle/starry.svg new file mode 100644 index 00000000..477e17fd --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/starry.svg @@ -0,0 +1,29951 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SwiftDraw/Tests/Test.bundle/stylesheet.svg b/SwiftDraw/Tests/Test.bundle/stylesheet.svg new file mode 100644 index 00000000..3fdda835 --- /dev/null +++ b/SwiftDraw/Tests/Test.bundle/stylesheet.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SwiftDrawTests/Test.bundle/symbol-test.svg b/SwiftDraw/Tests/Test.bundle/symbol-test.svg similarity index 100% rename from SwiftDrawTests/Test.bundle/symbol-test.svg rename to SwiftDraw/Tests/Test.bundle/symbol-test.svg diff --git a/SwiftDrawTests/UIImage+ImageTests.swift b/SwiftDraw/Tests/UIImage+SVGTests.swift similarity index 67% rename from SwiftDrawTests/UIImage+ImageTests.swift rename to SwiftDraw/Tests/UIImage+SVGTests.swift index 95440d91..11a2650a 100644 --- a/SwiftDrawTests/UIImage+ImageTests.swift +++ b/SwiftDraw/Tests/UIImage+SVGTests.swift @@ -1,5 +1,5 @@ // -// UIImage+ImageTests.swift +// UIImage+SVGTests.swift // SwiftDraw // // Created by Simon Whitty on 27/11/18. @@ -29,58 +29,54 @@ // 3. This notice may not be removed or altered from any source distribution. // -import XCTest @testable import SwiftDraw +import Testing #if canImport(UIKit) import UIKit -final class UIImageTests: XCTestCase { +struct UIImageSVGTests { - func testImageLoads() { + @Test + func imageLoads() { let image = UIImage(svgNamed: "lines.svg", in: .test) - XCTAssertNotNil(image) + #expect(image != nil) } - func testMissingImageDoesNotLoad() { + @Test + func missingImageDoesNotLoad() { let image = UIImage(svgNamed: "missing.svg", in: .test) - XCTAssertNil(image) + #expect(image == nil) } - func testImageSize() throws { - let image = try SVG.parse(#""" + @Test + func imageSize() throws { + let image = try SVG.parseXML(#""" """# ) - - XCTAssertEqual( - image.rasterize(scale: 1).size, - CGSize(width: 64, height: 64) + + #expect( + image.rasterize(scale: 1).size == CGSize(width: 64, height: 64) ) - XCTAssertEqual( - image.rasterize(scale: 1).scale, - 1 + #expect( + image.rasterize(scale: 1).scale == 1 ) - XCTAssertEqual( - image.rasterize(scale: 2).size, - CGSize(width: 64, height: 64) + #expect( + image.rasterize(scale: 2).size == CGSize(width: 64, height: 64) ) - XCTAssertEqual( - image.rasterize(scale: 2).scale, - 2 + #expect( + image.rasterize(scale: 2).scale == 2 ) } } -#endif - private extension SVG { - static func parse(_ code: String) throws -> SVG { - guard let data = code.data(using: .utf8), - let svg = SVG(data: data) else { + static func parseXML(_ xml: String) throws -> SVG { + guard let svg = SVG(xml: xml) else { throw InvalidSVG() } return svg @@ -90,3 +86,5 @@ private extension SVG { var errorDescription: String? = "Invalid SVG" } } + +#endif diff --git a/SwiftDraw/XML.Element.swift b/SwiftDraw/Tests/Utilities/Bundle+Extensions.swift similarity index 69% rename from SwiftDraw/XML.Element.swift rename to SwiftDraw/Tests/Utilities/Bundle+Extensions.swift index e3ed9b33..b0dc8e9b 100644 --- a/SwiftDraw/XML.Element.swift +++ b/SwiftDraw/Tests/Utilities/Bundle+Extensions.swift @@ -1,8 +1,8 @@ // -// XML.swift +// Bundle+Extensions.swift // SwiftDraw // -// Created by Simon Whitty on 31/12/16. +// Created by Simon Whitty on 19/11/18. // Copyright 2020 Simon Whitty // // Distributed under the permissive zlib license @@ -29,22 +29,23 @@ // 3. This notice may not be removed or altered from any source distribution. // -enum XML { /* namespace */ } -extension XML { - final class Element { - - let name: String - var attributes: [String: String] - var children = [Element]() - var innerText: String? - - var parsedLocation: (line: Int, column: Int)? - - init(name: String, attributes: [String: String] = [:]) { - self.name = name - self.attributes = attributes - self.innerText = nil +import Foundation + +extension Bundle { + + static let test = Bundle(url: Bundle.module.url(forResource: "Test", withExtension: "bundle")!)! + + func url(forResource named: String) throws -> URL { + guard let url = self.url(forResource: named, withExtension: nil) else { + throw Error.invalid } + return url + } + + private enum Error: Swift.Error { + case invalid } + + private class Marker { } } diff --git a/SwiftDrawTests/CGPath+SegmentTests.swift b/SwiftDraw/Tests/Utilities/CGPath+SegmentTests.swift similarity index 100% rename from SwiftDrawTests/CGPath+SegmentTests.swift rename to SwiftDraw/Tests/Utilities/CGPath+SegmentTests.swift diff --git a/SwiftDrawTests/NSBitmapImageRep+Extensions.swift b/SwiftDraw/Tests/Utilities/NSBitmapImageRep+Extensions.swift similarity index 100% rename from SwiftDrawTests/NSBitmapImageRep+Extensions.swift rename to SwiftDraw/Tests/Utilities/NSBitmapImageRep+Extensions.swift diff --git a/SwiftDrawTests/Utilities/PairedSequenceTests.swift b/SwiftDraw/Tests/Utilities/PairedSequenceTests.swift similarity index 100% rename from SwiftDrawTests/Utilities/PairedSequenceTests.swift rename to SwiftDraw/Tests/Utilities/PairedSequenceTests.swift diff --git a/SwiftDrawTests/StackTests.swift b/SwiftDraw/Tests/Utilities/StackTests.swift similarity index 100% rename from SwiftDrawTests/StackTests.swift rename to SwiftDraw/Tests/Utilities/StackTests.swift diff --git a/SwiftDrawTests/URL+DataTests.swift b/SwiftDraw/Tests/Utilities/URL+DataTests.swift similarity index 99% rename from SwiftDrawTests/URL+DataTests.swift rename to SwiftDraw/Tests/Utilities/URL+DataTests.swift index 9ea54cf0..6724a567 100644 --- a/SwiftDrawTests/URL+DataTests.swift +++ b/SwiftDraw/Tests/Utilities/URL+DataTests.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/SwiftDraw/XML.SAXParser.swift b/SwiftDraw/XML.SAXParser.swift deleted file mode 100644 index b4b786b4..00000000 --- a/SwiftDraw/XML.SAXParser.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// 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 - -extension XML { - - final class SAXParser: NSObject, XMLParserDelegate { - - #if canImport(FoundationXML) - typealias FoundationXMLParser = FoundationXML.XMLParser - #else - typealias FoundationXMLParser = Foundation.XMLParser - #endif - - private let parser: FoundationXMLParser - private let namespaceURI = "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 - } - - 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 - } - - static func parse(contentsOf url: URL) throws -> Element { - let data = try Data(contentsOf: url) - return try parse(data: data) - } - - func parser(_ parser: FoundationXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { - guard - self.parser === parser, - namespaceURI == self.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 - } - } - - func parser(_ parser: FoundationXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName _: String?) { - guard - namespaceURI == self.namespaceURI, - currentElement.name == elementName else { - return - } - - elements.removeLast() - } - - 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/SwiftDrawDOM.podspec.json b/SwiftDrawDOM.podspec.json new file mode 100644 index 00000000..c420a2f7 --- /dev/null +++ b/SwiftDrawDOM.podspec.json @@ -0,0 +1,27 @@ +{ + "name": "SwiftDrawDOM", + "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", + "license": { + "type": "zlib", + "file": "LICENSE.txt" + }, + "source": { + "git": "https://github.com/swhitty/SwiftDraw.git", + "tag": "0.25.0" + }, + "platforms": { + "ios": "13.0", + "osx": "10.15", + "tvos": "13.0", + "watchos": "6.0", + "visionos": "1.0" + }, + "source_files": "DOM/Sources/*.swift", + "swift_version": "6.0", + "pod_target_xcconfig": { + "OTHER_SWIFT_FLAGS": "-package-name SwiftDraw" + } +} diff --git a/SwiftDrawTests/CommandLine.ArgumentsTests.swift b/SwiftDrawTests/CommandLine.ArgumentsTests.swift deleted file mode 100644 index bdd9301d..00000000 --- a/SwiftDrawTests/CommandLine.ArgumentsTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// 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 { - let modifiers = try CommandLine.parseModifiers(from: ["--format", "some", "--output", "more", "--scale", "magnify", "--size", "huge"]) - XCTAssertEqual(modifiers, [.format: "some", .output: "more", .scale: "magnify", .size: "huge"]) - } - - 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/DOM+Extensions.swift b/SwiftDrawTests/DOM+Extensions.swift deleted file mode 100644 index dcd9cce9..00000000 --- a/SwiftDrawTests/DOM+Extensions.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// 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 -@testable import SwiftDraw - -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: Equatable { - public 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) - } -} - -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) - } -} - -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) - } -} - -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]) - } -} - - -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) - } - - enum Error: Swift.Error { - case missing - } -} diff --git a/SwiftDrawTests/DOM.ElementTests.swift b/SwiftDrawTests/DOM.ElementTests.swift deleted file mode 100644 index 321a8885..00000000 --- a/SwiftDrawTests/DOM.ElementTests.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class DOMElementTests: XCTestCase { - - func testLine() { - let element = DOM.createLine() - var another = DOM.createLine() - - XCTAssertEqual(element, another) - - another.x1 = 1 - XCTAssertNotEqual(element, another) - - another = DOM.createLine() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testCircle() { - let element = DOM.createCircle() - var another = DOM.createCircle() - - XCTAssertEqual(element, another) - - another.cx = 1 - XCTAssertNotEqual(element, another) - - another = DOM.createCircle() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testEllipse() { - let element = DOM.createEllipse() - var another = DOM.createEllipse() - - XCTAssertEqual(element, another) - - another.cx = 1 - XCTAssertNotEqual(element, another) - - another = DOM.createEllipse() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testRect() { - let element = DOM.createRect() - var another = DOM.createRect() - - XCTAssertEqual(element, another) - - another.x = 1 - XCTAssertNotEqual(element, another) - - another = DOM.createRect() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testPolygon() { - let element = DOM.createPolygon() - var another = DOM.createPolygon() - - XCTAssertEqual(element, another) - - another.points.append(DOM.Point(6, 7)) - XCTAssertNotEqual(element, another) - - another = DOM.createPolygon() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testPolyline() { - let element = DOM.createPolyline() - var another = DOM.createPolyline() - - XCTAssertEqual(element, another) - - another.points.append(DOM.Point(6, 7)) - XCTAssertNotEqual(element, another) - - another = DOM.createPolyline() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testText() { - let element = DOM.createText() - var another = DOM.createText() - - XCTAssertEqual(element, another) - - another.value = "Simon" - XCTAssertNotEqual(element, another) - - another = DOM.createText() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(element, another) - - another.attributes.fill = nil - XCTAssertEqual(element, another) - } - - func testGroup() { - let group = DOM.createGroup() - var another = DOM.createGroup() - - XCTAssertEqual(group, another) - - another.childElements.append(DOM.createCircle()) - XCTAssertNotEqual(group, another) - - another = DOM.createGroup() - another.attributes.fill = .color(.keyword(.black)) - XCTAssertNotEqual(group, another) - - another.attributes.fill = nil - XCTAssertEqual(group, another) - } -} diff --git a/SwiftDrawTests/Image+CoreGraphicsTests.swift b/SwiftDrawTests/Image+CoreGraphicsTests.swift deleted file mode 100644 index 0292d0b0..00000000 --- a/SwiftDrawTests/Image+CoreGraphicsTests.swift +++ /dev/null @@ -1,235 +0,0 @@ -// -// 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_WithInsetsZero() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsWide, - 100 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsWide, - 200 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsWide, - 300 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsWide, - 600 - ) - } - - func testPixelHigh_WithInsetsZero() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsHigh, - 50 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsHigh, - 100 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).pixelsHigh, - 200 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).pixelsHigh, - 400 - ) - } - - func testPixelWide_WithInsets() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(left: 5, right: 20)).pixelsWide, - 75 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(left: 5, right: 20)).pixelsWide, - 150 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(left: 5, right: 20)).pixelsWide, - 300 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(left: 5, right: 20)).pixelsWide, - 600 - ) - } - - func testPixelHigh_WithInsets() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 5 - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 10 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 200 - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 15, bottom: 30)).pixelsHigh, - 400 - ) - } - - func testBounds_WithInsetsZero() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 100, height: 50) - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 200, height: 100) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 300, height: 200) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 300, height: 200), - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .zero).bounds, - CGRect(x: 0, y: 0, width: 600, height: 400) - ) - } - - func testBounds_WithInsets() { - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 1, - insets: .make(top: 5, left: 10, bottom: 15, right: 20)).bounds, - CGRect(x: -10, y: -5, width: 100, height: 50) - ) - XCTAssertEqual( - SVG.makeBounds(size: nil, - defaultSize: CGSize(width: 100, height: 50), - scale: 2, - insets: .make(top: 5, left: 10, bottom: 15, right: 20)).bounds, - CGRect(x: -20, y: -10, width: 200, height: 100) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 200, height: 200), - defaultSize: CGSize(width: 100, height: 100), - scale: 1, - insets: .make(top: 10, left: 10, bottom: 10, right: 10)).bounds, - CGRect(x: -25, y: -25, width: 250, height: 250) - ) - XCTAssertEqual( - SVG.makeBounds(size: CGSize(width: 200, height: 200), - defaultSize: CGSize(width: 100, height: 100), - scale: 2, - insets: .make(top: 10, left: 10, bottom: 10, right: 10)).bounds, - CGRect(x: -50, y: -50, width: 500, height: 500) - ) - } -} - -private extension SVG.Insets { - static func make(top: CGFloat = 0, - left: CGFloat = 0, - bottom: CGFloat = 0, - right: CGFloat = 0) -> Self { - Self(top: top, left: left, bottom: bottom, right: right) - } -} - -#endif diff --git a/SwiftDrawTests/ImageTests.swift b/SwiftDrawTests/ImageTests.swift deleted file mode 100644 index 7145d020..00000000 --- a/SwiftDrawTests/ImageTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ScannerTests.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 XCTest -@testable import SwiftDraw - -final class ImageTests: XCTestCase { - - func testValidSVGLoads() { - XCTAssertNotNil(SVG(named: "lines.svg", in: .test)) - } - - func testInvalidSVGReturnsNil() { - XCTAssertNil(SVG(named: "invalids.svg", in: .test)) - } - - func testMissingSVGReturnsNil() { - XCTAssertNil(SVG(named: "missing.svg", in: .test)) - } - -#if canImport(CoreGraphics) - func testImageRasterizes() { - let image = SVG.makeLines() - let rendered = image.rasterize(scale: 1) - XCTAssertEqual(rendered.size, image.size) - XCTAssertNoThrow(try image.pngData()) - XCTAssertNoThrow(try image.jpegData()) - XCTAssertNoThrow(try image.pdfData()) - } - - func testImageRasterizeAndScales() { - let image = SVG.makeLines() - let doubleSize = CGSize(width: 200, height: 200) - let rendered = image.rasterize(with: doubleSize, scale: 1) - XCTAssertEqual(rendered.size, doubleSize) - XCTAssertNoThrow(try image.pngData(size: doubleSize)) - XCTAssertNoThrow(try image.jpegData(size: doubleSize)) - } - - func testShapesImageRasterizes() throws { - let image = try XCTUnwrap(SVG(named: "shapes.svg", in: .test)) - XCTAssertNoThrow(try image.pngData()) - XCTAssertNoThrow(try image.jpegData()) - XCTAssertNoThrow(try image.pdfData()) - } -#endif - -} - -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) - } -} diff --git a/SwiftDrawTests/NSImage+ImageTests.swift b/SwiftDrawTests/NSImage+ImageTests.swift deleted file mode 100644 index bd1f4aa8..00000000 --- a/SwiftDrawTests/NSImage+ImageTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// NSImage+ImageTests.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 XCTest -@testable import SwiftDraw -#if canImport(AppKit) && !targetEnvironment(macCatalyst) - -final class NSImageTests: XCTestCase { - - func testImageLoads() { - let image = NSImage(svgNamed: "lines.svg", in: .test) - XCTAssertNotNil(image) - } - - func testMissingImageDoesNotLoad() { - let image = NSImage(svgNamed: "missing.svg", in: .test) - XCTAssertNil(image) - } - - func testNSImageDraws() { - let canvas = NSBitmapImageRep(pixelsWide: 2, pixelsHigh: 2) - - canvas.lockFocus() - NSImage(svgNamed: "lines.svg", in: .test)?.draw(in: NSRect(x: 0, y: 0, width: 2, height: 2)) - canvas.unlockFocus() - } - - func testImageDraws() { - 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() - - XCTAssertEqual(canvas.colorAt(x: 0, y: 0), NSColor(deviceRed: 1.0, green: 0, blue: 0, alpha: 1.0)) - XCTAssertEqual(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)) - svg.childElements[1].attributes.fill = .color(DOM.Color.rgbi(0, 0, 255)) - return SVG(dom: svg, options: .default) - } -} - -#endif diff --git a/SwiftDrawTests/Parser.AttributesTests.swift b/SwiftDrawTests/Parser.AttributesTests.swift deleted file mode 100644 index d5c28e02..00000000 --- a/SwiftDrawTests/Parser.AttributesTests.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// 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. -// - -import XCTest -@testable import SwiftDraw - -final class AttributeParserTests: XCTestCase { - - // func testParser() { - // let parser = XMLParser.Att - // let att = ["x": "20"] - // XCTAssertThrowsError(try att.parse("x", { _ in throw XMLParser.Error.invalid })) - // } - - func testParserOrder() { - 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 - XCTAssertEqual(try att.parseColor("fill"), .keyword(.green)) - XCTAssertThrowsError(try att.parseFloat("x")) - - //missing throws error - XCTAssertThrowsError(try att.parseFloat("other")) - //missing returns optional - XCTAssertNil(try att.parseFloat("other") as DOM.Float?) - - //fall through to element - XCTAssertEqual(try att.parseFloat("y"), 20) - - //SkipInvalidAttributes - let another = XMLParser.Attributes(parser: parser, - options: [.skipInvalidAttributes], - element: att.element, - style: att.style) - - - XCTAssertEqual(try another.parseColor("fill"), .keyword(.green)) - XCTAssertEqual(try another.parseFloat("x"), 10) - XCTAssertEqual(try another.parseFloat("y"), 20) - - //missing throws error - XCTAssertThrowsError(try another.parseFloat("other")) - //missing returns optional - XCTAssertNil(try another.parseFloat("other") as DOM.Float?) - //invalid returns optional - XCTAssertNil(try another.parseColor("x") as DOM.Color?) - } - - func testDictionary() { - let att = ["x": "20", "y": "30", "fill": "#a0a0a0", "display": "none", "some": "random"] - - XCTAssertEqual(try att.parseCoordinate("x"), 20.0) - XCTAssertEqual(try att.parseCoordinate("y"), 30.0) - XCTAssertEqual(try att.parseColor("fill"), .hex(160, 160, 160)) - XCTAssertEqual(try att.parseRaw("display"), DOM.DisplayMode.none) - - XCTAssertThrowsError(try att.parseFloat("other")) - XCTAssertThrowsError(try att.parseColor("some")) - - //missing returns optional - XCTAssertNil(try att.parseFloat("other") as DOM.Float?) - } - - func testParseString() { - let att = ["x": "20", "some": "random"] - XCTAssertEqual(try att.parseString("x"), "20") - XCTAssertThrowsError(try att.parseString("missing")) - } - - func testParseFloat() { - let att = ["x": "20", "some": "random"] - XCTAssertEqual(try att.parseFloat("x"), 20.0) - XCTAssertNil(try att.parseFloat("missing")) - XCTAssertThrowsError(try att.parseFloat("some")) - } - - func testParseFloats() { - let att = ["x": "20 30 40", "some": "random"] - XCTAssertEqual(try att.parseFloats("x"), [20.0, 30.0, 40.0]) - XCTAssertThrowsError(try att.parseFloats("some")) - } - - func testParsePoints() { - let att = ["x": "20 30 40 50", "some": "random"] - XCTAssertEqual(try att.parsePoints("x"), [DOM.Point(20, 30), DOM.Point(40, 50)]) - XCTAssertNil(try att.parsePoints("missing")) - XCTAssertThrowsError(try att.parsePoints("some")) - XCTAssertThrowsError(try att.parsePoints("some") as [DOM.Point]?) - } - - func testParseLength() { - let att = ["x": "20", "y": "aa"] - XCTAssertEqual(try att.parseLength("x"), 20) - XCTAssertNil(try att.parseLength("missing")) - XCTAssertThrowsError(try att.parseLength("y")) - XCTAssertThrowsError(try att.parseLength("y") as DOM.Length?) - } - - func testParseBool() { - let att = ["x": "true", "y": "5"] - XCTAssertEqual(try att.parseBool("x"), true) - XCTAssertNil(try att.parseBool("missing")) - XCTAssertThrowsError(try att.parseBool("y")) - XCTAssertThrowsError(try att.parseBool("y") as Bool?) - } - - func testParseURL() { - let att = ["clip": "http://www.test.com", "mask": "20 twenty"] - XCTAssertEqual(try att.parseUrl("clip"), URL(string: "http://www.test.com")) - XCTAssertNil(try att.parseUrl("missing")) - XCTAssertThrowsError(try att.parseUrl(" ")) - } - - func testParseURLSelector() { - let att = ["clip": "url(#shape)", "mask": "aa"] - XCTAssertEqual(try att.parseUrlSelector("clip"), URL(string: "#shape")) - XCTAssertNil(try att.parseUrlSelector("missing")) - XCTAssertThrowsError(try att.parseUrlSelector("mask")) - } - // - // 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] { - -} - diff --git a/SwiftDrawTests/Parser.GraphicAttributeTests.swift b/SwiftDrawTests/Parser.GraphicAttributeTests.swift deleted file mode 100644 index a6171289..00000000 --- a/SwiftDrawTests/Parser.GraphicAttributeTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// 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. -// - - -import XCTest -@testable import SwiftDraw - -final class ParserGraphicAttributeTests: XCTestCase { - - func testPresentationAttributes() throws { - var parsed = try XMLParser().parsePresentationAttributes([:]) - XCTAssertNil(parsed.opacity) - XCTAssertNil(parsed.display) - XCTAssertNil(parsed.stroke) - XCTAssertNil(parsed.strokeWidth) - XCTAssertNil(parsed.strokeOpacity) - XCTAssertNil(parsed.strokeLineCap) - XCTAssertNil(parsed.strokeLineJoin) - XCTAssertNil(parsed.strokeDashArray) - XCTAssertNil(parsed.fill) - XCTAssertNil(parsed.fillOpacity) - XCTAssertNil(parsed.fillRule) - XCTAssertNil(parsed.transform) - XCTAssertNil(parsed.clipPath) - XCTAssertNil(parsed.mask) - - 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) - - XCTAssertEqual(parsed.opacity, 0.95) - XCTAssertEqual(parsed.display!, .none) - XCTAssertEqual(parsed.stroke, .color(.keyword(.green))) - XCTAssertEqual(parsed.strokeWidth, 15) - XCTAssertEqual(parsed.strokeOpacity, 0.756) - XCTAssertEqual(parsed.strokeLineCap, .butt) - XCTAssertEqual(parsed.strokeLineJoin, .miter) - XCTAssertEqual(parsed.strokeDashArray!, [1, 5, 10]) - XCTAssertEqual(parsed.fill, .color(.keyword(.purple))) - XCTAssertEqual(parsed.fillOpacity, 0.25) - XCTAssertEqual(parsed.fillRule, .evenodd) - XCTAssertEqual(parsed.transform!, [.scale(sx: 15, sy: 15)]) - XCTAssertEqual(parsed.clipPath?.fragment, "circlePath") - XCTAssertEqual(parsed.mask?.fragment, "fancyMask") - XCTAssertEqual(parsed.filter?.fragment, "blur") - } - - func testCircle() 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 - XCTAssertNotNil(circle) - XCTAssertEqual(circle?.style.clipPath?.fragment, "cp1") - XCTAssertEqual(circle?.style.fill, .color(.keyword(.black))) - XCTAssertEqual(circle?.style.strokeWidth, 2) - } - - func testDisplayMode() { - let parser = XMLParser.ValueParser() - - XCTAssertEqual(try parser.parseRaw("none"), DOM.DisplayMode.none) - XCTAssertEqual(try parser.parseRaw(" none "), DOM.DisplayMode.none) - XCTAssertThrowsError(try parser.parseRaw("ds") as DOM.DisplayMode ) - } - - func testStrokeLineCap() { - let parser = XMLParser.ValueParser() - - XCTAssertEqual(try parser.parseRaw("butt"), DOM.LineCap.butt) - XCTAssertEqual(try parser.parseRaw(" round"), DOM.LineCap.round) - XCTAssertThrowsError(try parser.parseRaw("squdare") as DOM.LineCap) - } - - func testStrokeLineJoin() { - let parser = XMLParser.ValueParser() - - XCTAssertEqual(try parser.parseRaw("miter"), DOM.LineJoin.miter) - XCTAssertEqual(try parser.parseRaw(" bevel"), DOM.LineJoin.bevel) - XCTAssertThrowsError(try parser.parseRaw("ds") as DOM.LineJoin) - } -} - diff --git a/SwiftDrawTests/Parser.SVGTests.swift b/SwiftDrawTests/Parser.SVGTests.swift deleted file mode 100644 index 142f3680..00000000 --- a/SwiftDrawTests/Parser.SVGTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// 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. -// - - -import XCTest -@testable import SwiftDraw - -final class SVGTests: XCTestCase { - - func testSVG() throws { - let node = XML.Element(name: "svg", attributes: ["width": "100", "height": "200"]) - let parser = XMLParser() - - var parsed = try parser.parseSVG(node) - let expected = DOM.SVG(width: 100, height: 200) - XCTAssertEqual(parsed, expected) - - expected.viewBox = DOM.SVG.ViewBox(x: 10, y: 20, width: 100, height: 200) - XCTAssertNotEqual(parsed, expected) - - node.attributes["viewBox"] = "10 20 100 200" - parsed = try parser.parseSVG(node) - XCTAssertEqual(parsed, expected) - - expected.attributes.fill = .color(.keyword(.red)) - XCTAssertNotEqual(parsed, expected) - } - - func testParseSVGInvalidNode() { - let node = XML.Element(name: "svg2", attributes: ["width": "100", "height": "200"]) - XCTAssertThrowsError(try XMLParser().parseSVG(node)) - } - - func testParseSVGMissingHeightInvalidNode() { - let node = XML.Element(name: "svg", attributes: ["width": "100"]) - XCTAssertThrowsError(try XMLParser().parseSVG(node)) - } - - func testParseSVGMissingWidthInvalidNode() { - let node = XML.Element(name: "svg", attributes: ["height": "100"]) - XCTAssertThrowsError(try XMLParser().parseSVG(node)) - } - - func testViewBox() { - let parsed = (try? XMLParser().parseViewBox(" 10\t20 300.0 5e2")!)! - XCTAssertEqual(parsed.x, 10) - XCTAssertEqual(parsed.y, 20) - XCTAssertEqual(parsed.width, 300) - XCTAssertEqual(parsed.height, 500) - - XCTAssertNotNil(try! XMLParser().parseViewBox("10 10 10 10")) - XCTAssertThrowsError(try XMLParser().parseViewBox("10 10 10 10a")) - XCTAssertThrowsError(try XMLParser().parseViewBox(" 10\t20 300")) - XCTAssertThrowsError(try XMLParser().parseViewBox("10 10 10 10a")) - } - - func testClipPath() throws { - - let node = XML.Element(name: "clipPath", attributes: ["id": "hello"]) - - var parsed = try XMLParser().parseClipPath(node) - XCTAssertEqual(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) - XCTAssertEqual(parsed.id, "hello") - XCTAssertEqual(parsed.childElements.count, 2) - } - - func testParseDefs() 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 SwiftDraw.XMLParser().parseSVGDefs(svg).elements - XCTAssertEqual(elements.count, 2) - } - -} diff --git a/SwiftDrawTests/Parser.XML.ColorTests.swift b/SwiftDrawTests/Parser.XML.ColorTests.swift deleted file mode 100644 index f258e7fc..00000000 --- a/SwiftDrawTests/Parser.XML.ColorTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// 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. -// - -import XCTest -@testable import SwiftDraw - -final class ParserColorTests: XCTestCase { - - func testColorNone() { - XCTAssertEqual(try XMLParser().parseColor("none"), .none) - XCTAssertEqual(try XMLParser().parseColor(" none"), .none) - XCTAssertEqual(try XMLParser().parseColor("\t none \t"), .none) - } - - func testColorTransparent() { - XCTAssertEqual(try XMLParser().parseColor("transparent"), .none) - XCTAssertEqual(try XMLParser().parseColor(" transparent"), .none) - XCTAssertEqual(try XMLParser().parseColor("\t transparent \t"), .none) - } - - func testColorCurrent() { - XCTAssertEqual(try XMLParser().parseColor("currentColor"), .currentColor) - XCTAssertEqual(try XMLParser().parseColor(" currentColor"), .currentColor) - XCTAssertEqual(try XMLParser().parseColor("\t currentColor \t"), .currentColor) - } - - func testColorKeyword() { - XCTAssertEqual(try XMLParser().parseColor("aliceblue"), .keyword(.aliceblue)) - XCTAssertEqual(try XMLParser().parseColor("wheat"), .keyword(.wheat)) - XCTAssertEqual(try XMLParser().parseColor("cornflowerblue"), .keyword(.cornflowerblue)) - XCTAssertEqual(try XMLParser().parseColor(" magenta"), .keyword(.magenta)) - XCTAssertEqual(try XMLParser().parseColor("black "), .keyword(.black)) - XCTAssertEqual(try XMLParser().parseColor("\t red \t"), .keyword(.red)) - } - - func testColorRGBi() { - // integer 0-255 - XCTAssertEqual(try XMLParser().parseColor("rgb(0,1,2)"), .rgbi(0, 1, 2)) - XCTAssertEqual(try XMLParser().parseColor(" rgb( 0 , 1 , 2) "), .rgbi(0, 1, 2)) - XCTAssertEqual(try XMLParser().parseColor("rgb(255,100,78)"), .rgbi(255, 100, 78)) - } - - func testColorRGBf() { - // percentage 0-100% - XCTAssertEqual(try XMLParser().parseColor("rgb(0,1%,99%)"), .rgbf(0.0, 0.01, 0.99)) - XCTAssertEqual(try XMLParser().parseColor("rgb( 0%, 52% , 100%) "), .rgbf(0.0, 0.52, 1.0)) - XCTAssertEqual(try XMLParser().parseColor("rgb(75%,25%,7%)"), .rgbf(0.75, 0.25, 0.07)) - } - - func testColorHex() { - XCTAssertEqual(try XMLParser().parseColor("#a06"), .hex(170, 0, 102)) - XCTAssertEqual(try XMLParser().parseColor("#123456"), .hex(18, 52, 86)) - XCTAssertEqual(try XMLParser().parseColor("#FF11DD"), .hex(255, 17, 221)) - XCTAssertThrowsError(try XMLParser().parseColor("#invalid")) - } - - func testColorP3() { - // percentage 0-100% - XCTAssertEqual(try XMLParser().parseColor("color(display-p3 0 0.5 0.9)"), .p3(0, 0.5, 0.9)) - XCTAssertEqual(try XMLParser().parseColor("color(display-p3 0.1, 0.2, 0)"), .p3(0.1, 0.2, 0)) - XCTAssertEqual(try XMLParser().parseColor("color(display-p3 1,0.3,0.5)"), .p3(1, 0.3, 0.5)) - } -} - -private extension SwiftDraw.XMLParser { - - func parseColor(_ value: String) throws -> DOM.Color { - return try parseFill(value).getColor() - } -} diff --git a/SwiftDrawTests/Parser.XML.ElementTests.swift b/SwiftDrawTests/Parser.XML.ElementTests.swift deleted file mode 100644 index c35f8337..00000000 --- a/SwiftDrawTests/Parser.XML.ElementTests.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// 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. -// - -import XCTest -@testable import SwiftDraw - -final class XMLParserElementTests: XCTestCase { - - func testLine() { - let node = ["x1": "0", - "y1": "10", - "x2": "50", - "y2": "60"] - - let parsed = try? XMLParser().parseLine(node) - XCTAssertEqual(DOM.Line(x1: 0, y1: 10, x2: 50, y2: 60), parsed) - } - - func testCircle() { - let node = ["cx": "0", - "cy": "10", - "r": "20"] - - let parsed = try? XMLParser().parseCircle(node) - XCTAssertEqual(DOM.Circle(cx: 0, cy: 10, r: 20), parsed) - } - - func testEllipse() { - let node = ["cx": "0", - "cy": "10", - "rx": "20", - "ry": "30"] - - let parsed = try? XMLParser().parseEllipse(node) - XCTAssertEqual(DOM.Ellipse(cx: 0, cy: 10, rx: 20, ry: 30), parsed) - } - - func testRect() { - var node = ["x": "0", - "y": "10", - "width": "20", - "height": "30"] - - let rect = DOM.Rect(x: 0, y: 10, width: 20, height: 30) - XCTAssertEqual(rect, try? XMLParser().parseRect(node)) - - node["rx"] = "3" - node["ry"] = "2" - rect.rx = 3 - rect.ry = 2 - XCTAssertEqual(rect, try? XMLParser().parseRect(node)) - } - - func testPolyline() throws { - let node = ["points": "0,1 2 3; 4;5;6;7;8 9"] - - let parsed = try? XMLParser().parsePolyline(node) - XCTAssertEqual(DOM.Polyline(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), parsed) - } - - func testPolygon() { - // - let att = ["points": "0, 1,2,3;4;5;6;7;8 9"] - let parsed = try? XMLParser().parsePolygon(att) - XCTAssertEqual(DOM.Polygon(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), parsed) - } - - func testPolygonFillRule() { - let att = ["points": "0,1,2,3;4;5;6;7;8 9"] - XCTAssertNil((try! XMLParser().parsePolygon(att)).attributes.fillRule) - - let node = XML.Element(name: "polygon") - node.attributes["points"] = "0,1,2,3" - - node.attributes["fill-rule"] = "nonzero" - XCTAssertEqual(try XMLParser().parseGraphicsElement(node)!.attributes.fillRule, .nonzero) - - node.attributes["fill-rule"] = "evenodd" - XCTAssertEqual(try XMLParser().parseGraphicsElement(node)!.attributes.fillRule, .evenodd) - - node.attributes["fill-rule"] = "asdf" - XCTAssertThrowsError(try XMLParser().parseGraphicsElement(node)!.attributes.fillRule) - } - - func testElementParserSkipsErrors() { - let error = XMLParser().parseError(for: XMLParser.Error.invalid, - parsing: XML.Element(name: "polygon"), - with: [.skipInvalidElements]) - - XCTAssertNil(error) - } - - func testElementParserErrorsPreserveLineNumbers() { - 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): - XCTAssertEqual(line, 100) - XCTAssertEqual(column, 50) - default: - XCTFail("not forwarderd") - } - } - - func testElementParserErrorsPreserveLineNumbersFromElement() { - 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): - XCTAssertEqual(line, 100) - XCTAssertEqual(column, 50) - default: - XCTFail("not forwarderd") - } - } -} diff --git a/SwiftDrawTests/Parser.XML.ImageTests.swift b/SwiftDrawTests/Parser.XML.ImageTests.swift deleted file mode 100644 index 3ec49073..00000000 --- a/SwiftDrawTests/Parser.XML.ImageTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// 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 - -import XCTest -@testable import SwiftDraw -#if canImport(CoreGraphics) -import CoreGraphics - -extension CGImage { - static func from(data: Data) -> CGImage? { - #if os(iOS) - return UIImage(data: data)?.cgImage - #elseif os(macOS) - 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 - } -} - -final class ParserXMLImageTests: XCTestCase { - - func testImage() throws { - var node = ["xlink:href": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="] - node["width"] = "10" - node["height"] = "10" - - let image = try XMLParser().parseImage(node) - - XCTAssertTrue(image.href.isDataURL) - - let decode = image.href.decodedData! - - XCTAssertEqual(decode.mimeType, "image/png") - - let cgImage = CGImage.from(data: decode.data) - - XCTAssertNotNil(cgImage) - XCTAssertEqual(cgImage?.width, 5) - XCTAssertEqual(cgImage?.height, 5) - } - - func testImageLineBreaks() 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) - - XCTAssertTrue(image.href.isDataURL) - - let decode = image.href.decodedData! - - XCTAssertEqual(decode.mimeType, "image/png") - - let cgImage = CGImage.from(data: decode.data) - - XCTAssertNotNil(cgImage) - XCTAssertEqual(cgImage?.width, 5) - XCTAssertEqual(cgImage?.height, 5) - } -} - -#endif diff --git a/SwiftDrawTests/Parser.XML.PathTests.swift b/SwiftDrawTests/Parser.XML.PathTests.swift deleted file mode 100644 index 5714644a..00000000 --- a/SwiftDrawTests/Parser.XML.PathTests.swift +++ /dev/null @@ -1,272 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -private typealias Coordinate = DOM.Coordinate -private typealias Segment = DOM.Path.Segment -private typealias CoordinateSpace = DOM.Path.Segment.CoordinateSpace - -final class ParserXMLPathTests: XCTestCase { - - func testScanBool() { - let scanner = XMLParser.PathScanner(string: "true FALSE 0 1") - - XCTAssertTrue(try scanner.scanBool()) - XCTAssertFalse(try scanner.scanBool()) - XCTAssertFalse(try scanner.scanBool()) - XCTAssertTrue(try scanner.scanBool()) - XCTAssertThrowsError(try scanner.scanBool()) - } - - func testScanCoordinate() { - let scanner = XMLParser.PathScanner(string: "10 20.0") - - XCTAssertEqual(try scanner.scanCoordinate(), 10.0) - XCTAssertEqual(try scanner.scanCoordinate(), 20.0) - XCTAssertThrowsError(try scanner.scanCoordinate()) - } - - func testEquality() { - XCTAssertEqual(Segment.move(x: 10, y: 20, space: .relative), - move(10, 20, .relative)) - - XCTAssertNotEqual(Segment.move(x: 20, y: 20, space: .absolute), - move(10, 20, .absolute)) - - XCTAssertNotEqual(Segment.move(x: 10, y: 20, space: .relative), - move(10, 20, .absolute)) - } - - func testMove() { - AssertSegmentEquals("M 10 20", move(10, 20, .absolute)) - AssertSegmentEquals("m 10 20", move(10, 20, .relative)) - AssertSegmentEquals("M10,20", move(10, 20, .absolute)) - AssertSegmentEquals("M10;20", move(10, 20, .absolute)) - AssertSegmentEquals("M 10; 20 ", move(10, 20, .absolute)) - AssertSegmentEquals("M10-20", move(10, -20, .absolute)) - - AssertSegmentsEquals("M10-20 5 1", [move(10, -20, .absolute), - line(5, 1, .absolute)]) - - AssertSegmentsEquals("m10-20 5 1", [move(10, -20, .relative), - line(5, 1, .relative)]) - } - - func testLine() { - AssertSegmentEquals("L 10 20", line(10, 20, .absolute)) - AssertSegmentEquals("l 10 20", line(10, 20, .relative)) - AssertSegmentEquals("L10,20", line(10, 20, .absolute)) - AssertSegmentEquals("L10;20", line(10, 20, .absolute)) - AssertSegmentEquals(" L 10;20 ", line(10, 20, .absolute)) - AssertSegmentEquals("L10-20 ", line(10, -20, .absolute)) - - AssertSegmentsEquals("L10-20 5 1", [line(10, -20, .absolute), - line(5, 1, .absolute)]) - } - - func testHorizontal() { - AssertSegmentEquals("H 10", horizontal(10, .absolute)) - AssertSegmentEquals("h 10", horizontal(10, .relative)) - AssertSegmentEquals("H10", horizontal(10, .absolute)) - AssertSegmentEquals("H10;", horizontal(10, .absolute)) - AssertSegmentEquals(" H10 ", horizontal(10, .absolute)) - - AssertSegmentsEquals("h10 5", [horizontal(10, .relative), - horizontal(5, .relative)]) - } - - func testVerical() { - AssertSegmentEquals("V 10", vertical(10, .absolute)) - AssertSegmentEquals("v 10", vertical(10, .relative)) - AssertSegmentEquals("V10", vertical(10, .absolute)) - AssertSegmentEquals("V10;", vertical(10, .absolute)) - AssertSegmentEquals(" V10 ", vertical(10, .absolute)) - } - -// func testCubic() { -// AssertSegmentEquals("C 10 20 30 40 50 60", cubic(10, 20, 30, 40, 50, 60, .absolute)) -// AssertSegmentEquals("c 10 20 30 40 50 60", cubic(10, 20, 30, 40, 50, 60, .relative)) -// AssertSegmentEquals("C10,20,30,40,50,60", cubic(10, 20, 30, 40, 50, 60, .absolute)) -// AssertSegmentEquals("C10;20;30;40;50;60", cubic(10, 20, 30, 40, 50, 60, .absolute)) -// AssertSegmentEquals(" C10; 20; 30 40; 50; 60", cubic(10, 20, 30, 40, 50, 60, .absolute)) -// } - - func testCubicSmooth() { - AssertSegmentEquals("S 10 20 50 60", cubicSmooth(10, 20, 50, 60, .absolute)) - AssertSegmentEquals("s 10 20 50 60", cubicSmooth(10, 20, 50, 60, .relative)) - AssertSegmentEquals("S10,20,50,60", cubicSmooth(10, 20, 50, 60, .absolute)) - AssertSegmentEquals("S10;20;50;60", cubicSmooth(10, 20, 50, 60, .absolute)) - AssertSegmentEquals(" S10; 20; 50; 60", cubicSmooth(10, 20, 50, 60, .absolute)) - } - - func testQuadratic() { - AssertSegmentEquals("Q 10 20 50 60", quadratic(10, 20, 50, 60, .absolute)) - AssertSegmentEquals("q 10 20 50 60", quadratic(10, 20, 50, 60, .relative)) - AssertSegmentEquals("Q10,20,50,60", quadratic(10, 20, 50, 60, .absolute)) - AssertSegmentEquals("Q10;20;50;60", quadratic(10, 20, 50, 60, .absolute)) - AssertSegmentEquals(" Q10; 20; 50; 60", quadratic(10, 20, 50, 60, .absolute)) - } - - func testQuadraticSmooth() { - AssertSegmentEquals("T 10 20", quadraticSmooth(10, 20, .absolute)) - AssertSegmentEquals("t 10 20", quadraticSmooth(10, 20, .relative)) - AssertSegmentEquals("T10,20", quadraticSmooth(10, 20, .absolute)) - AssertSegmentEquals("T10;20", quadraticSmooth(10, 20, .absolute)) - AssertSegmentEquals(" T10; 20;", quadraticSmooth(10, 20, .absolute)) - } - - func testArc() { - // -// AssertSegmentEquals("A 10 20 30 1 0 40 50", arc(10, 20, 30, true, false, 40, 50, .absolute)) -// AssertSegmentEquals("a 10 20 30 1 0 40 50", arc(10, 20, 30, true, false, 40, 50, .relative)) -// AssertSegmentEquals("A10,20,30,1,0,40,50", arc(10, 20, 30, true, false, 40, 50, .absolute)) -// AssertSegmentEquals("A10;20;30;1;0;40;50", arc(10, 20, 30, true, false, 40, 50, .absolute)) - AssertSegmentEquals(" A10; 20; 30; 1 0;40 50", arc(10, 20, 30, true, false, 40, 50, .absolute)) - } - - func testClose() { - AssertSegmentEquals("Z", .close) - AssertSegmentEquals("z", .close) - AssertSegmentEquals(" z ", .close) - } - - func testPath() { - let node = ["d": "M 10 10 h 10 v 10 h -10 v -10"] - let parser = XMLParser() - - let path = try! parser.parsePath(node) - - XCTAssertEqual(path.segments.count, 5) - - XCTAssertEqual(path.segments[0], .move(x: 10, y: 10, space: .absolute)) - XCTAssertEqual(path.segments[1], .horizontal(x: 10, space: .relative)) - XCTAssertEqual(path.segments[2], .vertical(y: 10, space: .relative)) - XCTAssertEqual(path.segments[3], .horizontal(x: -10, space: .relative)) - XCTAssertEqual(path.segments[4], .vertical(y: -10, space: .relative)) - } - - func testPathLineBreak() { - 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 = XMLParser() - - let path = try? parser.parsePath(node) - - XCTAssertEqual(path?.segments.count, 4) - } - - func testPathLong() throws { - - let node = ["d": "m10,2h-30v-40zm50,60"] - let parser = XMLParser() - - let path = try! parser.parsePath(node) - - XCTAssertEqual(path.segments.count, 5) - - XCTAssertEqual(path.segments[0], .move(x: 10, y: 2.0, space: .relative)) - XCTAssertEqual(path.segments[1], .horizontal(x: -30, space: .relative)) - XCTAssertEqual(path.segments[2], .vertical(y: -40, space: .relative)) - XCTAssertEqual(path.segments[3], .close) - XCTAssertEqual(path.segments[4], .move(x: 50, y: 60, space: .relative)) - } -} - -private func AssertSegmentEquals(_ text: String, _ expected: Segment, file: StaticString = #file, line: UInt = #line) { - let parsed = try? XMLParser().parsePathSegments(text) - XCTAssertEqual(parsed?.count, 1) - XCTAssertEqual(parsed![0], expected, file: file, line: line) -} - -private func AssertSegmentsEquals(_ text: String, _ expected: [Segment], file: StaticString = #file, line: UInt = #line) { - guard let parsed = try? XMLParser().parsePathSegments(text) else { - XCTFail("could not parse segments", file: file, line: line) - return - } - XCTAssertEqual(parsed, expected, file: file, line: line) -} - - -// 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) -} - - - -extension Segment: Equatable { - public static func ==(lhs: DOM.Path.Segment, rhs: DOM.Path.Segment) -> Bool { - let toString: (Any) -> String = { var text = ""; dump($0, to: &text); return text } - return toString(lhs) == toString(rhs) - } -} diff --git a/SwiftDrawTests/Parser.XML.PatternTests.swift b/SwiftDrawTests/Parser.XML.PatternTests.swift deleted file mode 100644 index 3e8305c0..00000000 --- a/SwiftDrawTests/Parser.XML.PatternTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -private typealias Coordinate = DOM.Coordinate - -final class ParserXMLPatternTests: XCTestCase { - - func testPattern() throws { - let pattern = try XMLParser().parsePattern(["id": "p1", "width": "10", "height": "20"]) - - XCTAssertEqual(pattern.id, "p1") - XCTAssertEqual(pattern.width, 10) - XCTAssertEqual(pattern.height, 20) - - XCTAssertThrowsError(try XMLParser().parsePattern(["width": "10", "height": "20"])) - XCTAssertThrowsError(try XMLParser().parsePattern(["id": "p1", "height": "20"])) - XCTAssertThrowsError(try XMLParser().parsePattern(["id": "p1", "width": "10"])) - } - - func testPatternUnits() throws { - var node = ["id": "p1", "width": "10", "height": "20"] - - var pattern = try XMLParser().parsePattern(node) - XCTAssertNil(pattern.patternUnits) - - node["patternUnits"] = "userSpaceOnUse" - pattern = try XMLParser().parsePattern(node) - XCTAssertEqual(pattern.patternUnits, .userSpaceOnUse) - - node["patternUnits"] = "objectBoundingBox" - pattern = try XMLParser().parsePattern(node) - XCTAssertEqual(pattern.patternUnits, .objectBoundingBox) - - node["patternUnits"] = "invalid" - XCTAssertThrowsError(try XMLParser().parsePattern(node)) - } - - func testPatternContentUnits() throws { - var node = ["id": "p1", "width": "10", "height": "20"] - - var pattern = try XMLParser().parsePattern(node) - XCTAssertNil(pattern.patternContentUnits) - - node["patternContentUnits"] = "userSpaceOnUse" - pattern = try XMLParser().parsePattern(node) - XCTAssertEqual(pattern.patternContentUnits, .userSpaceOnUse) - - node["patternContentUnits"] = "objectBoundingBox" - pattern = try XMLParser().parsePattern(node) - XCTAssertEqual(pattern.patternContentUnits, .objectBoundingBox) - - node["patternContentUnits"] = "invalid" - XCTAssertThrowsError(try XMLParser().parsePattern(node)) - } - - #if XCODE - func testParseFile() throws { - - let dom = try DOM.SVG.parse(fileNamed: "pattern.svg") - - XCTAssertEqual(dom.defs.patterns.count, 3) - XCTAssertNotNil(dom.defs.patterns.first(where: { $0.id == "checkerboard" })) - XCTAssertNotNil(dom.defs.patterns.first(where: { $0.id == "pattern1" })) - XCTAssertNotNil(dom.defs.patterns.first(where: { $0.id == "pattern2" })) - - XCTAssertEqual(dom.childElements.count, 3) - // XCTAssertNotNil(dom.childElements[0].fill) - // XCTAssertNotNil(dom.childElements[1].fill) - // XCTAssertNotNil(dom.childElements[2].fill) - } - #endif -} diff --git a/SwiftDrawTests/Parser.XML.StyleSheetTests.swift b/SwiftDrawTests/Parser.XML.StyleSheetTests.swift deleted file mode 100644 index a27bcbee..00000000 --- a/SwiftDrawTests/Parser.XML.StyleSheetTests.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class ParserXMLStyleSheetTests: XCTestCase { - - func testParsesStyleSheetsSelectors() throws { - let dom = try DOM.SVG.parse(fileNamed: "stylesheet.svg") - - XCTAssertEqual( - Set(dom.styles.flatMap(\.attributes.keys)), - [.class("s"), - .class("b"), - .element("rect"), - .class("o"), - .class("g"), - .element("circle"), - .element("g"), - .id("a")] - ) - } - - func testParsesSelectors() 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 */ - """ - ) - - XCTAssertEqual( - entries, - [.class("s"): ["stroke": "darkgray", "stroke-width": "5", "fill-opacity": "0.3"], - .class("b"): ["fill": "blue"], - .element("rect"): ["fill": "pink"]] - ) - } - - func testParsesStyleSheet() 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 - - XCTAssertEqual(sheet[.class("s")]?.stroke, .color(.keyword(.darkgray))) - XCTAssertEqual(sheet[.class("s")]?.strokeWidth, 5) - XCTAssertEqual(sheet[.class("s")]?.fillOpacity, 0.3) - XCTAssertEqual(sheet[.class("b")]?.fill, .color(.keyword(.blue))) - XCTAssertEqual(sheet[.element("rect")]?.fill, .color(.keyword(.pink))) - } -} diff --git a/SwiftDrawTests/Parser.XML.TransformTests.swift b/SwiftDrawTests/Parser.XML.TransformTests.swift deleted file mode 100644 index 56f09ad9..00000000 --- a/SwiftDrawTests/Parser.XML.TransformTests.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class ParserTransformTests: XCTestCase { - - func testMatrix() { - XCTAssertEqual(try XMLParser().parseTransform("matrix(0 1 2 3 4 5)"), - [.matrix(a: 0, b: 1, c: 2, d: 3, e: 4, f: 5)]) - XCTAssertEqual(try XMLParser().parseTransform("matrix(0,1,2,3,4,5)"), - [.matrix(a: 0, b: 1, c: 2, d: 3, e: 4, f: 5)]) - XCTAssertEqual(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)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("matrix(0 1 a b 4 5)")) - XCTAssertThrowsError(try XMLParser().parseTransform("matrix(0 1 2)")) - XCTAssertThrowsError(try XMLParser().parseTransform("matrix(0 1 2 3 4 5")) - XCTAssertThrowsError(try XMLParser().parseTransform("matrix 0 1 2 3 4 5)")) - } - - func testTranslate() { - XCTAssertEqual(try XMLParser().parseTransform("translate(5)"), - [.translate(tx: 5, ty: 0)]) - XCTAssertEqual(try XMLParser().parseTransform("translate(5, 6)"), - [.translate(tx: 5, ty: 6)]) - XCTAssertEqual(try XMLParser().parseTransform("translate(5 6)"), - [.translate(tx: 5, ty: 6)]) - XCTAssertEqual(try XMLParser().parseTransform("translate(1.3, 4.5)"), - [.translate(tx: 1.3, ty: 4.5)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("translate(5 a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("translate(0 1 2)")) - XCTAssertThrowsError(try XMLParser().parseTransform("translate(0 1")) - XCTAssertThrowsError(try XMLParser().parseTransform("translate 0 1)")) - } - - func testScale() { - XCTAssertEqual(try XMLParser().parseTransform("scale(5)"), - [.scale(sx: 5, sy: 5)]) - XCTAssertEqual(try XMLParser().parseTransform("scale(5, 6)"), - [.scale(sx: 5, sy: 6)]) - XCTAssertEqual(try XMLParser().parseTransform("scale(5 6)"), - [.scale(sx: 5, sy: 6)]) - XCTAssertEqual(try XMLParser().parseTransform("scale(1.3, 4.5)"), - [.scale(sx: 1.3, sy: 4.5)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("scale(5 a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("scale(0 1 2)")) - XCTAssertThrowsError(try XMLParser().parseTransform("scale(0 1")) - XCTAssertThrowsError(try XMLParser().parseTransform("scale 0 1)")) - } - - func testRotate() { - XCTAssertEqual(try XMLParser().parseTransform("rotate(5)"), - [.rotate(angle: 5)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("rotate(a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate()")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate(1")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate 1)")) - } - - func testRotatePoint() { - XCTAssertEqual(try XMLParser().parseTransform("rotate(5, 10, 20)"), - [.rotatePoint(angle: 5, cx: 10, cy: 20)]) - XCTAssertEqual(try XMLParser().parseTransform("rotate(5 10 20)"), - [.rotatePoint(angle: 5, cx: 10, cy: 20)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("rotate(5 10 a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate(5 10)")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate(5 10 20")) - XCTAssertThrowsError(try XMLParser().parseTransform("rotate 5 10 20)")) - } - - func testSkewX() { - XCTAssertEqual(try XMLParser().parseTransform("skewX(5)"), - [.skewX(angle: 5)]) - XCTAssertEqual(try XMLParser().parseTransform("skewX(6.7)"), - [.skewX(angle: 6.7)]) - XCTAssertEqual(try XMLParser().parseTransform("skewX(0)"), - [.skewX(angle: 0)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("skewX(a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewX()")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewX(1")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewX 1)")) - } - - func testSkewY() { - XCTAssertEqual(try XMLParser().parseTransform("skewY(5)"), - [.skewY(angle: 5)]) - XCTAssertEqual(try XMLParser().parseTransform("skewY(6.7)"), - [.skewY(angle: 6.7)]) - XCTAssertEqual(try XMLParser().parseTransform("skewY(0)"), - [.skewY(angle: 0)]) - - XCTAssertThrowsError(try XMLParser().parseTransform("skewY(a)")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewY()")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewY(1")) - XCTAssertThrowsError(try XMLParser().parseTransform("skewY 1)")) - } - - func testTransform() { - XCTAssertEqual(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/SwiftDrawTests/ParserSVGImageTests.swift b/SwiftDrawTests/ParserSVGImageTests.swift deleted file mode 100644 index 50d2f193..00000000 --- a/SwiftDrawTests/ParserSVGImageTests.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw -import Foundation - -final class ParserSVGImageTests: XCTestCase { - - func testShapes() throws { - let svg = try DOM.SVG.parse(fileNamed: "shapes.svg", in: .test) - - XCTAssertEqual(svg.width, 500) - XCTAssertEqual(svg.height, 700) - XCTAssertEqual(svg.viewBox?.width, 500) - XCTAssertEqual(svg.viewBox?.height, 700) - XCTAssertEqual(svg.defs.clipPaths.count, 2) - XCTAssertEqual(svg.defs.linearGradients.count, 1) - XCTAssertEqual(svg.defs.radialGradients.count, 1) - XCTAssertNotNil(svg.defs.elements["star"]) - XCTAssertEqual(svg.defs.elements.count, 2) - - var c = svg.childElements.enumerated().makeIterator() - - XCTAssertTrue(c.next()!.element is DOM.Ellipse) - XCTAssertTrue(c.next()!.element is DOM.Group) - XCTAssertTrue(c.next()!.element is DOM.Circle) - XCTAssertTrue(c.next()!.element is DOM.Group) - XCTAssertTrue(c.next()!.element is DOM.Line) - XCTAssertTrue(c.next()!.element is DOM.Path) - XCTAssertTrue(c.next()!.element is DOM.Path) - XCTAssertTrue(c.next()!.element is DOM.Path) - XCTAssertTrue(c.next()!.element is DOM.Path) - XCTAssertTrue(c.next()!.element is DOM.Polyline) - XCTAssertTrue(c.next()!.element is DOM.Polyline) - XCTAssertTrue(c.next()!.element is DOM.Polygon) - XCTAssertTrue(c.next()!.element is DOM.Group) - XCTAssertTrue(c.next()!.element is DOM.Circle) - XCTAssertTrue(c.next()!.element is DOM.Switch) - XCTAssertTrue(c.next()!.element is DOM.Rect) - XCTAssertTrue(c.next()!.element is DOM.Text) - XCTAssertTrue(c.next()!.element is DOM.Text) - XCTAssertTrue(c.next()!.element is DOM.Line) - XCTAssertTrue(c.next()!.element is DOM.Use) - XCTAssertTrue(c.next()!.element is DOM.Use) - XCTAssertTrue(c.next()!.element is DOM.Rect) - XCTAssertNil(c.next()) - } - - func testStarry() 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 { - XCTFail("missing group") - return - } - - XCTAssertEqual(svg.width, 500) - XCTAssertEqual(svg.height, 500) - - XCTAssertEqual(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 - } - - XCTAssertEqual(counter["Path"], 9314) - XCTAssertEqual(counter["Polygon"], 9) - } - - func testQuad() throws { - let svg = try DOM.SVG.parse(fileNamed: "quad.svg", in: .test) - XCTAssertEqual(svg.width, 1000) - XCTAssertEqual(svg.height, 500) - } - - func testCurves() throws { - let svg = try DOM.SVG.parse(fileNamed: "curves.svg", in: .test) - XCTAssertEqual(svg.width, 550) - XCTAssertEqual(svg.height, 350) - } -} diff --git a/SwiftDrawTests/StyleTests.swift b/SwiftDrawTests/StyleTests.swift deleted file mode 100644 index a5795ce4..00000000 --- a/SwiftDrawTests/StyleTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class StyleTests: XCTestCase { - - func testStyle() { - XCTAssertEqual(try XMLParser().parseStyleAttributes("selector: hi;"), - ["selector": "hi"]) - XCTAssertEqual(try XMLParser().parseStyleAttributes("selector: hi"), - ["selector": "hi"]) - XCTAssertEqual(try XMLParser().parseStyleAttributes("selector: hi "), - ["selector": "hi"]) - XCTAssertEqual(try XMLParser().parseStyleAttributes(" trans-form : rotate(4)"), - ["trans-form": "rotate(4)"]) - - XCTAssertThrowsError(try XMLParser().parseStyleAttributes("selector")) - XCTAssertThrowsError(try XMLParser().parseStyleAttributes(": hmm")) - } - - func testStyles() 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) - - XCTAssertEqual(try att.parseCoordinate("x"), 20.0) - XCTAssertEqual(try att.parseCoordinate("y"), 5.0) - XCTAssertEqual(try att.parseColor("stroke-color"), .keyword(.black)) - XCTAssertEqual(try att.parseColor("fill"), .keyword(.red)) - } -} diff --git a/SwiftDrawTests/ValueParserTests.swift b/SwiftDrawTests/ValueParserTests.swift deleted file mode 100644 index 5139ccbe..00000000 --- a/SwiftDrawTests/ValueParserTests.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class ValueParserTests: XCTestCase { - - var parser = XMLParser.ValueParser() - - func testFloat() { - XCTAssertEqual(try parser.parseFloat("10"), 10) - XCTAssertEqual(try parser.parseFloat("10.0"), 10.0) - - XCTAssertThrowsError(try parser.parseFloat("")) - //XCTAssertThrowsError(try parser.parseFloat("10a")) - } - - func testFloats() { - XCTAssertEqual(try parser.parseFloats("10 20 30.5"), [10, 20, 30.5]) - XCTAssertEqual(try parser.parseFloats("10.0"), [10.0]) - XCTAssertEqual(try parser.parseFloats("5 10 1 5"), [5, 10, 1, 5]) - XCTAssertEqual(try parser.parseFloats(" 1, 2.5, 3.5 "), [1, 2.5, 3.5]) - XCTAssertEqual(try parser.parseFloats(" "), []) - XCTAssertEqual(try parser.parseFloats(""), []) - - //XCTAssertThrowsError(try parser.parseFloats("")) - //XCTAssertThrowsError(try parser.parseFloat("10a")) - } - - func testPercentage() { - XCTAssertEqual(try parser.parsePercentage("0"), 0) - XCTAssertEqual(try parser.parsePercentage("1"), 1) - XCTAssertEqual(try parser.parsePercentage("0.45"), 0.45) - XCTAssertEqual(try parser.parsePercentage("0.0%"), 0) - XCTAssertEqual(try parser.parsePercentage("100%"), 1) - XCTAssertEqual(try parser.parsePercentage("55%"), 0.55) - XCTAssertEqual(try parser.parsePercentage("10.25%"), 0.1025) - - XCTAssertThrowsError(try parser.parsePercentage("100")) - XCTAssertThrowsError(try parser.parsePercentage("asd")) - XCTAssertThrowsError(try parser.parsePercentage(" ")) - //XCTAssertThrowsError(try parser.parseFloat("10a")) - } - - func testCoordinate() { - XCTAssertEqual(try parser.parseCoordinate("0"), 0) - XCTAssertEqual(try parser.parseCoordinate("0.0"), 0) - XCTAssertEqual(try parser.parseCoordinate("100"), 100) - XCTAssertEqual(try parser.parseCoordinate("25.0"), 25.0) - XCTAssertEqual(try parser.parseCoordinate("-25.0"), -25.0) - - XCTAssertThrowsError(try parser.parseCoordinate("asd")) - XCTAssertThrowsError(try parser.parseCoordinate(" ")) - } - - func testLength() { - XCTAssertEqual(try parser.parseLength("0"), 0) - XCTAssertEqual(try parser.parseLength("100"), 100) - XCTAssertEqual(try parser.parseLength("25"), 25) - XCTAssertEqual(try parser.parseLength("1.3"), 1) //should error? - - XCTAssertThrowsError(try parser.parseLength("asd")) - XCTAssertThrowsError(try parser.parseLength(" ")) - XCTAssertThrowsError(try parser.parseLength("-25")) - } - - func testBool() { - XCTAssertEqual(try parser.parseBool("false"), false) - XCTAssertEqual(try parser.parseBool("FALSE"), false) - XCTAssertEqual(try parser.parseBool("true"), true) - XCTAssertEqual(try parser.parseBool("TRUE"), true) - XCTAssertEqual(try parser.parseBool("1"), true) - XCTAssertEqual(try parser.parseBool("0"), false) - - XCTAssertThrowsError(try parser.parseBool("asd")) - XCTAssertThrowsError(try parser.parseBool("yes")) - } - - func testFill() { - XCTAssertEqual(try parser.parseFill("none"), .color(.none)) - XCTAssertEqual(try parser.parseFill("black"), .color(.keyword(.black))) - XCTAssertEqual(try parser.parseFill("red"), .color(.keyword(.red))) - - XCTAssertEqual(try parser.parseFill("rgb(10,20,30)"), .color(.rgbi(10, 20, 30))) - XCTAssertEqual(try parser.parseFill("rgb(10%,20%,100%)"), .color(.rgbf(0.1, 0.2, 1.0))) - XCTAssertEqual(try parser.parseFill("#AAFF00"), .color(.hex(170, 255, 0))) - - XCTAssertEqual(try parser.parseFill("url(#test)"), .url(URL(string: "#test")!)) - - XCTAssertThrowsError(try parser.parseFill("Ns ")) - XCTAssertThrowsError(try parser.parseFill("d")) - XCTAssertThrowsError(try parser.parseFill("url()")) - //XCTAssertThrowsError(try parser.parseFill("url(asdf")) - } - - func testUrl() { - XCTAssertEqual(try parser.parseUrl("#testingId").fragment, "testingId") - XCTAssertEqual(try parser.parseUrl("http://www.google.com").host, "www.google.com") - - //XCTAssertThrowsError(try parser.parseUrl("www.google.com")) - //XCTAssertThrowsError(try parser.parseUrl("sd")) - } - - func testUrlSelector() { - XCTAssertEqual(try parser.parseUrlSelector("url(#testingId)").fragment, "testingId") - XCTAssertEqual(try parser.parseUrlSelector("url(http://www.google.com)").host, "www.google.com") - - XCTAssertThrowsError(try parser.parseUrlSelector("url(#testingId) other")) - } - - func testPoints() { - XCTAssertEqual(try parser.parsePoints("0 1 2 3"), [DOM.Point(0, 1), DOM.Point(2, 3)]) - XCTAssertEqual(try parser.parsePoints("0,1 2,3"), [DOM.Point(0, 1), DOM.Point(2, 3)]) - XCTAssertEqual(try parser.parsePoints("0 1.5 1e4 2.4"), [DOM.Point(0, 1.5), DOM.Point(1e4, 2.4)]) - // XCTAssertEqual(try parser.parsePoints("0 1 2 3 5.0 6.5"), [0, 1 ,2]) - } - - func testRaw() { - XCTAssertEqual(try parser.parseRaw("evenodd"), DOM.FillRule.evenodd) - XCTAssertEqual(try parser.parseRaw("round"), DOM.LineCap.round) - XCTAssertEqual(try parser.parseRaw("miter"), DOM.LineJoin.miter) - - XCTAssertThrowsError((try parser.parseRaw("sd")) as DOM.LineJoin) - } -} - - - diff --git a/SwiftDrawTests/XML.Parser.ScannerTests.swift b/SwiftDrawTests/XML.Parser.ScannerTests.swift deleted file mode 100644 index f1d31d8a..00000000 --- a/SwiftDrawTests/XML.Parser.ScannerTests.swift +++ /dev/null @@ -1,235 +0,0 @@ -// -// 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. -// - -import XCTest -@testable import SwiftDraw - -final class ScannerTests: XCTestCase { - - func testIsEOF() throws { - var scanner = XMLParser.Scanner(text: "Hi") - XCTAssertFalse(scanner.isEOF) - try scanner.scanString("Hi") - XCTAssertTrue(scanner.isEOF) - } - - func testScanCharsetHex() { - var scanner = XMLParser.Scanner(text: " \t 8badf00d \t \t 007") - XCTAssertEqual(try scanner.scanString(matchingAny: .hexadecimal), "8badf00d") - XCTAssertEqual(try scanner.scanString(matchingAny: .hexadecimal), "007") - XCTAssertThrowsError(try scanner.scanString(matchingAny: .hexadecimal)) - } - - func testScanCharsetEmoji() { - var scanner = XMLParser.Scanner(text: " \t 8badf00d \t🐶 \tšŸŒžšŸ‡¦šŸ‡ŗ 007") - let emoji: Foundation.CharacterSet = "šŸ¤ šŸŒžšŸ’ŽšŸ¶\u{1f1e6}\u{1f1fa}" - - XCTAssertThrowsError(try scanner.scanString(matchingAny: emoji)) - XCTAssertEqual(try scanner.scanString(matchingAny: .hexadecimal), "8badf00d") - XCTAssertThrowsError(try scanner.scanString(matchingAny: .hexadecimal)) - XCTAssertEqual(try scanner.scanString(matchingAny: emoji), "🐶") - XCTAssertThrowsError(try scanner.scanString(matchingAny: .hexadecimal)) - XCTAssertEqual(try scanner.scanString(matchingAny: emoji), "šŸŒžšŸ‡¦šŸ‡ŗ") - XCTAssertThrowsError(try scanner.scanString(matchingAny: emoji)) - XCTAssertEqual(try scanner.scanString(matchingAny: .hexadecimal), "007") - } - - func testScanString() { - var scanner = XMLParser.Scanner(text: " \t The quick brown fox") - - XCTAssertThrowsError(try scanner.scanString("fox")) - XCTAssertNoThrow(try scanner.scanString("The")) - XCTAssertThrowsError(try scanner.scanString("quick fox")) - XCTAssertNoThrow(try scanner.scanString("quick brown")) - XCTAssertNoThrow(try scanner.scanString("fox")) - XCTAssertThrowsError(try scanner.scanString("fox")) - } - - func testScanCase() { - var scanner = XMLParser.Scanner(text: "NOT OK") - XCTAssertEqual(try scanner.scanCase(from: Token.self), .nok) - XCTAssertEqual(try scanner.scanCase(from: Token.self), .ok) - XCTAssertThrowsError(try scanner.scanCase(from: Token.self)) - } - - func testScanCharacter() { - var scanner = XMLParser.Scanner(text: " \t The fox 8badf00d ") - - XCTAssertThrowsError(_ = try scanner.scanCharacter(matchingAny: "qfxh")) - XCTAssertEqual(try scanner.scanCharacter(matchingAny: "fxT"), "T") - XCTAssertThrowsError(_ = try scanner.scanCharacter(matchingAny: "fxT")) - XCTAssertEqual(try scanner.scanCharacter(matchingAny: "qfxh"), "h") - XCTAssertNoThrow(try scanner.scanString("e fox")) - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "8") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "b") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "a") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "d") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "f") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "0") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "0") - XCTAssertEqual(try scanner.scanCharacter(matchingAny: .hexadecimal), "d") - } - - func testScanUInt8() { - AssertScanUInt8("0", 0) - AssertScanUInt8("124", 124) - AssertScanUInt8(" 045", 45) -#if canImport(Darwin) - AssertScanUInt8("-29", nil) -#endif - AssertScanUInt8("ab24", nil) - } - - func testScanFloat() { - AssertScanFloat("0", 0) - AssertScanFloat("124", 124) - AssertScanFloat(" 045", 45) - AssertScanFloat("-29", -29) - AssertScanFloat("ab24", nil) - } - - func testScanDouble() { - AssertScanDouble("0", 0) - AssertScanDouble("124", 124) - AssertScanDouble(" 045", 45) - AssertScanDouble("-29", -29) - AssertScanDouble("ab24", nil) - } - - func testScanLength() { - AssertScanLength("0", 0) - AssertScanLength("124", 124) - AssertScanLength(" 045", 45) - AssertScanLength("-29", nil) - AssertScanLength("ab24", nil) - } - - func testScanBool() { - AssertScanBool("0", false) - AssertScanBool("1", true) - AssertScanBool("true", true) - AssertScanBool("false", false) - AssertScanBool("false", false) - - var scanner = XMLParser.Scanner(text: "-29") - XCTAssertThrowsError(try scanner.scanBool()) - XCTAssertEqual(scanner.currentIndex, "".startIndex) - } - - func testScanPercentageFloat() { - AssertScanPercentageFloat("0", 0) - AssertScanPercentageFloat("0.5", 0.5) - AssertScanPercentageFloat("0.75", 0.75) - AssertScanPercentageFloat("1.0", 1.0) - AssertScanPercentageFloat("-0.5", nil) - AssertScanPercentageFloat("1.5", nil) - AssertScanPercentageFloat("as", nil) - AssertScanPercentageFloat("29", nil) - AssertScanPercentageFloat("24", nil) - } - - func testScanPercentage() { - AssertScanPercentage("0", 0) - AssertScanPercentage("0%", 0) - AssertScanPercentage("100%", 1.0) - AssertScanPercentage("100 %", 1.0) - AssertScanPercentage("45.5 %", 0.455) - AssertScanPercentage("0.5 %", 0.005) - AssertScanPercentage("as", nil) - AssertScanPercentage("29", nil) - AssertScanPercentage("24", nil) - } - - func testScanCoordinate() throws { - var scanner = XMLParser.Scanner(text: "10.05,12.04-49.05,30.02-10") - - XCTAssertEqual(try scanner.scanCoordinate(), 10.05) - _ = try? scanner.scanString(",") - XCTAssertEqual(try scanner.scanCoordinate(), 12.04) - _ = try? scanner.scanString(",") - XCTAssertEqual(try scanner.scanCoordinate(), -49.05) - _ = try? scanner.scanString(",") - XCTAssertEqual(try scanner.scanCoordinate(), 30.02) - _ = try? scanner.scanString(",") - XCTAssertEqual(try scanner.scanCoordinate(), -10) - } -} - -private func AssertScanUInt8(_ text: String, _ expected: UInt8?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanUInt8(), expected, file: file, line: line) -} - -private func AssertScanFloat(_ text: String, _ expected: Float?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanFloat(), expected, file: file, line: line) -} - -private func AssertScanDouble(_ text: String, _ expected: Double?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanDouble(), expected, file: file, line: line) -} - -private func AssertScanLength(_ text: String, _ expected: DOM.Length?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanLength(), expected, file: file, line: line) -} - -private func AssertScanBool(_ text: String, _ expected: Bool?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanBool(), expected, file: file, line: line) -} - -private func AssertScanPercentage(_ text: String, _ expected: Float?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanPercentage(), expected, file: file, line: line) -} - -private func AssertScanPercentageFloat(_ text: String, _ expected: Float?, file: StaticString = #file, line: UInt = #line) { - var scanner = XMLParser.Scanner(text: text) - XCTAssertEqual(try? scanner.scanPercentageFloat(), expected, file: file, line: line) -} - - -extension Foundation.CharacterSet: 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/SwiftDrawTests/XML.SAXParserTests.swift b/SwiftDrawTests/XML.SAXParserTests.swift deleted file mode 100644 index f6b9c117..00000000 --- a/SwiftDrawTests/XML.SAXParserTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// 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 XCTest -@testable import SwiftDraw - -final class SAXParserTests: XCTestCase { - - func testMissingFileThrows() { - let missingFile = URL(fileURLWithPath: "/user/tmp/SWIFTDraw/SwiftDraw/missing") - //let missingFile = URL(string: "http://www.test.com")! - XCTAssertThrowsError(try XML.SAXParser.parse(contentsOf: missingFile)) - } - - func testInvalidXMLThrows() { - let xml = "hi" - XCTAssertThrowsError(try XML.SAXParser.parse(data: xml.data(using: .utf8)!)) - } - - func testValidSVGParses() throws { - let xml = """ - - -""" - - let root = try XML.SAXParser.parse(data: xml.data(using: .utf8)!) - XCTAssertEqual(root.name, "svg") - XCTAssertTrue(root.children.isEmpty) - } - - - func testUnexpectedElementsThrows() throws { - let xml = """ - - - -""" -#if canImport(Darwin) - XCTAssertThrowsError(try XML.SAXParser.parse(data: xml.data(using: .utf8)!)) -#endif - } - - func testUnexpectedNamespaceElementsSkipped() throws { - let xml = """ - - - - -""" - let root = try XML.SAXParser.parse(data: xml.data(using: .utf8)!) - XCTAssertEqual(root.name, "svg") - XCTAssertEqual(root.children.count, 1) - XCTAssertEqual(root.children[0].name, "b") - } -}