Add swift svg

This commit is contained in:
isaac 2026-04-24 13:09:33 +04:00
parent ce64d84013
commit e51fef57d8
136 changed files with 11457 additions and 23 deletions

View file

@ -24,7 +24,7 @@ swift_library(
"//submodules/WebPBinding:WebPBinding",
"//submodules/AppBundle:AppBundle",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/Svg:Svg",
"//submodules/Svg/LegacyImpl",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/ImageCompression",
],

View file

@ -17,10 +17,10 @@ import TinyThumbnail
import ImageTransparency
import AppBundle
import MusicAlbumArtResources
import Svg
import RangeSet
import Accelerate
import ImageCompression
import LegacyImpl
private enum ResourceFileData {
case data(Data)

View file

@ -1,21 +1,16 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
objc_library(
swift_library(
name = "Svg",
enable_modules = True,
module_name = "Svg",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.c",
"Sources/**/*.h",
"Sources/**/*.swift",
]),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
copts = [
"-warnings-as-errors",
],
sdk_frameworks = [
"Foundation",
deps = [
"//submodules/Svg/LegacyImpl",
],
visibility = [
"//visibility:public",

View file

@ -0,0 +1,23 @@
objc_library(
name = "LegacyImpl",
enable_modules = True,
module_name = "LegacyImpl",
srcs = glob([
"Sources/**/*.m",
"Sources/**/*.c",
"Sources/**/*.h",
]),
hdrs = glob([
"PublicHeaders/**/*.h",
]),
includes = [
"PublicHeaders",
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)

View file

@ -27,6 +27,6 @@ GiftPatternData * _Nullable getGiftPatternData(NSData * _Nonnull data);
UIImage * _Nullable renderPreparedImage(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit);
UIImage * _Nullable renderPreparedImageWithSymbol(NSData * _Nonnull data, CGSize size, UIColor * _Nonnull backgroundColor, CGFloat scale, bool fit, UIImage * _Nullable symbolImage, int32_t modelRectIndex);
UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque);
UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor * _Nullable backgroundColor, UIColor * _Nullable foregroundColor, CGFloat scale, bool opaque);
#endif /* Lottie_h */

View file

@ -302,7 +302,7 @@ void renderShape(NSVGshape *shape, CGContextRef context, UIColor *foregroundColo
}
}
UIImage * _Nullable drawSvgImage(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) {
UIImage * _Nullable drawSvgImageImpl(NSData * _Nonnull data, CGSize size, UIColor *backgroundColor, UIColor *foregroundColor, CGFloat canvasScale, bool opaque) {
if (!data || data.length == 0) return nil;
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];

View file

@ -0,0 +1,7 @@
import Foundation
import UIKit
import LegacyImpl
public func drawSvgImage(data: Data, size: CGSize, backgroundColor: UIColor?, foregroundColor: UIColor?, scale: CGFloat, opaque: Bool) -> UIImage? {
return drawSvgImageImpl(data, size, backgroundColor, foregroundColor, scale, opaque)
}

View file

@ -219,7 +219,7 @@ swift_library(
"//submodules/ItemListVenueItem:ItemListVenueItem",
"//submodules/SemanticStatusNode:SemanticStatusNode",
"//submodules/AccountUtils:AccountUtils",
"//submodules/Svg:Svg",
"//submodules/Svg/LegacyImpl",
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
"//submodules/TooltipUI:TooltipUI",
"//submodules/ListMessageItem:ListMessageItem",

View file

@ -343,7 +343,7 @@ public final class PeerInfoRatingComponent: Component {
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) {
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) {
if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.borderColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
}
}
@ -372,7 +372,7 @@ public final class PeerInfoRatingComponent: Component {
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) {
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) {
if let image = generateTintedImage(image: drawSvgImage(data: data, size: size, backgroundColor: nil, foregroundColor: nil, scale: 0.0, opaque: false), color: component.backgroundColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
}
}

View file

@ -18,7 +18,7 @@ import WallpaperResources
import GZip
import TelegramUniversalVideoContent
import GradientBackground
import Svg
import LegacyImpl
import UniversalMediaPlayer
import RangeSet

View file

@ -21,7 +21,7 @@ swift_library(
"//submodules/PhotoResources:PhotoResources",
"//submodules/PersistentStringHash:PersistentStringHash",
"//submodules/AppBundle:AppBundle",
"//submodules/Svg:Svg",
"//submodules/Svg/LegacyImpl",
"//submodules/GZip:GZip",
"//submodules/GradientBackground:GradientBackground",
"//submodules/TelegramPresentationData:TelegramPresentationData",

View file

@ -14,7 +14,7 @@ import LocalMediaResources
import TelegramPresentationData
import TelegramUIPreferences
import AppBundle
import Svg
import LegacyImpl
import GradientBackground
import GZip
@ -1360,7 +1360,7 @@ public func themeImage(account: Account, accountManager: AccountManager<Telegram
case let .pattern(data, colors, intensity):
let wallpaperImage = generateImage(arguments.drawingSize, rotatedContext: { size, context in
drawWallpaperGradientImage(colors.map(UIColor.init(rgb:)), context: context, size: size)
if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let image = drawSvgImage(unpackedData, arguments.drawingSize, .clear, .black, 1.0, true) {
if let unpackedData = TGGUnzipData(data, 2 * 1024 * 1024), let image = drawSvgImageImpl(unpackedData, arguments.drawingSize, .clear, .black, 1.0, true) {
context.setBlendMode(.softLight)
context.setAlpha(abs(CGFloat(intensity)) / 100.0)
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: arguments.drawingSize))

17
third-party/Swift2D/BUILD vendored Normal file
View file

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "Swift2D",
module_name = "Swift2D",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-suppress-warnings",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,19 @@
#if canImport(CoreGraphics)
import CoreGraphics
#else
import Foundation
#endif
public extension Point {
/// Initialize a `Point` using the provided `CGPoint` values.
init(_ point: CGPoint) {
self.init(x: point.x, y: point.y)
}
}
public extension CGPoint {
/// Initialize a `CPPoint` using the provided `Point` values.
init(_ point: Point) {
self.init(x: point.x, y: point.y)
}
}

82
third-party/Swift2D/Sources/Point.swift vendored Normal file
View file

@ -0,0 +1,82 @@
/// The representation of a single point in a two-dimensional plane.
public struct Point: Hashable, Codable, Sendable, CustomStringConvertible {
public static let zero: Point = Point(x: 0, y: 0)
public static let nan: Point = Point(x: Double.nan, y: Double.nan)
public static let infinite: Point = Point(x: -Double.greatestFiniteMagnitude / 2, y: -Double.greatestFiniteMagnitude / 2)
public static let null: Point = Point(x: Double.infinity, y: Double.infinity)
public let x: Double
public let y: Double
public init(x: Double = 0.0, y: Double = 0.0) {
self.x = x
self.y = y
}
public init(x: Float, y: Float) {
self.x = Double(x)
self.y = Double(y)
}
public init(x: Int, y: Int) {
self.x = Double(x)
self.y = Double(y)
}
public var description: String { "Point(x: \(x), y: \(y))" }
public var isZero: Bool { self == .zero }
public var isNaN: Bool { self == .nan }
public var isInfinite: Bool { self == .infinite }
public var isNull: Bool { self == .null }
/// Create a new instance maintaining the `y` value while using the provided `x` value.
public func x(_ value: Double) -> Point {
Point(x: value, y: y)
}
/// Create a new instance maintaining the `x` value while using the provided `y` value.
public func y(_ value: Double) -> Point {
Point(x: x, y: value)
}
/// The _mirror_ of the instance, rotated 180° around the provided `point`
/// in a two-dimensional plane.
///
/// - parameters:
/// - point: The anchor to use in calculating the reflection.
public func reflecting(around point: Point) -> Point {
let x = (2 * point.x) - x
let y = (2 * point.y) - y
return Point(x: x, y: y)
}
public static func == (lhs: Point, rhs: Point) -> Bool {
if lhs.x.isNaN, rhs.x.isNaN, lhs.y.isNaN, rhs.y.isNaN {
return true
}
if lhs.x.isInfinite, rhs.x.isInfinite, lhs.y.isInfinite, rhs.y.isInfinite {
return true
}
return lhs.x == rhs.x && lhs.y == rhs.y
}
}
public extension Point {
@available(*, deprecated, renamed: "x(_:)")
func with(x value: Double) -> Point {
x(value)
}
@available(*, deprecated, renamed: "y(_:)")
func with(y value: Double) -> Point {
y(value)
}
@available(*, deprecated, renamed: "reflecting(around:)")
func reflection(using point: Point) -> Point {
reflecting(around: point)
}
}

View file

@ -0,0 +1,32 @@
#if canImport(CoreGraphics)
import CoreGraphics
#else
import Foundation
#endif
public extension Rect {
/// Initialize a `Rect` using the provided `CGRect` values.
init(_ rect: CGRect) {
self.init(
origin: Point(rect.origin),
size: Size(rect.size)
)
}
@available(*, deprecated, renamed: "CGRect(_:)", message: "Use CGRect initializer directly")
var cgRect: CGRect {
CGRect(self)
}
}
public extension CGRect {
/// Initialize a `CGRect` using the provided `Rect` values.
init(_ rect: Rect) {
self.init(
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
)
}
}

251
third-party/Swift2D/Sources/Rect.swift vendored Normal file
View file

@ -0,0 +1,251 @@
/// The location and dimensions of a rectangle.
public struct Rect: Hashable, Codable, Sendable, CustomStringConvertible {
public static let zero: Rect = Rect(origin: .zero, size: .zero)
public static let nan: Rect = Rect(origin: .nan, size: .nan)
public static let infinite: Rect = Rect(origin: .infinite, size: .infinite)
public static let null: Rect = Rect(origin: .null, size: .zero)
/// A point that specifies the coordinates of the rectangles origin.
public let origin: Point
/// A size that specifies the height and width of the rectangle.
public let size: Size
public init(origin: Point = .zero, size: Size = .zero) {
self.origin = origin
self.size = size
}
public init(x: Double, y: Double, width: Double, height: Double) {
origin = Point(x: x, y: y)
size = Size(width: width, height: height)
}
public init(x: Float, y: Float, width: Float, height: Float) {
origin = Point(x: x, y: y)
size = Size(width: width, height: height)
}
public init(x: Int, y: Int, width: Int, height: Int) {
origin = Point(x: x, y: y)
size = Size(width: width, height: height)
}
public var description: String { "Rect(origin: \(origin), size: \(size))" }
public var isZero: Bool { self == .zero }
public var isNaN: Bool { self == .nan }
public var isInfinite: Bool { self == .infinite }
public var isNull: Bool { origin == .infinite || origin == .null }
public var isEmpty: Bool { isNull || width == 0.0 || height == 0.0 }
/// The x-coordinate of the rectangle origin
public var x: Double { origin.x }
/// The y-coordinate of the rectangle origin
public var y: Double { origin.y }
/// The width of the rectangle.
public var width: Double { size.width }
/// The height of the rectangle.
public var height: Double { size.height }
/// The middle of the `Rect` both horizontally and vertically.
public var center: Point { Point(x: midX, y: midY) }
/// The smallest value for the x-coordinate of the rectangle.
public var minX: Double {
if size.width < 0.0 {
return origin.x - abs(size.width)
}
return origin.x
}
/// The x-coordinate that establishes the center of a rectangle.
public var midX: Double { minX + size.widthRadius }
/// The largest value of the x-coordinate for the rectangle.
public var maxX: Double {
if size.width < 0.0 {
return origin.x
}
return origin.x + size.width
}
/// The smallest value for the y-coordinate of the rectangle.
public var minY: Double {
if size.height < 0.0 {
return origin.y - abs(size.height)
}
return origin.y
}
/// The y-coordinate that establishes the center of the rectangle.
public var midY: Double { minY + size.heightRadius }
/// The largest value for the y-coordinate of the rectangle.
public var maxY: Double {
if size.height < 0.0 {
return origin.y
}
return origin.y + size.height
}
/// Returns a rectangle with a positive width and height.
public var standardized: Rect {
guard !isNull else {
return .null
}
return Rect(x: minX, y: minY, width: abs(width), height: abs(height))
}
public func origin(_ value: Point) -> Rect {
Rect(origin: value, size: size)
}
public func size(_ value: Size) -> Rect {
Rect(origin: origin, size: value)
}
public func contains(_ point: Point) -> Bool {
guard !isNull else {
return false
}
guard !isEmpty else {
return false
}
return (minX ..< maxX).contains(point.x) && (minY ..< maxY).contains(point.y)
}
public func contains(_ rect: Rect) -> Bool {
union(rect) == self
}
public func intersects(_ rect: Rect) -> Bool {
!intersection(rect).isNull
}
public func intersection(_ rect: Rect) -> Rect {
guard !isNull else {
return .null
}
guard !rect.isNull else {
return .null
}
let r1 = standardized
let r1spanH = r1.minX ... r1.maxX
let r1spanV = r1.minY ... r1.maxY
let r2 = rect.standardized
let r2spanH = r2.minX ... r2.maxX
let r2spanV = r2.minY ... r2.maxY
guard r1spanH.overlaps(r2spanH), r2spanV.overlaps(r2spanV) else {
return .null
}
let overlapH = r1spanH.clamped(to: r2spanH)
let width: Double = if overlapH == r1spanH {
r1.width
} else if overlapH == r2spanH {
r2.width
} else {
overlapH.upperBound - overlapH.lowerBound
}
let overlapV = r1spanV.clamped(to: r2spanV)
let height: Double = if overlapV == r1spanV {
r1.height
} else if overlapV == r2spanV {
r2.height
} else {
overlapV.upperBound - overlapV.lowerBound
}
return Rect(x: overlapH.lowerBound, y: overlapV.lowerBound, width: width, height: height)
}
public func union(_ rect: Rect) -> Rect {
guard !isNull else {
return rect
}
guard !rect.isNull else {
return self
}
let r1 = standardized
let r2 = rect.standardized
let minX = min(r1.minX, r2.minX)
let maxX = max(r1.maxX, r2.maxX)
let minY = min(r1.minY, r2.minY)
let maxY = max(r1.maxY, r2.maxY)
return Rect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
}
public func offsetBy(dx: Double, dy: Double) -> Rect {
guard !isNull else {
return self
}
let rect = standardized
return Rect(
origin: Point(
x: rect.origin.x + dx,
y: rect.origin.y + dy
),
size: rect.size
)
}
public func insetBy(dx: Double, dy: Double) -> Rect {
guard !isNull else {
return self
}
let normalized = standardized
let rect = Rect(
origin: Point(
x: normalized.x + dx,
y: normalized.y + dy
),
size: Size(
width: normalized.width - (dx * 2),
height: normalized.height - (dy * 2)
)
)
guard rect.size.width >= 0, rect.size.height >= 0 else {
return .null
}
return rect
}
public func expandedBy(dx: Double, dy: Double) -> Rect {
insetBy(dx: -dx, dy: -dy)
}
}
public extension Rect {
@available(*, deprecated, renamed: "origin(_:)")
func with(origin value: Point) -> Rect {
Rect(origin: value, size: size)
}
@available(*, deprecated, renamed: "size(_:)")
func with(size value: Size) -> Rect {
Rect(origin: origin, size: value)
}
}

View file

@ -0,0 +1,19 @@
#if canImport(CoreGraphics)
import CoreGraphics
#else
import Foundation
#endif
public extension Size {
/// Initialize a `Size` using the provided `CGSize` values.
init(_ size: CGSize) {
self.init(width: size.width, height: size.height)
}
}
public extension CGSize {
/// Initialize a `CGSize` using the provided `Size` values.
init(_ size: Size) {
self.init(width: size.width, height: size.height)
}
}

87
third-party/Swift2D/Sources/Size.swift vendored Normal file
View file

@ -0,0 +1,87 @@
/// A representation of two-dimensional width and height values.
public struct Size: Hashable, Codable, Sendable, CustomStringConvertible {
public static let zero: Size = Size(width: 0.0, height: 0.0)
public static let nan: Size = Size(width: Double.nan, height: Double.nan)
public static let infinite: Size = Size(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude)
public let width: Double
public let height: Double
public init(width: Double = 0.0, height: Double = 0.0) {
self.width = width
self.height = height
}
public init(width: Float, height: Float) {
self.width = Double(width)
self.height = Double(height)
}
public init(width: Int, height: Int) {
self.width = Double(width)
self.height = Double(height)
}
public var description: String { "Size(width: \(width), height: \(height))" }
public var isZero: Bool { self == .zero }
public var isNaN: Bool { self == .nan }
public var isInfinite: Bool { self == .infinite }
/// The radius defined by the `width` dimension.
public var widthRadius: Double { abs(width) / 2.0 }
/// The radius defined by the `height` dimension.
public var heightRadius: Double { abs(height) / 2.0 }
/// The largest of the width and height radii.
public var maxRadius: Double { max(widthRadius, heightRadius) }
/// The smallest of the width and height radii.
public var minRadius: Double { min(widthRadius, heightRadius) }
/// The `Point` at which the `widthRadius` & `heightRadius` intersect.
public var center: Point { Point(x: widthRadius, y: heightRadius) }
/// Create a new instance maintaining the `height` value while using the provided `width` value.
public func width(_ value: Double) -> Size {
Size(width: value, height: height)
}
/// Create a new instance maintaining the `width` value while using the provided `height` value.
public func height(_ value: Double) -> Size {
Size(width: width, height: value)
}
public static func == (lhs: Size, rhs: Size) -> Bool {
if lhs.width.isNaN, rhs.width.isNaN, lhs.height.isNaN, rhs.height.isNaN {
return true
}
return lhs.width == rhs.width && lhs.height == rhs.height
}
}
public extension Size {
@available(*, deprecated, renamed: "widthRadius")
var xRadius: Double { abs(width) / 2.0 }
@available(*, deprecated, renamed: "heightRadius")
var yRadius: Double { abs(height) / 2.0 }
@available(*, deprecated, renamed: "widthRadius")
var horizontalRadius: Double { xRadius }
@available(*, deprecated, renamed: "heightRadius")
var verticalRadius: Double { yRadius }
@available(*, deprecated, renamed: "width(_:)")
func with(width value: Double) -> Size {
width(value)
}
@available(*, deprecated, renamed: "height(_:)")
func with(height value: Double) -> Size {
height(value)
}
}

17
third-party/SwiftColor/BUILD vendored Normal file
View file

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SwiftColor",
module_name = "SwiftColor",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-suppress-warnings",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,20 @@
@propertyWrapper
public struct Clamping<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
public init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
public var wrappedValue: Value {
get {
value
}
set(newValue) {
value = min(max(range.lowerBound, newValue), range.upperBound)
}
}
}

View file

@ -0,0 +1,3 @@
public enum ColorSpace: Equatable, Sendable {
case rgba
}

View file

@ -0,0 +1,24 @@
import Foundation
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
import AppKit
public extension Pigment {
/// Initialize a `Pigment` using an `NSColor`.
init(_ color: NSColor) {
red = color.redComponent
green = color.greenComponent
blue = color.blueComponent
alpha = color.alphaComponent
}
var nsColor: NSColor {
NSColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
public extension NSColor {
var pigment: Pigment {
Pigment(self)
}
}
#endif

View file

@ -0,0 +1,39 @@
import Foundation
#if canImport(CoreGraphics)
import CoreGraphics
public extension Pigment {
/// Initialize a `Pigment` using an `CGColor`.
init?(_ color: CGColor) {
let components = color.components ?? []
switch components.count {
case 2:
// Monochrome/Grayscale
// TODO: Express this in 'Color'.
red = 0.0
green = 0.0
blue = 0.0
alpha = components[1]
case 4:
// RGB
red = components[0]
green = components[1]
blue = components[2]
alpha = components[3]
default:
return nil
}
}
}
public extension CGColor {
static func make(_ color: Pigment) -> CGColor {
if color.red == 0.0, color.green == 0.0, color.blue == 0.0, color.alpha == 0.0 {
// Return the CG color equivalent of `UIColor.clear`.
return CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.0)
}
return CGColor(srgbRed: color.red, green: color.green, blue: color.blue, alpha: color.alpha)
}
}
#endif

View file

@ -0,0 +1,95 @@
import Foundation
public extension Pigment {
/// Initialize a `Pigment` using `Float` values leaning towards the 'Red' spectrum.
///
/// - parameters
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
red: Float,
green: Float = 0.0,
blue: Float = 0.0,
alpha: Float = 1.0
) {
self.red = Double(red)
self.green = Double(green)
self.blue = Double(blue)
self.alpha = Double(alpha)
}
/// Initialize a `Pigment` using `Float` values leaning towards the 'Green' spectrum.
///
/// - parameters:
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
green: Float,
blue: Float = 0.0,
red: Float = 0.0,
alpha: Float = 1.0
) {
self.red = Double(red)
self.green = Double(green)
self.blue = Double(blue)
self.alpha = Double(alpha)
}
/// Initialize a `Pigment` using `Float` values leaning towards the 'Blue' spectrum.
///
/// - parameter:
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
blue: Float,
red: Float = 0.0,
green: Float = 0.0,
alpha: Float = 1.0
) {
self.red = Double(red)
self.green = Double(green)
self.blue = Double(blue)
self.alpha = Double(alpha)
}
/// Initialize a `Pigment` using variadic `Float` values.
///
/// All _values_ should be expressed in the range of 0.0 to 1.0.
///
/// - parameters:
/// - values: A number of `Float` which are mapped to **red**, **green**, **blue** in that order.
/// - alpha: Amount of _opacity/transparency_ to apply.
init(
_ values: Float...,
alpha: Float
) {
if values.count > 0 {
red = Double(values[0].clamped(to: 0 ... 1))
} else {
red = 0.0
}
if values.count > 1 {
green = Double(values[1].clamped(to: 0 ... 1))
} else {
green = 0.0
}
if values.count > 2 {
blue = Double(values[2].clamped(to: 0 ... 1))
} else {
blue = 0.0
}
self.alpha = Double(alpha.clamped(to: 0 ... 1))
}
}
extension Float {
func clamped(to range: ClosedRange<Float>) -> Float {
min(max(range.lowerBound, self), range.upperBound)
}
}

View file

@ -0,0 +1,145 @@
import Foundation
public extension Pigment {
/// Initializes a `Pigment` with an `Int` in the expected format of **0x000**.
///
/// Used as a short-hand. If the hex 0x123 is provided, it is interpreted as 0x112233.
init(
hex3 hex: Int,
@Clamping(0 ... 1) alpha: Double = 1.0
) {
let values = Self.hex3(hex: hex, alpha: alpha)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
}
/// Shorthand **0x0000** initializer similar to `init(hex3:alpha:)` where
/// the last digit represent the alpha component.
init(
hex4 hex: Int
) {
let values = Self.hex4(hex: hex)
red = values.red
green = values.green
blue = values.blue
alpha = values.alpha
}
/// Initializes with a standard format hex representation of color in the form of **0x1E2C3D**.
init(
hex6 hex: Int,
@Clamping(0 ... 1) alpha: Double = 1.0
) {
let values = Self.hex6(hex: hex, alpha: alpha)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
}
#if !os(watchOS)
/// Extended form of `init(hex6:alpha:)` expecting **0x112233FF**, that uses the last
/// bits for the alpha component.
init(
hex8 hex: Int
) {
let values = Self.hex8(hex: hex)
red = values.red
green = values.green
blue = values.blue
alpha = values.alpha
}
#endif
/// Initializes a `Pigment` with an `Int` representation of an RGB(a) Hex Value
///
/// This initializer will do its best to interpret the intentions of what is provided.
/// **YOUR RESULTS WILL VARY**, and it's best to use one of the `init(hex?:)` initializers.
///
/// - Parameter hex: Hex value
/// - Parameter alpha: The opacity value of the color object
init(
_ hex: Int,
alpha: Double? = nil
) {
#if !os(watchOS)
if hex > 0xFFFFFF {
let values = Self.hex8(hex: hex)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
return
}
#endif
if hex > 0xFFFF {
let values = Self.hex6(hex: hex, alpha: alpha ?? 1.0)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
} else if hex > 0xFFF {
let values = Self.hex4(hex: hex)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
} else {
let values = Self.hex3(hex: hex, alpha: alpha ?? 1.0)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
}
}
}
extension Pigment {
static func hex3(
hex: Int,
@Clamping(0 ... 1) alpha: Double = 1.0
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
let red = Double(duplicateBits((hex & 0xF00) >> 8)) / 255.0
let green = Double(duplicateBits((hex & 0x0F0) >> 4)) / 255.0
let blue = Double(duplicateBits((hex & 0x00F) >> 0)) / 255.0
return (red, green, blue, alpha)
}
static func hex4(
hex: Int
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
let red = Double(duplicateBits((hex & 0xF000) >> 12)) / 255.0
let green = Double(duplicateBits((hex & 0x0F00) >> 8)) / 255.0
let blue = Double(duplicateBits((hex & 0x00F0) >> 4)) / 255.0
let alpha = Double(duplicateBits((hex & 0x000F) >> 0)) / 255.0
return (red, green, blue, alpha)
}
static func hex6(
hex: Int,
@Clamping(0 ... 1) alpha: Double = 1.0
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
let red = Double((hex & 0xFF0000) >> 16) / 255.0
let green = Double((hex & 0x00FF00) >> 8) / 255.0
let blue = Double((hex & 0x0000FF) >> 0) / 255.0
return (red, green, blue, alpha)
}
#if !os(watchOS)
static func hex8(
hex: Int
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
let red = Double((hex & 0xFF00_0000) >> 24) / 255.0
let green = Double((hex & 0x00FF_0000) >> 16) / 255.0
let blue = Double((hex & 0x0000_FF00) >> 8) / 255.0
let alpha = Double((hex & 0x0000_00FF) >> 0) / 255.0
return (red, green, blue, alpha)
}
#endif
static func duplicateBits(_ value: Int) -> Int {
(value << 4) + value
}
}

View file

@ -0,0 +1,89 @@
import Foundation
public extension Pigment {
/// Initialize a `Pigment` using `Int` values leaning towards the 'Red' spectrum.
///
/// - parameters:
/// - red: A value in the range of 0 to 255 representing the **red** bits
/// - green: A value in the range of 0 to 255 representing the **green** bits
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
@Clamping(0 ... 255) red: Int,
@Clamping(0 ... 255) green: Int = 0,
@Clamping(0 ... 255) blue: Int = 0,
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
) {
self.red = Double(red) / 255.0
self.green = Double(green) / 255.0
self.blue = Double(blue) / 255.0
self.alpha = alpha
}
/// Initialize a `Pigment` using `Int` values leaning towards the 'Green' spectrum.
///
/// - parameters:
/// - green: A value in the range of 0 to 255 representing the **green** bits
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
/// - red: A value in the range of 0 to 255 representing the **red** bits
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
@Clamping(0 ... 255) green: Int,
@Clamping(0 ... 255) blue: Int = 0,
@Clamping(0 ... 255) red: Int = 0,
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
) {
self.red = Double(red) / 255.0
self.green = Double(green) / 255.0
self.blue = Double(blue) / 255.0
self.alpha = alpha
}
/// Initialize a `Pigment` using `Int` values leaning towards the 'Blue' spectrum.
///
/// - parameters:
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
/// - red: A value in the range of 0 to 255 representing the **red** bits
/// - green: A value in the range of 0 to 255 representing the **green** bits
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
@Clamping(0 ... 255) blue: Int,
@Clamping(0 ... 255) red: Int = 0,
@Clamping(0 ... 255) green: Int = 0,
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
) {
self.red = Double(red) / 255.0
self.green = Double(green) / 255.0
self.blue = Double(blue) / 255.0
self.alpha = alpha
}
/// Initialize a `Pigment` using variadic `Int` values.
///
/// All _values_ should be expressed in the range of 0 to 255.
///
/// - parameters:
/// - values: A number of `Int` which are mapped to **red**, **green**, **blue** in that order.
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
init(
_ values: Int...,
@Clamping(0 ... 1) alpha: Double
) {
if values.count > 0 {
red = Double(values[0]) / 255.0
} else {
red = 0.0
}
if values.count > 1 {
green = Double(values[1]) / 255.0
} else {
green = 0.0
}
if values.count > 2 {
blue = Double(values[2]) / 255.0
} else {
blue = 0.0
}
self.alpha = alpha
}
}

View file

@ -0,0 +1,470 @@
import Foundation
public extension Pigment {
@available(*, deprecated, renamed: "Name")
typealias Keyword = Name
enum Name: String, CaseIterable {
case aliceBlue
case antiqueWhite
case aqua
case aquamarine
case azure
case beige
case bisque
case black
case blanchedAlmond
case blue
case blueViolet
case brown
case burlywood
case cadetBlue
case chartreuse
case chocolate
case coral
case cornflowerBlue
case cornsilk
case crimson
case cyan
case darkBlue
case darkCyan
case darkGoldenrod
case darkGray
case darkGreen
case darkGrey
case darkKhaki
case darkMagenta
case darkOliveGreen
case darkOrange
case darkOrchid
case darkRed
case darkSalmon
case darkSeagreen
case darkSlateBlue
case darkSlateGray
case darkSlateGrey
case darkTurquoise
case darkViolet
case deepPink
case deepSkyblue
case dimGray
case dimGrey
case dodgerBlue
case firebrick
case floralWhite
case forestGreen
case fuchsia
case gainsboro
case ghostWhite
case gold
case goldenrod
case gray
case green
case greenYellow
case grey
case honeydew
case hotPink
case indianRed
case indigo
case ivory
case khaki
case lavender
case lavenderBlush
case lawnGreen
case lemonChiffon
case lightBlue
case lightCoral
case lightCyan
case lightGoldenrodYellow
case lightGray
case lightGreen
case lightGrey
case lightPink
case lightSalmon
case lightSeagreen
case lightSkyBlue
case lightSlateGray
case lightSlateGrey
case lightSteelBlue
case lightYellow
case lime
case limeGreen
case linen
case magenta
case maroon
case mediumAquamarine
case mediumBlue
case mediumOrchid
case mediumPurple
case mediumSeagreen
case mediumSlateBlue
case mediumSpringGreen
case mediumTurquoise
case mediumVioletRed
case midnightBlue
case mintCream
case mistyRose
case moccasin
case navajoWhite
case navy
case oldLace
case olive
case oliveDrab
case orange
case orangeRed
case orchid
case paleGoldenrod
case paleGreen
case paleTurquoise
case paleVioletRed
case papayaWhip
case peachPuff
case peru
case pink
case plum
case powderBlue
case purple
case red
case rosyBrown
case royalBlue
case saddleBrown
case salmon
case sandyBrown
case seagreen
case seashell
case sienna
case silver
case skyBlue
case slateBlue
case slateGray
case slateGrey
case snow
case springGreen
case steelBlue
case tan
case teal
case thistle
case tomato
case turquoise
case violet
case wheat
case white
case whitesmoke
case yellow
case yellowGreen
public var rgb: (red: Int, green: Int, blue: Int) {
switch self {
case .aliceBlue: (240, 248, 255)
case .antiqueWhite: (250, 235, 215)
case .aqua: (0, 255, 255)
case .aquamarine: (127, 255, 212)
case .azure: (240, 255, 255)
case .beige: (245, 245, 220)
case .bisque: (255, 228, 196)
case .black: (0, 0, 0)
case .blanchedAlmond: (255, 235, 205)
case .blue: (0, 0, 255)
case .blueViolet: (138, 43, 226)
case .brown: (165, 42, 42)
case .burlywood: (222, 184, 135)
case .cadetBlue: (95, 158, 160)
case .chartreuse: (127, 255, 0)
case .chocolate: (210, 105, 30)
case .coral: (255, 127, 80)
case .cornflowerBlue: (100, 149, 237)
case .cornsilk: (255, 248, 220)
case .crimson: (220, 20, 60)
case .cyan: (0, 255, 255)
case .darkBlue: (0, 0, 139)
case .darkCyan: (0, 139, 139)
case .darkGoldenrod: (184, 134, 11)
case .darkGray: (169, 169, 169)
case .darkGreen: (0, 100, 0)
case .darkGrey: (169, 169, 169)
case .darkKhaki: (189, 183, 107)
case .darkMagenta: (139, 0, 139)
case .darkOliveGreen: (85, 107, 47)
case .darkOrange: (255, 140, 0)
case .darkOrchid: (153, 50, 204)
case .darkRed: (139, 0, 0)
case .darkSalmon: (233, 150, 122)
case .darkSeagreen: (143, 188, 143)
case .darkSlateBlue: (72, 61, 139)
case .darkSlateGray: (47, 79, 79)
case .darkSlateGrey: (47, 79, 79)
case .darkTurquoise: (0, 206, 209)
case .darkViolet: (148, 0, 211)
case .deepPink: (255, 20, 147)
case .deepSkyblue: (0, 191, 255)
case .dimGray: (105, 105, 105)
case .dimGrey: (105, 105, 105)
case .dodgerBlue: (30, 144, 255)
case .firebrick: (178, 34, 34)
case .floralWhite: (255, 250, 240)
case .forestGreen: (34, 139, 34)
case .fuchsia: (255, 0, 255)
case .gainsboro: (220, 220, 220)
case .ghostWhite: (248, 248, 255)
case .gold: (255, 215, 0)
case .goldenrod: (218, 165, 32)
case .gray: (128, 128, 128)
case .green: (0, 128, 0)
case .greenYellow: (173, 255, 47)
case .grey: (128, 128, 128)
case .honeydew: (240, 255, 240)
case .hotPink: (255, 105, 180)
case .indianRed: (205, 92, 92)
case .indigo: (75, 0, 130)
case .ivory: (255, 255, 240)
case .khaki: (240, 230, 140)
case .lavender: (230, 230, 250)
case .lavenderBlush: (255, 240, 245)
case .lawnGreen: (124, 252, 0)
case .lemonChiffon: (255, 250, 205)
case .lightBlue: (173, 216, 230)
case .lightCoral: (240, 128, 128)
case .lightCyan: (224, 255, 255)
case .lightGoldenrodYellow: (250, 250, 210)
case .lightGray: (211, 211, 211)
case .lightGreen: (144, 238, 144)
case .lightGrey: (211, 211, 211)
case .lightPink: (255, 182, 193)
case .lightSalmon: (255, 160, 122)
case .lightSeagreen: (32, 178, 170)
case .lightSkyBlue: (135, 206, 250)
case .lightSlateGray: (119, 136, 153)
case .lightSlateGrey: (119, 136, 153)
case .lightSteelBlue: (176, 196, 222)
case .lightYellow: (255, 255, 224)
case .lime: (0, 255, 0)
case .limeGreen: (50, 205, 50)
case .linen: (250, 240, 230)
case .magenta: (255, 0, 255)
case .maroon: (128, 0, 0)
case .mediumAquamarine: (102, 205, 170)
case .mediumBlue: (0, 0, 205)
case .mediumOrchid: (186, 85, 211)
case .mediumPurple: (147, 112, 219)
case .mediumSeagreen: (60, 179, 113)
case .mediumSlateBlue: (123, 104, 238)
case .mediumSpringGreen: (0, 250, 154)
case .mediumTurquoise: (72, 209, 204)
case .mediumVioletRed: (199, 21, 133)
case .midnightBlue: (25, 25, 112)
case .mintCream: (245, 255, 250)
case .mistyRose: (255, 228, 225)
case .moccasin: (255, 228, 181)
case .navajoWhite: (255, 222, 173)
case .navy: (0, 0, 128)
case .oldLace: (253, 245, 230)
case .olive: (128, 128, 0)
case .oliveDrab: (107, 142, 35)
case .orange: (255, 165, 0)
case .orangeRed: (255, 69, 0)
case .orchid: (218, 112, 214)
case .paleGoldenrod: (238, 232, 170)
case .paleGreen: (152, 251, 152)
case .paleTurquoise: (175, 238, 238)
case .paleVioletRed: (219, 112, 147)
case .papayaWhip: (255, 239, 213)
case .peachPuff: (255, 218, 185)
case .peru: (205, 133, 63)
case .pink: (255, 192, 203)
case .plum: (221, 160, 221)
case .powderBlue: (176, 224, 230)
case .purple: (128, 0, 128)
case .red: (255, 0, 0)
case .rosyBrown: (188, 143, 143)
case .royalBlue: (65, 105, 225)
case .saddleBrown: (139, 69, 19)
case .salmon: (250, 128, 114)
case .sandyBrown: (244, 164, 96)
case .seagreen: (46, 139, 87)
case .seashell: (255, 245, 238)
case .sienna: (160, 82, 45)
case .silver: (192, 192, 192)
case .skyBlue: (135, 206, 235)
case .slateBlue: (106, 90, 205)
case .slateGray: (112, 128, 144)
case .slateGrey: (112, 128, 144)
case .snow: (255, 250, 250)
case .springGreen: (0, 255, 127)
case .steelBlue: (70, 130, 180)
case .tan: (210, 180, 140)
case .teal: (0, 128, 128)
case .thistle: (216, 191, 216)
case .tomato: (255, 99, 71)
case .turquoise: (64, 224, 208)
case .violet: (238, 130, 238)
case .wheat: (245, 222, 179)
case .white: (255, 255, 255)
case .whitesmoke: (245, 245, 245)
case .yellow: (255, 255, 0)
case .yellowGreen: (154, 205, 50)
}
}
public var pigment: Pigment {
switch self {
case .aliceBlue: Pigment(240, 248, 255, alpha: 1.0)
case .antiqueWhite: Pigment(250, 235, 215, alpha: 1.0)
case .aqua: Pigment(0, 255, 255, alpha: 1.0)
case .aquamarine: Pigment(127, 255, 212, alpha: 1.0)
case .azure: Pigment(240, 255, 255, alpha: 1.0)
case .beige: Pigment(245, 245, 220, alpha: 1.0)
case .bisque: Pigment(255, 228, 196, alpha: 1.0)
case .black: Pigment(0, 0, 0, alpha: 1.0)
case .blanchedAlmond: Pigment(255, 235, 205, alpha: 1.0)
case .blue: Pigment(0, 0, 255, alpha: 1.0)
case .blueViolet: Pigment(138, 43, 226, alpha: 1.0)
case .brown: Pigment(165, 42, 42, alpha: 1.0)
case .burlywood: Pigment(222, 184, 135, alpha: 1.0)
case .cadetBlue: Pigment(95, 158, 160, alpha: 1.0)
case .chartreuse: Pigment(127, 255, 0, alpha: 1.0)
case .chocolate: Pigment(210, 105, 30, alpha: 1.0)
case .coral: Pigment(255, 127, 80, alpha: 1.0)
case .cornflowerBlue: Pigment(100, 149, 237, alpha: 1.0)
case .cornsilk: Pigment(255, 248, 220, alpha: 1.0)
case .crimson: Pigment(220, 20, 60, alpha: 1.0)
case .cyan: Pigment(0, 255, 255, alpha: 1.0)
case .darkBlue: Pigment(0, 0, 139, alpha: 1.0)
case .darkCyan: Pigment(0, 139, 139, alpha: 1.0)
case .darkGoldenrod: Pigment(184, 134, 11, alpha: 1.0)
case .darkGray: Pigment(169, 169, 169, alpha: 1.0)
case .darkGreen: Pigment(0, 100, 0, alpha: 1.0)
case .darkGrey: Pigment(169, 169, 169, alpha: 1.0)
case .darkKhaki: Pigment(189, 183, 107, alpha: 1.0)
case .darkMagenta: Pigment(139, 0, 139, alpha: 1.0)
case .darkOliveGreen: Pigment(85, 107, 47, alpha: 1.0)
case .darkOrange: Pigment(255, 140, 0, alpha: 1.0)
case .darkOrchid: Pigment(153, 50, 204, alpha: 1.0)
case .darkRed: Pigment(139, 0, 0, alpha: 1.0)
case .darkSalmon: Pigment(233, 150, 122, alpha: 1.0)
case .darkSeagreen: Pigment(143, 188, 143, alpha: 1.0)
case .darkSlateBlue: Pigment(72, 61, 139, alpha: 1.0)
case .darkSlateGray: Pigment(47, 79, 79, alpha: 1.0)
case .darkSlateGrey: Pigment(47, 79, 79, alpha: 1.0)
case .darkTurquoise: Pigment(0, 206, 209, alpha: 1.0)
case .darkViolet: Pigment(148, 0, 211, alpha: 1.0)
case .deepPink: Pigment(255, 20, 147, alpha: 1.0)
case .deepSkyblue: Pigment(0, 191, 255, alpha: 1.0)
case .dimGray: Pigment(105, 105, 105, alpha: 1.0)
case .dimGrey: Pigment(105, 105, 105, alpha: 1.0)
case .dodgerBlue: Pigment(30, 144, 255, alpha: 1.0)
case .firebrick: Pigment(178, 34, 34, alpha: 1.0)
case .floralWhite: Pigment(255, 250, 240, alpha: 1.0)
case .forestGreen: Pigment(34, 139, 34, alpha: 1.0)
case .fuchsia: Pigment(255, 0, 255, alpha: 1.0)
case .gainsboro: Pigment(220, 220, 220, alpha: 1.0)
case .ghostWhite: Pigment(248, 248, 255, alpha: 1.0)
case .gold: Pigment(255, 215, 0, alpha: 1.0)
case .goldenrod: Pigment(218, 165, 32, alpha: 1.0)
case .gray: Pigment(128, 128, 128, alpha: 1.0)
case .green: Pigment(0, 128, 0, alpha: 1.0)
case .greenYellow: Pigment(173, 255, 47, alpha: 1.0)
case .grey: Pigment(128, 128, 128, alpha: 1.0)
case .honeydew: Pigment(240, 255, 240, alpha: 1.0)
case .hotPink: Pigment(255, 105, 180, alpha: 1.0)
case .indianRed: Pigment(205, 92, 92, alpha: 1.0)
case .indigo: Pigment(75, 0, 130, alpha: 1.0)
case .ivory: Pigment(255, 255, 240, alpha: 1.0)
case .khaki: Pigment(240, 230, 140, alpha: 1.0)
case .lavender: Pigment(230, 230, 250, alpha: 1.0)
case .lavenderBlush: Pigment(255, 240, 245, alpha: 1.0)
case .lawnGreen: Pigment(124, 252, 0, alpha: 1.0)
case .lemonChiffon: Pigment(255, 250, 205, alpha: 1.0)
case .lightBlue: Pigment(173, 216, 230, alpha: 1.0)
case .lightCoral: Pigment(240, 128, 128, alpha: 1.0)
case .lightCyan: Pigment(224, 255, 255, alpha: 1.0)
case .lightGoldenrodYellow: Pigment(250, 250, 210, alpha: 1.0)
case .lightGray: Pigment(211, 211, 211, alpha: 1.0)
case .lightGreen: Pigment(144, 238, 144, alpha: 1.0)
case .lightGrey: Pigment(211, 211, 211, alpha: 1.0)
case .lightPink: Pigment(255, 182, 193, alpha: 1.0)
case .lightSalmon: Pigment(255, 160, 122, alpha: 1.0)
case .lightSeagreen: Pigment(32, 178, 170, alpha: 1.0)
case .lightSkyBlue: Pigment(135, 206, 250, alpha: 1.0)
case .lightSlateGray: Pigment(119, 136, 153, alpha: 1.0)
case .lightSlateGrey: Pigment(119, 136, 153, alpha: 1.0)
case .lightSteelBlue: Pigment(176, 196, 222, alpha: 1.0)
case .lightYellow: Pigment(255, 255, 224, alpha: 1.0)
case .lime: Pigment(0, 255, 0, alpha: 1.0)
case .limeGreen: Pigment(50, 205, 50, alpha: 1.0)
case .linen: Pigment(250, 240, 230, alpha: 1.0)
case .magenta: Pigment(255, 0, 255, alpha: 1.0)
case .maroon: Pigment(128, 0, 0, alpha: 1.0)
case .mediumAquamarine: Pigment(102, 205, 170, alpha: 1.0)
case .mediumBlue: Pigment(0, 0, 205, alpha: 1.0)
case .mediumOrchid: Pigment(186, 85, 211, alpha: 1.0)
case .mediumPurple: Pigment(147, 112, 219, alpha: 1.0)
case .mediumSeagreen: Pigment(60, 179, 113, alpha: 1.0)
case .mediumSlateBlue: Pigment(123, 104, 238, alpha: 1.0)
case .mediumSpringGreen: Pigment(0, 250, 154, alpha: 1.0)
case .mediumTurquoise: Pigment(72, 209, 204, alpha: 1.0)
case .mediumVioletRed: Pigment(199, 21, 133, alpha: 1.0)
case .midnightBlue: Pigment(25, 25, 112, alpha: 1.0)
case .mintCream: Pigment(245, 255, 250, alpha: 1.0)
case .mistyRose: Pigment(255, 228, 225, alpha: 1.0)
case .moccasin: Pigment(255, 228, 181, alpha: 1.0)
case .navajoWhite: Pigment(255, 222, 173, alpha: 1.0)
case .navy: Pigment(0, 0, 128, alpha: 1.0)
case .oldLace: Pigment(253, 245, 230, alpha: 1.0)
case .olive: Pigment(128, 128, 0, alpha: 1.0)
case .oliveDrab: Pigment(107, 142, 35, alpha: 1.0)
case .orange: Pigment(255, 165, 0, alpha: 1.0)
case .orangeRed: Pigment(255, 69, 0, alpha: 1.0)
case .orchid: Pigment(218, 112, 214, alpha: 1.0)
case .paleGoldenrod: Pigment(238, 232, 170, alpha: 1.0)
case .paleGreen: Pigment(152, 251, 152, alpha: 1.0)
case .paleTurquoise: Pigment(175, 238, 238, alpha: 1.0)
case .paleVioletRed: Pigment(219, 112, 147, alpha: 1.0)
case .papayaWhip: Pigment(255, 239, 213, alpha: 1.0)
case .peachPuff: Pigment(255, 218, 185, alpha: 1.0)
case .peru: Pigment(205, 133, 63, alpha: 1.0)
case .pink: Pigment(255, 192, 203, alpha: 1.0)
case .plum: Pigment(221, 160, 221, alpha: 1.0)
case .powderBlue: Pigment(176, 224, 230, alpha: 1.0)
case .purple: Pigment(128, 0, 128, alpha: 1.0)
case .red: Pigment(255, 0, 0, alpha: 1.0)
case .rosyBrown: Pigment(188, 143, 143, alpha: 1.0)
case .royalBlue: Pigment(65, 105, 225, alpha: 1.0)
case .saddleBrown: Pigment(139, 69, 19, alpha: 1.0)
case .salmon: Pigment(250, 128, 114, alpha: 1.0)
case .sandyBrown: Pigment(244, 164, 96, alpha: 1.0)
case .seagreen: Pigment(46, 139, 87, alpha: 1.0)
case .seashell: Pigment(255, 245, 238, alpha: 1.0)
case .sienna: Pigment(160, 82, 45, alpha: 1.0)
case .silver: Pigment(192, 192, 192, alpha: 1.0)
case .skyBlue: Pigment(135, 206, 235, alpha: 1.0)
case .slateBlue: Pigment(106, 90, 205, alpha: 1.0)
case .slateGray: Pigment(112, 128, 144, alpha: 1.0)
case .slateGrey: Pigment(112, 128, 144, alpha: 1.0)
case .snow: Pigment(255, 250, 250, alpha: 1.0)
case .springGreen: Pigment(0, 255, 127, alpha: 1.0)
case .steelBlue: Pigment(70, 130, 180, alpha: 1.0)
case .tan: Pigment(210, 180, 140, alpha: 1.0)
case .teal: Pigment(0, 128, 128, alpha: 1.0)
case .thistle: Pigment(216, 191, 216, alpha: 1.0)
case .tomato: Pigment(255, 99, 71, alpha: 1.0)
case .turquoise: Pigment(64, 224, 208, alpha: 1.0)
case .violet: Pigment(238, 130, 238, alpha: 1.0)
case .wheat: Pigment(245, 222, 179, alpha: 1.0)
case .white: Pigment(255, 255, 255, alpha: 1.0)
case .whitesmoke: Pigment(245, 245, 245, alpha: 1.0)
case .yellow: Pigment(255, 255, 0, alpha: 1.0)
case .yellowGreen: Pigment(154, 205, 50, alpha: 1.0)
}
}
}
init(
_ name: Name,
@Clamping(0 ... 1) alpha: Double = 1.0
) {
red = Double(name.rgb.red) / 255.0
green = Double(name.rgb.green) / 255.0
blue = Double(name.rgb.blue) / 255.0
self.alpha = alpha
}
}

View file

@ -0,0 +1,76 @@
import Foundation
public extension Pigment {
init(_ value: String, alpha: Double = 1.0) {
if let keyword = Name.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
red = keyword.pigment.red
green = keyword.pigment.green
blue = keyword.pigment.blue
self.alpha = alpha
return
}
if ExtendedKeyword.allCases.contains(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
red = 1.0
green = 1.0
blue = 1.0
self.alpha = alpha
return
}
var hex = value
if hex.hasPrefix("#") {
hex = String(hex.dropFirst())
}
guard let hexValue = Int(hex, radix: 16) else {
red = 1.0
green = 1.0
blue = 1.0
self.alpha = alpha
return
}
switch hex.count {
case 3:
let values = Self.hex3(hex: hexValue)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
case 4:
let values = Self.hex4(hex: hexValue)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
case 6:
let values = Self.hex6(hex: hexValue, alpha: alpha)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
#if !os(watchOS)
case 8:
let values = Self.hex8(hex: hexValue)
red = values.red
green = values.green
blue = values.blue
self.alpha = values.alpha
#endif
default:
red = 1.0
green = 1.0
blue = 1.0
self.alpha = alpha
}
}
}
private extension Pigment {
enum ExtendedKeyword: String, CaseIterable {
case none
case clear
case transparent
}
}

View file

@ -0,0 +1,9 @@
#if canImport(SwiftUI)
import SwiftUI
public extension Pigment {
var color: Color {
Color(red: red, green: green, blue: blue, opacity: alpha)
}
}
#endif

View file

@ -0,0 +1,37 @@
import Foundation
#if canImport(UIKit)
import UIKit
public extension Pigment {
init(_ color: UIColor) {
var redComponent: CGFloat = 1.0
var greenComponent: CGFloat = 1.0
var blueComponent: CGFloat = 1.0
var alphaComponent: CGFloat = 1.0
guard color.getRed(&redComponent, green: &greenComponent, blue: &blueComponent, alpha: &alphaComponent) else {
// TODO: Fail Initializer? Default Colors?
red = redComponent
green = greenComponent
blue = blueComponent
alpha = alphaComponent
return
}
red = redComponent
green = greenComponent
blue = blueComponent
alpha = alphaComponent
}
var uiColor: UIColor {
UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}
public extension UIColor {
var pigment: Pigment {
Pigment(self)
}
}
#endif

View file

@ -0,0 +1,53 @@
/// A platform agnostic representation of Color
///
/// The components - red, green, blue, & alpha - are maintained as a floating-point representation.
/// Each value can range from 0.0 to 1.0 (e.g. 0 to 100 percent).
///
/// 'Pure White' is represented by values all equal to **1.0**.
public struct Pigment: Sendable {
public let colorSpace: ColorSpace = .rgba
public let red: Double
public let green: Double
public let blue: Double
public let alpha: Double
public init(
@Clamping(0 ... 1) red: Double = 1.0,
@Clamping(0 ... 1) green: Double = 1.0,
@Clamping(0 ... 1) blue: Double = 1.0,
@Clamping(0 ... 1) alpha: Double = 1.0
) {
self.red = red
self.green = green
self.blue = blue
self.alpha = alpha
}
}
extension Pigment: CustomStringConvertible {
public var description: String {
String(format: "Pigment(red: %.4f, green: %.4f, blue: %.4f, alpha: %.2f)", red, green, blue, alpha)
}
}
extension Pigment: Equatable {
public static func == (lhs: Pigment, rhs: Pigment) -> Bool {
guard lhs.red == rhs.red else {
return false
}
guard lhs.green == rhs.green else {
return false
}
guard lhs.blue == rhs.blue else {
return false
}
guard lhs.alpha == rhs.alpha else {
return false
}
guard lhs.colorSpace == rhs.colorSpace else {
return false
}
return true
}
}

19
third-party/SwiftSVG/BUILD vendored Normal file
View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SwiftSVG",
module_name = "SwiftSVG",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//third-party/XMLCoder",
"//third-party/Swift2D",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,93 @@
import Swift2D
import XMLCoder
/// Basic shape, used to draw circles based on a center point and a radius.
///
/// The arc of a circle element begins at the "3 o'clock" point on the radius and progresses towards the
/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform
/// in the same manner as the geometry of the element.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#CircleElement)
public struct Circle: Element {
/// The x-axis coordinate of the center of the circle.
public var x: Double = 0.0
/// The y-axis coordinate of the center of the circle.
public var y: Double = 0.0
/// The radius of the circle.
public var r: Double = 0.0
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case x = "cx"
case y = "cy"
case r
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(x: Double, y: Double, r: Double) {
self.x = x
self.y = y
self.r = r
}
}
extension Circle: CustomStringConvertible {
public var description: String {
let desc = "<circle cx=\"\(x)\" cy=\"\(y)\" r=\"\(r)\""
return desc + " \(attributeDescription) />"
}
}
extension Circle: DirectionalCommandRepresentable {
public func commands(clockwise: Bool) throws -> [Path.Command] {
EllipseProcessor(circle: self).commands(clockwise: clockwise)
}
}
extension Circle: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Circle: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,15 @@
/// Elements conforming to `CommandRepresentable` can be expressed in the form of `Path.Command`s.
public protocol CommandRepresentable {
func commands() throws -> [Path.Command]
}
public protocol DirectionalCommandRepresentable: CommandRepresentable {
func commands(clockwise: Bool) throws -> [Path.Command]
}
public extension DirectionalCommandRepresentable {
/// Defaults to anti/counter-clockwise commands.
func commands() throws -> [Path.Command] {
try commands(clockwise: false)
}
}

View file

@ -0,0 +1,58 @@
public protocol Container {
var circles: [Circle]? { get set }
var ellipses: [Ellipse]? { get set }
var groups: [Group]? { get set }
var lines: [Line]? { get set }
var paths: [Path]? { get set }
var polygons: [Polygon]? { get set }
var polylines: [Polyline]? { get set }
var rectangles: [Rectangle]? { get set }
var texts: [Text]? { get set }
}
enum ContainerKeys: String, CodingKey {
case circles = "circle"
case ellipses = "ellipse"
case groups = "g"
case lines = "line"
case paths = "path"
case polylines = "polyline"
case polygons = "polygon"
case rectangles = "rect"
case texts = "text"
}
public extension Container {
var containerDescription: String {
var contents: String = ""
let circles = circles?.compactMap(\.description) ?? []
circles.forEach { contents.append("\n\($0)") }
let ellipses = ellipses?.compactMap(\.description) ?? []
ellipses.forEach { contents.append("\n\($0)") }
let groups = groups?.compactMap(\.description) ?? []
groups.forEach { contents.append("\n\($0)") }
let lines = lines?.compactMap(\.description) ?? []
lines.forEach { contents.append("\n\($0)") }
let paths = paths?.compactMap(\.description) ?? []
paths.forEach { contents.append("\n\($0)") }
let polylines = polylines?.compactMap(\.description) ?? []
polylines.forEach { contents.append("\n\($0)") }
let polygons = polygons?.compactMap(\.description) ?? []
polygons.forEach { contents.append("\n\($0)") }
let rectangles = rectangles?.compactMap(\.description) ?? []
rectangles.forEach { contents.append("\n\($0)") }
let texts = texts?.compactMap(\.description) ?? []
texts.forEach { contents.append("\n\($0)") }
return contents
}
}

View file

@ -0,0 +1,17 @@
public protocol CoreAttributes {
var id: String? { get set }
}
enum CoreAttributesKeys: String, CodingKey {
case id
}
public extension CoreAttributes {
var coreDescription: String {
if let id {
"\(CoreAttributesKeys.id.rawValue)=\"\(id)\""
} else {
""
}
}
}

View file

@ -0,0 +1,46 @@
public protocol Element: CoreAttributes, PresentationAttributes, StylingAttributes {}
public extension Element {
var attributeDescription: String {
var components: [String] = []
if !coreDescription.isEmpty {
components.append(coreDescription)
}
if !presentationDescription.isEmpty {
components.append(presentationDescription)
}
if !stylingDescription.isEmpty {
components.append(stylingDescription)
}
return components.joined(separator: " ")
}
}
public extension CommandRepresentable where Self: Element {
/// When a `Path` is accessed on an element, the path that is returned should have the supplied transformations
/// applied.
///
/// For instance, if
/// * a `Path.data` contains relative elements,
/// * and `transformations` contains a `.translate`
///
/// Than the path created will not only use 'absolute' instructions, but those instructions will be modified to
/// include the required transformation.
func path(applying transformations: [Transformation] = []) throws -> Path {
var _transformations = transformations
_transformations.append(contentsOf: self.transformations)
let commands = try commands().map { $0.applying(transformations: _transformations) }
var path = Path(commands: commands)
path.fillColor = fillColor
path.fillOpacity = fillOpacity
path.strokeColor = strokeColor
path.strokeOpacity = strokeOpacity
path.strokeWidth = strokeWidth
return path
}
}

View file

@ -0,0 +1,93 @@
import Swift2D
import XMLCoder
/// SVG basic shape, used to create ellipses based on a center coordinate, and both their x and y radius.
///
/// The arc of an ellipse element begins at the "3 o'clock" point on the radius and progresses towards the
/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same
/// manner as the geometry of the element.
public struct Ellipse: Element {
/// The x position of the ellipse.
public var x: Double = 0.0
/// The y position of the ellipse.
public var y: Double = 0.0
/// The radius of the ellipse on the x axis.
public var rx: Double = 0.0
/// The radius of the ellipse on the y axis.
public var ry: Double = 0.0
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case x = "cx"
case y = "cy"
case rx
case ry
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(x: Double, y: Double, rx: Double, ry: Double) {
self.x = x
self.y = y
self.rx = rx
self.ry = ry
}
}
extension Ellipse: CustomStringConvertible {
public var description: String {
let desc = "<ellipse cx=\"\(x)\" cy=\"\(y)\" rx=\"\(rx)\" ry=\"\(ry)\""
return desc + " \(attributeDescription) />"
}
}
extension Ellipse: DirectionalCommandRepresentable {
public func commands(clockwise: Bool) throws -> [Path.Command] {
EllipseProcessor(ellipse: self).commands(clockwise: clockwise)
}
}
extension Ellipse: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Ellipse: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,37 @@
import Swift2D
extension Point {
static var nan: Point {
Point(x: Double.nan, y: Double.nan)
}
var hasNaN: Bool {
x.isNaN || y.isNaN
}
/// Returns a copy of the instance with the **x** value replaced with the provided value.
func with(x value: Double) -> Point {
Point(x: value, y: y)
}
/// Returns a copy of the instance with the **y** value replaced with the provided value.
func with(y value: Double) -> Point {
Point(x: x, y: value)
}
/// Adjusts the **x** value by the provided amount.
///
/// This will explicitly check for `.isNaN`, and if encountered, will simply
/// use the provided value.
func adjusting(x value: Double) -> Point {
(x.isNaN) ? with(x: value) : with(x: x + value)
}
/// Adjusts the **y** value by the provided amount.
///
/// This will explicitly check for `.isNaN`, and if encountered, will simply
/// use the provided value.
func adjusting(y value: Double) -> Point {
(y.isNaN) ? with(y: value) : with(y: y + value)
}
}

44
third-party/SwiftSVG/Sources/Fill.swift vendored Normal file
View file

@ -0,0 +1,44 @@
import Swift2D
public struct Fill {
public var color: String?
public var opacity: Double?
public var rule: Rule = .nonZero
public init() {}
/// Presentation attribute defining the algorithm to use to determine the inside part of a shape.
///
/// The default `Rule` is `.nonzero`.
public enum Rule: String, Sendable, Codable, CaseIterable {
/// The value evenodd determines the "insideness" of a point in the shape by drawing a ray from that point to
/// infinity in any direction and counting the number of path segments from the given shape that the ray
/// crosses. If this number is odd, the point is inside; if even, the point is outside.
case evenOdd = "evenodd"
/// The value nonzero determines the "insideness" of a point in the shape by drawing a ray from that point to
/// infinity in any direction, and then examining the places where a segment of the shape crosses the ray.
/// Starting with a count of zero, add one each time a path segment crosses the ray from left to right and
/// subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if
/// the result is zero then the point is outside the path. Otherwise, it is inside.
case nonZero = "nonzero"
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
guard let rule = Rule(rawValue: rawValue) else {
print("Attempts to decode Fill.Rule with rawValue: '\(rawValue)'")
self = .nonZero
return
}
self = rule
}
}
}
extension Fill.Rule: CustomStringConvertible {
public var description: String {
rawValue
}
}

152
third-party/SwiftSVG/Sources/Group.swift vendored Normal file
View file

@ -0,0 +1,152 @@
import XMLCoder
/// A container used to group other SVG elements.
///
/// Grouping constructs, when used in conjunction with the desc and title elements, provide information
/// about document structure and semantics.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
/// | [W3](https://www.w3.org/TR/SVG11/struct.html#Groups)
public struct Group: Container, Element {
// Container
public var circles: [Circle]?
public var ellipses: [Ellipse]?
public var groups: [Group]?
public var lines: [Line]?
public var paths: [Path]?
public var polygons: [Polygon]?
public var polylines: [Polyline]?
public var rectangles: [Rectangle]?
public var texts: [Text]?
// MARK: CoreAttributes
public var id: String?
public var title: String?
public var desc: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case circles = "circle"
case ellipses = "ellipse"
case groups = "g"
case lines = "line"
case paths = "path"
case polylines = "polyline"
case polygons = "polygon"
case rectangles = "rect"
case texts = "text"
case id
case title
case desc
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
/// A representation of all the sub-`Path`s in the `Group`.
public func subpaths(applying transformations: [Transformation] = []) throws -> [Path] {
var _transformations = transformations
_transformations.append(contentsOf: self.transformations)
var output: [Path] = []
if let circles {
try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
}
if let ellipses {
try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
}
if let rectangles {
try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
}
if let polygons {
try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
}
if let polylines {
try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
}
if let paths {
try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
}
if let groups {
try groups.forEach {
try output.append(contentsOf: $0.subpaths(applying: _transformations))
}
}
return output
}
}
extension Group: CustomStringConvertible {
public var description: String {
var contents: String = ""
if let title {
contents.append("\n<title>\(title)</title>")
}
if let desc {
contents.append("\n<desc>\(desc)</desc>")
}
contents.append(containerDescription)
return "<g \(attributeDescription) >\(contents)\n</g>"
}
}
extension Group: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
if let _ = ContainerKeys(stringValue: key.stringValue) {
return .element
}
return .attribute
}
}
extension Group: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
if let _ = ContainerKeys(stringValue: key.stringValue) {
return .element
}
return .attribute
}
}

View file

@ -0,0 +1,88 @@
import Foundation
import Swift2D
struct EllipseProcessor {
let x: Double
let y: Double
let rx: Double
let ry: Double
/// The _optimal_ offset for control points when representing a
/// circle/ellipse as 4 bezier curves.
///
/// [Stack Overflow](https://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves)
static func controlPointOffset(_ radius: Double) -> Double {
(Double(4.0 / 3.0) * tan(Double.pi / 8.0)) * radius
}
init(ellipse: Ellipse) {
x = ellipse.x
y = ellipse.y
rx = ellipse.rx
ry = ellipse.ry
}
init(circle: Circle) {
x = circle.x
y = circle.y
rx = circle.r
ry = circle.r
}
func commands(clockwise: Bool) -> [Path.Command] {
var commands: [Path.Command] = []
let xOffset = Self.controlPointOffset(rx)
let yOffset = Self.controlPointOffset(ry)
let zero = Point(x: x + rx, y: y)
let ninety = Point(x: x, y: y - ry)
let oneEighty = Point(x: x - rx, y: y)
let twoSeventy = Point(x: x, y: y + ry)
var cp1: Point = .zero
var cp2: Point = .zero
// Starting at degree 0 (the right most point)
commands.append(.moveTo(point: zero))
if clockwise {
cp1 = Point(x: zero.x, y: zero.y + yOffset)
cp2 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
cp1 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
cp2 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
cp1 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
cp2 = Point(x: ninety.x - xOffset, y: ninety.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
cp1 = Point(x: ninety.x + xOffset, y: ninety.y)
cp2 = Point(x: zero.x, y: zero.y - yOffset)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
} else {
cp1 = Point(x: zero.x, y: zero.y - yOffset)
cp2 = Point(x: ninety.x + xOffset, y: ninety.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
cp1 = Point(x: ninety.x - xOffset, y: ninety.y)
cp2 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
cp1 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
cp2 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
cp1 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
cp2 = Point(x: zero.x, y: zero.y + yOffset)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
}
commands.append(.closePath)
return commands
}
}

View file

@ -0,0 +1,12 @@
import Foundation
struct PathProcessor {
let data: String
func commands() throws -> [Path.Command] {
let parser = Path.ComponentParser()
let components = try Path.Component.components(from: data)
return try parser.parse(components)
}
}

View file

@ -0,0 +1,52 @@
import Foundation
import Swift2D
struct PolygonProcessor {
let points: String
func commands() throws -> [Path.Command] {
let pairs = points.components(separatedBy: " ")
let components = pairs.flatMap { $0.components(separatedBy: ",") }
guard components.count > 0 else {
return []
}
guard components.count % 2 == 0 else {
// An odd number of components means that parsing probably failed
return []
}
var commands: [Path.Command] = []
var firstValue: Bool = true
for (idx, component) in components.enumerated() {
guard let _value = Double(component) else {
return commands
}
let value = Double(_value)
if firstValue {
if idx == 0 {
commands.append(.moveTo(point: Point(x: value, y: .nan)))
} else {
commands.append(.lineTo(point: Point(x: value, y: .nan)))
}
firstValue = false
} else {
let count = commands.count
guard let modified = try? commands.last?.adjustingArgument(at: 1, by: value) else {
return commands
}
commands[count - 1] = modified
firstValue = true
}
}
commands.append(.closePath)
return commands
}
}

View file

@ -0,0 +1,54 @@
import Foundation
import Swift2D
struct PolylineProcessor {
let points: String
func commands() throws -> [Path.Command] {
let pairs = points.components(separatedBy: " ")
let components = pairs.flatMap { $0.components(separatedBy: ",") }
let values = components.compactMap { Double($0) }.map { Double($0) }
guard values.count > 2 else {
// More than just a starting point is required.
return []
}
guard values.count % 2 == 0 else {
// An odd number of components means that parsing probably failed
return []
}
var commands: [Path.Command] = []
let move = values.prefix(upTo: 2)
let segments = values.suffix(from: 2)
commands.append(.moveTo(point: Point(x: move[0], y: move[1])))
var _value: Double = .nan
for value in segments {
if _value.isNaN {
_value = value
} else {
commands.append(.lineTo(point: Point(x: _value, y: value)))
_value = .nan
}
}
let reversedSegments = segments.dropLast(2).reversed()
for value in reversedSegments {
if _value.isNaN {
_value = value
} else {
commands.append(.lineTo(point: Point(x: _value, y: value)))
_value = .nan
}
}
commands.append(.closePath)
return commands
}
}

View file

@ -0,0 +1,175 @@
import Swift2D
struct RectangleProcessor {
let rectangle: Rectangle
func commands(clockwise: Bool) -> [Path.Command] {
var rx = rectangle.rx
var ry = rectangle.ry
if let _rx = rx, _rx > (rectangle.width / 2.0) {
rx = rectangle.width / 2.0
}
if let _ry = ry, _ry > (rectangle.height / 2.0) {
ry = rectangle.height / 2.0
}
var commands: [Path.Command] = []
switch (rx, ry) {
case (.some(let radiusX), .some(let radiusY)) where radiusX != radiusY:
// Use Cubic Bezier Curve to form rounded corners
// TODO: Verify that the control points are right
var cp1: Point = .zero
var cp2: Point = .zero
var point: Point = Point(x: rectangle.x + radiusX, y: rectangle.y)
commands.append(.moveTo(point: point))
if clockwise {
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
cp2 = cp1
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
cp2 = cp1
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
cp2 = cp1
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x, y: rectangle.y + radiusY)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x, y: rectangle.y)
cp2 = cp1
point = .init(x: rectangle.x + radiusX, y: rectangle.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
} else {
cp1 = .init(x: rectangle.x, y: rectangle.y)
cp2 = cp1
point = .init(x: rectangle.x, y: rectangle.y + radiusY)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
cp2 = cp1
point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
cp2 = cp1
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
commands.append(.lineTo(point: point))
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
cp2 = cp1
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
}
case (.some(let radius), .none), (.none, .some(let radius)), (.some(let radius), _):
// use Quadratic Bezier Curve to form rounded corners
var cp: Point = .zero
var point: Point = Point(x: rectangle.x + radius, y: rectangle.y)
commands.append(.moveTo(point: point))
if clockwise {
point = .init(x: (rectangle.x + rectangle.width) - radius, y: rectangle.y)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x + rectangle.width, y: (rectangle.y + rectangle.height) - radius)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x, y: rectangle.y + radius)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x, y: rectangle.y)
point = .init(x: rectangle.x + radius, y: rectangle.y)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
} else {
cp = .init(x: rectangle.x, y: rectangle.y)
point = .init(x: rectangle.x, y: rectangle.y + radius)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radius)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
commands.append(.lineTo(point: point))
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y)
commands.append(.quadraticBezierCurve(cp: cp, point: point))
}
case (.none, .none):
// draw three line segments.
commands.append(.moveTo(point: Point(x: rectangle.x, y: rectangle.y)))
if clockwise {
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
} else {
commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
}
}
commands.append(.closePath)
return commands
}
}

98
third-party/SwiftSVG/Sources/Line.swift vendored Normal file
View file

@ -0,0 +1,98 @@
import Swift2D
import XMLCoder
/// SVG basic shape used to create a line connecting two points.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
/// | [W3](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
public struct Line: Element {
/// Defines the x-axis coordinate of the line starting point.
public var x1: Double = 0.0
/// Defines the x-axis coordinate of the line ending point.
public var y1: Double = 0.0
/// Defines the y-axis coordinate of the line starting point.
public var x2: Double = 0.0
/// Defines the y-axis coordinate of the line ending point.
public var y2: Double = 0.0
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case x1
case y1
case x2
case y2
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(x1: Double, y1: Double, x2: Double, y2: Double) {
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
}
}
extension Line: CommandRepresentable {
public func commands() throws -> [Path.Command] {
[
.moveTo(point: Point(x: x1, y: y1)),
.lineTo(point: Point(x: x2, y: y2)),
.lineTo(point: Point(x: x1, y: y1)),
.closePath,
]
}
}
extension Line: CustomStringConvertible {
public var description: String {
let desc = "<line x1=\"\(x1)\" y1=\"\(y1)\" x2=\"\(x2)\" y2=\"\(y2)\""
return desc + " \(attributeDescription) />"
}
}
extension Line: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Line: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,276 @@
import Foundation
import Swift2D
public extension Path {
/// Path commands are instructions that define a path to be drawn.
///
/// Each command is composed of a command letter and numbers that represent the command parameters.
enum Command: Equatable, Sendable, CustomStringConvertible {
/// Moves the current drawing point
case moveTo(point: Point)
/// Draw a straight line from the current point to the point provided
case lineTo(point: Point)
/// Draw a smooth curve using three points (+ origin)
case cubicBezierCurve(cp1: Point, cp2: Point, point: Point)
/// Draw a smooth curve using two points (+ origin)
case quadraticBezierCurve(cp: Point, point: Point)
/// Draw a curve defined as a portion of an ellipse
case ellipticalArcCurve(rx: Double, ry: Double, angle: Double, largeArc: Bool, clockwise: Bool, point: Point)
/// ClosePath instructions draw a straight line from the current position to the first point in the path.
case closePath
public enum Prefix: Character, CaseIterable {
case move = "M"
case relativeMove = "m"
case line = "L"
case relativeLine = "l"
case horizontalLine = "H"
case relativeHorizontalLine = "h"
case verticalLine = "V"
case relativeVerticalLine = "v"
case cubicBezierCurve = "C"
case relativeCubicBezierCurve = "c"
case smoothCubicBezierCurve = "S"
case relativeSmoothCubicBezierCurve = "s"
case quadraticBezierCurve = "Q"
case relativeQuadraticBezierCurve = "q"
case smoothQuadraticBezierCurve = "T"
case relativeSmoothQuadraticBezierCurve = "t"
case ellipticalArcCurve = "A"
case relativeEllipticalArcCurve = "a"
case close = "Z"
case relativeClose = "z"
public static var characterSet: CharacterSet {
CharacterSet(charactersIn: allCases.map { String($0.rawValue) }.joined())
}
}
public enum Coordinates {
case absolute
case relative
}
public enum Error: Swift.Error {
case message(String)
case invalidAdjustment(Path.Command)
case invalidArgumentPosition(Int, Path.Command)
case invalidRelativeCommand
}
public var description: String {
switch self {
case .moveTo(let point):
return "\(Prefix.move.rawValue)\(point.x),\(point.y)"
case .lineTo(let point):
return "\(Prefix.line.rawValue)\(point.x),\(point.y)"
case .cubicBezierCurve(let cp1, let cp2, let point):
return "\(Prefix.cubicBezierCurve.rawValue)\(cp1.x),\(cp1.y) \(cp2.x),\(cp2.y) \(point.x),\(point.y)"
case .quadraticBezierCurve(let cp, let point):
return "\(Prefix.quadraticBezierCurve.rawValue)\(cp.x),\(cp.y) \(point.x),\(point.y)"
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
let la = largeArc ? 1 : 0
let cw = clockwise ? 1 : 0
return "\(Prefix.ellipticalArcCurve.rawValue)\(rx) \(ry) \(angle) \(la) \(cw) \(point.x) \(point.y)"
case .closePath:
return "\(Prefix.close.rawValue)"
}
}
/// The primary point that dictates the commands action.
public var point: Point {
switch self {
case .moveTo(let point): point
case .lineTo(let point): point
case .cubicBezierCurve(_, _, let point): point
case .quadraticBezierCurve(_, let point): point
case .ellipticalArcCurve(_, _, _, _, _, let point): point
case .closePath: .zero
}
}
}
}
public extension Path.Command {
/// Applies the provided `Transformation` to the instances values.
func applying(transformation: Transformation) -> Path.Command {
switch transformation {
case .translate(let x, let y):
switch self {
case .moveTo(let point):
let _point = point.adjusting(x: x).adjusting(y: y)
return .moveTo(point: _point)
case .lineTo(let point):
let _point = point.adjusting(x: x).adjusting(y: y)
return .lineTo(point: _point)
case .cubicBezierCurve(let cp1, let cp2, let point):
let _cp1 = cp1.adjusting(x: x).adjusting(y: y)
let _cp2 = cp2.adjusting(x: x).adjusting(y: y)
let _point = point.adjusting(x: x).adjusting(y: y)
return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
case .quadraticBezierCurve(let cp, let point):
let _cp = cp.adjusting(x: x).adjusting(y: y)
let _point = point.adjusting(x: x).adjusting(y: y)
return .quadraticBezierCurve(cp: _cp, point: _point)
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
let _point = point.adjusting(x: x).adjusting(y: y)
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
case .closePath:
return self
}
case .matrix:
// TODO: What should occur here?
return self
}
}
/// Applies multiple transformations in the order they are specified.
func applying(transformations: [Transformation]) -> Path.Command {
var command = self
for transformation in transformations {
command = command.applying(transformation: transformation)
}
return command
}
}
extension Path.Command {
/// Determines if all values are provided (i.e. !.isNaN)
var isComplete: Bool {
switch self {
case .moveTo(let point), .lineTo(let point):
!point.hasNaN
case .cubicBezierCurve(let cp1, let cp2, let point):
!cp1.hasNaN && !cp2.hasNaN && !point.hasNaN
case .quadraticBezierCurve(let cp, let point):
!cp.hasNaN && !point.hasNaN
case .ellipticalArcCurve(let rx, let ry, let angle, _, _, let point):
!rx.isNaN && !ry.isNaN && !angle.isNaN && !point.hasNaN
case .closePath:
true
}
}
/// The last control point used in drawing the path.
///
/// Only valid for curves.
var lastControlPoint: Point? {
switch self {
case .cubicBezierCurve(_, let cp2, _):
cp2
case .quadraticBezierCurve(let cp, _):
cp
default:
nil
}
}
/// A mirror representation of `lastControlPoint`.
var lastControlPointMirror: Point? {
guard let cp = lastControlPoint else {
return nil
}
return Point(x: point.x + (point.x - cp.x), y: point.y + (point.y - cp.y))
}
/// The total number of argument values the command requires.
var arguments: Int {
switch self {
case .moveTo: 2
case .lineTo: 2
case .cubicBezierCurve: 6
case .quadraticBezierCurve: 4
case .ellipticalArcCurve: 7
case .closePath: 0
}
}
/// Adjusts a Command argument by a specified amount.
///
/// A `Point` consumes two positions. So, in the example `.quadraticBezierCurve(cp: .zero, point: .zero)`:
/// * position 0 = Control Point X
/// * position 1 = Control Point Y
/// * position 2 = Point X
/// * position 3 = Point Y
///
/// - parameter position: The index of the argument parameter to adjust.
/// - parameter value: The value to add to the existing value. If the current value equal `.isNaN`, than the
/// supplied value is used as-is.
/// - throws: `Path.Command.Error`
func adjustingArgument(at position: Int, by value: Double) throws -> Path.Command {
switch self {
case .moveTo(let point):
switch position {
case 0:
return .moveTo(point: point.adjusting(x: value))
case 1:
return .moveTo(point: point.adjusting(y: value))
default:
throw Path.Command.Error.invalidArgumentPosition(position, self)
}
case .lineTo(let point):
switch position {
case 0:
return .lineTo(point: point.adjusting(x: value))
case 1:
return .lineTo(point: point.adjusting(y: value))
default:
throw Path.Command.Error.invalidArgumentPosition(position, self)
}
case .cubicBezierCurve(let cp1, let cp2, let point):
switch position {
case 0:
return .cubicBezierCurve(cp1: cp1.adjusting(x: value), cp2: cp2, point: point)
case 1:
return .cubicBezierCurve(cp1: cp1.adjusting(y: value), cp2: cp2, point: point)
case 2:
return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(x: value), point: point)
case 3:
return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(y: value), point: point)
case 4:
return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(x: value))
case 5:
return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(y: value))
default:
throw Path.Command.Error.invalidArgumentPosition(position, self)
}
case .quadraticBezierCurve(let cp, let point):
switch position {
case 0:
return .quadraticBezierCurve(cp: cp.adjusting(x: value), point: point)
case 1:
return .quadraticBezierCurve(cp: cp.adjusting(y: value), point: point)
case 2:
return .quadraticBezierCurve(cp: cp, point: point.adjusting(x: value))
case 3:
return .quadraticBezierCurve(cp: cp, point: point.adjusting(y: value))
default:
throw Path.Command.Error.invalidArgumentPosition(position, self)
}
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
switch position {
case 0:
return .ellipticalArcCurve(rx: value, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
case 1:
return .ellipticalArcCurve(rx: rx, ry: value, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
case 2:
return .ellipticalArcCurve(rx: rx, ry: ry, angle: value, largeArc: largeArc, clockwise: clockwise, point: point)
case 3:
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: !value.isZero, clockwise: clockwise, point: point)
case 4:
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: !value.isZero, point: point)
case 5:
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(x: value))
case 6:
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(y: value))
default:
throw Path.Command.Error.invalidArgumentPosition(position, self)
}
case .closePath:
throw Path.Command.Error.invalidAdjustment(self)
}
}
}

View file

@ -0,0 +1,98 @@
import Foundation
public extension Path {
/// A unit of a SVG path data string.
enum Component {
case prefix(Command.Prefix)
case value(Double)
/// Interprets a `Path` `data` attribute into individual `Component`s for command processing.
public static func components(from data: String) throws -> [Component] {
var blocks: [String] = []
var block: String = ""
for scalar in data.unicodeScalars {
if scalar == "e" {
// Account for exponential value notation.
block.append(String(scalar))
continue
}
if CharacterSet.letters.contains(scalar) {
if !block.isEmpty {
blocks.append(block)
block = ""
}
blocks.append(String(scalar))
continue
}
if CharacterSet.whitespaces.contains(scalar) {
if !block.isEmpty {
blocks.append(block)
block = ""
}
continue
}
if CharacterSet(charactersIn: ",").contains(scalar) {
if !block.isEmpty {
blocks.append(block)
block = ""
}
continue
}
if CharacterSet(charactersIn: "-").contains(scalar) {
if !block.isEmpty, block.last != "e" {
// Again, account for exponential values.
blocks.append(block)
block = ""
}
block.append(String(scalar))
continue
}
if CharacterSet(charactersIn: ".").contains(scalar) {
if block.contains(".") {
// Already decimal value, this is a new value
blocks.append(block)
block = ""
}
block.append(String(scalar))
continue
}
if CharacterSet.decimalDigits.contains(scalar) {
block.append(String(scalar))
continue
}
print("Unhandled Character: \(scalar)")
}
if !block.isEmpty {
blocks.append(block)
block = ""
}
return blocks
.filter { !$0.isEmpty }
.compactMap {
if let prefix = Path.Command.Prefix(rawValue: $0.first!) {
.prefix(prefix)
} else if let value = Double($0) {
.value(value)
} else {
// throw in the future?
nil
}
}
}
}
}

View file

@ -0,0 +1,275 @@
import Swift2D
public extension Path {
/// Utility used to construct a collection of `Path.Command` from a collection of `Path.Component`.
class ComponentParser {
/// The command currently being built
private var command: Path.Command?
/// Coordinate system being used
private var coordinates: Path.Command.Coordinates = .absolute
/// The argument position of the _command_ to be processed.
private var position: Int = 0
/// Indicates that only a single value will be processed on the next component pass.
private var singleValue: Bool = false
/// The originating coordinates of the path.
private var pathOrigin: Point = .nan
/// The last point as processed by the parser.
private var currentPoint: Point = .zero
public init() {}
public func parse(_ components: [Path.Component]) throws -> [Path.Command] {
var commands: [Path.Command] = []
try components.forEach { component in
if let command = try parse(component, lastCommand: commands.last) {
commands.append(command)
}
}
return commands
}
private func parse(_ component: Path.Component, lastCommand: Path.Command?) throws -> Path.Command? {
switch component {
case .prefix(let prefix):
setup(prefix: prefix, lastCommand: lastCommand)
case .value(let value):
try process(value: value, lastCommand: lastCommand)
}
}
private func setup(prefix: Path.Command.Prefix, lastCommand: Path.Command?) -> Path.Command? {
position = 0
singleValue = false
switch prefix {
case .move:
command = .moveTo(point: .nan)
coordinates = .absolute
case .relativeMove:
command = .moveTo(point: currentPoint)
coordinates = .relative
case .line:
command = .lineTo(point: .nan)
coordinates = .absolute
case .relativeLine:
command = .lineTo(point: currentPoint)
coordinates = .relative
case .horizontalLine:
command = .lineTo(point: currentPoint.with(x: .nan))
coordinates = .absolute
case .relativeHorizontalLine:
command = .lineTo(point: currentPoint)
coordinates = .relative
singleValue = true
case .verticalLine:
command = .lineTo(point: currentPoint.with(y: .nan))
coordinates = .absolute
position = 1
case .relativeVerticalLine:
command = .lineTo(point: currentPoint)
coordinates = .relative
position = 1
singleValue = true
case .cubicBezierCurve:
command = .cubicBezierCurve(cp1: .nan, cp2: .nan, point: .nan)
coordinates = .absolute
case .relativeCubicBezierCurve:
command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
coordinates = .relative
case .smoothCubicBezierCurve:
if case .cubicBezierCurve(_, let cp, _) = lastCommand {
command = .cubicBezierCurve(cp1: cp.reflecting(around: currentPoint), cp2: .nan, point: .nan)
} else {
command = .cubicBezierCurve(cp1: currentPoint, cp2: .nan, point: .nan)
}
coordinates = .absolute
position = 2
case .relativeSmoothCubicBezierCurve:
if case .cubicBezierCurve(_, let cp, _) = lastCommand {
command = .cubicBezierCurve(cp1: cp.reflecting(around: cp.reflecting(around: currentPoint)), cp2: currentPoint, point: currentPoint)
} else {
command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
}
coordinates = .relative
position = 2
case .quadraticBezierCurve:
command = .quadraticBezierCurve(cp: .nan, point: .nan)
coordinates = .absolute
case .relativeQuadraticBezierCurve:
command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
coordinates = .relative
case .smoothQuadraticBezierCurve:
if case .quadraticBezierCurve(let cp, _) = lastCommand {
command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: .nan)
} else {
command = .quadraticBezierCurve(cp: currentPoint, point: .nan)
}
coordinates = .absolute
position = 2
case .relativeSmoothQuadraticBezierCurve:
if case .quadraticBezierCurve(let cp, _) = lastCommand {
command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: currentPoint)
} else {
command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
}
coordinates = .relative
position = 2
case .ellipticalArcCurve:
command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
coordinates = .absolute
case .relativeEllipticalArcCurve:
command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: currentPoint)
coordinates = .relative
case .close, .relativeClose:
currentPoint = pathOrigin
reset()
return .closePath
}
return nil
}
private func process(value: Double, lastCommand: Path.Command?) throws -> Path.Command? {
if let command {
try continueCommand(command, with: value)
} else {
try nextCommand(with: value, lastCommand: lastCommand)
}
if let command, command.isComplete {
switch coordinates {
case .relative:
guard position == -1 else {
return nil
}
fallthrough
case .absolute:
currentPoint = command.point
if case .moveTo = command {
pathOrigin = command.point
}
reset()
return command
}
} else {
return nil
}
}
private func continueCommand(_ command: Path.Command, with value: Double) throws {
switch command {
case .moveTo, .cubicBezierCurve, .quadraticBezierCurve, .ellipticalArcCurve:
self.command = try command.adjustingArgument(at: position, by: value)
switch coordinates {
case .absolute:
position += 1
case .relative:
switch position {
case 0 ... (command.arguments - 2):
position += 1
case command.arguments - 1:
position = -1
default:
break // throw?
}
}
case .lineTo:
self.command = try command.adjustingArgument(at: position, by: value)
switch coordinates {
case .absolute:
position += 1
case .relative:
switch position {
case 0:
if singleValue {
singleValue = false
position = -1
} else {
position += 1
}
case 1:
if singleValue {
singleValue = false
}
position = -1
default:
break // throw?
}
}
case .closePath:
break
}
}
private func nextCommand(with value: Double, lastCommand: Path.Command?) throws {
guard let command = lastCommand else {
throw Path.Command.Error.invalidRelativeCommand
}
switch command {
case .moveTo:
switch coordinates {
case .absolute:
self.command = .lineTo(point: Point(x: value, y: .nan))
position = 1
case .relative:
let c = Path.Command.lineTo(point: command.point)
self.command = try c.adjustingArgument(at: 0, by: value)
position = 1
}
case .lineTo:
switch coordinates {
case .absolute:
self.command = .lineTo(point: Point(x: value, y: .nan))
position = 1
case .relative:
let c = Path.Command.lineTo(point: command.point)
self.command = try c.adjustingArgument(at: 0, by: value)
position = 1
}
case .cubicBezierCurve:
switch coordinates {
case .absolute:
self.command = .cubicBezierCurve(cp1: Point(x: value, y: .nan), cp2: .nan, point: .nan)
position = 1
case .relative:
let c = Path.Command.cubicBezierCurve(cp1: command.point, cp2: command.point, point: command.point)
self.command = try c.adjustingArgument(at: 0, by: value)
position = 1
}
case .quadraticBezierCurve:
switch coordinates {
case .absolute:
self.command = .quadraticBezierCurve(cp: Point(x: value, y: .nan), point: .nan)
position = 1
case .relative:
let c = Path.Command.quadraticBezierCurve(cp: command.point, point: command.point)
self.command = try c.adjustingArgument(at: 0, by: value)
position = 1
}
case .ellipticalArcCurve:
switch coordinates {
case .absolute:
self.command = .ellipticalArcCurve(rx: value, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
position = 1
case .relative:
let c = Path.Command.ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: command.point)
self.command = try c.adjustingArgument(at: 0, by: value)
position = 1
}
case .closePath:
break
}
}
private func reset() {
command = nil
coordinates = .absolute
position = 0
singleValue = false
}
}
}

101
third-party/SwiftSVG/Sources/Path.swift vendored Normal file
View file

@ -0,0 +1,101 @@
import XMLCoder
/// Generic element to define a shape.
///
/// A path is defined by including a path element in a SVG document which contains a **d="(path data)"**
/// attribute, where the **d** attribute contains the moveto, line, curve (both Cubic and Quadratic Bézier),
/// arc and closepath instructions.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
/// | [W3](https://www.w3.org/TR/SVG11/paths.html)
public struct Path: Element {
/// The definition of the outline of a shape.
public var data: String = ""
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case data = "d"
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(data: String) {
self.init()
self.data = data
}
public init(commands: [Path.Command]) {
self.init()
data = commands.map(\.description).joined()
}
}
extension Path: CommandRepresentable {
public func commands() throws -> [Command] {
try PathProcessor(data: data).commands()
}
}
extension Path: CustomStringConvertible {
public var description: String {
"<path d=\"\(data)\" \(attributeDescription) />"
}
}
extension Path: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Path: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}
extension Path: Equatable {
public static func == (lhs: Path, rhs: Path) -> Bool {
do {
let lhsCommands = try lhs.commands()
let rhsCommands = try rhs.commands()
return lhsCommands == rhsCommands
} catch {
return false
}
}
}

View file

@ -0,0 +1,82 @@
import XMLCoder
/// Defines a closed shape consisting of a set of connected straight line segments.
///
/// The last point is connected to the first point. For open shapes, see the `Polyline` element. If an odd number of
/// coordinates is provided, then the element is in error.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolygonElement)
public struct Polygon: Element {
/// The points that make up the polygon.
public var points: String = ""
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case points
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(points: String) {
self.points = points
}
}
extension Polygon: CommandRepresentable {
public func commands() throws -> [Path.Command] {
try PolygonProcessor(points: points).commands()
}
}
extension Polygon: CustomStringConvertible {
public var description: String {
"<polygon points=\"\(points)\" \(attributeDescription) />"
}
}
extension Polygon: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Polygon: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,81 @@
import XMLCoder
/// SVG basic shape that creates straight lines connecting several points.
///
/// Typically a polyline is used to create open shapes as the last point doesn't have to be connected to the first
/// point. For closed shapes see the `Polygon` element.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolylineElement)
public struct Polyline: Element {
public var points: String = ""
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case points
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(points: String) {
self.points = points
}
}
extension Polyline: CommandRepresentable {
public func commands() throws -> [Path.Command] {
try PolylineProcessor(points: points).commands()
}
}
extension Polyline: CustomStringConvertible {
public var description: String {
"<polyline points=\"\(points)\" \(attributeDescription) />"
}
}
extension Polyline: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Polyline: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,120 @@
import Foundation
import Swift2D
public protocol PresentationAttributes {
var fillColor: String? { get set }
var fillOpacity: Double? { get set }
var fillRule: Fill.Rule? { get set }
var strokeColor: String? { get set }
var strokeWidth: Double? { get set }
var strokeOpacity: Double? { get set }
var strokeLineCap: Stroke.LineCap? { get set }
var strokeLineJoin: Stroke.LineJoin? { get set }
var strokeMiterLimit: Double? { get set }
var transform: String? { get set }
}
enum PresentationAttributesKeys: String, CodingKey {
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
}
public extension PresentationAttributes {
var presentationDescription: String {
var attributes: [String] = []
if let fillColor {
attributes.append("\(PresentationAttributesKeys.fillColor.rawValue)=\"\(fillColor)\"")
}
if let fillOpacity {
attributes.append("\(PresentationAttributesKeys.fillOpacity.rawValue)=\"\(fillOpacity)\"")
}
if let fillRule {
attributes.append("\(PresentationAttributesKeys.fillRule.rawValue)=\"\(fillRule.description)\"")
}
if let strokeColor {
attributes.append("\(PresentationAttributesKeys.strokeColor.rawValue)=\"\(strokeColor)\"")
}
if let strokeWidth {
attributes.append("\(PresentationAttributesKeys.strokeWidth.rawValue)=\"\(strokeWidth)\"")
}
if let strokeOpacity {
attributes.append("\(PresentationAttributesKeys.strokeOpacity.rawValue)=\"\(strokeOpacity)\"")
}
if let strokeLineCap {
attributes.append("\(PresentationAttributesKeys.strokeLineCap.rawValue)=\"\(strokeLineCap.description)\"")
}
if let strokeLineJoin {
attributes.append("\(PresentationAttributesKeys.strokeLineJoin.rawValue)=\"\(strokeLineJoin.description)\"")
}
if let strokeMiterLimit {
attributes.append("\(PresentationAttributesKeys.strokeMiterLimit.rawValue)=\"\(strokeMiterLimit)\"")
}
if let transform {
attributes.append("\(PresentationAttributesKeys.transform.rawValue)=\"\(transform)\"")
}
return attributes.joined(separator: " ")
}
var transformations: [Transformation] {
let value = transform?.replacingOccurrences(of: " ", with: "") ?? ""
guard !value.isEmpty else {
return []
}
let values = value.split(separator: ")").map { $0.appending(")") }
return values.compactMap { Transformation($0) }
}
var fill: Fill? {
get {
if fillColor == nil, fillOpacity == nil {
return nil
}
var fill = Fill()
fill.color = fillColor ?? "black"
fill.opacity = fillOpacity ?? 1.0
return fill
}
set {
fillColor = newValue?.color
fillOpacity = newValue?.opacity
fillRule = newValue?.rule
}
}
var stroke: Stroke? {
get {
if strokeColor == nil, strokeOpacity == nil {
return nil
}
var stroke = Stroke()
stroke.color = strokeColor ?? "black"
stroke.opacity = strokeOpacity ?? 1.0
stroke.width = strokeWidth ?? 1.0
stroke.lineCap = strokeLineCap ?? .butt
stroke.lineJoin = strokeLineJoin ?? .miter
stroke.miterLimit = strokeMiterLimit
return stroke
}
set {
strokeColor = newValue?.color
strokeOpacity = newValue?.opacity
strokeWidth = newValue?.width
strokeLineCap = newValue?.lineCap
strokeLineJoin = newValue?.lineJoin
strokeMiterLimit = newValue?.miterLimit
}
}
}

View file

@ -0,0 +1,115 @@
import Swift2D
import XMLCoder
/// Basic SVG shape that draws rectangles, defined by their position, width, and height.
///
/// The values used for the x- and y-axis rounded corner radii are determined implicitly
/// if the rx or ry attributes (or both) are not specified, or are specified but with invalid values.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#RectElement)
public struct Rectangle: Element {
/// The x-axis coordinate of the side of the rectangle which
/// has the smaller x-axis coordinate value.
public var x: Double = 0.0
/// The y-axis coordinate of the side of the rectangle which
/// has the smaller y-axis coordinate value
public var y: Double = 0.0
/// The width of the rectangle.
public var width: Double = 0.0
/// The height of the rectangle.
public var height: Double = 0.0
/// For rounded rectangles, the x-axis radius of the ellipse used
/// to round off the corners of the rectangle.
public var rx: Double?
/// For rounded rectangles, the y-axis radius of the ellipse used
/// to round off the corners of the rectangle.
public var ry: Double?
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case x
case y
case width
case height
case rx
case ry
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(x: Double, y: Double, width: Double, height: Double, rx: Double? = nil, ry: Double? = nil) {
self.x = x
self.y = y
self.width = width
self.height = height
self.rx = rx
self.ry = ry
}
}
extension Rectangle: CustomStringConvertible {
public var description: String {
var desc = "<rect x=\"\(x)\" y=\"\(y)\" width=\"\(width)\" height=\"\(height)\""
if let rx {
desc.append(" rx=\"\(rx)\"")
}
if let ry {
desc.append(" ry=\"\(ry)\"")
}
return desc + " \(attributeDescription) />"
}
}
extension Rectangle: DirectionalCommandRepresentable {
public func commands(clockwise: Bool) throws -> [Path.Command] {
RectangleProcessor(rectangle: self).commands(clockwise: clockwise)
}
}
extension Rectangle: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
.attribute
}
}
extension Rectangle: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
.attribute
}
}

View file

@ -0,0 +1,62 @@
import Foundation
import Swift2D
public extension SVG {
/// Original size of the document image.
///
/// Primarily uses the `viewBox` attribute, and will fallback to the 'pixelSize'
var originalSize: Size {
(viewBoxSize ?? pixelSize) ?? .zero
}
/// Size of the design in a square 'viewBox'.
///
/// All paths created by this framework are outputted in a 'square'.
var outputSize: Size {
let size = (pixelSize ?? viewBoxSize) ?? .zero
let maxDimension = max(size.width, size.height)
return Size(width: maxDimension, height: maxDimension)
}
/// Size derived from the `viewBox` document attribute
var viewBoxSize: Size? {
guard let viewBox else {
return nil
}
let components = viewBox.components(separatedBy: .whitespaces)
guard components.count == 4 else {
return nil
}
guard let width = Double(components[2]) else {
return nil
}
guard let height = Double(components[3]) else {
return nil
}
return Size(width: width, height: height)
}
/// Size derived from the 'width' & 'height' document attributes
var pixelSize: Size? {
guard let width, !width.isEmpty else {
return nil
}
guard let height, !height.isEmpty else {
return nil
}
let widthRawValue = width.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
let heightRawValue = height.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
guard let w = Double(widthRawValue), let h = Double(heightRawValue) else {
return nil
}
return Size(width: w, height: h)
}
}

160
third-party/SwiftSVG/Sources/SVG.swift vendored Normal file
View file

@ -0,0 +1,160 @@
import Foundation
import XMLCoder
/// SVG is a language for describing two-dimensional graphics in XML.
///
/// The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost
/// element of SVG documents, but it can also be used to embed a SVG fragment inside an SVG or HTML document.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
/// | [W3](https://www.w3.org/TR/SVG11/)
public struct SVG: Container {
public var viewBox: String?
public var width: String?
public var height: String?
public var title: String?
public var desc: String?
// Container
public var circles: [Circle]?
public var ellipses: [Ellipse]?
public var groups: [Group]?
public var lines: [Line]?
public var paths: [Path]?
public var polygons: [Polygon]?
public var polylines: [Polyline]?
public var rectangles: [Rectangle]?
public var texts: [Text]?
/// A non-optional, non-spaced representation of the `title`.
public var name: String {
let name = title ?? "SVG Document"
let newTitle = name.components(separatedBy: .punctuationCharacters).joined(separator: "_")
return newTitle.replacingOccurrences(of: " ", with: "_")
}
enum CodingKeys: String, CodingKey {
case width
case height
case viewBox
case title
case desc
case circles = "circle"
case ellipses = "ellipse"
case groups = "g"
case lines = "line"
case paths = "path"
case polylines = "polyline"
case polygons = "polygon"
case rectangles = "rect"
case texts = "text"
}
public init() {}
public init(width: Int, height: Int) {
self.width = "\(width)px"
self.height = "\(height)px"
viewBox = "0 0 \(width) \(height)"
}
public static func make(from url: URL) throws -> SVG {
guard FileManager.default.fileExists(atPath: url.path) else {
throw CocoaError(.fileNoSuchFile)
}
let data = try Data(contentsOf: url)
return try make(with: data)
}
public static func make(with data: Data, decoder: XMLDecoder = XMLDecoder()) throws -> SVG {
try decoder.decode(SVG.self, from: data)
}
/// A collection of all `Path`s in the document.
public func subpaths() throws -> [Path] {
var output: [Path] = []
let _transformations: [Transformation] = []
if let circles {
try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
}
if let ellipses {
try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
}
if let rectangles {
try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
}
if let polygons {
try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
}
if let polylines {
try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
}
if let paths {
try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
}
if let groups {
try groups.forEach {
try output.append(contentsOf: $0.subpaths(applying: _transformations))
}
}
return output
}
/// A singular path that represents all of the `Command`s within the document.
public func coalescedPath() throws -> Path {
let paths = try subpaths()
let commands = try paths.flatMap { try $0.commands() }
return Path(commands: commands)
}
}
extension SVG: CustomStringConvertible {
public var description: String {
var contents: String = ""
if let title {
contents.append("\n<title>\(title)</title>")
}
if let desc {
contents.append("\n<desc>\(desc)</desc>")
}
contents.append(containerDescription)
return "<svg viewBox=\"\(viewBox ?? "")\" width=\"\(width ?? "")\" height=\"\(height ?? "")\">\(contents)\n</svg>"
}
}
extension SVG: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
switch key {
case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
.attribute
default:
.element
}
}
}
extension SVG: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
.attribute
default:
.element
}
}
}

View file

@ -0,0 +1,64 @@
import Swift2D
public struct Stroke {
public var color: String?
public var width: Double?
public var opacity: Double?
public var lineCap: LineCap = .butt
public var lineJoin: LineJoin = .miter
public var miterLimit: Double?
public init() {}
/// Presentation attribute defining the shape to be used at the end of open subpaths when they are stroked.
///
/// The default `LineCap` is `.butt`
public enum LineCap: String, Sendable, Codable, CaseIterable {
/// The stroke for each subpath does not extend beyond its two endpoints.
case butt
/// The end of each subpath the stroke will be extended by a half circle with a diameter equal to the stroke
/// width.
case round
/// The end of each subpath the stroke will be extended by a rectangle with a width equal to half the width of
/// the stroke and a height equal to the width of the stroke.
case square
}
/// Presentation attribute defining the shape to be used at the corners of paths when they are stroked.
///
/// The default `LineJoin` is `.miter`
public enum LineJoin: String, Sendable, Codable, CaseIterable {
/// An arcs corner is to be used to join path segments.
///
/// The arcs shape is formed by extending the outer edges of the stroke at the join point with arcs that have
/// the same curvature as the outer edges at the join point.
case arcs
/// The bevel value indicates that a bevelled corner is to be used to join path segments.
case bevel
/// Indicates that a sharp corner is to be used to join path segments.
///
/// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
/// they intersect.
case miter
/// A sharp corner is to be used to join path segments.
///
/// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
/// they intersect.
case miterClip = "miter-clip"
/// The round value indicates that a round corner is to be used to join path segments.
case round
}
}
extension Stroke.LineCap: CustomStringConvertible {
public var description: String {
rawValue
}
}
extension Stroke.LineJoin: CustomStringConvertible {
public var description: String {
rawValue
}
}

View file

@ -0,0 +1,17 @@
public protocol StylingAttributes {
var style: String? { get set }
}
enum StylingAttributesKeys: String, CodingKey {
case style
}
public extension StylingAttributes {
var stylingDescription: String {
if let style {
"\(StylingAttributesKeys.style.rawValue)=\"\(style)\""
} else {
""
}
}
}

110
third-party/SwiftSVG/Sources/Text.swift vendored Normal file
View file

@ -0,0 +1,110 @@
import Foundation
import XMLCoder
/// Graphics element consisting of text
///
/// It's possible to apply a gradient, pattern, clipping path, mask, or filter to `Text`, like any other SVG graphics element.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text)
/// | [W3](https://www.w3.org/TR/SVG11/text.html#TextElement)
public struct Text: Element {
public var value: String = ""
public var x: Double?
public var y: Double?
public var dx: Double?
public var dy: Double?
// MARK: CoreAttributes
public var id: String?
// MARK: PresentationAttributes
public var fillColor: String?
public var fillOpacity: Double?
public var fillRule: Fill.Rule?
public var strokeColor: String?
public var strokeWidth: Double?
public var strokeOpacity: Double?
public var strokeLineCap: Stroke.LineCap?
public var strokeLineJoin: Stroke.LineJoin?
public var strokeMiterLimit: Double?
public var transform: String?
// MARK: StylingAttributes
public var style: String?
enum CodingKeys: String, CodingKey {
case value = ""
case x
case y
case dx
case dy
case id
case fillColor = "fill"
case fillOpacity = "fill-opacity"
case fillRule = "fill-rule"
case strokeColor = "stroke"
case strokeWidth = "stroke-width"
case strokeOpacity = "stroke-opacity"
case strokeLineCap = "stroke-linecap"
case strokeLineJoin = "stroke-linejoin"
case strokeMiterLimit = "stroke-miterlimit"
case transform
case style
}
public init() {}
public init(value: String) {
self.value = value
}
}
extension Text: CustomStringConvertible {
public var description: String {
var components: [String] = []
if let x, !x.isNaN, !x.isZero {
components.append(String(format: "x=\"%.5f\"", x))
}
if let y, !y.isNaN, !y.isZero {
components.append(String(format: "y=\"%.5f\"", y))
}
if let dx, !dx.isNaN, !dx.isZero {
components.append(String(format: "dx=\"%.5f\"", dx))
}
if let dy, !dy.isNaN, !dy.isZero {
components.append(String(format: "dy=\"%.5f\"", dy))
}
components.append(attributeDescription)
return "<text " + components.joined(separator: " ") + " >\(value)</text>"
}
}
extension Text: DynamicNodeDecoding {
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
switch key {
case CodingKeys.value:
.element
default:
.attribute
}
}
}
extension Text: DynamicNodeEncoding {
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.value:
.element
default:
.attribute
}
}
}

View file

@ -0,0 +1,113 @@
import Foundation
import Swift2D
/// A modification that should be applied to an element and its children.
///
/// If a list of transforms is provided, then the net effect is as if each transform had been specified separately in
/// the order provided.
///
/// For example,
/// ```
/// <g transform="translate(-10,-20) scale(2) rotate(45) translate(5,10)">
/// <!-- graphics elements go here -->
/// </g>
/// ```
/// is functionally equivalent to:
/// ```
/// <g transform="translate(-10,-20)">
/// <g transform="scale(2)">
/// <g transform="rotate(45)">
/// <g transform="translate(5,10)">
/// <!-- graphics elements go here -->
/// </g>
/// </g>
/// </g>
/// </g>
/// ```
///
/// The transform attribute is applied to an element before processing any other coordinate or length values supplied
/// for that element.
///
/// ## Documentation
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform)
/// | [W3](https://www.w3.org/TR/SVG11/coords.html#TransformAttribute)
public enum Transformation {
/// Moves an object by x & y. (Y is assumed to be '0' if not provided)
case translate(x: Double, y: Double)
/// Specifies a transformation in the form of a transformation matrix of six values.
case matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)
public enum Prefix: String, CaseIterable {
case translate
case matrix
}
/// Initializes a new `Transformation` with a raw SVG transformation string.
public init?(_ string: String) {
guard let prefix = Prefix.allCases.first(where: { string.lowercased().hasPrefix($0.rawValue) }) else {
return nil
}
switch prefix {
case .translate:
guard let start = string.firstIndex(of: "(") else {
return nil
}
guard let stop = string.lastIndex(of: ")") else {
return nil
}
var substring = String(string[start ... stop])
substring = substring.replacingOccurrences(of: "(", with: "")
substring = substring.replacingOccurrences(of: ")", with: "")
var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
components = components.flatMap { $0.components(separatedBy: ",") }
let values = components.compactMap { Double($0) }.map { Double($0) }
guard values.count > 0 else {
return nil
}
if values.count > 1 {
self = .translate(x: values[0], y: values[1])
} else {
self = .translate(x: values[0], y: 0.0)
}
case .matrix:
guard let start = string.firstIndex(of: "(") else {
return nil
}
guard let stop = string.lastIndex(of: ")") else {
return nil
}
var substring = String(string[start ... stop])
substring = substring.replacingOccurrences(of: "(", with: "")
substring = substring.replacingOccurrences(of: ")", with: "")
var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
components = components.flatMap { $0.components(separatedBy: ",") }
let values = components.compactMap { Double($0) }.map { Double($0) }
guard values.count > 5 else {
return nil
}
self = .matrix(a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5])
}
}
}
extension Transformation: CustomStringConvertible {
public var description: String {
switch self {
case .translate(let x, let y):
"translate(\(x), \(y))"
case .matrix(let a, let b, let c, let d, let e, let f):
"matrix(\(a), \(b), \(c), \(d), \(e), \(f))"
}
}
}

19
third-party/VectorPlus/BUILD vendored Normal file
View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "VectorPlus",
module_name = "VectorPlus",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//third-party/SwiftColor",
"//third-party/SwiftSVG",
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,72 @@
import Swift2D
import SwiftColor
import SwiftSVG
#if canImport(CoreGraphics)
import CoreGraphics
public extension CGContext {
func render(path: Path, from: Rect, to: Rect) throws {
saveGState()
let cgPath = CGMutablePath()
let commands = (try? path.commands()) ?? []
for (idx, command) in commands.enumerated() {
let previous: Point? = if idx > 0 {
commands[idx - 1].previousPoint
} else {
nil
}
cgPath.addCommand(command, from: from, to: to, previousPoint: previous)
}
if let fill = path.fill {
let _color = Pigment(fill.color ?? "black")
if _color.alpha != 0.0 {
let cgColor = CGColor.make(_color)
let color = cgColor.copy(alpha: CGFloat(fill.opacity ?? 1.0)) ?? cgColor
let rule = fill.rule.cgFillRule
setFillColor(color)
addPath(cgPath)
fillPath(using: rule)
}
}
if let stroke = path.stroke {
let _color = Pigment(stroke.color ?? "black")
if _color.alpha != 0.0 {
let cgColor = CGColor.make(_color)
let color = cgColor.copy(alpha: CGFloat(stroke.opacity ?? 1.0)) ?? cgColor
let width = stroke.width ?? 1.0
let lineWidth = width * (to.size.width / from.size.width)
setLineWidth(CGFloat(lineWidth))
setStrokeColor(color)
setLineCap(stroke.lineCap.cgLineCap)
setLineJoin(stroke.lineJoin.cgLineJoin)
if let miterLimit = stroke.miterLimit, stroke.lineJoin == .miter {
setMiterLimit(CGFloat(miterLimit))
}
addPath(cgPath)
strokePath()
}
}
if path.fill == nil, path.stroke == nil {
let color = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
setFillColor(color)
addPath(cgPath)
fillPath(using: path.fill?.rule.cgFillRule ?? .winding)
}
restoreGState()
}
func render(path: Path, in rect: Rect) throws {
try render(path: path, from: rect, to: rect)
}
}
#endif

View file

@ -0,0 +1,45 @@
import Swift2D
import SwiftSVG
#if canImport(CoreGraphics)
import CoreGraphics
public extension CGMutablePath {
/// Adds a `Path.Command` to a _CoreGraphics path_, using the provided rectangles to correctly scale the parameters.
///
/// - parameter command: The `Path.Command` to append
/// - parameter from: The `Rect` which originally had the instruction. This is typically the `Document.originalSize`.
/// - parameter to: The `Rect` defining the new size.
/// - parameter previousPoint: The last `Point`, used for Elliptical Arc calculations
func addCommand(_ command: Path.Command, from: Rect, to: Rect, previousPoint: Point? = nil) {
let translated = command.translate(from: from, to: to)
switch translated {
case .moveTo(let point):
move(to: CGPoint(point))
case .lineTo(let point):
addLine(to: CGPoint(point))
case .cubicBezierCurve(let cp1, let cp2, let point):
addCurve(to: CGPoint(point), control1: CGPoint(cp1), control2: CGPoint(cp2))
case .quadraticBezierCurve(let cp, let point):
addQuadCurve(to: CGPoint(point), control: CGPoint(cp))
case .ellipticalArcCurve(_, _, _, _, _, let point):
guard let previousPoint else {
addLine(to: CGPoint(point))
return
}
do {
let curves = try command.convertToCubicBezierCurves(with: previousPoint)
for curve in curves {
addCommand(curve, from: from, to: to)
}
} catch {
print(error)
addLine(to: CGPoint(point))
}
case .closePath:
closeSubpath()
}
}
}
#endif

View file

@ -0,0 +1,14 @@
import Foundation
import SwiftSVG
#if canImport(CoreGraphics)
import CoreGraphics
public extension Fill.Rule {
var cgFillRule: CGPathFillRule {
switch self {
case .evenOdd: .evenOdd
case .nonZero: .winding
}
}
}
#endif

View file

@ -0,0 +1,37 @@
import Swift2D
import SwiftSVG
#if canImport(CoreGraphics)
import CoreGraphics
public extension SVG {
func path(size: Size) -> CGPath {
guard size.height > 0.0, size.width > 0.0 else {
return CGMutablePath()
}
guard let paths = try? subpaths() else {
return CGMutablePath()
}
let from = Rect(origin: .zero, size: originalSize)
let to = Rect(origin: .zero, size: size)
let path = CGMutablePath()
for p in paths {
let commands = (try? p.commands()) ?? []
for (idx, command) in commands.enumerated() {
let previous: Point? = if idx > 0 {
commands[idx - 1].previousPoint
} else {
nil
}
path.addCommand(command, from: from, to: to, previousPoint: previous)
}
}
return path
}
}
#endif

View file

@ -0,0 +1,25 @@
import Foundation
import SwiftSVG
#if canImport(CoreGraphics)
import CoreGraphics
public extension Stroke.LineCap {
var cgLineCap: CGLineCap {
switch self {
case .butt: .butt
case .round: .round
case .square: .square
}
}
}
public extension Stroke.LineJoin {
var cgLineJoin: CGLineJoin {
switch self {
case .bevel: .bevel
case .arcs, .miter, .miterClip: .miter
case .round: .round
}
}
}
#endif

View file

@ -0,0 +1,29 @@
import SwiftColor
import SwiftSVG
public extension Fill {
@available(*, deprecated, renamed: "pigment")
var swiftColor: Pigment? { pigment }
var pigment: Pigment? {
guard let color, !color.isEmpty else {
return nil
}
let _color = Pigment(color)
guard _color.alpha != 0.0 else {
return nil
}
return _color
}
}
public extension Fill.Rule {
var coreGraphicsDescription: String {
switch self {
case .evenOdd: ".evenOdd"
case .nonZero: ".winding"
}
}
}

View file

@ -0,0 +1,34 @@
import Swift2D
import SwiftSVG
public extension Path {
func asCoreGraphicsDescription(variable: String = "path", originalSize: Size) throws -> String {
var outputs: [String] = []
let commands = (try? commands()) ?? []
for (idx, command) in commands.enumerated() {
let previous: Point? = if idx > 0 {
commands[idx - 1].previousPoint
} else {
nil
}
let method = command.coreGraphicsDescription(originalSize: originalSize, previousPoint: previous)
let code = "\(variable)\(method)"
outputs.append(code)
}
return outputs.joined(separator: "\n ")
}
}
public extension Path.Command {
var previousPoint: Point {
switch self {
case .moveTo(let point): point
case .lineTo(let point): point
case .cubicBezierCurve(_, _, let point): point
case .quadraticBezierCurve(_, let point): point
case .ellipticalArcCurve(_, _, _, _, _, let point): point
case .closePath: .zero
}
}
}

View file

@ -0,0 +1,216 @@
import Foundation
import Swift2D
import SwiftSVG
public extension Path.Command {
/// Uses the _Power of Math_ to translate a commands controls/points from one `Rect` to another `Rect`.
func translate(from: Rect, to: Rect) -> Path.Command {
switch self {
case .moveTo(let point):
let _point = VectorPoint(point: point, in: from).translate(to: to)
return .moveTo(point: _point)
case .lineTo(let point):
let _point = VectorPoint(point: point, in: from).translate(to: to)
return .lineTo(point: _point)
case .cubicBezierCurve(let cp1, let cp2, let point):
let _cp1 = VectorPoint(point: cp1, in: from).translate(to: to)
let _cp2 = VectorPoint(point: cp2, in: from).translate(to: to)
let _point = VectorPoint(point: point, in: from).translate(to: to)
return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
case .quadraticBezierCurve(let cp, let point):
let _cp = VectorPoint(point: cp, in: from).translate(to: to)
let _point = VectorPoint(point: point, in: from).translate(to: to)
return .quadraticBezierCurve(cp: _cp, point: _point)
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
let _rx = rx * (from.size.maxRadius / to.size.minRadius)
let _ry = ry * (from.size.maxRadius / to.size.minRadius)
let _point = VectorPoint(point: point, in: from).translate(to: to)
return .ellipticalArcCurve(rx: _rx, ry: _ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
case .closePath:
return self
}
}
}
public extension Path.Command {
func coreGraphicsDescription(originalSize: Size, previousPoint: Point? = nil) -> String {
let rect = Rect(origin: .zero, size: originalSize)
switch self {
case .moveTo(let point):
let _point = VectorPoint(point: point, in: rect)
return ".move(to: \(_point.coreGraphicsDescription))"
case .lineTo(let point):
let _point = VectorPoint(point: point, in: rect)
return ".addLine(to: \(_point.coreGraphicsDescription))"
case .cubicBezierCurve(let cp1, let cp2, let point):
let _cp1 = VectorPoint(point: cp1, in: rect)
let _cp2 = VectorPoint(point: cp2, in: rect)
let _point = VectorPoint(point: point, in: rect)
return ".addCurve(to: \(_point.coreGraphicsDescription), control1: \(_cp1.coreGraphicsDescription), control2: \(_cp2.coreGraphicsDescription))"
case .quadraticBezierCurve(let cp, let point):
let _cp = VectorPoint(point: cp, in: rect)
let _point = VectorPoint(point: point, in: rect)
return ".addQuadCurve(to: \(_point.coreGraphicsDescription), control: \(_cp.coreGraphicsDescription))"
case .ellipticalArcCurve(_, _, _, _, _, let point):
guard let previousPoint else {
return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
}
do {
let curves = try convertToCubicBezierCurves(with: previousPoint)
return curves.map { $0.coreGraphicsDescription(originalSize: originalSize) }.joined(separator: "\n")
} catch {
print(error)
return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
}
case .closePath:
return ".closeSubpath()"
}
}
}
extension Path.Command {
/// Converts an `.ellipticalArcCurve` into one or more `.cubicBezierCurve`s.
/// https://github.com/colinmeinke/svg-arc-to-cubic-bezier/blob/master/src/index.js
func convertToCubicBezierCurves(with previousPoint: Point) throws -> [Path.Command] {
guard case let .ellipticalArcCurve(rx, ry, angle, largeArg, clockwise, point) = self else {
throw Path.Command.Error.message("\(#function); Only .ellipticalArcCurve is allowed.")
}
var curves: [Path.Command] = []
guard rx > 0.0, ry > 0.0 else {
throw Path.Command.Error.message("\(#function); rx/ry must be greater than 0.0 (zero).")
}
let sinφ = sin(angle * (.pi * 2.0) / 360.0)
let cosφ = cos(angle * (.pi * 2.0) / 360.0)
let pxp = cosφ * (previousPoint.x - point.x) / 2 + sinφ * (previousPoint.y - point.y) / 2.0
let pyp = -sinφ * (previousPoint.x - point.x) / 2 + cosφ * (previousPoint.y - point.y) / 2.0
guard pxp != 0.0, pyp != 0.0 else {
throw Path.Command.Error.message("\(#function); math")
}
var _rx = abs(rx)
var _ry = abs(ry)
let λ = pow(pxp, 2.0) / pow(_rx, 2.0) + pow(pyp, 2.0) / pow(_ry, 2.0)
if λ > 1.0 {
_rx *= sqrt(λ)
_ry *= sqrt(λ)
}
let _arcCenter = arcCenter(previousPoint: previousPoint, point: point, rx: _rx, ry: _ry, largeArc: largeArg, clockwise: clockwise, sinφ: sinφ, cosφ: cosφ, pxp: pxp, pyp: pyp)
let center = _arcCenter.center
var angle1 = _arcCenter.angle1
var angle2 = _arcCenter.angle2
var ratio = abs(angle2) / ((.pi * 2.0) / 4.0)
if abs(1.0 - ratio) < 0.0000001 {
ratio = 1.0
}
let segments = max(ceil(ratio), 1)
angle2 /= segments
var rawCurves: [(Point, Point, Point)] = []
for _ in 0 ... Int(segments) {
rawCurves.append(approximateUnitArc(angle1: angle1, angle2: angle2))
angle1 += angle2
}
for rawCurf in rawCurves {
let _cp1 = mapToEllipse(point: rawCurf.0, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
let _cp2 = mapToEllipse(point: rawCurf.1, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
let _point = mapToEllipse(point: rawCurf.2, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
curves.append(.cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point))
}
return curves
}
}
private func arcCenter(previousPoint: Point, point: Point, rx: Double, ry: Double, largeArc: Bool, clockwise: Bool, sinφ: Double, cosφ: Double, pxp: Double, pyp: Double) ->
(center: Point, angle1: Double, angle2: Double)
{
let rxsq = pow(rx, 2.0)
let rysq = pow(ry, 2.0)
let pxpsq = pow(pxp, 2.0)
let pypsq = pow(pyp, 2.0)
var radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)
if radicant < 0.0 {
radicant = 0.0
}
radicant /= (rxsq * pypsq) + (rysq * pxpsq)
radicant = sqrt(radicant) * (largeArc == clockwise ? -1.0 : 1.0)
let centerxp = radicant * rx / ry * pyp
let centeryp = radicant * -ry / rx * pxp
let centerx = cosφ * centerxp - sinφ * centeryp + (previousPoint.x + point.x) / 2.0
let centery = sinφ * centerxp + cosφ * centeryp + (previousPoint.x + point.x) / 2.0
let vx1 = (pxp - centerxp) / rx
let vy1 = (pyp - centeryp) / ry
let vx2 = (-pxp - centerxp) / rx
let vy2 = (-pyp - centeryp) / ry
let angle1 = vectorAngle(u: Point(x: 1, y: 0), v: Point(x: vx1, y: vy1))
var angle2 = vectorAngle(u: Point(x: vx1, y: vy1), v: Point(x: vx2, y: vy2))
if clockwise == false, angle2 > 0.0 {
angle2 -= (.pi * 2.0)
} else if clockwise == true, angle2 < 0.0 {
angle2 += (.pi * 2.0)
}
return (Point(x: centerx, y: centery), angle1, angle2)
}
private func vectorAngle(u: Point, v: Point) -> Double {
let sign: Double = ((u.x * v.y - u.y * v.x) < 0.0) ? -1.0 : 1.0
var dot = u.x * v.x + u.y * v.y
if dot > 1.0 {
dot = 1.0
} else if dot < -1.0 {
dot = -1.0
}
return sign * acos(dot)
}
private func approximateUnitArc(angle1: Double, angle2: Double) -> (Point, Point, Point) {
// If 90 degree circular arc, use a constant
// as derived from http://spencermortensen.com/articles/bezier-circle
let a: Double = switch angle2 {
case 1.5707963267948966:
0.551915024494
case -1.5707963267948966:
-0.551915024494
default:
4.0 / 3.0 * tan(angle2 / 4.0)
}
let x1 = cos(angle1)
let y1 = sin(angle1)
let x2 = cos(angle1 + angle2)
let y2 = sin(angle1 + angle2)
return (Point(x: x1 - y1 * a, y: y1 + x1 * a), Point(x: x2 + y2 * a, y: y2 - x2 * 1), Point(x: x2, y: y2))
}
private func mapToEllipse(point: Point, rx: Double, ry: Double, sinφ: Double, cosφ: Double, center: Point) -> Point {
let x = point.x * rx
let y = point.y * ry
let xp = cosφ * x - sinφ * y
let yp = sinφ * x + cosφ * y
return Point(x: xp + center.x, y: yp + center.y)
}

View file

@ -0,0 +1,7 @@
import SwiftColor
public extension Pigment {
var coreGraphicsDescription: String {
"CGColor(srgbRed: \(red), green: \(green), blue: \(blue), alpha: \(alpha))"
}
}

View file

@ -0,0 +1,7 @@
import Swift2D
public extension Point {
var coreGraphicsDescription: String {
"CGPoint(x: \(x), y: \(y))"
}
}

View file

@ -0,0 +1,7 @@
import Swift2D
public extension Rect {
var coreGraphicsDescription: String {
"CGRect(origin: \(origin.coreGraphicsDescription), size: \(size.coreGraphicsDescription))"
}
}

View file

@ -0,0 +1,324 @@
import Foundation
import Swift2D
import SwiftSVG
import XMLCoder
public extension SVG {
static func encodeDocument(_ document: SVG, encoder: XMLEncoder = XMLEncoder()) throws -> Data {
let rootAttributes: [String: String] = [
"version": "1.1",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
]
let header = XMLHeader(version: 1.0, encoding: "UTF-8", standalone: nil)
return try encoder.encode(document, withRootKey: "svg", rootAttributes: rootAttributes, header: header)
}
static func appleSymbols(path: Path, in rect: Rect) throws -> SVG {
var document = SVG(width: 3300, height: 2200)
document.groups = try [.appleSymbolsNotes, .appleSymbolsGuides, .appleSymbols(path: path, in: rect)]
return document
}
}
public extension Group {
static var appleSymbolsNotes: Group {
var group = Group()
group.id = "Notes"
group.rectangles = []
group.lines = []
group.texts = []
group.groups = []
var artboard = Rectangle(x: 0, y: 0, width: 3300, height: 2200)
artboard.id = "artboard"
artboard.style = "fill:white;opacity:1"
group.rectangles?.append(artboard)
var topLine = Line(x1: 263, y1: 292, x2: 3036, y2: 292)
topLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
group.lines?.append(topLine)
var bottomLine = Line(x1: 263, y1: 1903, x2: 3036, y2: 1903)
bottomLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
group.lines?.append(bottomLine)
var text = Text()
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
text.transform = "matrix(1 0 0 1 263 322)"
text.value = "Weight/Scale Variations"
group.texts?.append(text)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:middle"
text.transform = "matrix(1 0 0 1 559.711 322)"
text.value = "Ultralight"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 856.422 322)"
text.value = "Thin"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 1153.13 322)"
text.value = "Light"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 1449.84 322)"
text.value = "Regular"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 1746.56 322)"
text.value = "Medium"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 2043.27 322)"
text.value = "Semibold"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 2339.98 322)"
text.value = "Bold"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 2636.69 322)"
text.value = "Heavy"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 2933.4 322)"
text.value = "Black"
group.texts?.append(text)
var path = Path(data: "M 9.24805 0.830078 C 13.5547 0.830078 17.1387 -2.74414 17.1387 -7.05078 C 17.1387 -11.3574 13.5449 -14.9316 9.23828 -14.9316 C 4.94141 -14.9316 1.36719 -11.3574 1.36719 -7.05078 C 1.36719 -2.74414 4.95117 0.830078 9.24805 0.830078 Z M 9.24805 -0.654297 C 5.70312 -0.654297 2.87109 -3.49609 2.87109 -7.05078 C 2.87109 -10.6055 5.69336 -13.4473 9.23828 -13.4473 C 12.793 -13.4473 15.6348 -10.6055 15.6445 -7.05078 C 15.6543 -3.49609 12.8027 -0.654297 9.24805 -0.654297 Z M 9.22852 -3.42773 C 9.69727 -3.42773 9.9707 -3.74023 9.9707 -4.25781 L 9.9707 -6.31836 L 12.1973 -6.31836 C 12.6953 -6.31836 13.0371 -6.57227 13.0371 -7.04102 C 13.0371 -7.51953 12.7148 -7.7832 12.1973 -7.7832 L 9.9707 -7.7832 L 9.9707 -10.0098 C 9.9707 -10.5273 9.69727 -10.8496 9.22852 -10.8496 C 8.75977 -10.8496 8.50586 -10.5078 8.50586 -10.0098 L 8.50586 -7.7832 L 6.29883 -7.7832 C 5.78125 -7.7832 5.44922 -7.51953 5.44922 -7.04102 C 5.44922 -6.57227 5.80078 -6.31836 6.29883 -6.31836 L 8.50586 -6.31836 L 8.50586 -4.25781 C 8.50586 -3.75977 8.75977 -3.42773 9.22852 -3.42773 Z")
var subGroup = Group("", path: path, transform: "matrix(1 0 0 1 263 1933)")
group.groups?.append(subGroup)
path.data = "M 11.709 2.91016 C 17.1582 2.91016 21.6699 -1.60156 21.6699 -7.05078 C 21.6699 -12.4902 17.1484 -17.0117 11.6992 -17.0117 C 6.25977 -17.0117 1.74805 -12.4902 1.74805 -7.05078 C 1.74805 -1.60156 6.26953 2.91016 11.709 2.91016 Z M 11.709 1.25 C 7.09961 1.25 3.41797 -2.44141 3.41797 -7.05078 C 3.41797 -11.6504 7.08984 -15.3516 11.6992 -15.3516 C 16.3086 -15.3516 20 -11.6504 20.0098 -7.05078 C 20.0195 -2.44141 16.3184 1.25 11.709 1.25 Z M 11.6895 -2.41211 C 12.207 -2.41211 12.5195 -2.77344 12.5195 -3.33984 L 12.5195 -6.23047 L 15.5762 -6.23047 C 16.123 -6.23047 16.5039 -6.51367 16.5039 -7.03125 C 16.5039 -7.55859 16.1426 -7.86133 15.5762 -7.86133 L 12.5195 -7.86133 L 12.5195 -10.9277 C 12.5195 -11.5039 12.207 -11.8555 11.6895 -11.8555 C 11.1719 -11.8555 10.8789 -11.4844 10.8789 -10.9277 L 10.8789 -7.86133 L 7.83203 -7.86133 C 7.26562 -7.86133 6.89453 -7.55859 6.89453 -7.03125 C 6.89453 -6.51367 7.28516 -6.23047 7.83203 -6.23047 L 10.8789 -6.23047 L 10.8789 -3.33984 C 10.8789 -2.79297 11.1719 -2.41211 11.6895 -2.41211 Z"
subGroup.paths = [path]
subGroup.transform = "matrix(1 0 0 1 281.506 1933)"
group.groups?.append(subGroup)
path.data = "M 14.9707 5.67383 C 21.9336 5.67383 27.6953 -0.078125 27.6953 -7.04102 C 27.6953 -14.0039 21.9238 -19.7559 14.9609 -19.7559 C 8.00781 -19.7559 2.25586 -14.0039 2.25586 -7.04102 C 2.25586 -0.078125 8.01758 5.67383 14.9707 5.67383 Z M 14.9707 3.85742 C 8.93555 3.85742 4.08203 -1.00586 4.08203 -7.04102 C 4.08203 -13.0762 8.92578 -17.9395 14.9609 -17.9395 C 21.0059 -17.9395 25.8594 -13.0762 25.8691 -7.04102 C 25.8789 -1.00586 21.0156 3.85742 14.9707 3.85742 Z M 14.9512 -1.06445 C 15.5176 -1.06445 15.8691 -1.45508 15.8691 -2.06055 L 15.8691 -6.13281 L 20.1074 -6.13281 C 20.6934 -6.13281 21.1133 -6.46484 21.1133 -7.02148 C 21.1133 -7.59766 20.7227 -7.93945 20.1074 -7.93945 L 15.8691 -7.93945 L 15.8691 -12.1875 C 15.8691 -12.8027 15.5176 -13.1934 14.9512 -13.1934 C 14.3848 -13.1934 14.0625 -12.7832 14.0625 -12.1875 L 14.0625 -7.93945 L 9.83398 -7.93945 C 9.21875 -7.93945 8.80859 -7.59766 8.80859 -7.02148 C 8.80859 -6.46484 9.23828 -6.13281 9.83398 -6.13281 L 14.0625 -6.13281 L 14.0625 -2.06055 C 14.0625 -1.47461 14.3848 -1.06445 14.9512 -1.06445 Z"
subGroup.paths = [path]
subGroup.transform = "matrix(1 0 0 1 304.924 1933)"
group.groups?.append(subGroup)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
text.transform = "matrix(1 0 0 1 263 1953)"
text.value = "Design Variations"
group.texts?.append(text)
text.style = "none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
text.transform = "matrix(1 0 0 1 263 1971)"
text.value = "Symbols are supported in up to nine weights and three scales."
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 263 1989)"
text.value = "For optimal layout with text and other symbols, vertically align"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 263 2007)"
text.value = "symbols with the adjacent text."
group.texts?.append(text)
var rect = Rectangle(x: 776, y: 1919, width: 3, height: 14)
rect.style = "fill:#00AEEF;stroke:none;opacity:0.4;"
group.rectangles?.append(rect)
path.data = "M 10.5273 0 L 12.373 0 L 7.17773 -14.0918 L 5.43945 -14.0918 L 0.244141 0 L 2.08984 0 L 3.50586 -4.0332 L 9.11133 -4.0332 Z M 6.2793 -11.9531 L 6.33789 -11.9531 L 8.59375 -5.52734 L 4.02344 -5.52734 Z"
subGroup.paths = [path]
subGroup.transform = "matrix(1 0 0 1 779 1933)"
group.groups?.append(subGroup)
rect.x = 791.617
group.rectangles?.append(rect)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
text.transform = "matrix(1 0 0 1 776 1953)"
text.value = "Margins"
group.texts?.append(text)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
text.transform = "matrix(1 0 0 1 776 1971)"
text.value = "Leading and trailing margins on the left and right side of each symbol"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 776 1989)"
text.value = "can be adjusted by modifying the width of the blue rectangles."
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 776 2007)"
text.value = "Modifications are automatically applied proportionally to all"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 776 2025)"
text.value = "scales and weights."
group.texts?.append(text)
path.data = "M 2.83203 3.11523 L 4.375 4.6582 C 5.22461 5.48828 6.19141 5.41992 7.06055 4.46289 L 17.2754 -6.66016 C 17.7051 -6.36719 18.0957 -6.37695 18.5645 -6.47461 L 19.6094 -6.68945 L 20.3027 -5.99609 L 20.2539 -5.47852 C 20.1855 -4.95117 20.3516 -4.53125 20.8496 -4.0332 L 21.6602 -3.22266 C 22.168 -2.71484 22.8223 -2.68555 23.3008 -3.16406 L 26.5527 -6.41602 C 27.0312 -6.89453 27.0117 -7.54883 26.5039 -8.05664 L 25.6836 -8.87695 C 25.1855 -9.375 24.7754 -9.55078 24.2383 -9.47266 L 23.7109 -9.41406 L 23.0566 -10.0781 L 23.3398 -11.2207 C 23.4863 -11.7871 23.3398 -12.2559 22.7148 -12.8613 L 20.3027 -15.2539 C 16.7578 -18.7793 12.2266 -18.6719 9.11133 -15.5371 C 8.69141 -15.1074 8.64258 -14.5215 8.91602 -14.0918 C 9.15039 -13.7207 9.62891 -13.4961 10.2734 -13.6621 C 11.7871 -14.043 13.3008 -13.9258 14.7852 -12.9199 L 14.1602 -11.3379 C 13.9258 -10.752 13.9453 -10.2734 14.1797 -9.83398 L 3.01758 0.439453 C 2.08008 1.30859 1.97266 2.25586 2.83203 3.11523 Z M 10.6738 -15.1465 C 13.3398 -17.1387 16.6504 -16.8262 19.0527 -14.4141 L 21.6797 -11.8066 C 21.9141 -11.5723 21.9434 -11.3867 21.8848 -11.0938 L 21.5039 -9.53125 L 23.0762 -7.95898 L 24.043 -8.04688 C 24.3262 -8.07617 24.4141 -8.05664 24.6387 -7.83203 L 25.2637 -7.20703 L 22.5098 -4.46289 L 21.8848 -5.07812 C 21.6602 -5.30273 21.6406 -5.40039 21.6699 -5.68359 L 21.7578 -6.64062 L 20.1953 -8.20312 L 18.5742 -7.89062 C 18.291 -7.83203 18.1445 -7.83203 17.9102 -8.07617 L 15.7324 -10.2539 C 15.5078 -10.4883 15.4785 -10.625 15.6055 -10.9473 L 16.5527 -13.2227 C 14.9512 -14.7559 12.8418 -15.6055 10.8008 -14.9512 C 10.7129 -14.9219 10.6445 -14.9414 10.6152 -14.9805 C 10.5859 -15.0293 10.5859 -15.0781 10.6738 -15.1465 Z M 4.10156 2.41211 C 3.61328 1.91406 3.78906 1.61133 4.12109 1.30859 L 15.0781 -8.80859 L 16.3086 -7.57812 L 6.15234 3.34961 C 5.84961 3.68164 5.46875 3.7793 5.06836 3.37891 Z"
subGroup.paths = [path]
subGroup.transform = "matrix(1 0 0 1 1289 1933)"
group.groups?.append(subGroup)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
text.transform = "matrix(1 0 0 1 1289 1953)"
text.value = "Exporting"
group.texts?.append(text)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
text.transform = "matrix(1 0 0 1 1289 1971)"
text.value = "Symbols should be outlined when exporting to ensure the"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 1289 1989)"
text.value = "design is preserved when submitting to Xcode."
group.texts?.append(text)
text.id = "template-version"
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:end;"
text.transform = "matrix(1 0 0 1 3036 1933)"
text.value = "Template v.2.0"
group.texts?.append(text)
text.id = nil
text.transform = "matrix(1 0 0 1 3036 1969)"
text.value = "Typeset at 100 points"
group.texts?.append(text)
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
text.transform = "matrix(1 0 0 1 263 726)"
text.value = "Small"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 263 1156)"
text.value = "Medium"
group.texts?.append(text)
text.transform = "matrix(1 0 0 1 263 1586)"
text.value = "Large"
group.texts?.append(text)
return group
}
static var appleSymbolsGuides: Group {
var group = Group()
group.id = "Guides"
group.groups = []
group.lines = []
var hRef = Group()
hRef.id = "H-reference"
hRef.style = "fill:#27AAE1;stroke:none;"
hRef.transform = "matrix(1 0 0 1 339 696)"
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
group.groups?.append(hRef)
var baseline = Line(x1: 263, y1: 696, x2: 3036, y2: 696)
baseline.id = "Baseline-S"
baseline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
group.lines?.append(baseline)
var capline = Line(x1: 263, y1: 625.541, x2: 3036, y2: 625.541)
capline.id = "Capline-S"
capline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
group.lines?.append(capline)
hRef.transform = "matrix(1 0 0 1 339 1126)"
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
group.groups?.append(hRef)
baseline.id = "Baseline-M"
baseline.y1 = 1126
baseline.y2 = 1126
group.lines?.append(baseline)
capline.id = "Capline-M"
capline.y1 = 1055.54
capline.y2 = 1055.54
group.lines?.append(capline)
hRef.transform = "matrix(1 0 0 1 339 1556)"
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
group.groups?.append(hRef)
baseline.id = "Baseline-L"
baseline.y1 = 1556
baseline.y2 = 1556
group.lines?.append(baseline)
capline.id = "Capline-L"
capline.y1 = 1485.54
capline.y2 = 1485.54
group.lines?.append(capline)
var margin = Line(x1: 1399.72, y1: 1030.79, x2: 1399.72, y2: 1150.12)
margin.id = "left-margin"
margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
group.lines?.append(margin)
margin = Line(x1: 1499.97, y1: 1030.79, x2: 1499.97, y2: 1150.12)
margin.id = "right-margin"
margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
group.lines?.append(margin)
return group
}
static func appleSymbols(path: Path, in rect: Rect) throws -> Group {
var group = Group()
group.id = "Symbols"
let translations: [(name: String, size: Float, center: Point)] = [
("Ultralight-S", 0.75, .init(x: 559.0, y: 661.0)),
("Thin-S", 0.76, .init(x: 857.0, y: 661.0)),
("Light-S", 0.78, .init(x: 1153.0, y: 661.0)),
("Regular-S", 0.79, .init(x: 1449.5, y: 661.0)),
("Medium-S", 0.80, .init(x: 1747.0, y: 661.0)),
("Semibold-S", 0.81, .init(x: 2043.0, y: 661.0)),
("Bold-S", 0.82, .init(x: 2340.0, y: 661.0)),
("Heavy-S", 0.85, .init(x: 2636.5, y: 661.0)),
("Black-S", 0.86, .init(x: 2933.0, y: 661.0)),
("Ultralight-M", 0.95, .init(x: 559.0, y: 1091.0)),
("Thin-M", 0.96, .init(x: 857.0, y: 1091.0)),
("Light-M", 0.98, .init(x: 1153.0, y: 1091.0)),
("Regular-M", 1.00, .init(x: 1449.5, y: 1091.0)),
("Medium-M", 1.02, .init(x: 1747.0, y: 1091.0)),
("Semibold-M", 1.03, .init(x: 2043.0, y: 1091.0)),
("Bold-M", 1.05, .init(x: 2340.0, y: 1091.0)),
("Heavy-M", 1.07, .init(x: 2636.5, y: 1091.0)),
("Black-M", 1.10, .init(x: 2933.0, y: 1091.0)),
("Ultralight-L", 1.22, .init(x: 559.0, y: 1521.0)),
("Thin-L", 1.24, .init(x: 857.0, y: 1521.0)),
("Light-L", 1.26, .init(x: 1153.0, y: 1521.0)),
("Regular-L", 1.28, .init(x: 1449.5, y: 1521.0)),
("Medium-L", 1.30, .init(x: 1747.0, y: 1521.0)),
("Semibold-L", 1.31, .init(x: 2043.0, y: 1521.0)),
("Bold-L", 1.33, .init(x: 2340.0, y: 1521.0)),
("Heavy-L", 1.36, .init(x: 2636.5, y: 1521.0)),
("Black-L", 1.39, .init(x: 2933.0, y: 1521.0)),
]
group.groups = try translations.map { symbol -> Group in
let size = Size(width: 100.0 * symbol.size, height: 100.0 * symbol.size)
let to = Rect(origin: .zero, size: size)
let commands = try path.commands().map { $0.translate(from: rect, to: to) }
let p = Path(commands: commands)
let matrixOrigin = Point(x: symbol.center.x - size.width / 2.0, y: symbol.center.y - size.height / 2.0)
let matrix: Transformation = .matrix(a: 1, b: 0, c: 0, d: 1, e: matrixOrigin.x, f: matrixOrigin.y)
return Group(symbol.name, path: p, transform: matrix.description)
}
return group
}
}
private extension Group {
init(_ id: String, path: Path, transform: String) {
self.init()
self.id = id
self.transform = transform
paths = [path]
}
}

View file

@ -0,0 +1,56 @@
import Foundation
import Swift2D
import SwiftSVG
public extension SVG {
func asImageViewSubclass() throws -> String {
let instructions = try asCoreGraphicsDescription()
let renders = try asCGContextDescription()
return imageViewSubclassTemplate
.replacingOccurrences(of: "{{name}}", with: name)
.replacingOccurrences(of: "{{width}}", with: String(format: "%.1f", originalSize.width))
.replacingOccurrences(of: "{{height}}", with: String(format: "%.1f", originalSize.height))
.replacingOccurrences(of: "{{instructions}}", with: instructions)
.replacingOccurrences(of: "{{render}}", with: renders)
}
}
private extension SVG {
func asCoreGraphicsDescription(variable: String = "path") throws -> String {
try subpaths().map { try $0.asCoreGraphicsDescription(variable: variable, originalSize: originalSize) }.joined(separator: "\n ")
}
func asCGContextDescription() throws -> String {
var outputs: [String] = []
let paths = try subpaths()
try paths.forEach { path in
let instructions = try path.asCoreGraphicsDescription(variable: "path", originalSize: originalSize)
let fillColor = path.fill?.pigment?.coreGraphicsDescription ?? "nil"
let fillOpacity = (path.fillOpacity != nil) ? "\(path.fillOpacity!)" : "nil"
let fillRule = (path.fillRule ?? .nonZero).coreGraphicsDescription
let strokeColor = path.stroke?.pigment?.coreGraphicsDescription ?? "nil"
let strokeOpacity = (path.strokeOpacity != nil) ? "\(path.strokeOpacity!)" : "nil"
let strokeWidth = (path.strokeWidth != nil) ? "\(path.strokeWidth!) * (size.width / width)" : "nil"
let strokeLineCap = (path.strokeLineCap != nil) ? "\(path.strokeLineCap!.coreGraphicsDescription)" : "nil"
let strokeLineJoin = (path.strokeLineJoin != nil) ? "\(path.strokeLineJoin!.coreGraphicsDescription)" : "nil"
let strokeMiterLimit = (path.strokeMiterLimit != nil) ? "\(path.strokeMiterLimit!)" : "nil"
outputs.append(contextTemplate
.replacingOccurrences(of: "{{instructions}}", with: instructions)
.replacingOccurrences(of: "{{fillColor}}", with: fillColor)
.replacingOccurrences(of: "{{fillOpacity}}", with: fillOpacity)
.replacingOccurrences(of: "{{fillRule}}", with: fillRule)
.replacingOccurrences(of: "{{strokeColor}}", with: strokeColor)
.replacingOccurrences(of: "{{strokeOpacity}}", with: strokeOpacity)
.replacingOccurrences(of: "{{strokeWidth}}", with: strokeWidth)
.replacingOccurrences(of: "{{strokeLineCap}}", with: strokeLineCap)
.replacingOccurrences(of: "{{strokeLineJoin}}", with: strokeLineJoin)
.replacingOccurrences(of: "{{strokeMiterLimit}}", with: strokeMiterLimit)
)
}
return outputs.joined(separator: "\n ")
}
}

View file

@ -0,0 +1,7 @@
import Swift2D
public extension Size {
var coreGraphicsDescription: String {
"CGSize(width: \(width), height: \(height))"
}
}

View file

@ -0,0 +1,40 @@
import SwiftColor
import SwiftSVG
public extension Stroke {
@available(*, deprecated, renamed: "pigment")
var swiftColor: Pigment? { pigment }
var pigment: Pigment? {
guard let color, !color.isEmpty else {
return nil
}
let _color = Pigment(color)
guard _color.alpha != 0.0 else {
return nil
}
return _color
}
}
public extension Stroke.LineCap {
var coreGraphicsDescription: String {
switch self {
case .butt: ".butt"
case .round: ".round"
case .square: ".square"
}
}
}
public extension Stroke.LineJoin {
var coreGraphicsDescription: String {
switch self {
case .bevel: ".bevel"
case .arcs, .miter, .miterClip: ".miter"
case .round: ".round"
}
}
}

View file

@ -0,0 +1,177 @@
let imageViewSubclassTemplate: String = """
#if canImport(UIKit)
import UIKit
@IBDesignable
public class {{name}}: UIImageView {
public static let width: CGFloat = {{width}}
public static let height: CGFloat = {{height}}
public let width: CGFloat = {{width}}
public let height: CGFloat = {{height}}
public var widthToHeightAspectRatio: CGFloat {
guard width != .nan, width > 0.0 else {
return 0.0
}
guard height != .nan, height > 0.0 else {
return 0.0
}
return width / height
}
public var heightToWidthAspectRatio: CGFloat {
guard height != .nan, height > 0.0 else {
return 0.0
}
guard width != .nan, width > 0.0 else {
return 0.0
}
return height / width
}
public override init(frame: CGRect) {
super.init(frame: frame)
updateSubviews()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
updateSubviews()
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: width, height: height)
}
public override var bounds: CGRect {
didSet {
updateSubviews()
}
}
public override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateSubviews()
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateSubviews()
}
public func updateSubviews() {
image = Self.image(size: bounds.size)
}
public static func path(size: CGSize) -> CGPath {
guard size.height > 0.0 && size.width > 0.0 else {
return CGMutablePath()
}
let radius = max(size.width / 2.0, size.height / 2.0)
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
let path = CGMutablePath()
{{instructions}}
return path
}
public static func image(size: CGSize) -> UIImage? {
guard size.height > 0.0 && size.width > 0.0 else {
return nil
}
let radius = max(size.width / 2.0, size.height / 2.0)
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
defer {
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
{{render}}
return UIGraphicsGetImageFromCurrentImageContext()
}
private static func radians(_ degree: Float) -> CGFloat {
return CGFloat(degree) * (.pi / CGFloat(180))
}
}
private extension CGContext {
func rendering(_ block: (CGContext) -> Void) {
block(self)
}
}
#endif
"""
let contextTemplate: String = """
context.rendering { (ctx) in
ctx.saveGState()
let path = CGMutablePath()
{{instructions}}
let defaultColor: CGColor = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
let pathFillColor: CGColor? = {{fillColor}}
let pathFillOpacity: CGFloat? = {{fillOpacity}}
let pathFillRule: CGPathFillRule = {{fillRule}}
let pathStrokeColor: CGColor? = {{strokeColor}}
let pathStrokeOpacity: CGFloat? = {{strokeOpacity}}
let pathStrokeWidth: CGFloat? = {{strokeWidth}}
let pathStrokeLineCap: CGLineCap? = {{strokeLineCap}}
let pathStrokeLineJoin: CGLineJoin? = {{strokeLineJoin}}
let pathStrokeMiterLimit: CGFloat? = {{strokeMiterLimit}}
if pathFillColor != nil && pathFillOpacity != nil {
let opacity = pathFillOpacity ?? 1.0
let color = (pathFillColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
ctx.setFillColor(color)
ctx.addPath(path)
ctx.fillPath(using: pathFillRule)
}
if pathStrokeColor != nil && pathStrokeOpacity != nil {
let opacity = pathStrokeOpacity ?? 1.0
let color = (pathStrokeColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
let lineWidth = pathStrokeWidth ?? 1.0
ctx.setLineWidth(lineWidth)
ctx.setStrokeColor(color)
if let lineCap = pathStrokeLineCap {
ctx.setLineCap(lineCap)
}
if let lineJoin = pathStrokeLineJoin {
ctx.setLineJoin(lineJoin)
if let miterLimit = pathStrokeMiterLimit, lineJoin == .miter {
ctx.setMiterLimit(miterLimit)
}
}
ctx.addPath(path)
ctx.strokePath()
}
if (pathFillColor == nil && pathFillOpacity == nil) && (pathStrokeColor == nil && pathStrokeOpacity == nil) {
ctx.setFillColor(defaultColor)
ctx.addPath(path)
ctx.fillPath(using: pathFillRule)
}
ctx.restoreGState()
}
"""

View file

@ -0,0 +1,43 @@
#if canImport(UIKit)
import Swift2D
import SwiftSVG
import UIKit
public extension SVG {
func uiImage(size: Size) -> UIImage? {
guard size.height > 0.0, size.width > 0.0 else {
return nil
}
let from = Rect(origin: .zero, size: originalSize)
let to = Rect(origin: .zero, size: size)
let paths: [Path]
do {
paths = try subpaths()
} catch {
return nil
}
defer {
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContextWithOptions(CGSize(size), false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
for path in paths {
try? context.render(path: path, from: from, to: to)
}
return UIGraphicsGetImageFromCurrentImageContext()
}
func pngData(size: Size) -> Data? {
uiImage(size: size)?.pngData()
}
}
#endif

View file

@ -0,0 +1,81 @@
import Swift2D
import SwiftSVG
#if canImport(UIKit) && !os(watchOS)
import UIKit
@IBDesignable open class SVGImageView: UIImageView {
public var width: CGFloat {
CGFloat(svg.originalSize.width)
}
public var height: CGFloat {
CGFloat(svg.originalSize.height)
}
open var svg: SVG = SVG() {
didSet {
updateSubviews()
}
}
public var widthToHeightAspectRatio: CGFloat {
guard !width.isNaN, width > 0.0 else {
return 0.0
}
guard !height.isNaN, height > 0.0 else {
return 0.0
}
return width / height
}
public var heightToWidthAspectRatio: CGFloat {
guard !height.isNaN, height > 0.0 else {
return 0.0
}
guard !width.isNaN, width > 0.0 else {
return 0.0
}
return height / width
}
override public init(frame: CGRect) {
super.init(frame: frame)
updateSubviews()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
updateSubviews()
}
override public var intrinsicContentSize: CGSize {
CGSize(width: width, height: height)
}
override public var bounds: CGRect {
didSet {
updateSubviews()
}
}
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateSubviews()
}
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateSubviews()
}
public func updateSubviews() {
image = svg.uiImage(size: Size(bounds.size))
}
}
#endif

View file

@ -0,0 +1,126 @@
import Swift2D
/// A cartesian-based struct that describes the relationship of any particular `Point` to the _origin_ of a `Rect`.
public struct VectorPoint {
public enum Sign: String {
case plus = "+"
case minus = "-"
}
public typealias Offset = (sign: Sign, multiplier: Double)
public var x: Offset
public var y: Offset
public init(x: Offset, y: Offset) {
self.x = x
self.y = y
}
/// Initializes a `VectorPoint` for a given `Point` container in the provided `Rect`.
public init(point: Point, in rect: Rect) {
let radius = rect.size.maxRadius
let cartesianPoint = Self.cartesianPoint(for: point, in: rect)
if cartesianPoint.x < 0 {
x = (.minus, abs(cartesianPoint.x) / radius)
} else {
x = (.plus, cartesianPoint.x / radius)
}
if cartesianPoint.y < 0 {
y = (.plus, abs(cartesianPoint.y) / radius)
} else {
y = (.minus, cartesianPoint.y / radius)
}
}
}
// MARK: - CustomStringConvertible
extension VectorPoint: CustomStringConvertible {
public var description: String {
"VectorPoint(x: (\(x.sign.rawValue), \(x.multiplier)), y: (\(y.sign.rawValue), \(y.multiplier)))"
}
}
// MARK: - Equatable
extension VectorPoint: Equatable {
public static func == (lhs: VectorPoint, rhs: VectorPoint) -> Bool {
guard lhs.x.sign == rhs.x.sign else {
return false
}
guard lhs.x.multiplier == rhs.x.multiplier else {
return false
}
guard lhs.y.sign == rhs.y.sign else {
return false
}
guard lhs.y.multiplier == rhs.y.multiplier else {
return false
}
return true
}
}
// MARK: -
public extension VectorPoint {
/// Translates the provided point within the `Rect` from using the top-left
/// as the _origin_, to using the center as the _origin_.
///
/// For example: Given `Rect(x: 0, y: 0, width: 100, height: 100)`, the point
/// `Point(x: 25, y: 25)` would translate to `Point(x: -25, y: 25)`.
static func cartesianPoint(for point: Point, in rect: Rect) -> Point {
let origin = Point(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var cartesianPoint: Point = .zero
if point.x < origin.x {
cartesianPoint = cartesianPoint.x(-(origin.x - point.x))
} else if point.x > origin.x {
cartesianPoint = cartesianPoint.x(point.x - origin.x)
}
if point.y > origin.y {
cartesianPoint = cartesianPoint.y(-(point.y - origin.y))
} else if point.y < origin.y {
cartesianPoint = cartesianPoint.y(origin.y - point.y)
}
return cartesianPoint
}
}
// MARK: - Instance Functionality
public extension VectorPoint {
/// Calculates the `Point` for this instance in the specified `Rect`.
func translate(to rect: Rect) -> Point {
translate(to: rect.size)
}
/// Calculates the `Point` in the desired output size
func translate(to outputSize: Size) -> Point {
let center = outputSize.center
let radius = outputSize.minRadius
switch (x.sign, y.sign) {
case (.plus, .plus):
return Point(x: center.x + (radius * x.multiplier), y: center.y + (radius * y.multiplier))
case (.plus, .minus):
return Point(x: center.x + (radius * x.multiplier), y: center.y - (radius * y.multiplier))
case (.minus, .plus):
return Point(x: center.x - (radius * x.multiplier), y: center.y + (radius * y.multiplier))
case (.minus, .minus):
return Point(x: center.x - (radius * x.multiplier), y: center.y - (radius * y.multiplier))
}
}
}
public extension VectorPoint {
var coreGraphicsDescription: String {
"CGPoint(x: center.x \(x.sign.rawValue) (radius * \(x.multiplier)), y: center.y \(y.sign.rawValue) (radius * \(y.multiplier)))"
}
}

17
third-party/XMLCoder/BUILD vendored Normal file
View file

@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "XMLCoder",
module_name = "XMLCoder",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-suppress-warnings",
],
deps = [
],
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,99 @@
//
// XMLAttribute.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 6/3/20.
//
protocol XMLAttributeProtocol {}
/** Property wrapper specifying that a given property should be encoded and decoded as an XML attribute.
For example, this type
```swift
struct Book: Codable {
@Attribute var id: Int
}
```
will encode value `Book(id: 42)` as `<Book id="42"></Book>`. And vice versa,
it will decode the former into the latter.
*/
@propertyWrapper
public struct Attribute<Value>: XMLAttributeProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
self.wrappedValue = wrappedValue
}
}
extension Attribute: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
try wrappedValue.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
try wrappedValue = .init(from: decoder)
}
}
extension Attribute: Equatable where Value: Equatable {}
extension Attribute: Hashable where Value: Hashable {}
extension Attribute: Sendable where Value: Sendable {}
extension Attribute: ExpressibleByIntegerLiteral where Value: ExpressibleByIntegerLiteral {
public typealias IntegerLiteralType = Value.IntegerLiteralType
public init(integerLiteral value: Value.IntegerLiteralType) {
wrappedValue = Value(integerLiteral: value)
}
}
extension Attribute: ExpressibleByUnicodeScalarLiteral where Value: ExpressibleByUnicodeScalarLiteral {
public init(unicodeScalarLiteral value: Value.UnicodeScalarLiteralType) {
wrappedValue = Value(unicodeScalarLiteral: value)
}
public typealias UnicodeScalarLiteralType = Value.UnicodeScalarLiteralType
}
extension Attribute: ExpressibleByExtendedGraphemeClusterLiteral where Value: ExpressibleByExtendedGraphemeClusterLiteral {
public typealias ExtendedGraphemeClusterLiteralType = Value.ExtendedGraphemeClusterLiteralType
public init(extendedGraphemeClusterLiteral value: Value.ExtendedGraphemeClusterLiteralType) {
wrappedValue = Value(extendedGraphemeClusterLiteral: value)
}
}
extension Attribute: ExpressibleByStringLiteral where Value: ExpressibleByStringLiteral {
public typealias StringLiteralType = Value.StringLiteralType
public init(stringLiteral value: Value.StringLiteralType) {
wrappedValue = Value(stringLiteral: value)
}
}
extension Attribute: ExpressibleByBooleanLiteral where Value: ExpressibleByBooleanLiteral {
public typealias BooleanLiteralType = Value.BooleanLiteralType
public init(booleanLiteral value: Value.BooleanLiteralType) {
wrappedValue = Value(booleanLiteral: value)
}
}
extension Attribute: ExpressibleByNilLiteral where Value: ExpressibleByNilLiteral {
public init(nilLiteral: ()) {
wrappedValue = Value(nilLiteral: ())
}
}
protocol XMLOptionalAttributeProtocol: XMLAttributeProtocol {
init()
}
extension Attribute: XMLOptionalAttributeProtocol where Value: AnyOptional {
init() {
wrappedValue = Value()
}
}

View file

@ -0,0 +1,53 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct BoolBox: Equatable {
typealias Unboxed = Bool
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init?(xmlString: String) {
switch xmlString.lowercased() {
case "false", "0", "n", "no": self.init(false)
case "true", "1", "y", "yes": self.init(true)
case _: return nil
}
}
}
extension BoolBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Boolean has a lexical representation consisting of the following
/// legal literals {`true`, `false`, `1`, `0`}.
///
/// # Canonical representation
/// The canonical representation for boolean is the set of literals {`true`, `false`}.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#boolean)
var xmlString: String? {
return (unboxed) ? "true" : "false"
}
}
extension BoolBox: SimpleBox {}
extension BoolBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
protocol Box {
var isNull: Bool { get }
var xmlString: String? { get }
}
/// A box that only describes a single atomic value.
protocol SimpleBox: Box {
// A simple tagging protocol, for now.
}
protocol TypeErasedSharedBoxProtocol {
func typeErasedUnbox() -> Box
}
protocol SharedBoxProtocol: TypeErasedSharedBoxProtocol {
associatedtype B: Box
func unbox() -> B
}
extension SharedBoxProtocol {
func typeErasedUnbox() -> Box {
return unbox()
}
}

View file

@ -0,0 +1,41 @@
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by James Bean on 7/18/19.
//
/// A `Box` which represents an element which is known to contain an XML choice element.
struct ChoiceBox {
var key: String = ""
var element: Box = NullBox()
}
extension ChoiceBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
extension ChoiceBox: SimpleBox {}
extension ChoiceBox {
init?(_ keyedBox: KeyedBox) {
guard
let firstKey = keyedBox.elements.keys.first,
let firstElement = keyedBox.elements[firstKey].first
else {
return nil
}
self.init(key: firstKey, element: firstElement)
}
init(_ singleKeyedBox: SingleKeyedBox) {
self.init(key: singleKeyedBox.key, element: singleKeyedBox.element)
}
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/19/18.
//
import Foundation
struct DataBox: Equatable {
enum Format: Equatable {
case base64
}
typealias Unboxed = Data
let unboxed: Unboxed
let format: Format
init(_ unboxed: Unboxed, format: Format) {
self.unboxed = unboxed
self.format = format
}
init?(base64 string: String) {
guard let data = Data(base64Encoded: string) else {
return nil
}
self.init(data, format: .base64)
}
func xmlString(format: Format) -> String {
switch format {
case .base64:
return unboxed.base64EncodedString()
}
}
}
extension DataBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return xmlString(format: format)
}
}
extension DataBox: SimpleBox {}
extension DataBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,99 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/18/18.
//
import Foundation
struct DateBox: Equatable, Sendable {
enum Format: Equatable {
case secondsSince1970
case millisecondsSince1970
case iso8601
case formatter(DateFormatter)
}
typealias Unboxed = Date
let unboxed: Unboxed
let format: Format
init(_ unboxed: Unboxed, format: Format) {
self.unboxed = unboxed
self.format = format
}
init?(secondsSince1970 string: String) {
guard let seconds = TimeInterval(string) else {
return nil
}
let unboxed = Date(timeIntervalSince1970: seconds)
self.init(unboxed, format: .secondsSince1970)
}
init?(millisecondsSince1970 string: String) {
guard let milliseconds = TimeInterval(string) else {
return nil
}
let unboxed = Date(timeIntervalSince1970: milliseconds / 1000.0)
self.init(unboxed, format: .millisecondsSince1970)
}
init?(iso8601 string: String) {
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
guard let unboxed = ISO8601DateFormatter.xmlCoderFormatter().date(from: string) else {
return nil
}
self.init(unboxed, format: .iso8601)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
}
init?(xmlString: String, formatter: DateFormatter) {
guard let date = formatter.date(from: xmlString) else {
return nil
}
self.init(date, format: .formatter(formatter))
}
func xmlString(format: Format) -> String {
switch format {
case .secondsSince1970:
let seconds = unboxed.timeIntervalSince1970
return seconds.description
case .millisecondsSince1970:
let milliseconds = unboxed.timeIntervalSince1970 * 1000.0
return milliseconds.description
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
return ISO8601DateFormatter.xmlCoderFormatter().string(from: self.unboxed)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
case let .formatter(formatter):
return formatter.string(from: unboxed)
}
}
}
extension DateBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return xmlString(format: format)
}
}
extension DateBox: SimpleBox {}
extension DateBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,62 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
import Foundation
struct DecimalBox: Equatable {
typealias Unboxed = Decimal
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init?(xmlString: String) {
guard let unboxed = Unboxed(string: xmlString) else {
return nil
}
self.init(unboxed)
}
}
extension DecimalBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Decimal has a lexical representation consisting of a finite-length sequence of
/// decimal digits separated by a period as a decimal indicator.
/// An optional leading sign is allowed. If the sign is omitted, `"+"` is assumed.
/// Leading and trailing zeroes are optional. If the fractional part is zero,
/// the period and following zero(es) can be omitted.
/// For example: `-1.23`, `12678967.543233`, `+100000.00`, `210`.
///
/// # Canonical representation
/// The canonical representation for decimal is defined by prohibiting certain
/// options from the Lexical representation. Specifically, the preceding optional
/// `"+"` sign is prohibited. The decimal point is required. Leading and trailing
/// zeroes are prohibited subject to the following: there must be at least one
/// digit to the right and to the left of the decimal point which may be a zero.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#decimal)
var xmlString: String? {
return "\(unboxed)"
}
}
extension DecimalBox: SimpleBox {}
extension DecimalBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,49 @@
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Max Desiatov on 05/10/2019.
//
struct DoubleBox: Equatable, ValueBox {
typealias Unboxed = Double
let unboxed: Unboxed
init(_ value: Unboxed) {
unboxed = value
}
init?(xmlString: String) {
guard let unboxed = Double(xmlString) else { return nil }
self.init(unboxed)
}
}
extension DoubleBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
guard !unboxed.isNaN else {
return "NaN"
}
guard !unboxed.isInfinite else {
return (unboxed > 0.0) ? "INF" : "-INF"
}
return unboxed.description
}
}
extension DoubleBox: SimpleBox {}
extension DoubleBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,78 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct FloatBox: Equatable, ValueBox {
typealias Unboxed = Float
let unboxed: Unboxed
init<Float: BinaryFloatingPoint>(_ unboxed: Float) {
self.unboxed = Unboxed(unboxed)
}
init?(xmlString: String) {
guard let unboxed = Unboxed(xmlString) else {
return nil
}
self.init(unboxed)
}
}
extension FloatBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// float values have a lexical representation consisting of a mantissa followed, optionally,
/// by the character `"E"` or `"e"`, followed by an exponent. The exponent **must** be an integer.
/// The mantissa **must** be a decimal number. The representations for exponent and mantissa **must**
/// follow the lexical rules for integer and decimal. If the `"E"` or `"e"` and the following
/// exponent are omitted, an exponent value of `0` is assumed.
///
/// The special values positive and negative infinity and not-a-number have lexical
/// representations `INF`, `-INF` and `NaN`, respectively. Lexical representations for zero
/// may take a positive or negative sign.
///
/// For example, `-1E4`, `1267.43233E12`, `12.78e-2`, `12` , `-0`, `0` and `INF` are all
/// legal literals for float.
///
/// # Canonical representation
/// The canonical representation for float is defined by prohibiting certain options from the
/// Lexical representation. Specifically, the exponent must be indicated by `"E"`.
/// Leading zeroes and the preceding optional `"+"` sign are prohibited in the exponent.
/// If the exponent is zero, it must be indicated by `"E0"`. For the mantissa, the preceding
/// optional `"+"` sign is prohibited and the decimal point is required. Leading and trailing
/// zeroes are prohibited subject to the following: number representations must be normalized
/// such that there is a single digit which is non-zero to the left of the decimal point and
/// at least a single digit to the right of the decimal point unless the value being represented
/// is zero. The canonical representation for zero is `0.0E0`.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#float)
var xmlString: String? {
guard !unboxed.isNaN else {
return "NaN"
}
guard !unboxed.isInfinite else {
return (unboxed > 0.0) ? "INF" : "-INF"
}
return unboxed.description
}
}
extension FloatBox: SimpleBox {}
extension FloatBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,59 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct IntBox: Equatable {
typealias Unboxed = Int64
let unboxed: Unboxed
init<Integer: SignedInteger>(_ unboxed: Integer) {
self.unboxed = Unboxed(unboxed)
}
init?(xmlString: String) {
guard let unboxed = Unboxed(xmlString) else {
return nil
}
self.init(unboxed)
}
func unbox<Integer: BinaryInteger>() -> Integer? {
return Integer(exactly: unboxed)
}
}
extension IntBox: Box {
var isNull: Bool {
return false
}
/// # Lexical representation
/// Integer has a lexical representation consisting of a finite-length sequence of
/// decimal digits with an optional leading sign. If the sign is omitted, `"+"` is assumed.
/// For example: `-1`, `0`, `12678967543233`, `+100000`.
///
/// # Canonical representation
/// The canonical representation for integer is defined by prohibiting certain
/// options from the Lexical representation. Specifically, the preceding optional
/// `"+"` sign is prohibited and leading zeroes are prohibited.
///
/// ---
///
/// [Schema definition](https://www.w3.org/TR/xmlschema-2/#integer)
var xmlString: String? {
return unboxed.description
}
}
extension IntBox: SimpleBox {}
extension IntBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 11/19/18.
//
struct KeyedBox {
typealias Key = String
typealias Attribute = SimpleBox
typealias Element = Box
typealias Attributes = KeyedStorage<Key, Attribute>
typealias Elements = KeyedStorage<Key, Element>
var elements = Elements()
var attributes = Attributes()
var unboxed: (elements: Elements, attributes: Attributes) {
return (
elements: elements,
attributes: attributes
)
}
var value: SimpleBox? {
return elements.values.first as? SimpleBox
}
}
extension KeyedBox {
init<E, A>(elements: E, attributes: A)
where E: Sequence, E.Element == (Key, Element),
A: Sequence, A.Element == (Key, Attribute)
{
let elements = Elements(elements)
let attributes = Attributes(attributes)
self.init(elements: elements, attributes: attributes)
}
}
extension KeyedBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}
extension KeyedBox: CustomStringConvertible {
var description: String {
return "{attributes: \(attributes), elements: \(elements)}"
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct NullBox {}
extension NullBox: Box {
var isNull: Bool {
return true
}
var xmlString: String? {
return nil
}
}
extension NullBox: SimpleBox {}
extension NullBox: Equatable {
static func ==(_: NullBox, _: NullBox) -> Bool {
return true
}
}
extension NullBox: CustomStringConvertible {
var description: String {
return "null"
}
}

View file

@ -0,0 +1,35 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/22/18.
//
class SharedBox<Unboxed: Box> {
private(set) var unboxed: Unboxed
init(_ wrapped: Unboxed) {
unboxed = wrapped
}
func withShared<T>(_ body: (inout Unboxed) throws -> T) rethrows -> T {
return try body(&unboxed)
}
}
extension SharedBox: Box {
var isNull: Bool {
return unboxed.isNull
}
var xmlString: String? {
return unboxed.xmlString
}
}
extension SharedBox: SharedBoxProtocol {
func unbox() -> Unboxed {
return unboxed
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2019-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by James Bean on 7/15/19.
//
/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent
/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using
/// enums with associated values).
struct SingleKeyedBox: SimpleBox {
var key: String
var element: Box
}
extension SingleKeyedBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return nil
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) 2018-2020 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Vincent Esche on 12/17/18.
//
struct StringBox: Equatable {
typealias Unboxed = String
let unboxed: Unboxed
init(_ unboxed: Unboxed) {
self.unboxed = unboxed
}
init(xmlString: Unboxed) {
self.init(xmlString)
}
}
extension StringBox: Box {
var isNull: Bool {
return false
}
var xmlString: String? {
return unboxed.description
}
}
extension StringBox: SimpleBox {}
extension StringBox: CustomStringConvertible {
var description: String {
return unboxed.description
}
}

Some files were not shown because too many files have changed in this diff Show more