Skip to content

Commit b6d7b59

Browse files
committed
Adds SwiftLibgd package
This commit introduces the SwiftLibgd package, providing Swift bindings for the libgd graphics library. It incorporates the necessary module map and header file for gd, defines core data structures like Color, Point, Size, and Rectangle, along with image format handling and basic image manipulation capabilities.
1 parent 52f7cfb commit b6d7b59

12 files changed

Lines changed: 569 additions & 8 deletions

File tree

Package.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
// swift-tools-version: 6.2
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
3-
42
import PackageDescription
53

64
let package = Package(
75
name: "SwiftLibgd",
86
products: [
9-
// Products define the executables and libraries a package produces, making them visible to other packages.
107
.library(
118
name: "SwiftLibgd",
129
targets: ["SwiftLibgd"]
1310
),
1411
],
1512
targets: [
16-
// Targets are the basic building blocks of a package, defining a module or a test suite.
17-
// Targets can depend on other targets in this package and products from dependencies.
13+
.systemLibrary(
14+
name: "gd",
15+
pkgConfig: "gdlib",
16+
providers: [
17+
.brew(["gd"]),
18+
.apt(["libgd-dev"])
19+
]
20+
),
1821
.target(
19-
name: "SwiftLibgd"
22+
name: "SwiftLibgd",
23+
dependencies: ["gd"],
24+
path: "Sources"
2025
),
2126
.testTarget(
2227
name: "SwiftLibgdTests",

Sources/SwiftLibgd/Color.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
public struct Color {
2+
public var redComponent: Double
3+
public var greenComponent: Double
4+
public var blueComponent: Double
5+
public var alphaComponent: Double
6+
7+
public init(red: Double, green: Double, blue: Double, alpha: Double = 1) {
8+
redComponent = red
9+
greenComponent = green
10+
blueComponent = blue
11+
alphaComponent = alpha
12+
}
13+
}
14+
15+
// MARK: - Presets
16+
17+
@MainActor
18+
extension Color {
19+
public static let red = Color(red: 1, green: 0, blue: 0)
20+
public static let green = Color(red: 0, green: 1, blue: 0)
21+
public static let blue = Color(red: 0, green: 0, blue: 1)
22+
public static let black = Color(red: 0, green: 0, blue: 0)
23+
public static let white = Color(red: 1, green: 1, blue: 1)
24+
}
25+
26+
// MARK: - Hex Support
27+
28+
extension Color {
29+
/// Creates a color from hex string like "#ff0000" or "f00"
30+
/// Supports: RGB, RGBA, #RGB, #RGBA, RRGGBB, RRGGBBAA, #RRGGBB, #RRGGBBAA
31+
public init(hex: String, leadingAlpha: Bool = false) throws {
32+
let sanitized = try Self.sanitize(hex: hex, leadingAlpha: leadingAlpha)
33+
guard let code = Int(sanitized, radix: 16) else {
34+
throw Error.invalidColor(reason: "\(hex) is not valid hex")
35+
}
36+
self.init(hex: code, leadingAlpha: leadingAlpha)
37+
}
38+
39+
/// Creates a color from hex integer like 0xff0000
40+
public init(hex: Int, leadingAlpha: Bool = false) {
41+
let r = Double((hex >> 24) & 0xff) / 255.0
42+
let g = Double((hex >> 16) & 0xff) / 255.0
43+
let b = Double((hex >> 8) & 0xff) / 255.0
44+
let a = Double(hex & 0xff) / 255.0
45+
46+
self = leadingAlpha
47+
? Color(red: g, green: b, blue: a, alpha: r) // ARGB
48+
: Color(red: r, green: g, blue: b, alpha: a) // RGBA
49+
}
50+
51+
private static func sanitize(hex: String, leadingAlpha: Bool) throws -> String {
52+
var s = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex
53+
54+
// Expand short forms: "f0a" -> "ff00aa"
55+
if s.count == 3 || s.count == 4 {
56+
s = s.map { "\($0)\($0)" }.joined()
57+
}
58+
59+
// Add alpha if missing
60+
switch s.count {
61+
case 6: return leadingAlpha ? "ff" + s : s + "ff"
62+
case 8: return s
63+
default: throw Error.invalidColor(reason: "\(hex) has invalid length")
64+
}
65+
}
66+
}

Sources/SwiftLibgd/Error.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public enum Error: Swift.Error {
2+
case invalidFormat
3+
case invalidImage(reason: String)
4+
case invalidColor(reason: String)
5+
case invalidMaxColors(reason: String)
6+
}

Sources/SwiftLibgd/Format.swift

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#if os(Linux)
2+
import Glibc
3+
#else
4+
import Darwin
5+
#endif
6+
7+
import Foundation
8+
import gd
9+
10+
// MARK: - Import Formats
11+
12+
public enum ImportableFormat {
13+
case bmp, gif, jpg, png, tiff, tga, wbmp, webp, avif, any
14+
15+
public func imagePtr(of data: Data) throws -> gdImagePtr {
16+
if case .any = self {
17+
return try tryAllFormats(data: data)
18+
}
19+
20+
let (ptr, size) = try data.memoryPointer()
21+
22+
let creator: (Int32, UnsafeMutableRawPointer) -> gdImagePtr? = switch self {
23+
case .bmp: gdImageCreateFromBmpPtr
24+
case .gif: gdImageCreateFromGifPtr
25+
case .jpg: gdImageCreateFromJpegPtr
26+
case .png: gdImageCreateFromPngPtr
27+
case .tiff: gdImageCreateFromTiffPtr
28+
case .tga: gdImageCreateFromTgaPtr
29+
case .wbmp: gdImageCreateFromWBMPPtr
30+
case .webp: gdImageCreateFromWebpPtr
31+
case .avif: gdImageCreateFromAvifPtr
32+
case .any: gdImageCreateFromPngPtr // Never reached
33+
}
34+
35+
guard let image = creator(size, ptr) else {
36+
throw Error.invalidFormat
37+
}
38+
return image
39+
}
40+
41+
private func tryAllFormats(data: Data) throws -> gdImagePtr {
42+
let formats: [ImportableFormat] = [.jpg, .png, .gif, .webp, .tiff, .bmp, .wbmp]
43+
for format in formats {
44+
if let image = try? format.imagePtr(of: data) {
45+
return image
46+
}
47+
}
48+
throw Error.invalidImage(reason: "No matching format found")
49+
}
50+
}
51+
52+
// MARK: - Export Formats
53+
54+
public enum ExportableFormat {
55+
case bmp(compression: Bool = false)
56+
case gif
57+
case jpg(quality: Int32 = -1)
58+
case png
59+
case tiff
60+
case wbmp(index: Int32)
61+
case webp
62+
case avif
63+
64+
public func data(of imagePtr: gdImagePtr) throws -> Data {
65+
var size: Int32 = 0
66+
67+
let bytes: UnsafeMutableRawPointer? = switch self {
68+
case .bmp(let compress):
69+
gdImageBmpPtr(imagePtr, &size, compress ? 1 : 0)
70+
case .gif:
71+
gdImageGifPtr(imagePtr, &size)
72+
case .jpg(let quality):
73+
gdImageJpegPtr(imagePtr, &size, quality)
74+
case .png:
75+
gdImagePngPtr(imagePtr, &size)
76+
case .tiff:
77+
gdImageTiffPtr(imagePtr, &size)
78+
case .wbmp(let index):
79+
gdImageWBMPPtr(imagePtr, &size, index)
80+
case .webp:
81+
gdImageWebpPtr(imagePtr, &size)
82+
case .avif:
83+
gdImageAvifPtr(imagePtr, &size)
84+
}
85+
86+
guard let bytes = bytes else {
87+
throw Error.invalidFormat
88+
}
89+
90+
// Use custom deallocator for formats that need gdFree
91+
if case .bmp = self {
92+
return Data(bytesNoCopy: bytes, count: Int(size),
93+
deallocator: .custom({ ptr, _ in gdFree(ptr) }))
94+
} else if case .jpg = self {
95+
return Data(bytesNoCopy: bytes, count: Int(size),
96+
deallocator: .custom({ ptr, _ in gdFree(ptr) }))
97+
} else if case .wbmp = self {
98+
return Data(bytesNoCopy: bytes, count: Int(size),
99+
deallocator: .custom({ ptr, _ in gdFree(ptr) }))
100+
}
101+
102+
return Data(bytes: bytes, count: Int(size))
103+
}
104+
}
105+
106+
// MARK: - Data Helper
107+
108+
private extension Data {
109+
func memoryPointer() throws -> (pointer: UnsafeMutableRawPointer, size: Int32) {
110+
guard count < Int32.max else {
111+
throw Error.invalidImage(reason: "Image data exceeds Int32.max")
112+
}
113+
114+
return try withUnsafeBytes { bytes in
115+
guard let baseAddress = bytes.baseAddress else {
116+
throw Error.invalidImage(reason: "Invalid memory address")
117+
}
118+
return (UnsafeMutableRawPointer(mutating: baseAddress), Int32(count))
119+
}
120+
}
121+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
public struct Angle {
2+
public var radians: Double
3+
4+
public var degrees: Double {
5+
get { radians * 180 / .pi }
6+
set { radians = newValue * .pi / 180 }
7+
}
8+
9+
public init(radians: Double) {
10+
self.radians = radians
11+
}
12+
13+
public init(degrees: Double) {
14+
self.radians = degrees * .pi / 180
15+
}
16+
17+
@MainActor
18+
public static let zero = Angle(radians: 0)
19+
20+
public static func radians(_ radians: Double) -> Angle {
21+
Angle(radians: radians)
22+
}
23+
24+
public static func degrees(_ degrees: Double) -> Angle {
25+
Angle(degrees: degrees)
26+
}
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
public struct Point: Equatable, Hashable {
2+
public var x: Int
3+
public var y: Int
4+
5+
public init(x: Int, y: Int) {
6+
self.x = x
7+
self.y = y
8+
}
9+
10+
public init(x: Int32, y: Int32) {
11+
self.init(x: Int(x), y: Int(y))
12+
}
13+
14+
@MainActor
15+
public static let zero = Point(x: 0, y: 0)
16+
17+
public static func + (lhs: Point, rhs: Point) -> Point {
18+
Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
19+
}
20+
21+
public static func - (lhs: Point, rhs: Point) -> Point {
22+
Point(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
23+
}
24+
25+
public static func * (lhs: Point, rhs: Int) -> Point {
26+
Point(x: lhs.x * rhs, y: lhs.y * rhs)
27+
}
28+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
public struct Rectangle: Equatable {
2+
public var point: Point
3+
public var size: Size
4+
5+
public init(point: Point, size: Size) {
6+
self.point = point
7+
self.size = size
8+
}
9+
10+
public init(x: Int, y: Int, width: Int, height: Int) {
11+
self.init(point: Point(x: x, y: y), size: Size(width: width, height: height))
12+
}
13+
14+
public init(x: Int32, y: Int32, width: Int32, height: Int32) {
15+
self.init(x: Int(x), y: Int(y), width: Int(width), height: Int(height))
16+
}
17+
18+
@MainActor
19+
public static let zero = Rectangle(point: .zero, size: .zero)
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
public struct Size: Comparable {
2+
public var width: Int
3+
public var height: Int
4+
5+
public init(width: Int, height: Int) {
6+
self.width = width
7+
self.height = height
8+
}
9+
10+
public init(width: Int32, height: Int32) {
11+
self.init(width: Int(width), height: Int(height))
12+
}
13+
14+
@MainActor
15+
public static let zero = Size(width: 0, height: 0)
16+
17+
public static func < (lhs: Size, rhs: Size) -> Bool {
18+
lhs.width < rhs.width && lhs.height < rhs.height
19+
}
20+
}

0 commit comments

Comments
 (0)