├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── Sources └── SwiftImage │ ├── AnyImage.swift │ ├── AppKit.swift │ ├── ColorLiteral.swift │ ├── Convolution.swift │ ├── Convolution.swift.gyb │ ├── CoreGraphics.swift │ ├── ExtrapolatedImage.swift │ ├── Extrapolation.swift │ ├── HigherOrderFunctions.swift │ ├── Image.swift │ ├── ImageFormat.swift │ ├── ImageIterator.swift │ ├── ImageOperators.swift │ ├── ImageOperators.swift.gyb │ ├── ImageProtocol.swift │ ├── ImageSlice.swift │ ├── Interpolation.swift │ ├── NumericPixel.swift │ ├── NumericPixel.swift.gyb │ ├── PremultipliedRGBA.swift │ ├── RGB.swift │ ├── RGBA.swift │ ├── RGBAOperators.swift │ ├── RGBAOperators.swift.gyb │ ├── Resizing.swift │ ├── Rotation.swift │ ├── SwiftImage.h │ ├── TypicalChannel.swift │ ├── UIKit.swift │ └── Util.swift └── Tests └── SwiftImageTests ├── AnyImageTests.swift ├── AppKitTests.swift ├── AppUIKitTests.swift ├── AutoreleaseTests.swift ├── ColorLiteralTests.swift ├── ConvolutionTests.swift ├── CoreGraphicsTests.swift ├── ExtrapolationTests.swift ├── HigherOrderFunctionsTests.swift ├── ImageOperatorsTests.swift ├── ImageProtocolTests.swift ├── ImageSliceTests.swift ├── ImageTests.swift ├── InterpolationTests.swift ├── PremultipliedRGBATests.swift ├── RGBAOperatorsTests.swift ├── RGBATests.swift ├── RGBTests.swift ├── ResizingTests.swift ├── RotationTests.swift ├── SwiftImageTests.swift ├── Test2x1.png ├── Test2x2.png ├── Test4x4.png ├── UIKitTests.swift └── Util.swift /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | mac-os: 11 | name: macOS 12 | runs-on: macos-14 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Xcode 15 16 | uses: maxim-lobanov/setup-xcode@v1 17 | with: 18 | xcode-version: '15' 19 | - name: Build for macOS 20 | run: swift build -c release -v 21 | - name: Test for macOS 22 | run: swift test -c release -v 23 | - name: Set up iOS Simulator 24 | run: | 25 | xcrun simctl boot "iPhone 15 Pro" 26 | - name: Build for iOS 27 | run: xcodebuild -scheme SwiftImage -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build 28 | - name: Test on iOS Simulator 29 | run: xcodebuild -scheme SwiftImage -destination 'platform=iOS Simulator,name=iPhone 15 Pro' test 30 | 31 | linux: 32 | name: Linux 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Build for Linux 37 | run: swift build -c release -v 38 | - name: Test for Linux 39 | run: swift test -c release -v 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj/project.xcworkspace/xcuserdata 6 | /*.xcodeproj/xcuserdata 7 | /Carthage 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuta Koshizawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftImage", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftImage", 12 | targets: ["SwiftImage"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "SwiftImage", 23 | dependencies: [], 24 | exclude: [ 25 | "Convolution.swift.gyb", 26 | "RGBAOperators.swift.gyb", 27 | "NumericPixel.swift.gyb", 28 | "ImageOperators.swift.gyb" 29 | ], 30 | swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]), 31 | .testTarget( 32 | name: "SwiftImageTests", 33 | dependencies: ["SwiftImage"], 34 | resources: [ 35 | .process("Test2x1.png"), 36 | .process("Test2x2.png"), 37 | .process("Test4x4.png"), 38 | ]), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftImage", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SwiftImage", 12 | targets: ["SwiftImage"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "SwiftImage", 23 | dependencies: [], 24 | exclude: [ 25 | "Convolution.swift.gyb", 26 | "RGBAOperators.swift.gyb", 27 | "NumericPixel.swift.gyb", 28 | "ImageOperators.swift.gyb" 29 | ], 30 | swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]), 31 | .testTarget( 32 | name: "SwiftImageTests", 33 | dependencies: ["SwiftImage"], 34 | resources: [ 35 | .process("Test2x1.png"), 36 | .process("Test2x2.png"), 37 | .process("Test4x4.png"), 38 | ]), 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SwiftImage CI](https://github.com/koher/swift-image/actions/workflows/tests.yml/badge.svg) 2 | # SwiftImage 3 | 4 | SwiftImage is an image library written in Swift, which provides Swifty APIs and image types with [value semantics](https://developer.apple.com/videos/play/wwdc2015/414/). 5 | 6 | ```swift 7 | var image = Image>(named: "ImageName")! 8 | 9 | let pixel: RGBA = image[x, y] 10 | image[x, y] = RGBA(red: 255, green: 0, blue: 0, alpha: 127) 11 | image[x, y] = RGBA(0xFF00007F) // red: 255, green: 0, blue: 0, alpha: 127 12 | 13 | // Iterates over all pixels 14 | for pixel in image { 15 | // ... 16 | } 17 | 18 | // Image processing (e.g. binarizations) 19 | let binarized: Image = image.map { $0.gray >= 127 } 20 | 21 | // From/to `UIImage` 22 | image = Image>(uiImage: imageView.image!) 23 | imageView.image = image.uiImage 24 | ``` 25 | 26 | ## Introduction 27 | 28 | SwiftImage makes it easy to access pixels of images. The `Image` type in SwiftImage can be used intuitively like 2D `Array`. 29 | 30 | ```swift 31 | var image: Image = Image(width: 640, height: 480, pixels: [255, 248, /* ... */]) 32 | 33 | let pixel: UInt8 = image[x, y] 34 | image[x, y] = 255 35 | 36 | let width: Int = image.width // 640 37 | let height: Int = image.height // 480 38 | ``` 39 | 40 | We can also access pixels of images using CoreGraphics. However, CoreGraphics requires us to struggle with complicated formats, old C APIs and painful memory management. SwiftImage provides clear and Swifty APIs for images. 41 | 42 | Typically `Image` is used with the `RGBA` type. `RGBA` is a simple `struct` declared as follows. 43 | 44 | ```swift 45 | struct RGBA { 46 | var red: Channel 47 | var green: Channel 48 | var blue: Channel 49 | var alpha: Channel 50 | } 51 | ``` 52 | 53 | Because `RGBA` is a generic type, it can represent various formats of pixels. For example, `RGBA` represents a pixel of 8-bit RGBA image (each channel has a value in `0...255`). Similarly, `RGBA` represents a pixel of 16-bit RGBA image (`0...65535`). `RGBA` can represent a pixel whose channels are `Float`s, which is often used for machine learning. A pixel of binary images, which have only black or white pixels and are used for fax, can be represented using `RGBA`. 54 | 55 | When `RGBA` is used with `Image`, type parameters are nested like `Image>` because both `Image` and `RGBA` are generic types. On the other hand, grayscale images can be represented without nested parameters: `Image` for 8-bit grayscale images and `Image` for 16-bit grayscale images. 56 | 57 | `Image` and `RGBA` provide powerful APIs to handle images. For example, it is possible to convert a RGBA image to grayscale combining `Image.map` with `RGBA.gray` in one line. 58 | 59 | ```swift 60 | let image: Image> = // ... 61 | let grayscale: Image = image.map { $0.gray } 62 | ``` 63 | 64 | Another notable feature of SwiftImage is that `Image` is a `struct` with [value semantics](https://developer.apple.com/videos/play/wwdc2015/414/), which is achieved using copy-on-write. Therefore, 65 | 66 | - `Image` instances never be shared 67 | - defensive copying is unnecessary 68 | - there are no wasteful copying of `Image` instances 69 | - copying is executed lazily only when it is inevitable 70 | 71 | ```swift 72 | var another: Image = image // Not copied here because of copy-on-write 73 | another[x, y] = 255 // Copied here lazily 74 | another[x, y] == image[x, y] // false: Instances are never shared 75 | ``` 76 | 77 | ## Usage 78 | 79 | ### Import 80 | 81 | ```swift 82 | import SwiftImage 83 | ``` 84 | 85 | ### Initialization 86 | 87 | ```swift 88 | let image = Image>(named: "ImageName")! 89 | let image = Image>(contentsOfFile: "path/to/file")! 90 | let image = Image>(data: Data(/* ... */))! 91 | let image = Image>(uiImage: imageView.image!) // from a UIImage 92 | let image = Image>(nsImage: imageView.image!) // from a NSImage 93 | let image = Image>(cgImage: cgImage) // from a CGImage 94 | let image = Image>(width: 640, height: 480, pixels: pixels) // from a pixel array 95 | let image = Image>(width: 640, height: 480, pixel: .black) // a black RGBA image 96 | let image = Image(width: 640, height: 480, pixel: 0) // a black grayscale image 97 | let image = Image(width: 640, height: 480, pixel: false) // a black binary image 98 | ``` 99 | 100 | ### Access to a pixel 101 | 102 | ```swift 103 | // Gets a pixel by subscripts 104 | let pixel = image[x, y] 105 | ``` 106 | 107 | ```swift 108 | // Sets a pixel by subscripts 109 | image[x, y] = RGBA(0xFF0000FF) 110 | image[x, y].alpha = 127 111 | ``` 112 | 113 | ```swift 114 | // Safe get for a pixel 115 | if let pixel = image.pixelAt(x: x, y: y) { 116 | print(pixel.red) 117 | print(pixel.green) 118 | print(pixel.blue) 119 | print(pixel.alpha) 120 | 121 | print(pixel.gray) // (red + green + blue) / 3 122 | print(pixel) // formatted like "#FF0000FF" 123 | } else { 124 | // `pixel` is safe: `nil` is returned when out of bounds 125 | print("Out of bounds") 126 | } 127 | ``` 128 | 129 | ### Iteration 130 | 131 | ```swift 132 | for pixel in image { 133 | ... 134 | } 135 | ``` 136 | 137 | ### Rotation 138 | 139 | ```swift 140 | let result = image.rotated(by: .pi) // Rotated clockwise by π 141 | ``` 142 | 143 | ```swift 144 | let result = image.rotated(byDegrees: 180) // Rotated clockwise by 180 degrees 145 | ``` 146 | 147 | ```swift 148 | // Rotated clockwise by π / 4 and fill the background with red 149 | let result = image.rotated(by: .pi / 4, extrapolatedBy: .filling(.red)) 150 | ``` 151 | 152 | ### Flip 153 | 154 | ```swift 155 | let result = image.xReversed() // Flip Horizontally 156 | ``` 157 | 158 | ```swift 159 | let result = image.yReversed() // Flip Vertically 160 | ``` 161 | 162 | ### Resizing 163 | 164 | ```swift 165 | let result = image.resizedTo(width: 320, height: 240) 166 | ``` 167 | 168 | ```swift 169 | let result = image.resizedTo(width: 320, height: 240, 170 | interpolatedBy: .nearestNeighbor) // Nearest neighbor 171 | ``` 172 | 173 | ### Crop 174 | 175 | Slicing is executed with no copying costs. 176 | 177 | ```swift 178 | let slice: ImageSlice> = image[32..<64, 32..<64] // No copying costs 179 | let cropped = Image>(slice) // Copying is executed here 180 | ``` 181 | 182 | ### Conversion 183 | 184 | `Image` can be converted by `map` in the same way as `Array`. Followings are the examples. 185 | 186 | #### Grayscale 187 | 188 | ```swift 189 | let result: Image = image.map { (pixel: RGBA) -> UInt8 in 190 | pixel.gray 191 | } 192 | ``` 193 | 194 | ```swift 195 | // Shortened form 196 | let result = image.map { $0.gray } 197 | ``` 198 | 199 | #### Binarization 200 | 201 | ```swift 202 | let result: Image = image.map { (pixel: RGBA) -> Bool in 203 | pixel.gray >= 128 204 | } 205 | ``` 206 | 207 | ```swift 208 | // Shortened form 209 | let result = image.map { $0.gray >= 128 } 210 | ``` 211 | 212 | #### Binarization (auto threshold) 213 | 214 | ```swift 215 | let threshold = UInt8(image.reduce(0) { $0 + $1.grayInt } / image.count) 216 | let result = image.map { $0.gray >= threshold } 217 | ``` 218 | 219 | #### Mean filter 220 | 221 | ```swift 222 | let kernel = Image(width: 3, height: 3, pixel: 1.0 / 9.0) 223 | let result = image.convoluted(kernel) 224 | ``` 225 | 226 | #### Gaussian filter 227 | 228 | ```swift 229 | let kernel = Image(width: 5, height: 5, pixels: [ 230 | 1, 4, 6, 4, 1, 231 | 4, 16, 24, 16, 4, 232 | 6, 24, 36, 24, 6, 233 | 4, 16, 24, 16, 4, 234 | 1, 4, 6, 4, 1, 235 | ]).map { Float($0) / 256.0 } 236 | let result = image.convoluted(kernel) 237 | ``` 238 | 239 | ### With `UIImage` 240 | 241 | ```swift 242 | // From `UIImage` 243 | let image = Image>(uiImage: imageView.image!) 244 | 245 | // To `UIImage` 246 | imageView.image = image.uiImage 247 | ``` 248 | 249 | ### With `NSImage` 250 | 251 | ```swift 252 | // From `NSImage` 253 | let image = Image>(nsImage: imageView.image!) 254 | 255 | // To `NSImage` 256 | imageView.image = image.nsImage 257 | ``` 258 | 259 | ### With CoreGraphics 260 | 261 | ```swift 262 | // Drawing on images with CoreGraphics 263 | var image = Image>(uiImage: imageView.image!) 264 | image.withCGContext { context in 265 | context.setLineWidth(1) 266 | context.setStrokeColor(UIColor.red.cgColor) 267 | context.move(to: CGPoint(x: -1, y: -1)) 268 | context.addLine(to: CGPoint(x: 640, y: 480)) 269 | context.strokePath() 270 | } 271 | imageView.image = image.uiImage 272 | ``` 273 | 274 | ## Requirements 275 | 276 | - Swift 5.9 or later 277 | 278 | ## License 279 | 280 | [The MIT License](LICENSE) 281 | -------------------------------------------------------------------------------- /Sources/SwiftImage/AnyImage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct AnyImage : ImageProtocol { 4 | public typealias SubImage = AnyImage 5 | public typealias Element = Pixel // FIXME: Remove this line in the future. Swift 4.1 needs it to build `AnyImage`. 6 | 7 | private var box: AnyImageBox 8 | private let lock = NSLock() 9 | 10 | public let xRange: Range 11 | public let yRange: Range 12 | 13 | private init(box: AnyImageBox, xRange: Range, yRange: Range) { 14 | self.box = box 15 | self.xRange = xRange 16 | self.yRange = yRange 17 | } 18 | 19 | internal init(_ image: I, xRange: Range, yRange: Range) where I.Pixel == Pixel { 20 | self.box = ImageBox(image) 21 | self.xRange = xRange 22 | self.yRange = yRange 23 | } 24 | 25 | public init(_ image: I) where I.Pixel == Pixel { 26 | self.init(image, xRange: image.xRange, yRange: image.yRange) 27 | } 28 | 29 | public init(width: Int, height: Int, pixels: [Pixel]) { 30 | self.init(Image(width: width, height: height, pixels: pixels)) 31 | } 32 | 33 | public subscript(x: Int, y: Int) -> Pixel { 34 | get { 35 | return box[x, y] 36 | } 37 | 38 | set { 39 | lock.lock() 40 | defer { lock.unlock() } 41 | 42 | if !isKnownUniquelyReferenced(&box) { 43 | box = box.copied() 44 | } 45 | box[x, y] = newValue 46 | } 47 | } 48 | 49 | public subscript(xRange: Range, yRange: Range) -> AnyImage { 50 | return AnyImage(box: self.box, xRange: xRange, yRange: yRange) 51 | } 52 | } 53 | 54 | extension AnyImage : Equatable where Pixel : Equatable { 55 | public static func ==(lhs: AnyImage, rhs: AnyImage) -> Bool { 56 | guard lhs.width == rhs.width else { return false } 57 | guard lhs.height == rhs.height else { return false } 58 | return zip(lhs, rhs).reduce(true) { $0 && $1.0 == $1.1 } 59 | } 60 | } 61 | 62 | extension AnyImage { 63 | private class AnyImageBox<_Pixel> { 64 | public subscript(x: Int, y: Int) -> _Pixel { 65 | get { 66 | fatalError("Abstract") 67 | } 68 | 69 | set { 70 | fatalError("Abstract") 71 | } 72 | } 73 | 74 | public func copied() -> AnyImageBox<_Pixel> { 75 | fatalError("Abstract") 76 | } 77 | } 78 | 79 | private class ImageBox : AnyImageBox { 80 | private var base: I 81 | 82 | public init(_ base: I) { 83 | self.base = base 84 | } 85 | 86 | override public subscript(x: Int, y: Int) -> I.Pixel { 87 | get { 88 | return base[x, y] 89 | } 90 | 91 | set { 92 | base[x, y] = newValue 93 | } 94 | } 95 | 96 | override public func copied() -> ImageBox { 97 | return ImageBox(base) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/SwiftImage/AppKit.swift: -------------------------------------------------------------------------------- 1 | #if canImport(AppKit) && canImport(CoreGraphics) 2 | import Foundation 3 | import AppKit 4 | extension ImageProtocol where Self: _CGImageConvertible, Self: Sendable, Pixel: _CGPixel { 5 | @inlinable 6 | public init(nsImage: NSImage) { 7 | if let cgImage: CGImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) { 8 | self.init(cgImage: cgImage) 9 | } else { 10 | precondition(nsImage.size == .zero, "The `size` of the given `NSImage` instance (\(nsImage)) must be equal to `.zero` when the `cgImage` of the instance is `nil`.") 11 | self.init(width: 0, height: 0, pixels: []) 12 | } 13 | } 14 | 15 | @inlinable 16 | internal init?(nsImageOrNil: NSImage?) { 17 | guard let nsImage: NSImage = nsImageOrNil else { return nil } 18 | self.init(nsImage: nsImage) 19 | } 20 | 21 | @inlinable 22 | public init?(named name: NSImage.Name) { 23 | self.init(nsImageOrNil: NSImage(named: name)) 24 | } 25 | 26 | @inlinable 27 | public init?(contentsOfFile path: String) { 28 | self.init(nsImageOrNil: NSImage(contentsOfFile: path)) 29 | } 30 | 31 | @inlinable 32 | public init?(data: Data) { 33 | self.init(nsImageOrNil: NSImage(data: data)) 34 | } 35 | 36 | @inlinable 37 | public var nsImage: NSImage { 38 | return NSImage(cgImage: cgImage, size: .zero) 39 | } 40 | 41 | @inlinable 42 | public func pngData() -> Data? { 43 | guard width > 0 && height > 0 else { return nil } 44 | 45 | return autoreleasepool { 46 | let imageRep = NSBitmapImageRep(cgImage: cgImage) 47 | imageRep.size = CGSize(width: CGFloat(width), height: CGFloat(height)) 48 | return imageRep.representation(using: .png, properties: [:]) 49 | } 50 | } 51 | 52 | @inlinable 53 | public func jpegData(compressionQuality: Double) -> Data? { 54 | guard width > 0 && height > 0 else { return nil } 55 | 56 | return autoreleasepool { 57 | let imageRep = NSBitmapImageRep(cgImage: cgImage) 58 | imageRep.size = CGSize(width: CGFloat(width), height: CGFloat(height)) 59 | return imageRep.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: compressionQuality)]) 60 | } 61 | } 62 | 63 | @inlinable 64 | public func data(using format: ImageFormat) -> Data? { 65 | switch format { 66 | case .png: 67 | return pngData() 68 | case .jpeg(let compressionQuality): 69 | return jpegData(compressionQuality: compressionQuality) 70 | } 71 | } 72 | 73 | @inlinable 74 | public func write(to url: URL, atomically: Bool, format: ImageFormat) throws { 75 | guard let data = data(using: format) else { 76 | throw ImageFormat.FormattingError(image: self, format: format) 77 | } 78 | try data.write(to: url, options: atomically ? .atomic : .init(rawValue: 0)) 79 | } 80 | 81 | @inlinable 82 | public func write(toFile path: S, atomically: Bool, format: ImageFormat) throws { 83 | try write(to: URL(fileURLWithPath: String(path)), atomically: atomically, format: format) 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ColorLiteral.swift: -------------------------------------------------------------------------------- 1 | public protocol _ColorLiteralChannel { 2 | init(_ez_colorLiteralFloatChannel: Float) 3 | } 4 | 5 | extension UInt8: _ColorLiteralChannel { 6 | public init(_ez_colorLiteralFloatChannel floatChannel: Float) { 7 | self = UInt8(clamp(floatChannel * 255, lower: 0, upper: 255)) 8 | } 9 | } 10 | 11 | extension UInt16: _ColorLiteralChannel { 12 | public init(_ez_colorLiteralFloatChannel floatChannel: Float) { 13 | self = UInt16(clamp(floatChannel * 65535, lower: 0, upper: 65535)) 14 | } 15 | } 16 | 17 | extension Float: _ColorLiteralChannel { 18 | public init(_ez_colorLiteralFloatChannel floatChannel: Float) { 19 | self = floatChannel 20 | } 21 | } 22 | 23 | extension Double: _ColorLiteralChannel { 24 | public init(_ez_colorLiteralFloatChannel floatChannel: Float) { 25 | self = Double(floatChannel) 26 | } 27 | } 28 | 29 | extension RGB: _ExpressibleByColorLiteral where Channel: _ColorLiteralChannel { 30 | public init(_colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) { 31 | self.init( 32 | red: .init(_ez_colorLiteralFloatChannel: red * alpha), 33 | green: .init(_ez_colorLiteralFloatChannel: green * alpha), 34 | blue: .init(_ez_colorLiteralFloatChannel: blue * alpha) 35 | ) 36 | } 37 | } 38 | 39 | extension RGBA: _ExpressibleByColorLiteral where Channel: _ColorLiteralChannel { 40 | public init(_colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) { 41 | self.init( 42 | red: .init(_ez_colorLiteralFloatChannel: red), 43 | green: .init(_ez_colorLiteralFloatChannel: green), 44 | blue: .init(_ez_colorLiteralFloatChannel: blue), 45 | alpha: .init(_ez_colorLiteralFloatChannel: alpha) 46 | ) 47 | } 48 | } 49 | 50 | extension PremultipliedRGBA: _ExpressibleByColorLiteral where Channel: _ColorLiteralChannel { 51 | public init(_colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) { 52 | self.init( 53 | red: .init(_ez_colorLiteralFloatChannel: red * alpha), 54 | green: .init(_ez_colorLiteralFloatChannel: green * alpha), 55 | blue: .init(_ez_colorLiteralFloatChannel: blue * alpha), 56 | alpha: .init(_ez_colorLiteralFloatChannel: alpha) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Convolution.swift: -------------------------------------------------------------------------------- 1 | extension ImageProtocol where Pixel : _NumericPixel { 2 | @inlinable 3 | public func convoluted( 4 | with kernel: Kernel, 5 | extrapolatedBy extrapolationMethod: ExtrapolationMethod = .edge 6 | ) -> Image where Kernel.Pixel == Int { 7 | switch extrapolationMethod { 8 | case .constant(let value): 9 | return _convoluted(with: kernel) { x, y in 10 | extrapolatedPixelByFillingAt(x: x, y: y, by: value) 11 | } 12 | case .edge: 13 | return _convoluted(with: kernel) { x, y in 14 | extrapolatedPixelByEdgeAt(x: x, y: y, xRange: ClosedRange(xRange), yRange: ClosedRange(yRange)) 15 | } 16 | case .repeat: 17 | return _convoluted(with: kernel) { x, y in 18 | extrapolatedPixelByRepeatAt(x: x, y: y, minX: xRange.lowerBound, minY: yRange.lowerBound, width: width, height: height) 19 | } 20 | case .reflection: 21 | let doubleWidth = width * 2 22 | let doubleHeight = height * 2 23 | return _convoluted(with: kernel) { x, y in 24 | extrapolatedPixelByReflectionAt( 25 | x: x, 26 | y: y, 27 | minX: xRange.lowerBound, 28 | minY: yRange.lowerBound, 29 | doubleWidth: doubleWidth, 30 | doubleHeight: doubleHeight, 31 | doubleWidthMinusOne: doubleWidth - 1, 32 | doubleHeightMinusOne: doubleHeight - 1 33 | ) 34 | } 35 | } 36 | } 37 | 38 | @inlinable 39 | public func convoluted( 40 | with kernel: Kernel, 41 | extrapolatedBy extrapolationMethod: ExtrapolationMethod = .edge 42 | ) -> Image where Kernel.Pixel == Float { 43 | switch extrapolationMethod { 44 | case .constant(let value): 45 | return _convoluted(with: kernel) { x, y in 46 | extrapolatedPixelByFillingAt(x: x, y: y, by: value) 47 | } 48 | case .edge: 49 | return _convoluted(with: kernel) { x, y in 50 | extrapolatedPixelByEdgeAt(x: x, y: y, xRange: ClosedRange(xRange), yRange: ClosedRange(yRange)) 51 | } 52 | case .repeat: 53 | return _convoluted(with: kernel) { x, y in 54 | extrapolatedPixelByRepeatAt(x: x, y: y, minX: xRange.lowerBound, minY: yRange.lowerBound, width: width, height: height) 55 | } 56 | case .reflection: 57 | let doubleWidth = width * 2 58 | let doubleHeight = height * 2 59 | return _convoluted(with: kernel) { x, y in 60 | extrapolatedPixelByReflectionAt( 61 | x: x, 62 | y: y, 63 | minX: xRange.lowerBound, 64 | minY: yRange.lowerBound, 65 | doubleWidth: doubleWidth, 66 | doubleHeight: doubleHeight, 67 | doubleWidthMinusOne: doubleWidth - 1, 68 | doubleHeightMinusOne: doubleHeight - 1 69 | ) 70 | } 71 | } 72 | } 73 | 74 | @inlinable 75 | public func convoluted( 76 | with kernel: Kernel, 77 | extrapolatedBy extrapolationMethod: ExtrapolationMethod = .edge 78 | ) -> Image where Kernel.Pixel == Double { 79 | switch extrapolationMethod { 80 | case .constant(let value): 81 | return _convoluted(with: kernel) { x, y in 82 | extrapolatedPixelByFillingAt(x: x, y: y, by: value) 83 | } 84 | case .edge: 85 | return _convoluted(with: kernel) { x, y in 86 | extrapolatedPixelByEdgeAt(x: x, y: y, xRange: ClosedRange(xRange), yRange: ClosedRange(yRange)) 87 | } 88 | case .repeat: 89 | return _convoluted(with: kernel) { x, y in 90 | extrapolatedPixelByRepeatAt(x: x, y: y, minX: xRange.lowerBound, minY: yRange.lowerBound, width: width, height: height) 91 | } 92 | case .reflection: 93 | let doubleWidth = width * 2 94 | let doubleHeight = height * 2 95 | return _convoluted(with: kernel) { x, y in 96 | extrapolatedPixelByReflectionAt( 97 | x: x, 98 | y: y, 99 | minX: xRange.lowerBound, 100 | minY: yRange.lowerBound, 101 | doubleWidth: doubleWidth, 102 | doubleHeight: doubleHeight, 103 | doubleWidthMinusOne: doubleWidth - 1, 104 | doubleHeightMinusOne: doubleHeight - 1 105 | ) 106 | } 107 | } 108 | } 109 | 110 | @usableFromInline 111 | internal func _convoluted( 112 | with kernel: Kernel, 113 | pixelAt: (Int, Int) -> Pixel 114 | ) -> Image where Kernel.Pixel == Int { 115 | precondition(kernel.width % 2 == 1, "The width of the `kernel` must be odd: \(kernel.width)") 116 | precondition(kernel.height % 2 == 1, "The height of the `kernel` must be odd: \(kernel.height)") 117 | 118 | let xRange = self.xRange 119 | let yRange = self.yRange 120 | 121 | let kxRange = kernel.xRange 122 | let kyRange = kernel.yRange 123 | 124 | let hw = kxRange.count / 2 // halfWidth 125 | let hh = kyRange.count / 2 // halfHeight 126 | 127 | var pixels: [Pixel] = [] 128 | pixels.reserveCapacity(count) 129 | 130 | for y in yRange { 131 | for x in xRange { 132 | var weightedValues: [Pixel._ez_AdditiveInt] = [] 133 | for ky in kyRange { 134 | for kx in kxRange { 135 | let dx = (kx - kxRange.lowerBound) - hw 136 | let dy = (ky - kyRange.lowerBound) - hh 137 | let additivePixel = pixelAt(x + dx, y + dy)._ez_additiveInt 138 | let weight = kernel[kx, ky] 139 | weightedValues.append(Pixel._ez_productInt(additivePixel, weight)) 140 | } 141 | } 142 | pixels.append(Pixel.init(_ez_additiveInt: weightedValues.reduce(Pixel._ez_AdditiveInt.zero, +))) 143 | } 144 | } 145 | 146 | return Image(width: width, height: height, pixels: pixels) 147 | } 148 | 149 | @usableFromInline 150 | internal func _convoluted( 151 | with kernel: Kernel, 152 | pixelAt: (Int, Int) -> Pixel 153 | ) -> Image where Kernel.Pixel == Float { 154 | precondition(kernel.width % 2 == 1, "The width of the `kernel` must be odd: \(kernel.width)") 155 | precondition(kernel.height % 2 == 1, "The height of the `kernel` must be odd: \(kernel.height)") 156 | 157 | let xRange = self.xRange 158 | let yRange = self.yRange 159 | 160 | let kxRange = kernel.xRange 161 | let kyRange = kernel.yRange 162 | 163 | let hw = kxRange.count / 2 // halfWidth 164 | let hh = kyRange.count / 2 // halfHeight 165 | 166 | var pixels: [Pixel] = [] 167 | pixels.reserveCapacity(count) 168 | 169 | for y in yRange { 170 | for x in xRange { 171 | var weightedValues: [Pixel._ez_AdditiveFloat] = [] 172 | for ky in kyRange { 173 | for kx in kxRange { 174 | let dx = (kx - kxRange.lowerBound) - hw 175 | let dy = (ky - kyRange.lowerBound) - hh 176 | let additivePixel = pixelAt(x + dx, y + dy)._ez_additiveFloat 177 | let weight = kernel[kx, ky] 178 | weightedValues.append(Pixel._ez_productFloat(additivePixel, weight)) 179 | } 180 | } 181 | pixels.append(Pixel.init(_ez_additiveFloat: weightedValues.reduce(Pixel._ez_AdditiveFloat.zero, +))) 182 | } 183 | } 184 | 185 | return Image(width: width, height: height, pixels: pixels) 186 | } 187 | 188 | @usableFromInline 189 | internal func _convoluted( 190 | with kernel: Kernel, 191 | pixelAt: (Int, Int) -> Pixel 192 | ) -> Image where Kernel.Pixel == Double { 193 | precondition(kernel.width % 2 == 1, "The width of the `kernel` must be odd: \(kernel.width)") 194 | precondition(kernel.height % 2 == 1, "The height of the `kernel` must be odd: \(kernel.height)") 195 | 196 | let xRange = self.xRange 197 | let yRange = self.yRange 198 | 199 | let kxRange = kernel.xRange 200 | let kyRange = kernel.yRange 201 | 202 | let hw = kxRange.count / 2 // halfWidth 203 | let hh = kyRange.count / 2 // halfHeight 204 | 205 | var pixels: [Pixel] = [] 206 | pixels.reserveCapacity(count) 207 | 208 | for y in yRange { 209 | for x in xRange { 210 | var weightedValues: [Pixel._ez_AdditiveDouble] = [] 211 | for ky in kyRange { 212 | for kx in kxRange { 213 | let dx = (kx - kxRange.lowerBound) - hw 214 | let dy = (ky - kyRange.lowerBound) - hh 215 | let additivePixel = pixelAt(x + dx, y + dy)._ez_additiveDouble 216 | let weight = kernel[kx, ky] 217 | weightedValues.append(Pixel._ez_productDouble(additivePixel, weight)) 218 | } 219 | } 220 | pixels.append(Pixel.init(_ez_additiveDouble: weightedValues.reduce(Pixel._ez_AdditiveDouble.zero, +))) 221 | } 222 | } 223 | 224 | return Image(width: width, height: height, pixels: pixels) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Convolution.swift.gyb: -------------------------------------------------------------------------------- 1 | % weight_types = ['Int', 'Float', 'Double'] 2 | % pixel_types = ['UInt8', 'UInt16', 'Int', 'Float', 'Double'] 3 | % pixel_types = pixel_types + [f'RGBA<{type}>' for type in pixel_types] 4 | % 5 | extension ImageProtocol where Pixel : _NumericPixel { 6 | % for i, type in enumerate(weight_types): 7 | % if i > 0: 8 | 9 | % end 10 | @inlinable 11 | public func convoluted( 12 | with kernel: Kernel, 13 | extrapolatedBy extrapolationMethod: ExtrapolationMethod = .edge 14 | ) -> Image where Kernel.Pixel == ${type} { 15 | switch extrapolationMethod { 16 | case .constant(let value): 17 | return _convoluted(with: kernel) { x, y in 18 | extrapolatedPixelByFillingAt(x: x, y: y, by: value) 19 | } 20 | case .edge: 21 | return _convoluted(with: kernel) { x, y in 22 | extrapolatedPixelByEdgeAt(x: x, y: y, xRange: ClosedRange(xRange), yRange: ClosedRange(yRange)) 23 | } 24 | case .repeat: 25 | return _convoluted(with: kernel) { x, y in 26 | extrapolatedPixelByRepeatAt(x: x, y: y, minX: xRange.lowerBound, minY: yRange.lowerBound, width: width, height: height) 27 | } 28 | case .reflection: 29 | let doubleWidth = width * 2 30 | let doubleHeight = height * 2 31 | return _convoluted(with: kernel) { x, y in 32 | extrapolatedPixelByReflectionAt( 33 | x: x, 34 | y: y, 35 | minX: xRange.lowerBound, 36 | minY: yRange.lowerBound, 37 | doubleWidth: doubleWidth, 38 | doubleHeight: doubleHeight, 39 | doubleWidthMinusOne: doubleWidth - 1, 40 | doubleHeightMinusOne: doubleHeight - 1 41 | ) 42 | } 43 | } 44 | } 45 | % end 46 | % 47 | % for type in weight_types: 48 | 49 | @usableFromInline 50 | internal func _convoluted( 51 | with kernel: Kernel, 52 | pixelAt: (Int, Int) -> Pixel 53 | ) -> Image where Kernel.Pixel == ${type} { 54 | precondition(kernel.width % 2 == 1, "The width of the `kernel` must be odd: \(kernel.width)") 55 | precondition(kernel.height % 2 == 1, "The height of the `kernel` must be odd: \(kernel.height)") 56 | 57 | let xRange = self.xRange 58 | let yRange = self.yRange 59 | 60 | let kxRange = kernel.xRange 61 | let kyRange = kernel.yRange 62 | 63 | let hw = kxRange.count / 2 // halfWidth 64 | let hh = kyRange.count / 2 // halfHeight 65 | 66 | var pixels: [Pixel] = [] 67 | pixels.reserveCapacity(count) 68 | 69 | for y in yRange { 70 | for x in xRange { 71 | var weightedValues: [Pixel._ez_Additive${type}] = [] 72 | for ky in kyRange { 73 | for kx in kxRange { 74 | let dx = (kx - kxRange.lowerBound) - hw 75 | let dy = (ky - kyRange.lowerBound) - hh 76 | let additivePixel = pixelAt(x + dx, y + dy)._ez_additive${type} 77 | let weight = kernel[kx, ky] 78 | weightedValues.append(Pixel._ez_product${type}(additivePixel, weight)) 79 | } 80 | } 81 | pixels.append(Pixel.init(_ez_additive${type}: weightedValues.reduce(Pixel._ez_Additive${type}.zero, +))) 82 | } 83 | } 84 | 85 | return Image(width: width, height: height, pixels: pixels) 86 | } 87 | % end 88 | } 89 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ExtrapolatedImage.swift: -------------------------------------------------------------------------------- 1 | internal struct ExtrapolatedImage : ImageProtocol { 2 | public typealias SubImage = AnyImage 3 | public typealias Element = Pixels.Pixel // FIXME: Remove this line in the future. Swift 4.1 needs it to build `ExtrapolatedImage`. 4 | 5 | private var image: Pixels 6 | private let extrapolationMethod: ExtrapolationMethod 7 | 8 | public let xRange: Range = .min ..< .max 9 | public let yRange: Range = .min ..< .max 10 | 11 | private var pixels: [Coordinate: Pixel] 12 | 13 | internal init(image: Pixels, extrapolationMethod: ExtrapolationMethod) { 14 | self.image = image 15 | self.extrapolationMethod = extrapolationMethod 16 | self.pixels = [:] 17 | } 18 | 19 | public subscript(x: Int, y: Int) -> Pixels.Pixel { 20 | get { 21 | return pixels[Coordinate(x: x, y: y)] ?? image[x, y, extrapolation: extrapolationMethod] 22 | } 23 | set { 24 | pixels[Coordinate(x: x, y: y)] = newValue 25 | } 26 | } 27 | 28 | public subscript(xRange: Range, yRange: Range) -> AnyImage { 29 | return AnyImage(self, xRange: xRange, yRange: yRange) 30 | } 31 | } 32 | 33 | private struct Coordinate : Hashable { 34 | var x: Int 35 | var y: Int 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Extrapolation.swift: -------------------------------------------------------------------------------- 1 | public enum ExtrapolationMethod { 2 | case constant(Pixel) 3 | case edge 4 | case `repeat` 5 | case reflection 6 | } 7 | 8 | private func reminder(_ a: Int, _ b: Int) -> Int { 9 | let result = a % b 10 | return result < 0 ? result + b : result 11 | } 12 | 13 | extension ImageProtocol { 14 | public subscript(x: Int, y: Int, extrapolation extrapolationMethod: ExtrapolationMethod) -> Pixel { 15 | switch extrapolationMethod { 16 | case .constant(let value): 17 | return extrapolatedPixelByFillingAt(x: x, y: y, by: value) 18 | case .edge: 19 | return extrapolatedPixelByEdgeAt(x: x, y: y, xRange: ClosedRange(xRange), yRange: ClosedRange(yRange)) 20 | case .repeat: 21 | return extrapolatedPixelByRepeatAt(x: x, y: y, minX: xRange.lowerBound, minY: yRange.lowerBound, width: width, height: height) 22 | case .reflection: 23 | let doubleWidth = width * 2 24 | let doubleHeight = height * 2 25 | return extrapolatedPixelByReflectionAt( 26 | x: x, 27 | y: y, 28 | minX: xRange.lowerBound, 29 | minY: yRange.lowerBound, 30 | doubleWidth: doubleWidth, 31 | doubleHeight: doubleHeight, 32 | doubleWidthMinusOne: doubleWidth - 1, 33 | doubleHeightMinusOne: doubleHeight - 1 34 | ) 35 | } 36 | } 37 | 38 | @usableFromInline 39 | internal func extrapolatedPixelByFillingAt(x: Int, y: Int, by value: Pixel) -> Pixel { 40 | guard xRange.contains(x) && yRange.contains(y) else { 41 | return value 42 | } 43 | return self[x, y] 44 | } 45 | 46 | @usableFromInline 47 | internal func extrapolatedPixelByEdgeAt(x: Int, y: Int, xRange: ClosedRange, yRange: ClosedRange) -> Pixel { 48 | return self[clamp(x, lower: xRange.lowerBound, upper: xRange.upperBound), clamp(y, lower: yRange.lowerBound, upper: yRange.upperBound)] 49 | } 50 | 51 | @usableFromInline 52 | internal func extrapolatedPixelByRepeatAt(x: Int, y: Int, minX: Int, minY: Int, width: Int, height: Int) -> Pixel { 53 | let x2 = reminder(x - minX, width) + minX 54 | let y2 = reminder(y - minY, height) + minY 55 | return self[x2, y2] 56 | } 57 | 58 | @usableFromInline 59 | internal func extrapolatedPixelByReflectionAt( 60 | x: Int, 61 | y: Int, 62 | minX: Int, 63 | minY: Int, 64 | doubleWidth: Int, 65 | doubleHeight: Int, 66 | doubleWidthMinusOne: Int, 67 | doubleHeightMinusOne: Int 68 | ) -> Pixel { 69 | let width = self.width 70 | let height = self.height 71 | let x2 = reminder(x - minX, doubleWidth) 72 | let y2 = reminder(y - minY, doubleHeight) 73 | let x3 = (x2 < width ? x2 : doubleWidthMinusOne - x2) + minX 74 | let y3 = (y2 < height ? y2 : doubleHeightMinusOne - y2) + minY 75 | return self[x3, y3] 76 | } 77 | } 78 | 79 | extension ImageProtocol { 80 | public subscript(xRange: Range, yRange: Range, extrapolation extrapolationMethod: ExtrapolationMethod) -> AnyImage { 81 | return ExtrapolatedImage( 82 | image: self, 83 | extrapolationMethod: extrapolationMethod 84 | )[xRange, yRange] 85 | } 86 | 87 | public subscript(xRange: R1, yRange: R2, extrapolation extrapolationMethod: ExtrapolationMethod) -> AnyImage where R1.Bound == Int, R2.Bound == Int { 88 | return self[range(from: xRange, relativeTo: self.xRange), range(from: yRange, relativeTo: self.yRange), extrapolation: extrapolationMethod] 89 | } 90 | 91 | public subscript(xRange: R1, yRange: UnboundedRange, extrapolation extrapolationMethod: ExtrapolationMethod) -> AnyImage where R1.Bound == Int { 92 | return self[range(from: xRange, relativeTo: self.xRange), self.yRange, extrapolation: extrapolationMethod] 93 | } 94 | 95 | public subscript(xRange: UnboundedRange, yRange: R2, extrapolation extrapolationMethod: ExtrapolationMethod) -> AnyImage where R2.Bound == Int { 96 | return self[self.xRange, range(from: yRange, relativeTo: self.yRange), extrapolation: extrapolationMethod] 97 | } 98 | 99 | public subscript(xRange: UnboundedRange, yRange: UnboundedRange, extrapolation extrapolationMethod: ExtrapolationMethod) -> AnyImage { 100 | return self[self.xRange, self.yRange, extrapolation: extrapolationMethod] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/SwiftImage/HigherOrderFunctions.swift: -------------------------------------------------------------------------------- 1 | extension ImageProtocol { 2 | @inlinable 3 | public func map(_ transform: (Pixel) throws -> T) rethrows -> Image { 4 | let pixels: [T] = try map(transform) 5 | return Image(width: width, height: height, pixels: pixels) 6 | } 7 | 8 | // This method is experimental. When a similar method is added to `MutableCollection` in the Swift standard library, the name of this method should follow it. It is under discussion if such a method should be added to the standard library. So it keeps this method experimental until a conclusion is made to avoid renaming proper `public` methods. 9 | // - https://forums.swift.org/t/in-place-map-for-mutablecollection/11111 10 | // - https://forums.swift.org/t/idea-mutatingforeach-for-collections/18442 11 | @inlinable 12 | public mutating func _update(_ body: (inout Pixel) throws -> ()) rethrows { 13 | for y in yRange { 14 | for x in xRange { 15 | try body(&self[x, y]) 16 | } 17 | } 18 | } 19 | } 20 | 21 | // Special implementation for `Image` for performance 22 | #if swift(>=4.1) 23 | extension Image { 24 | @inlinable 25 | public mutating func _update(_ body: (inout Pixel) throws -> ()) rethrows { 26 | let count = self.count 27 | guard count > 0 else { return } 28 | try pixels.withUnsafeMutableBufferPointer { 29 | for pointer in $0.baseAddress! ..< $0.baseAddress! + count { 30 | try body(&pointer.pointee) 31 | } 32 | } 33 | } 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Image.swift: -------------------------------------------------------------------------------- 1 | public struct Image : ImageProtocol { 2 | public typealias SubImage = ImageSlice 3 | 4 | public let width: Int 5 | public let height: Int 6 | @usableFromInline internal var pixels: [Pixel] 7 | 8 | public init(width: Int, height: Int, pixels: [Pixel]) { 9 | precondition(width >= 0, "`width` must be greater than or equal to 0: \(width)") 10 | precondition(height >= 0, "`height` must be greater than or equal to 0: \(height)") 11 | let count = width * height 12 | precondition(pixels.count == count, "`pixels.count` (\(pixels.count)) must be equal to `width` (\(width)) * `height` (\(height)) (= \(count)).") 13 | 14 | self.width = width 15 | self.height = height 16 | self.pixels = pixels 17 | } 18 | 19 | public var xRange: Range { 20 | return 0.. { 24 | return 0.. Pixel { 28 | get { 29 | return pixels[pixelIndexAt(x: x, y: y)] 30 | } 31 | set { 32 | pixels[pixelIndexAt(x: x, y: y)] = newValue 33 | } 34 | } 35 | 36 | public subscript(xRange: Range, yRange: Range) -> ImageSlice { 37 | return ImageSlice(image: self, xRange: xRange, yRange: yRange) 38 | } 39 | } 40 | 41 | extension Image : Sendable where Pixel : Sendable {} 42 | 43 | extension Image { // Initializers for ImageSlice 44 | public init(_ imageSlice: ImageSlice) { 45 | self.init(width: imageSlice.width, height: imageSlice.height, pixels: Array(imageSlice)) 46 | } 47 | } 48 | 49 | extension Image { 50 | public func makeIterator() -> IndexingIterator<[Pixel]> { 51 | return pixels.makeIterator() 52 | } 53 | } 54 | 55 | extension Image { // Pointers 56 | public func withUnsafeBufferPointer(_ body: (UnsafeBufferPointer) throws -> R) rethrows -> R { 57 | return try pixels.withUnsafeBufferPointer(body) 58 | } 59 | 60 | public mutating func withUnsafeMutableBufferPointer(_ body: (inout UnsafeMutableBufferPointer) throws -> R) rethrows -> R { 61 | return try pixels.withUnsafeMutableBufferPointer(body) 62 | } 63 | 64 | public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { 65 | return try pixels.withUnsafeBytes(body) 66 | } 67 | 68 | public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R { 69 | return try pixels.withUnsafeMutableBytes(body) 70 | } 71 | } 72 | 73 | extension Image { 74 | internal func pixelIndexAt(x: Int, y: Int) -> Int { 75 | precondition(xRange.contains(x), "`x` is out of bounds: \(x)") 76 | precondition(yRange.contains(y), "`y` is out of bounds: \(y)") 77 | return y * width + x 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ImageFormat.swift: -------------------------------------------------------------------------------- 1 | public enum ImageFormat: Sendable { 2 | case png 3 | case jpeg(compressionQuality: Double) 4 | 5 | public struct FormattingError : Error where Image: ImageProtocol, Image: Sendable { 6 | public let image: Image 7 | public let format: ImageFormat 8 | 9 | @usableFromInline 10 | internal init(image: Image, format: ImageFormat) { 11 | self.image = image 12 | self.format = format 13 | } 14 | } 15 | } 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ImageIterator.swift: -------------------------------------------------------------------------------- 1 | public struct ImageIterator : IteratorProtocol { 2 | public typealias Element = Pixels.Element 3 | 4 | private let image: Pixels 5 | 6 | private var x: Int 7 | private var y: Int 8 | 9 | internal init(_ image: Pixels) { 10 | self.image = image 11 | 12 | self.x = image.xRange.lowerBound 13 | self.y = image.yRange.lowerBound 14 | } 15 | 16 | public mutating func next() -> Element? { 17 | if x == image.xRange.upperBound { 18 | x = image.xRange.lowerBound 19 | y += 1 20 | } 21 | 22 | guard y < image.yRange.upperBound else { return nil } 23 | defer { x += 1 } 24 | 25 | return image[x, y] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ImageOperators.swift.gyb: -------------------------------------------------------------------------------- 1 | % types = [ 2 | % 'Numeric', 3 | % 'SignedNumeric', 4 | % 'BinaryInteger', 5 | % 'FixedWidthInteger', 6 | % 'FloatingPoint', 7 | % 'Equatable', 8 | % 'Comparable', 9 | % 'Bool', 10 | % 'RGBA', 11 | % 'RGBA', 12 | % 'RGBA', 13 | % 'RGBA', 14 | % 'RGBA', 15 | % 'RGBA', 16 | % 'RGBA', 17 | % 'RGBA', 18 | % 'RGBA', 19 | % 'RGBA', 20 | % 'RGBA', 21 | % 'RGBA', 22 | % 'RGBA', 23 | % ] 24 | % image_types = [ 25 | % 'Image', 26 | % 'ImageSlice', 27 | % ] 28 | % concrete_types = set([ 29 | % 'Bool', 30 | % 'RGBA', 31 | % 'RGBA', 32 | % 'RGBA', 33 | % 'RGBA', 34 | % 'RGBA', 35 | % 'RGBA', 36 | % 'RGBA', 37 | % 'RGBA', 38 | % 'RGBA', 39 | % 'RGBA', 40 | % 'RGBA', 41 | % 'RGBA', 42 | % 'RGBA', 43 | % ]) 44 | % type_to_operators = { 45 | % 'Numeric': ['+', '-', '*'], 46 | % 'SignedNumeric': [], 47 | % 'BinaryInteger': ['/', '%', '&', '|', '^', '<<', '>>'], 48 | % 'FixedWidthInteger': ['&+', '&-', '&*', '&<<', '&>>'], 49 | % 'FloatingPoint': ['/'], 50 | % 'Equatable': [], 51 | % 'Comparable': [], 52 | % 'Bool': ['&&', '||'], 53 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 54 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 55 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 56 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 57 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 58 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 59 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 60 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 61 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 62 | % 'RGBA': ['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '&+', '&-', '&*', '&<<', '&>>'], 63 | % 'RGBA': ['+', '-', '*', '/'], 64 | % 'RGBA': ['+', '-', '*', '/'], 65 | % 'RGBA': ['&&', '||'], 66 | % } 67 | % type_to_compound_assignment_operators = { 68 | % 'Numeric': ['+=', '-=', '*='], 69 | % 'SignedNumeric': [], 70 | % 'BinaryInteger': ['/=', '%=', '&=', '|=', '^=', '<<=', '>>='], 71 | % 'FixedWidthInteger': ['&<<=', '&>>='], 72 | % 'FloatingPoint': ['/='], 73 | % 'Equatable': [], 74 | % 'Comparable': [], 75 | % 'Bool': [], 76 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 77 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 78 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 79 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 80 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 81 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 82 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 83 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 84 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 85 | % 'RGBA': ['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '&<<=', '&>>='], 86 | % 'RGBA': ['+=', '-=', '*=', '/='], 87 | % 'RGBA': ['+=', '-=', '*=', '/='], 88 | % 'RGBA': [], 89 | % } 90 | % type_to_equality_operators = { 91 | % 'Numeric': [], 92 | % 'SignedNumeric': [], 93 | % 'BinaryInteger': [], 94 | % 'FixedWidthInteger': [], 95 | % 'FloatingPoint': [], 96 | % 'Equatable': ['==', '!='], 97 | % 'Comparable': [], 98 | % 'Bool': [], 99 | % 'RGBA': ['==', '!='], 100 | % 'RGBA': ['==', '!='], 101 | % 'RGBA': ['==', '!='], 102 | % 'RGBA': ['==', '!='], 103 | % 'RGBA': ['==', '!='], 104 | % 'RGBA': ['==', '!='], 105 | % 'RGBA': ['==', '!='], 106 | % 'RGBA': ['==', '!='], 107 | % 'RGBA': ['==', '!='], 108 | % 'RGBA': ['==', '!='], 109 | % 'RGBA': ['==', '!='], 110 | % 'RGBA': ['==', '!='], 111 | % 'RGBA': ['==', '!='], 112 | % } 113 | % type_to_comparison_operators = { 114 | % 'Numeric': [], 115 | % 'SignedNumeric': [], 116 | % 'BinaryInteger': [], 117 | % 'FixedWidthInteger': [], 118 | % 'FloatingPoint': [], 119 | % 'Equatable': [], 120 | % 'Comparable': ['<', '<=', '>', '>='], 121 | % 'Bool': [], 122 | % 'RGBA': ['<', '<=', '>', '>='], 123 | % 'RGBA': ['<', '<=', '>', '>='], 124 | % 'RGBA': ['<', '<=', '>', '>='], 125 | % 'RGBA': ['<', '<=', '>', '>='], 126 | % 'RGBA': ['<', '<=', '>', '>='], 127 | % 'RGBA': ['<', '<=', '>', '>='], 128 | % 'RGBA': ['<', '<=', '>', '>='], 129 | % 'RGBA': ['<', '<=', '>', '>='], 130 | % 'RGBA': ['<', '<=', '>', '>='], 131 | % 'RGBA': ['<', '<=', '>', '>='], 132 | % 'RGBA': ['<', '<=', '>', '>='], 133 | % 'RGBA': ['<', '<=', '>', '>='], 134 | % 'RGBA': [], 135 | % } 136 | % type_to_prefix_operators = { 137 | % 'Numeric': ['+'], 138 | % 'SignedNumeric': ['-'], 139 | % 'BinaryInteger': [], 140 | % 'FixedWidthInteger': [], 141 | % 'FloatingPoint': [], 142 | % 'Equatable': [], 143 | % 'Comparable': [], 144 | % 'Bool': ['!'], 145 | % 'RGBA': ['+', '-'], 146 | % 'RGBA': ['+', '-'], 147 | % 'RGBA': ['+', '-'], 148 | % 'RGBA': ['+', '-'], 149 | % 'RGBA': ['+', '-'], 150 | % 'RGBA': ['+'], 151 | % 'RGBA': ['+'], 152 | % 'RGBA': ['+'], 153 | % 'RGBA': ['+'], 154 | % 'RGBA': ['+'], 155 | % 'RGBA': ['+', '-'], 156 | % 'RGBA': ['+', '-'], 157 | % 'RGBA': [], 158 | % } 159 | % type_to_concrete_types = { 160 | % 'Numeric': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Float', 'Double'], 161 | % 'SignedNumeric': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'Float', 'Double'], 162 | % 'BinaryInteger': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64'], 163 | % 'FixedWidthInteger': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64'], 164 | % 'FloatingPoint': ['Float', 'Double'], 165 | % 'Equatable': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Float', 'Double', 'Bool'], 166 | % 'Comparable': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Float', 'Double'], 167 | % 'Bool': [], 168 | % 'RGBA': [], 169 | % 'RGBA': [], 170 | % 'RGBA': [], 171 | % 'RGBA': [], 172 | % 'RGBA': [], 173 | % 'RGBA': [], 174 | % 'RGBA': [], 175 | % 'RGBA': [], 176 | % 'RGBA': [], 177 | % 'RGBA': [], 178 | % 'RGBA': [], 179 | % 'RGBA': [], 180 | % 'RGBA': [], 181 | % } 182 | % for i, type in enumerate(types): 183 | % if i > 0: 184 | 185 | % end 186 | % if type == 'Equatable': 187 | % for image_type in image_types: 188 | extension ${image_type} : Equatable where Pixel : Equatable {} 189 | % end 190 | % end 191 | extension ImageProtocol where Element ${'==' if type in concrete_types else ':'} ${type} { 192 | % first = True 193 | % for i, operator in enumerate(type_to_operators[type]): 194 | % if first: 195 | % first = False 196 | % else: 197 | 198 | % end 199 | @inlinable 200 | public static func ${operator}(lhs: Self, rhs: I) -> Image where I.Pixel == Pixel { 201 | precondition(lhs.width == rhs.width && lhs.height == rhs.height, "`${operator}` cannot be applied for images with different sizes: (\(lhs.width), \(lhs.height)), (\(rhs.width), \(rhs.height))") 202 | let pixels = zip(lhs, rhs).map { $0 ${operator} $1 } 203 | return Image(width: lhs.width, height: lhs.height, pixels: pixels) 204 | } 205 | % end 206 | % 207 | % for i, operator in enumerate(type_to_compound_assignment_operators[type]): 208 | % if first: 209 | % first = False 210 | % else: 211 | 212 | % end 213 | @inlinable 214 | public static func ${operator}(lhs: inout Self, rhs: I) where I.Pixel == Pixel { 215 | precondition(lhs.width == rhs.width && lhs.height == rhs.height, "`${operator}` cannot be applied for images with different sizes: (\(lhs.width), \(lhs.height)), (\(rhs.width), \(rhs.height))") 216 | for (y1, y2) in zip(lhs.yRange, rhs.yRange) { 217 | for (x1, x2) in zip(lhs.xRange, rhs.xRange) { 218 | lhs[x1, y1] ${operator} rhs[x2, y2] 219 | } 220 | } 221 | } 222 | % end 223 | % 224 | % for operator in type_to_equality_operators[type]: 225 | % if first: 226 | % first = False 227 | % else: 228 | 229 | % end 230 | @inlinable 231 | % initial, and_or, different_size, = ('true', '&&', 'false') if operator == '==' else ('false', '||', 'true') 232 | public static func ${operator}(lhs: Self, rhs: I) -> Bool where I.Pixel == Pixel { 233 | guard lhs.width == rhs.width && lhs.height == rhs.height else { return ${different_size} } 234 | return zip(lhs, rhs).reduce(${initial}) { $0 ${and_or} $1.0 ${operator} $1.1 } 235 | } 236 | % end 237 | % 238 | % for operator in type_to_comparison_operators[type]: 239 | % if first: 240 | % first = False 241 | % else: 242 | 243 | % end 244 | @inlinable 245 | % return_pixel_type = 'RGBA' if type.startswith('RGBA<') else 'Bool' 246 | public static func ${operator}(lhs: Self, rhs: I) -> Image<${return_pixel_type}> where I.Pixel == Pixel { 247 | precondition(lhs.width == rhs.width && lhs.height == rhs.height, "`${operator}` cannot be applied for images with different sizes: (\(lhs.width), \(lhs.height)), (\(rhs.width), \(rhs.height))") 248 | let pixels = zip(lhs, rhs).map { $0 ${operator} $1 } 249 | return Image<${return_pixel_type}>(width: lhs.width, height: lhs.height, pixels: pixels) 250 | } 251 | % end 252 | % 253 | % for operator in type_to_prefix_operators[type]: 254 | % if first: 255 | % first = False 256 | % else: 257 | 258 | % end 259 | @inlinable 260 | prefix public static func ${operator}(a: Self) -> Image { 261 | return Image(width: a.width, height: a.height, pixels: a.map { ${operator}$0 }) 262 | } 263 | % end 264 | } 265 | % end 266 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ImageProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ImageProtocol : Sequence { 4 | typealias Pixel = Element 5 | associatedtype SubImage : ImageProtocol where SubImage.Element == Element 6 | 7 | init(width: Int, height: Int, pixels: [Pixel]) 8 | 9 | var width: Int { get } 10 | var height: Int { get } 11 | 12 | var xRange: Range { get } 13 | var yRange: Range { get } 14 | 15 | subscript(x: Int, y: Int) -> Pixel { get set } 16 | 17 | subscript(xRange: Range, yRange: Range) -> SubImage { get } 18 | subscript(xRange: R1, yRange: R2) -> SubImage where R1.Bound == Int, R2.Bound == Int { get } 19 | subscript(xRange: R1, yRange: UnboundedRange) -> SubImage where R1.Bound == Int { get } 20 | subscript(xRange: UnboundedRange, yRange: R2) -> SubImage where R2.Bound == Int { get } 21 | subscript(xRange: UnboundedRange, yRange: UnboundedRange) -> SubImage { get } 22 | 23 | func map(_ transform: (Pixel) throws -> T) rethrows -> Image 24 | mutating func _update(_ body: (inout Pixel) throws -> ()) rethrows 25 | } 26 | 27 | extension ImageProtocol { 28 | public var width: Int { 29 | return xRange.count 30 | } 31 | 32 | public var height: Int { 33 | return yRange.count 34 | } 35 | 36 | public subscript(xRange: R1, yRange: R2) -> SubImage where R1.Bound == Int, R2.Bound == Int { 37 | return self[range(from: xRange, relativeTo: self.xRange), range(from: yRange, relativeTo: self.yRange)] 38 | } 39 | 40 | public subscript(xRange: R1, yRange: UnboundedRange) -> SubImage where R1.Bound == Int { 41 | return self[range(from: xRange, relativeTo: self.xRange), self.yRange] 42 | } 43 | 44 | public subscript(xRange: UnboundedRange, yRange: R2) -> SubImage where R2.Bound == Int { 45 | return self[self.xRange, range(from: yRange, relativeTo: self.yRange)] 46 | } 47 | 48 | public subscript(xRange: UnboundedRange, yRange: UnboundedRange) -> SubImage { 49 | return self[self.xRange, self.yRange] 50 | } 51 | 52 | public func makeIterator() -> ImageIterator { 53 | return ImageIterator(self) 54 | } 55 | } 56 | 57 | extension ImageProtocol { 58 | public init() { 59 | self.init(width: 0, height: 0, pixels: []) 60 | } 61 | 62 | public init(pixel: Pixel) { 63 | self.init(width: 1, height: 1, pixel: pixel) 64 | } 65 | 66 | public init(width: Int, height: Int, pixel: Pixel) { 67 | self.init(width: width, height: height, pixels: [Pixel](repeating: pixel, count: width * height)) 68 | } 69 | 70 | public init(width: Int, height: Int, pixels: S) where S.Element == Pixel { 71 | self.init(width: width, height: height, pixels: Array(pixels)) 72 | } 73 | 74 | public init(_ image: I) where I.Pixel == Pixel { 75 | self.init(width: image.width, height: image.height, pixels: image) 76 | } 77 | 78 | public init(width: Int, height: Int, pixelAt: (_ x: Int, _ y: Int) throws -> Pixel) rethrows { 79 | var pixels = [Pixel]() 80 | pixels.reserveCapacity(width * height) 81 | 82 | for y in 0.. Pixel? { 96 | guard xRange.contains(x) else { return nil } 97 | guard yRange.contains(y) else { return nil } 98 | return self[x, y] 99 | } 100 | } 101 | 102 | extension ImageProtocol { 103 | public func transposed() -> Image { 104 | var pixels = [Pixel]() 105 | pixels.reserveCapacity(count) 106 | 107 | for x in xRange { 108 | for y in yRange { 109 | pixels.append(self[x, y]) 110 | } 111 | } 112 | 113 | return Image(width: height, height: width, pixels: pixels) 114 | } 115 | 116 | public func xReversed() -> Image { 117 | var pixels = [Pixel]() 118 | pixels.reserveCapacity(count) 119 | 120 | for y in yRange { 121 | for x in xRange.reversed() { 122 | pixels.append(self[x, y]) 123 | } 124 | } 125 | 126 | return Image(width: width, height: height, pixels: pixels) 127 | } 128 | 129 | public func yReversed() -> Image { 130 | var pixels = [Pixel]() 131 | pixels.reserveCapacity(count) 132 | 133 | for y in yRange.reversed() { 134 | for x in xRange { 135 | pixels.append(self[x, y]) 136 | } 137 | } 138 | 139 | return Image(width: width, height: height, pixels: pixels) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/SwiftImage/ImageSlice.swift: -------------------------------------------------------------------------------- 1 | public struct ImageSlice : ImageProtocol { 2 | public typealias SubImage = ImageSlice 3 | public typealias Element = Pixel // FIXME: Remove this line in the future. Swift 4.1 needs it to build `ImageSlice`. 4 | 5 | @usableFromInline 6 | internal var image: Image 7 | public let xRange: Range 8 | public let yRange: Range 9 | 10 | internal init(image: Image, xRange: Range, yRange: Range) { 11 | precondition(image.xRange.isSuperset(of: xRange), "`xRange` is out of bounds: \(xRange)") 12 | precondition(image.yRange.isSuperset(of: yRange), "`yRange` is out of bounds: \(yRange)") 13 | self.image = image 14 | self.xRange = xRange 15 | self.yRange = yRange 16 | } 17 | 18 | public init(width: Int, height: Int, pixels: [Pixel]) { 19 | self.init(image: Image(width: width, height: height, pixels: pixels), xRange: 0.. Pixel { 23 | get { 24 | precondition(xRange.contains(x), "`x` is out of bounds: \(x)") 25 | precondition(yRange.contains(y), "`y` is out of bounds: \(y)") 26 | return image[x, y] 27 | } 28 | 29 | set { 30 | precondition(xRange.contains(x), "`x` is out of bounds: \(x)") 31 | precondition(yRange.contains(y), "`y` is out of bounds: \(y)") 32 | image[x, y] = newValue 33 | } 34 | } 35 | 36 | public subscript(xRange: Range, yRange: Range) -> ImageSlice { 37 | precondition(self.xRange.isSuperset(of: xRange), "`xRange` is out of bounds: \(xRange)") 38 | precondition(self.yRange.isSuperset(of: yRange), "`yRange` is out of bounds: \(yRange)") 39 | return image[xRange, yRange] 40 | } 41 | } 42 | 43 | extension ImageSlice : Sendable where Pixel : Sendable {} 44 | 45 | extension ImageSlice { 46 | public init(_ image: Image) { 47 | self.init(image: image, xRange: image.xRange, yRange: image.yRange) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Interpolation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum InterpolationMethod { 4 | case nearestNeighbor 5 | case bilinear 6 | // case bicubic // Unimplemented yet 7 | } 8 | 9 | extension ImageProtocol { 10 | public subscript(x: Double, y: Double) -> Pixel { 11 | return interpolatedPixelByNearestNeighbor(x: x, y: y) { self[$0, $1] } 12 | } 13 | 14 | public subscript(x: Double, y: Double, extrapolation extrapolationMethod: ExtrapolationMethod) -> Pixel { 15 | return interpolatedPixelByNearestNeighbor(x: x, y: y) { self[$0, $1, extrapolation: extrapolationMethod] } 16 | } 17 | 18 | internal func interpolatedPixelByNearestNeighbor(x: Double, y: Double, pixelAt: (Int, Int) -> Pixel) -> Pixel { 19 | let xi = Int(round(x)) 20 | let yi = Int(round(y)) 21 | return pixelAt(xi, yi) 22 | } 23 | } 24 | 25 | extension ImageProtocol where Pixel : _NumericPixel { 26 | // Not implemented by default parameter values to improve performance especially when this `subscript` is called repeatedly 27 | public subscript(x: Double, y: Double) -> Pixel { 28 | return interpolatedPixelByBilinear(x: x, y: y) { self[$0, $1] } 29 | } 30 | 31 | public subscript(x: Double, y: Double, interpolation interpolationMethod: InterpolationMethod) -> Pixel { 32 | switch interpolationMethod { 33 | case .nearestNeighbor: 34 | return interpolatedPixelByNearestNeighbor(x: x, y: y) { self[$0, $1] } 35 | case .bilinear: 36 | return interpolatedPixelByBilinear(x: x, y: y) { self[$0, $1] } 37 | } 38 | } 39 | 40 | public subscript(x: Double, y: Double, extrapolation extrapolationMethod: ExtrapolationMethod) -> Pixel { 41 | return interpolatedPixelByBilinear(x: x, y: y) { self[$0, $1, extrapolation: extrapolationMethod] } 42 | } 43 | 44 | public subscript(x: Double, y: Double, interpolation interpolationMethod: InterpolationMethod, extrapolation extrapolationMethod: ExtrapolationMethod) -> Pixel { 45 | switch interpolationMethod { 46 | case .nearestNeighbor: 47 | return interpolatedPixelByNearestNeighbor(x: x, y: y) { self[$0, $1, extrapolation: extrapolationMethod] } 48 | case .bilinear: 49 | return interpolatedPixelByBilinear(x: x, y: y) { self[$0, $1, extrapolation: extrapolationMethod] } 50 | } 51 | } 52 | 53 | internal func interpolatedPixelByBilinear( 54 | x: Double, 55 | y: Double, 56 | pixelAt: (Int, Int) -> Pixel 57 | ) -> Pixel { 58 | let x0 = Int(floor(x)) 59 | let y0 = Int(floor(y)) 60 | let x1 = Int(ceil(x)) 61 | let y1 = Int(ceil(y)) 62 | 63 | let v00 = pixelAt(x0, y0) 64 | let v01 = pixelAt(x1, y0) 65 | let v10 = pixelAt(x0, y1) 66 | let v11 = pixelAt(x1, y1) 67 | 68 | let wx = x - Double(x0) 69 | let wy = y - Double(y0) 70 | let w00 = (1.0 - wx) * (1.0 - wy) 71 | let w01 = wx * (1.0 - wy) 72 | let w10 = (1.0 - wx) * wy 73 | let w11 = wx * wy 74 | 75 | return Pixel.init( 76 | _ez_additiveDouble: Pixel._ez_productDouble(v00._ez_additiveDouble, w00) 77 | + Pixel._ez_productDouble(v01._ez_additiveDouble, w01) 78 | + Pixel._ez_productDouble(v10._ez_additiveDouble, w10) 79 | + Pixel._ez_productDouble(v11._ez_additiveDouble, w11) 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SwiftImage/NumericPixel.swift.gyb: -------------------------------------------------------------------------------- 1 | % types = ['UInt8', 'UInt16', 'Int', 'Float', 'Double'] 2 | % additive_types = ['Int', 'Int64', 'Float', 'Double'] 3 | % type_to_additives = { 4 | % 'UInt8' : ['Int', 'Float', 'Double'], 5 | % 'UInt16': ['Int', 'Float', 'Double'], 6 | % 'Int' : ['Int', 'Float', 'Double'], 7 | % 'Float' : ['Float', 'Float', 'Double'], 8 | % 'Double': ['Double', 'Double', 'Double'], 9 | % } 10 | % additive_sybmols = ['Int', 'Float', 'Double'] 11 | % additive_weights = ['Int', 'Float', 'Double'] 12 | % 13 | public protocol _NumericPixel { 14 | associatedtype _ez_AdditiveInt : AdditiveArithmetic 15 | associatedtype _ez_AdditiveFloat : AdditiveArithmetic 16 | associatedtype _ez_AdditiveDouble : AdditiveArithmetic 17 | 18 | init(_ez_additiveInt: _ez_AdditiveInt) 19 | init(_ez_additiveFloat: _ez_AdditiveFloat) 20 | init(_ez_additiveDouble: _ez_AdditiveDouble) 21 | var _ez_additiveInt: _ez_AdditiveInt { get } 22 | var _ez_additiveFloat: _ez_AdditiveFloat { get } 23 | var _ez_additiveDouble: _ez_AdditiveDouble { get } 24 | static var _ez_zero: Self { get } 25 | static func _ez_productInt(_ lhs: _ez_AdditiveInt, _ rhs: Int) -> _ez_AdditiveInt 26 | static func _ez_productFloat(_ lhs: _ez_AdditiveFloat, _ rhs: Float) -> _ez_AdditiveFloat 27 | static func _ez_productDouble(_ lhs: _ez_AdditiveDouble, _ rhs: Double) -> _ez_AdditiveDouble 28 | static func _ez_quotientInt(_ lhs: _ez_AdditiveInt, _ rhs: Int) -> _ez_AdditiveInt 29 | static func _ez_quotientFloat(_ lhs: _ez_AdditiveFloat, _ rhs: Float) -> _ez_AdditiveFloat 30 | static func _ez_quotientDouble(_ lhs: _ez_AdditiveDouble, _ rhs: Double) -> _ez_AdditiveDouble 31 | } 32 | 33 | extension _NumericPixel where Self: AdditiveArithmetic { 34 | public static var _ez_zero: Self { return .zero } 35 | } 36 | 37 | extension RGBA : _NumericPixel where Channel : _NumericPixel { 38 | public typealias _ez_AdditiveInt = RGBA 39 | public typealias _ez_AdditiveFloat = RGBA 40 | public typealias _ez_AdditiveDouble = RGBA 41 | 42 | @inlinable 43 | public init(_ez_additiveInt: RGBA) { 44 | self = RGBA(red: Channel.init(_ez_additiveInt: _ez_additiveInt.red), green: Channel.init(_ez_additiveInt: _ez_additiveInt.green), blue: Channel.init(_ez_additiveInt: _ez_additiveInt.blue), alpha: Channel.init(_ez_additiveInt: _ez_additiveInt.alpha)) 45 | } 46 | 47 | @inlinable 48 | public init(_ez_additiveFloat: RGBA) { 49 | self = RGBA(red: Channel.init(_ez_additiveFloat: _ez_additiveFloat.red), green: Channel.init(_ez_additiveFloat: _ez_additiveFloat.green), blue: Channel.init(_ez_additiveFloat: _ez_additiveFloat.blue), alpha: Channel.init(_ez_additiveFloat: _ez_additiveFloat.alpha)) 50 | } 51 | 52 | @inlinable 53 | public init(_ez_additiveDouble: RGBA) { 54 | self = RGBA(red: Channel.init(_ez_additiveDouble: _ez_additiveDouble.red), green: Channel.init(_ez_additiveDouble: _ez_additiveDouble.green), blue: Channel.init(_ez_additiveDouble: _ez_additiveDouble.blue), alpha: Channel.init(_ez_additiveDouble: _ez_additiveDouble.alpha)) 55 | } 56 | 57 | public var _ez_additiveInt: RGBA { 58 | return RGBA(red: red._ez_additiveInt, green: green._ez_additiveInt, blue: blue._ez_additiveInt, alpha: alpha._ez_additiveInt) 59 | } 60 | 61 | public var _ez_additiveFloat: RGBA { 62 | return RGBA(red: red._ez_additiveFloat, green: green._ez_additiveFloat, blue: blue._ez_additiveFloat, alpha: alpha._ez_additiveFloat) 63 | } 64 | 65 | public var _ez_additiveDouble: RGBA { 66 | return RGBA(red: red._ez_additiveDouble, green: green._ez_additiveDouble, blue: blue._ez_additiveDouble, alpha: alpha._ez_additiveDouble) 67 | } 68 | 69 | public static var _ez_zero: RGBA { 70 | return RGBA(red: Channel._ez_zero, green: Channel._ez_zero, blue: Channel._ez_zero, alpha: Channel._ez_zero) 71 | } 72 | 73 | @inlinable 74 | public static func _ez_productInt(_ lhs: RGBA, _ rhs: Int) -> RGBA { 75 | return RGBA(red: Channel._ez_productInt(lhs.red, rhs), green: Channel._ez_productInt(lhs.green, rhs), blue: Channel._ez_productInt(lhs.blue, rhs), alpha: Channel._ez_productInt(lhs.alpha, rhs)) 76 | } 77 | 78 | @inlinable 79 | public static func _ez_productFloat(_ lhs: RGBA, _ rhs: Float) -> RGBA { 80 | return RGBA(red: Channel._ez_productFloat(lhs.red, rhs), green: Channel._ez_productFloat(lhs.green, rhs), blue: Channel._ez_productFloat(lhs.blue, rhs), alpha: Channel._ez_productFloat(lhs.alpha, rhs)) 81 | } 82 | 83 | @inlinable 84 | public static func _ez_productDouble(_ lhs: RGBA, _ rhs: Double) -> RGBA { 85 | return RGBA(red: Channel._ez_productDouble(lhs.red, rhs), green: Channel._ez_productDouble(lhs.green, rhs), blue: Channel._ez_productDouble(lhs.blue, rhs), alpha: Channel._ez_productDouble(lhs.alpha, rhs)) 86 | } 87 | 88 | @inlinable 89 | public static func _ez_quotientInt(_ lhs: RGBA, _ rhs: Int) -> RGBA { 90 | return RGBA(red: Channel._ez_quotientInt(lhs.red, rhs), green: Channel._ez_quotientInt(lhs.green, rhs), blue: Channel._ez_quotientInt(lhs.blue, rhs), alpha: Channel._ez_quotientInt(lhs.alpha, rhs)) 91 | } 92 | 93 | @inlinable 94 | public static func _ez_quotientFloat(_ lhs: RGBA, _ rhs: Float) -> RGBA { 95 | return RGBA(red: Channel._ez_quotientFloat(lhs.red, rhs), green: Channel._ez_quotientFloat(lhs.green, rhs), blue: Channel._ez_quotientFloat(lhs.blue, rhs), alpha: Channel._ez_quotientFloat(lhs.alpha, rhs)) 96 | } 97 | 98 | @inlinable 99 | public static func _ez_quotientDouble(_ lhs: RGBA, _ rhs: Double) -> RGBA { 100 | return RGBA(red: Channel._ez_quotientDouble(lhs.red, rhs), green: Channel._ez_quotientDouble(lhs.green, rhs), blue: Channel._ez_quotientDouble(lhs.blue, rhs), alpha: Channel._ez_quotientDouble(lhs.alpha, rhs)) 101 | } 102 | } 103 | % for type in types: 104 | 105 | extension ${type} : _NumericPixel { 106 | % additives = type_to_additives[type] 107 | % 108 | % for i, (additive_symbol, additive) in enumerate(zip(additive_sybmols, additives)): 109 | % if i > 0: 110 | 111 | % end 112 | public init(_ez_additive${additive_symbol}: ${additive}) { 113 | % if additive == type: 114 | self = _ez_additive${additive_symbol} 115 | % else: 116 | self = ${type}(_ez_additive${additive_symbol}) 117 | % end 118 | } 119 | % end 120 | % 121 | % for additive_symbol, additive in zip(additive_sybmols, additives): 122 | 123 | public var _ez_additive${additive_symbol}: ${additive} { 124 | % if additive == type: 125 | return self 126 | % else: 127 | return ${additive}(self) 128 | % end 129 | } 130 | % end 131 | 132 | % for additive_symbol, additive_weight, additive in zip(additive_sybmols, additive_weights, additives): 133 | 134 | public static func _ez_product${additive_symbol}(_ lhs: ${additive}, _ rhs: ${additive_weight}) -> ${additive} { 135 | % if additive_weight == additive: 136 | return lhs * rhs 137 | % else: 138 | return lhs * ${additive}(rhs) 139 | % end 140 | } 141 | % end 142 | % 143 | % for additive_symbol, additive_weight, additive in zip(additive_sybmols, additive_weights, additives): 144 | 145 | public static func _ez_quotient${additive_symbol}(_ lhs: ${additive}, _ rhs: ${additive_weight}) -> ${additive} { 146 | % if additive_weight == additive: 147 | return lhs / rhs 148 | % else: 149 | return lhs / ${additive}(rhs) 150 | % end 151 | } 152 | % end 153 | } 154 | % end 155 | -------------------------------------------------------------------------------- /Sources/SwiftImage/PremultipliedRGBA.swift: -------------------------------------------------------------------------------- 1 | public struct PremultipliedRGBA where Channel : Numeric{ 2 | public var red: Channel 3 | public var green: Channel 4 | public var blue: Channel 5 | public var alpha: Channel 6 | 7 | public init(red: Channel, green: Channel, blue: Channel, alpha: Channel) { 8 | self.red = red 9 | self.green = green 10 | self.blue = blue 11 | self.alpha = alpha 12 | } 13 | } 14 | 15 | extension PremultipliedRGBA where Channel : UnsignedInteger & FixedWidthInteger { 16 | public init(_ rgb: RGB) { 17 | self.init(red: rgb.red, green: rgb.green, blue: rgb.blue, alpha: .max) 18 | } 19 | } 20 | 21 | extension PremultipliedRGBA where Channel : _NumericPixel & UnsignedInteger & FixedWidthInteger, Channel._ez_AdditiveInt : FixedWidthInteger { 22 | public init(_ rgba: RGBA) { 23 | let numericAlpha: Channel._ez_AdditiveInt = rgba.alpha._ez_additiveInt 24 | let numericMaxAlpha: Channel._ez_AdditiveInt = Channel.max._ez_additiveInt 25 | 26 | self.init( 27 | red: .init(_ez_additiveInt: rgba.red._ez_additiveInt * numericAlpha / numericMaxAlpha), 28 | green: .init(_ez_additiveInt: rgba.green._ez_additiveInt * numericAlpha / numericMaxAlpha), 29 | blue: .init(_ez_additiveInt: rgba.blue._ez_additiveInt * numericAlpha / numericMaxAlpha), 30 | alpha: rgba.alpha 31 | ) 32 | } 33 | } 34 | 35 | extension PremultipliedRGBA where Channel : FloatingPoint { 36 | public init(_ rgb: RGB) { 37 | self.init(red: rgb.red, green: rgb.green, blue: rgb.blue, alpha: 1) 38 | } 39 | 40 | public init(_ rgba: RGBA) { 41 | self.init( 42 | red: rgba.red * rgba.alpha, 43 | green: rgba.green * rgba.alpha, 44 | blue: rgba.blue * rgba.alpha, 45 | alpha: rgba.alpha 46 | ) 47 | } 48 | } 49 | 50 | extension PremultipliedRGBA { // Additional initializers 51 | public init(gray: Channel, alpha: Channel) { 52 | self.init(red: gray, green: gray, blue: gray, alpha: alpha) 53 | } 54 | } 55 | 56 | extension PremultipliedRGBA { 57 | public func map(_ transform: (Channel) -> T) -> PremultipliedRGBA where T : Numeric { 58 | return PremultipliedRGBA( 59 | red: transform(red), 60 | green: transform(green), 61 | blue: transform(blue), 62 | alpha: transform(alpha) 63 | ) 64 | } 65 | } 66 | 67 | extension PremultipliedRGBA where Channel == UInt8 { 68 | public init(_ rgbaInt: UInt32) { 69 | self.init(red: UInt8((rgbaInt >> 24) & 0xFF), green: UInt8((rgbaInt >> 16) & 0xFF), blue: UInt8((rgbaInt >> 8) & 0xFF), alpha: UInt8(rgbaInt & 0xFF)) 70 | } 71 | } 72 | 73 | extension PremultipliedRGBA : CustomStringConvertible { 74 | public var description: String { 75 | return "PremultipliedRGBA(red: \(red), green: \(green), blue: \(blue), alpha: \(alpha))" 76 | } 77 | } 78 | 79 | extension PremultipliedRGBA : CustomDebugStringConvertible { 80 | public var debugDescription: String { 81 | return description 82 | } 83 | } 84 | 85 | extension PremultipliedRGBA : Equatable where Channel : Equatable { 86 | @inlinable 87 | public static func ==(lhs: PremultipliedRGBA, rhs: PremultipliedRGBA) -> Bool { 88 | return lhs.red == rhs.red && lhs.green == rhs.green && lhs.blue == rhs.blue && lhs.alpha == rhs.alpha 89 | } 90 | 91 | @inlinable 92 | public static func !=(lhs: PremultipliedRGBA, rhs: PremultipliedRGBA) -> Bool { 93 | return lhs.red != rhs.red || lhs.green != rhs.green || lhs.blue != rhs.blue || lhs.alpha != rhs.alpha 94 | } 95 | } 96 | 97 | extension PremultipliedRGBA : Sendable where Channel : Sendable {} 98 | -------------------------------------------------------------------------------- /Sources/SwiftImage/RGBAOperators.swift: -------------------------------------------------------------------------------- 1 | extension RGBA where Channel : Numeric { 2 | @inlinable 3 | public static func +(lhs: RGBA, rhs: RGBA) -> RGBA { 4 | return RGBA(red: lhs.red + rhs.red, green: lhs.green + rhs.green, blue: lhs.blue + rhs.blue, alpha: lhs.alpha + rhs.alpha) 5 | } 6 | 7 | @inlinable 8 | public static func -(lhs: RGBA, rhs: RGBA) -> RGBA { 9 | return RGBA(red: lhs.red - rhs.red, green: lhs.green - rhs.green, blue: lhs.blue - rhs.blue, alpha: lhs.alpha - rhs.alpha) 10 | } 11 | 12 | @inlinable 13 | public static func *(lhs: RGBA, rhs: RGBA) -> RGBA { 14 | return RGBA(red: lhs.red * rhs.red, green: lhs.green * rhs.green, blue: lhs.blue * rhs.blue, alpha: lhs.alpha * rhs.alpha) 15 | } 16 | 17 | @inlinable 18 | public static func +=(lhs: inout RGBA, rhs: RGBA) { 19 | lhs.red += rhs.red 20 | lhs.green += rhs.green 21 | lhs.blue += rhs.blue 22 | lhs.alpha += rhs.alpha 23 | } 24 | 25 | @inlinable 26 | public static func -=(lhs: inout RGBA, rhs: RGBA) { 27 | lhs.red -= rhs.red 28 | lhs.green -= rhs.green 29 | lhs.blue -= rhs.blue 30 | lhs.alpha -= rhs.alpha 31 | } 32 | 33 | @inlinable 34 | public static func *=(lhs: inout RGBA, rhs: RGBA) { 35 | lhs.red *= rhs.red 36 | lhs.green *= rhs.green 37 | lhs.blue *= rhs.blue 38 | lhs.alpha *= rhs.alpha 39 | } 40 | 41 | @inlinable 42 | prefix public static func +(a: RGBA) -> RGBA { 43 | return RGBA(red: +a.red, green: +a.green, blue: +a.blue, alpha: +a.alpha) 44 | } 45 | } 46 | 47 | extension RGBA where Channel : SignedNumeric { 48 | @inlinable 49 | prefix public static func -(a: RGBA) -> RGBA { 50 | return RGBA(red: -a.red, green: -a.green, blue: -a.blue, alpha: -a.alpha) 51 | } 52 | } 53 | 54 | extension RGBA where Channel : BinaryInteger { 55 | @inlinable 56 | public static func /(lhs: RGBA, rhs: RGBA) -> RGBA { 57 | return RGBA(red: lhs.red / rhs.red, green: lhs.green / rhs.green, blue: lhs.blue / rhs.blue, alpha: lhs.alpha / rhs.alpha) 58 | } 59 | 60 | @inlinable 61 | public static func %(lhs: RGBA, rhs: RGBA) -> RGBA { 62 | return RGBA(red: lhs.red % rhs.red, green: lhs.green % rhs.green, blue: lhs.blue % rhs.blue, alpha: lhs.alpha % rhs.alpha) 63 | } 64 | 65 | @inlinable 66 | public static func &(lhs: RGBA, rhs: RGBA) -> RGBA { 67 | return RGBA(red: lhs.red & rhs.red, green: lhs.green & rhs.green, blue: lhs.blue & rhs.blue, alpha: lhs.alpha & rhs.alpha) 68 | } 69 | 70 | @inlinable 71 | public static func |(lhs: RGBA, rhs: RGBA) -> RGBA { 72 | return RGBA(red: lhs.red | rhs.red, green: lhs.green | rhs.green, blue: lhs.blue | rhs.blue, alpha: lhs.alpha | rhs.alpha) 73 | } 74 | 75 | @inlinable 76 | public static func ^(lhs: RGBA, rhs: RGBA) -> RGBA { 77 | return RGBA(red: lhs.red ^ rhs.red, green: lhs.green ^ rhs.green, blue: lhs.blue ^ rhs.blue, alpha: lhs.alpha ^ rhs.alpha) 78 | } 79 | 80 | @inlinable 81 | public static func <<(lhs: RGBA, rhs: RGBA) -> RGBA { 82 | return RGBA(red: lhs.red << rhs.red, green: lhs.green << rhs.green, blue: lhs.blue << rhs.blue, alpha: lhs.alpha << rhs.alpha) 83 | } 84 | 85 | @inlinable 86 | public static func >>(lhs: RGBA, rhs: RGBA) -> RGBA { 87 | return RGBA(red: lhs.red >> rhs.red, green: lhs.green >> rhs.green, blue: lhs.blue >> rhs.blue, alpha: lhs.alpha >> rhs.alpha) 88 | } 89 | 90 | @inlinable 91 | public static func /=(lhs: inout RGBA, rhs: RGBA) { 92 | lhs.red /= rhs.red 93 | lhs.green /= rhs.green 94 | lhs.blue /= rhs.blue 95 | lhs.alpha /= rhs.alpha 96 | } 97 | 98 | @inlinable 99 | public static func %=(lhs: inout RGBA, rhs: RGBA) { 100 | lhs.red %= rhs.red 101 | lhs.green %= rhs.green 102 | lhs.blue %= rhs.blue 103 | lhs.alpha %= rhs.alpha 104 | } 105 | 106 | @inlinable 107 | public static func &=(lhs: inout RGBA, rhs: RGBA) { 108 | lhs.red &= rhs.red 109 | lhs.green &= rhs.green 110 | lhs.blue &= rhs.blue 111 | lhs.alpha &= rhs.alpha 112 | } 113 | 114 | @inlinable 115 | public static func |=(lhs: inout RGBA, rhs: RGBA) { 116 | lhs.red |= rhs.red 117 | lhs.green |= rhs.green 118 | lhs.blue |= rhs.blue 119 | lhs.alpha |= rhs.alpha 120 | } 121 | 122 | @inlinable 123 | public static func ^=(lhs: inout RGBA, rhs: RGBA) { 124 | lhs.red ^= rhs.red 125 | lhs.green ^= rhs.green 126 | lhs.blue ^= rhs.blue 127 | lhs.alpha ^= rhs.alpha 128 | } 129 | 130 | @inlinable 131 | public static func <<=(lhs: inout RGBA, rhs: RGBA) { 132 | lhs.red <<= rhs.red 133 | lhs.green <<= rhs.green 134 | lhs.blue <<= rhs.blue 135 | lhs.alpha <<= rhs.alpha 136 | } 137 | 138 | @inlinable 139 | public static func >>=(lhs: inout RGBA, rhs: RGBA) { 140 | lhs.red >>= rhs.red 141 | lhs.green >>= rhs.green 142 | lhs.blue >>= rhs.blue 143 | lhs.alpha >>= rhs.alpha 144 | } 145 | } 146 | 147 | extension RGBA where Channel : FixedWidthInteger { 148 | @inlinable 149 | public static func &+(lhs: RGBA, rhs: RGBA) -> RGBA { 150 | return RGBA(red: lhs.red &+ rhs.red, green: lhs.green &+ rhs.green, blue: lhs.blue &+ rhs.blue, alpha: lhs.alpha &+ rhs.alpha) 151 | } 152 | 153 | @inlinable 154 | public static func &-(lhs: RGBA, rhs: RGBA) -> RGBA { 155 | return RGBA(red: lhs.red &- rhs.red, green: lhs.green &- rhs.green, blue: lhs.blue &- rhs.blue, alpha: lhs.alpha &- rhs.alpha) 156 | } 157 | 158 | @inlinable 159 | public static func &*(lhs: RGBA, rhs: RGBA) -> RGBA { 160 | return RGBA(red: lhs.red &* rhs.red, green: lhs.green &* rhs.green, blue: lhs.blue &* rhs.blue, alpha: lhs.alpha &* rhs.alpha) 161 | } 162 | 163 | @inlinable 164 | public static func &<<(lhs: RGBA, rhs: RGBA) -> RGBA { 165 | return RGBA(red: lhs.red &<< rhs.red, green: lhs.green &<< rhs.green, blue: lhs.blue &<< rhs.blue, alpha: lhs.alpha &<< rhs.alpha) 166 | } 167 | 168 | @inlinable 169 | public static func &>>(lhs: RGBA, rhs: RGBA) -> RGBA { 170 | return RGBA(red: lhs.red &>> rhs.red, green: lhs.green &>> rhs.green, blue: lhs.blue &>> rhs.blue, alpha: lhs.alpha &>> rhs.alpha) 171 | } 172 | 173 | @inlinable 174 | public static func &<<=(lhs: inout RGBA, rhs: RGBA) { 175 | lhs.red &<<= rhs.red 176 | lhs.green &<<= rhs.green 177 | lhs.blue &<<= rhs.blue 178 | lhs.alpha &<<= rhs.alpha 179 | } 180 | 181 | @inlinable 182 | public static func &>>=(lhs: inout RGBA, rhs: RGBA) { 183 | lhs.red &>>= rhs.red 184 | lhs.green &>>= rhs.green 185 | lhs.blue &>>= rhs.blue 186 | lhs.alpha &>>= rhs.alpha 187 | } 188 | } 189 | 190 | extension RGBA where Channel : FloatingPoint { 191 | @inlinable 192 | public static func /(lhs: RGBA, rhs: RGBA) -> RGBA { 193 | return RGBA(red: lhs.red / rhs.red, green: lhs.green / rhs.green, blue: lhs.blue / rhs.blue, alpha: lhs.alpha / rhs.alpha) 194 | } 195 | 196 | @inlinable 197 | public static func /=(lhs: inout RGBA, rhs: RGBA) { 198 | lhs.red /= rhs.red 199 | lhs.green /= rhs.green 200 | lhs.blue /= rhs.blue 201 | lhs.alpha /= rhs.alpha 202 | } 203 | } 204 | 205 | extension RGBA where Channel : Comparable { 206 | @inlinable 207 | public static func <(lhs: RGBA, rhs: RGBA) -> RGBA { 208 | return RGBA(red: lhs.red < rhs.red, green: lhs.green < rhs.green, blue: lhs.blue < rhs.blue, alpha: lhs.alpha < rhs.alpha) 209 | } 210 | 211 | @inlinable 212 | public static func <=(lhs: RGBA, rhs: RGBA) -> RGBA { 213 | return RGBA(red: lhs.red <= rhs.red, green: lhs.green <= rhs.green, blue: lhs.blue <= rhs.blue, alpha: lhs.alpha <= rhs.alpha) 214 | } 215 | 216 | @inlinable 217 | public static func >(lhs: RGBA, rhs: RGBA) -> RGBA { 218 | return RGBA(red: lhs.red > rhs.red, green: lhs.green > rhs.green, blue: lhs.blue > rhs.blue, alpha: lhs.alpha > rhs.alpha) 219 | } 220 | 221 | @inlinable 222 | public static func >=(lhs: RGBA, rhs: RGBA) -> RGBA { 223 | return RGBA(red: lhs.red >= rhs.red, green: lhs.green >= rhs.green, blue: lhs.blue >= rhs.blue, alpha: lhs.alpha >= rhs.alpha) 224 | } 225 | } 226 | 227 | extension RGBA where Channel == Bool { 228 | @inlinable 229 | public static func &&(lhs: RGBA, rhs: RGBA) -> RGBA { 230 | return RGBA(red: lhs.red && rhs.red, green: lhs.green && rhs.green, blue: lhs.blue && rhs.blue, alpha: lhs.alpha && rhs.alpha) 231 | } 232 | 233 | @inlinable 234 | public static func ||(lhs: RGBA, rhs: RGBA) -> RGBA { 235 | return RGBA(red: lhs.red || rhs.red, green: lhs.green || rhs.green, blue: lhs.blue || rhs.blue, alpha: lhs.alpha || rhs.alpha) 236 | } 237 | 238 | @inlinable 239 | prefix public static func !(a: RGBA) -> RGBA { 240 | return RGBA(red: !a.red, green: !a.green, blue: !a.blue, alpha: !a.alpha) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Sources/SwiftImage/RGBAOperators.swift.gyb: -------------------------------------------------------------------------------- 1 | % types = [ 2 | % 'Numeric', 3 | % 'SignedNumeric', 4 | % 'BinaryInteger', 5 | % 'FixedWidthInteger', 6 | % 'FloatingPoint', 7 | % 'Comparable', 8 | % 'Bool', 9 | % ] 10 | % concrete_types = set([ 11 | % 'Bool', 12 | % ]) 13 | % type_to_operators = { 14 | % 'Numeric': ['+', '-', '*'], 15 | % 'SignedNumeric': [], 16 | % 'BinaryInteger': ['/', '%', '&', '|', '^', '<<', '>>'], 17 | % 'FixedWidthInteger': ['&+', '&-', '&*', '&<<', '&>>'], 18 | % 'FloatingPoint': ['/'], 19 | % 'Comparable': [], 20 | % 'Bool': ['&&', '||'], 21 | % } 22 | % type_to_compound_assignment_operators = { 23 | % 'Numeric': ['+=', '-=', '*='], 24 | % 'SignedNumeric': [], 25 | % 'BinaryInteger': ['/=', '%=', '&=', '|=', '^=', '<<=', '>>='], 26 | % 'FixedWidthInteger': ['&<<=', '&>>='], 27 | % 'FloatingPoint': ['/='], 28 | % 'Comparable': [], 29 | % 'Bool': [], 30 | % } 31 | % type_to_equality_operators = { 32 | % 'Numeric': [], 33 | % 'SignedNumeric': [], 34 | % 'BinaryInteger': [], 35 | % 'FixedWidthInteger': [], 36 | % 'FloatingPoint': [], 37 | % 'Comparable': [], 38 | % 'Bool': [], 39 | % } 40 | % type_to_comparison_operators = { 41 | % 'Numeric': [], 42 | % 'SignedNumeric': [], 43 | % 'BinaryInteger': [], 44 | % 'FixedWidthInteger': [], 45 | % 'FloatingPoint': [], 46 | % 'Comparable': ['<', '<=', '>', '>='], 47 | % 'Bool': [], 48 | % } 49 | % type_to_prefix_operators = { 50 | % 'Numeric': ['+'], 51 | % 'SignedNumeric': ['-'], 52 | % 'BinaryInteger': [], 53 | % 'FixedWidthInteger': [], 54 | % 'FloatingPoint': [], 55 | % 'Comparable': [], 56 | % 'Bool': ['!'], 57 | % } 58 | % type_to_concrete_types = { 59 | % 'Numeric': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Float', 'Double'], 60 | % 'SignedNumeric': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'Float', 'Double'], 61 | % 'BinaryInteger': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64'], 62 | % 'FixedWidthInteger': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64'], 63 | % 'FloatingPoint': ['Float', 'Double'], 64 | % 'Comparable': ['Int', 'Int8', 'Int16', 'Int32', 'Int64', 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64', 'Float', 'Double'], 65 | % 'Bool': [], 66 | % } 67 | % for i, type in enumerate(types): 68 | % if i > 0: 69 | 70 | % end 71 | extension RGBA where Channel ${'==' if type in concrete_types else ':'} ${type} { 72 | % first = True 73 | % for operator in type_to_operators[type]: 74 | % if first: 75 | % first = False 76 | % else: 77 | 78 | % end 79 | @inlinable 80 | public static func ${operator}(lhs: RGBA, rhs: RGBA) -> RGBA { 81 | return RGBA(red: lhs.red ${operator} rhs.red, green: lhs.green ${operator} rhs.green, blue: lhs.blue ${operator} rhs.blue, alpha: lhs.alpha ${operator} rhs.alpha) 82 | } 83 | % end 84 | % 85 | % for operator in type_to_compound_assignment_operators[type]: 86 | % if first: 87 | % first = False 88 | % else: 89 | 90 | % end 91 | @inlinable 92 | public static func ${operator}(lhs: inout RGBA, rhs: RGBA) { 93 | lhs.red ${operator} rhs.red 94 | lhs.green ${operator} rhs.green 95 | lhs.blue ${operator} rhs.blue 96 | lhs.alpha ${operator} rhs.alpha 97 | } 98 | % end 99 | % 100 | % for operator in type_to_equality_operators[type]: 101 | % if first: 102 | % first = False 103 | % else: 104 | 105 | % end 106 | @inlinable 107 | % and_or = '&&' if operator == '==' else '||' 108 | public static func ${operator}(lhs: RGBA, rhs: RGBA) -> Bool { 109 | return lhs.red ${operator} rhs.red ${and_or} lhs.green ${operator} rhs.green ${and_or} lhs.blue ${operator} rhs.blue ${and_or} lhs.alpha ${operator} rhs.alpha 110 | } 111 | % end 112 | % 113 | % for operator in type_to_comparison_operators[type]: 114 | % if first: 115 | % first = False 116 | % else: 117 | 118 | % end 119 | @inlinable 120 | public static func ${operator}(lhs: RGBA, rhs: RGBA) -> RGBA { 121 | return RGBA(red: lhs.red ${operator} rhs.red, green: lhs.green ${operator} rhs.green, blue: lhs.blue ${operator} rhs.blue, alpha: lhs.alpha ${operator} rhs.alpha) 122 | } 123 | % end 124 | % 125 | % for operator in type_to_prefix_operators[type]: 126 | % if first: 127 | % first = False 128 | % else: 129 | 130 | % end 131 | @inlinable 132 | prefix public static func ${operator}(a: RGBA) -> RGBA { 133 | return RGBA(red: ${operator}a.red, green: ${operator}a.green, blue: ${operator}a.blue, alpha: ${operator}a.alpha) 134 | } 135 | % end 136 | } 137 | % end 138 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Resizing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension ImageProtocol { 4 | public func resizedTo(width: Int, height: Int) -> Image { 5 | if width == 0 || height == 0 { 6 | return Image(width: width, height: height, pixels: []) 7 | } 8 | 9 | let ox = xRange.lowerBound 10 | let oy = yRange.lowerBound 11 | if ox == 0 && oy == 0 { 12 | return resizedByInterpolationTo(width: width, height: height) { x, y in self[Int(round(x)), Int(round(y)), extrapolation: .edge] } 13 | } else { 14 | let dox = Double(ox) 15 | let doy = Double(oy) 16 | return resizedByInterpolationTo(width: width, height: height) { x, y in self[Int(round(dox + x)), Int(round(doy + y)), extrapolation: .edge] } 17 | } 18 | } 19 | 20 | private func resizedByInterpolationTo(width: Int, height: Int, pixelAt: (Double, Double) -> Pixel) -> Image { 21 | let sx = Double(xRange.count) / Double(width) 22 | let sy = Double(yRange.count) / Double(height) 23 | return Image(width: width, height: height, pixelAt: { x, y in 24 | pixelAt((Double(x) + 0.5) * sx - 0.5, (Double(y) + 0.5) * sy - 0.5) 25 | }) 26 | } 27 | } 28 | 29 | extension ImageProtocol where Pixel : _NumericPixel { 30 | public func resizedTo(width: Int, height: Int) -> Image { 31 | let ox = xRange.lowerBound 32 | let oy = yRange.lowerBound 33 | if ox == 0 && oy == 0 { 34 | return resizedTo( 35 | width: width, 36 | height: height, 37 | isAntialiased: true, 38 | pixelAt: { x, y in self[x, y] }, 39 | extrapolatedPixelAt: { x, y in self[x, y, extrapolation: .edge] } 40 | ) 41 | } else { 42 | let dox = Double(ox) 43 | let doy = Double(oy) 44 | return resizedTo( 45 | width: width, 46 | height: height, 47 | isAntialiased: true, 48 | pixelAt: { x, y in self[dox + x, doy + y] }, 49 | extrapolatedPixelAt: { x, y in self[dox + x, doy + y, extrapolation: .edge] } 50 | ) 51 | } 52 | } 53 | 54 | public func resizedTo(width: Int, height: Int, interpolatedBy interpolationMethod: InterpolationMethod) -> Image { 55 | let ox = xRange.lowerBound 56 | let oy = yRange.lowerBound 57 | let isAntialiased: Bool 58 | if case .nearestNeighbor = interpolationMethod { 59 | isAntialiased = false 60 | } else { 61 | isAntialiased = true 62 | } 63 | if ox == 0 && oy == 0 { 64 | return resizedTo( 65 | width: width, 66 | height: height, 67 | isAntialiased: isAntialiased, 68 | pixelAt: { x, y in self[x, y, interpolation: interpolationMethod] }, 69 | extrapolatedPixelAt: { x, y in self[x, y, interpolation: interpolationMethod, extrapolation: .edge] } 70 | ) 71 | } else { 72 | let dox = Double(ox) 73 | let doy = Double(oy) 74 | return resizedTo( 75 | width: width, 76 | height: height, 77 | isAntialiased: isAntialiased, 78 | pixelAt: { x, y in self[dox + x, doy + y, interpolation: interpolationMethod] }, 79 | extrapolatedPixelAt: { x, y in self[dox + x, doy + y, interpolation: interpolationMethod, extrapolation: .edge] } 80 | ) 81 | } 82 | } 83 | 84 | private func resizedTo( 85 | width: Int, 86 | height: Int, 87 | isAntialiased: Bool, 88 | pixelAt: (Double, Double) -> Pixel, 89 | extrapolatedPixelAt: (Double, Double) -> Pixel 90 | ) -> Image { 91 | if width == 0 || height == 0 { 92 | return Image(width: width, height: height, pixels: []) 93 | } 94 | 95 | let xRange = self.xRange 96 | let yRange = self.yRange 97 | 98 | if width == xRange.count && height == yRange.count { 99 | if let image = self as? Image { 100 | return image 101 | } else { 102 | return Image(self) 103 | } 104 | } 105 | 106 | let isTargetWidthLarger = width >= xRange.count 107 | let isTargetHeightLarger = height >= yRange.count 108 | 109 | if !isAntialiased || (isTargetWidthLarger && isTargetHeightLarger) { 110 | return resizedByInterpolationTo(width: width, height: height, pixelAt: extrapolatedPixelAt) 111 | } 112 | 113 | let targetWidth = isTargetWidthLarger ? width : (xRange.count / width) * width 114 | let targetHeight = isTargetHeightLarger ? height : (yRange.count / height) * height 115 | 116 | let multiple: Image 117 | if width < xRange.count && height < yRange.count { 118 | multiple = resizedByInterpolationTo(width: targetWidth, height: targetHeight, pixelAt: pixelAt) 119 | } else { 120 | multiple = resizedByInterpolationTo(width: targetWidth, height: targetHeight, pixelAt: extrapolatedPixelAt) 121 | } 122 | return multiple.resizedByMeanTo( 123 | width: width, 124 | height: height 125 | ) 126 | } 127 | 128 | private func resizedByMeanTo(width: Int, height: Int) -> Image { 129 | let xRange = self.xRange 130 | let yRange = self.yRange 131 | 132 | assert(width <= xRange.count && xRange.count % width == 0) 133 | assert(height <= yRange.count && yRange.count % height == 0) 134 | 135 | let sx = xRange.count / width 136 | let sy = yRange.count / height 137 | let n = sx * sy 138 | 139 | var pixels = [Pixel]() 140 | for y in 0..(width: width, height: height, pixels: pixels) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Rotation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension ImageProtocol { 4 | public func rotated(byDegrees angle: Int) -> Image { 5 | precondition(angle % 90 == 0, "`angle` must be a multiple of 90: \(angle)") 6 | return rotated(byRightAngleInDegrees: angle) 7 | } 8 | 9 | private func rotated(byRightAngleInDegrees angle: Int) -> Image { 10 | assert(angle % 90 == 0, "`angle` must be a multiple of 90: \(angle)") 11 | 12 | switch (angle / 90) % 4 { 13 | case 0: 14 | if let zelf = self as? Image { 15 | return zelf 16 | } else { 17 | return Image(self) 18 | } 19 | case 1, -3: 20 | var pixels = [Pixel]() 21 | pixels.reserveCapacity(count) 22 | 23 | for y in xRange { 24 | for x in yRange.reversed() { 25 | pixels.append(self[y, x]) 26 | } 27 | } 28 | 29 | return Image(width: height, height: width, pixels: pixels) 30 | case 2, -2: 31 | var pixels = [Pixel]() 32 | pixels.reserveCapacity(count) 33 | 34 | for y in yRange.reversed() { 35 | for x in xRange.reversed() { 36 | pixels.append(self[x, y]) 37 | } 38 | } 39 | 40 | return Image(width: width, height: height, pixels: pixels) 41 | case 3, -1: 42 | var pixels = [Pixel]() 43 | pixels.reserveCapacity(count) 44 | 45 | for y in xRange.reversed() { 46 | for x in yRange { 47 | pixels.append(self[y, x]) 48 | } 49 | } 50 | 51 | return Image(width: height, height: width, pixels: pixels) 52 | default: 53 | fatalError("Never reaches here.") 54 | } 55 | } 56 | 57 | public func rotated(by angle: Double, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 58 | return rotatedImageWith(angle: angle) { self[Int(round($0)), Int(round($1)), extrapolation: extrapolationMethod] } 59 | } 60 | 61 | public func rotated(byDegrees angle: Double, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 62 | return rotated(by: angle / 180.0 * .pi, extrapolatedBy: extrapolationMethod) 63 | } 64 | 65 | private func rotatedImageWith( 66 | angle: Double, 67 | pixelAt: (Double, Double) -> Pixel 68 | ) -> Image { 69 | let s = sin(angle) 70 | let c = cos(angle) 71 | 72 | let xRange = self.xRange 73 | let yRange = self.yRange 74 | 75 | let ox0 = Double(xRange.lowerBound) 76 | let oy0 = Double(yRange.lowerBound) 77 | 78 | let w0 = xRange.count 79 | let h0 = yRange.count 80 | 81 | let hw0 = Double(w0) / 2 82 | let hh0 = Double(h0) / 2 83 | 84 | let hw9d = Swift.max(abs(hw0 * c - hh0 * s), abs(hw0 * c - (-hh0) * s)) 85 | let hh9d = Swift.max(abs(hw0 * s + hh0 * c), abs(hw0 * s + (-hh0) * c)) 86 | 87 | let w9 = Int(round(hw9d * 2)) 88 | let h9 = Int(round(hh9d * 2)) 89 | 90 | let hw9 = Double(w9) / 2 91 | let hh9 = Double(h9) / 2 92 | 93 | return Image(width: w9, height: h9, pixelAt: { x9, y9 in 94 | let x2 = Double(x9) - (hw9 - 0.5) 95 | let y2 = Double(y9) - (hh9 - 0.5) 96 | 97 | let x1 = x2 * c - y2 * (-s) 98 | let y1 = x2 * (-s) + y2 * c 99 | 100 | let x0 = x1 + (hw0 - 0.5) 101 | let y0 = y1 + (hh0 - 0.5) 102 | 103 | return pixelAt(x0 + ox0, y0 + oy0) 104 | }) 105 | } 106 | } 107 | 108 | extension ImageProtocol where Pixel : _NumericPixel { 109 | public func rotated(byDegrees angle: Int) -> Image { 110 | if angle % 90 == 0 { 111 | return rotated(byRightAngleInDegrees: angle) 112 | } else { 113 | return rotated(byDegrees: Double(angle)) 114 | } 115 | } 116 | 117 | public func rotated(by angle: Double) -> Image { 118 | return rotatedImageWith(angle: angle) { self[$0, $1, interpolation: .bilinear, extrapolation: .constant(._ez_zero)] } 119 | } 120 | 121 | public func rotated(byDegrees angle: Double) -> Image { 122 | return rotated(by: angle / 180.0 * .pi) 123 | } 124 | 125 | public func rotated(by angle: Double, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 126 | return rotatedImageWith(angle: angle) { self[$0, $1, interpolation: .bilinear, extrapolation: extrapolationMethod] } 127 | } 128 | 129 | public func rotated(byDegrees angle: Double, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 130 | return rotated(by: angle / 180.0 * .pi, extrapolatedBy: extrapolationMethod) 131 | } 132 | 133 | public func rotated(by angle: Double, interpolatedBy interpolationMethod: InterpolationMethod, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 134 | return rotatedImageWith(angle: angle) { self[$0, $1, interpolation: interpolationMethod, extrapolation: extrapolationMethod] } 135 | } 136 | 137 | public func rotated(byDegrees angle: Double, interpolatedBy interpolationMethod: InterpolationMethod, extrapolatedBy extrapolationMethod: ExtrapolationMethod) -> Image { 138 | return rotated(by: angle / 180.0 * .pi, interpolatedBy: interpolationMethod, extrapolatedBy: extrapolationMethod) 139 | } 140 | } 141 | 142 | extension ImageProtocol where Element == Bool { // Rotation 143 | public func rotated(byDegrees angle: Int) -> Image { 144 | if angle % 90 == 0 { 145 | return rotated(byRightAngleInDegrees: angle) 146 | } else { 147 | return rotated(byDegrees: Double(angle)) 148 | } 149 | } 150 | 151 | public func rotated(by angle: Double) -> Image { 152 | return rotatedImageWith(angle: angle) { self[Int(round($0)), Int(round($1)), extrapolation: .constant(false)] } 153 | } 154 | 155 | public func rotated(byDegrees angle: Double) -> Image { 156 | return rotated(by: angle / 180.0 * .pi) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Sources/SwiftImage/SwiftImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftImage.h 3 | // SwiftImage 4 | // 5 | // Created by Yuta Koshizawa on 2017/10/04. 6 | // Copyright © 2017 koherent.org. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SwiftImage. 12 | FOUNDATION_EXPORT double SwiftImageVersionNumber; 13 | 14 | //! Project version string for SwiftImage. 15 | FOUNDATION_EXPORT const unsigned char SwiftImageVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/SwiftImage/TypicalChannel.swift: -------------------------------------------------------------------------------- 1 | public protocol _TypicalChannel { 2 | init(_: Int) 3 | init(_: Int8) 4 | init(_: Int16) 5 | init(_: Int32) 6 | init(_: Int64) 7 | init(_: UInt) 8 | init(_: UInt8) 9 | init(_: UInt16) 10 | init(_: UInt32) 11 | init(_: UInt64) 12 | init(_: Float) 13 | init(_: Double) 14 | } 15 | 16 | extension Int: _TypicalChannel {} 17 | extension Int8: _TypicalChannel {} 18 | extension Int16: _TypicalChannel {} 19 | extension Int32: _TypicalChannel {} 20 | extension Int64: _TypicalChannel {} 21 | extension UInt: _TypicalChannel {} 22 | extension UInt8: _TypicalChannel {} 23 | extension UInt16: _TypicalChannel {} 24 | extension UInt32: _TypicalChannel {} 25 | extension UInt64: _TypicalChannel {} 26 | extension Float: _TypicalChannel {} 27 | extension Double: _TypicalChannel {} 28 | -------------------------------------------------------------------------------- /Sources/SwiftImage/UIKit.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && canImport(CoreGraphics) 2 | import Foundation 3 | import UIKit 4 | #if os(iOS) || os(tvOS) 5 | import CoreImage 6 | #endif 7 | 8 | #if os(watchOS) 9 | // Hack to simplify code 10 | @usableFromInline 11 | internal enum CIImage { 12 | @usableFromInline 13 | var extent: CGRect { fatalError() } 14 | } 15 | @usableFromInline 16 | internal class CIContext { 17 | @usableFromInline 18 | init() {} 19 | @usableFromInline 20 | func createCGImage(_ image: CIImage, from: CGRect) -> CGImage? { return nil } 21 | } 22 | extension UIImage { 23 | @usableFromInline 24 | internal var ciImage: CIImage? { return nil } 25 | } 26 | #endif 27 | 28 | extension ImageProtocol where Self: _CGImageConvertible, Self: Sendable, Pixel: _CGPixel { 29 | @inlinable 30 | public init(uiImage: UIImage) { 31 | if let cgImage = uiImage.cgImage { 32 | self.init(cgImage: cgImage) 33 | } else if let ciImage = uiImage.ciImage { 34 | let context = CIContext() 35 | // Fails when the `ciImage` has an infinite extent. 36 | guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { 37 | fatalError("Failed to create a `CGImage` from an internal `CIImage` object from the given `UIImage` instance (\(uiImage)).") 38 | } 39 | self.init(cgImage: cgImage) 40 | } else { 41 | // This `gurad` can be replaced with `assert` if you are sure that the `size` is always equal to `.zero`. 42 | guard uiImage.size == .zero else { 43 | fatalError("The `size` of the given `UIImage` instance (\(uiImage)) is not equal to `.zero` though both the `cgImage` and the `ciImage` of the instance are `nil`.") 44 | } 45 | self.init(width: 0, height: 0, pixels: []) 46 | } 47 | } 48 | 49 | @usableFromInline 50 | internal init?(uiImageOrNil: UIImage?) { 51 | guard let uiImage: UIImage = uiImageOrNil else { return nil } 52 | self.init(uiImage: uiImage) 53 | } 54 | 55 | @inlinable 56 | public init?(named name: String) { 57 | self.init(uiImageOrNil: UIImage(named: name)) 58 | } 59 | 60 | #if os(iOS) || os(tvOS) 61 | @inlinable 62 | public init?(named name: String, in bundle: Bundle?, compatibleWith traitCollection: UITraitCollection?) { 63 | self.init(uiImageOrNil: UIImage(named: name, in: bundle, compatibleWith: traitCollection)) 64 | } 65 | #endif 66 | 67 | @inlinable 68 | public init?(contentsOfFile path: String) { 69 | self.init(uiImageOrNil: UIImage(contentsOfFile: path)) 70 | } 71 | 72 | @inlinable 73 | public init?(data: Data) { 74 | self.init(uiImageOrNil: UIImage(data: data)) 75 | } 76 | 77 | @inlinable 78 | public var uiImage: UIImage { 79 | return UIImage(cgImage: cgImage) 80 | } 81 | 82 | @inlinable 83 | public func pngData() -> Data? { 84 | guard width > 0 && height > 0 else { return nil } 85 | return autoreleasepool { 86 | return uiImage.pngData() 87 | } 88 | } 89 | 90 | @inlinable 91 | public func jpegData(compressionQuality: Double) -> Data? { 92 | guard width > 0 && height > 0 else { return nil } 93 | return autoreleasepool { 94 | return uiImage.jpegData(compressionQuality: CGFloat(compressionQuality)) 95 | } 96 | } 97 | 98 | @inlinable 99 | public func data(using format: ImageFormat) -> Data? { 100 | switch format { 101 | case .png: 102 | return pngData() 103 | case .jpeg(let compressionQuality): 104 | return jpegData(compressionQuality: compressionQuality) 105 | } 106 | } 107 | 108 | @inlinable 109 | public func write(to url: URL, atomically: Bool, format: ImageFormat) throws { 110 | guard let data = data(using: format) else { 111 | throw ImageFormat.FormattingError(image: self, format: format) 112 | } 113 | try data.write(to: url, options: atomically ? .atomic : .init(rawValue: 0)) 114 | } 115 | 116 | @inlinable 117 | public func write(toFile path: S, atomically: Bool, format: ImageFormat) throws { 118 | try write(to: URL(fileURLWithPath: String(path)), atomically: atomically, format: format) 119 | } 120 | } 121 | #endif 122 | -------------------------------------------------------------------------------- /Sources/SwiftImage/Util.swift: -------------------------------------------------------------------------------- 1 | @inlinable 2 | internal func clamp(_ x: T, lower: T, upper: T) -> T { 3 | return min(max(x, lower), upper) 4 | } 5 | 6 | extension Range { 7 | internal func isSuperset(of other: Range) -> Bool { 8 | return lowerBound <= other.lowerBound && other.upperBound <= upperBound || other.isEmpty 9 | } 10 | } 11 | 12 | internal func range(from range: R, relativeTo collection: Range) -> Range where R.Bound == Int { 13 | let all = Int.min ..< Int.max 14 | let boundedRange: Range = range.relative(to: all) 15 | let lowerBound: Int 16 | let upperBound: Int 17 | if boundedRange.lowerBound == .min { 18 | lowerBound = Swift.max(boundedRange.lowerBound, collection.lowerBound) 19 | } else { 20 | lowerBound = boundedRange.lowerBound 21 | } 22 | if boundedRange.upperBound == .max { 23 | upperBound = Swift.min(collection.upperBound, boundedRange.upperBound) 24 | } else { 25 | upperBound = boundedRange.upperBound 26 | } 27 | return lowerBound..(width: 3, height: 2, pixels: [ 5 | 1, 2, 3, 6 | 4, 5, 6, 7 | ]) 8 | private let slice: ImageSlice = Image(width: 5, height: 4, pixels: [ 9 | 0, 0, 0, 0, 0, 10 | 0, 1, 2, 3, 0, 11 | 0, 4, 5, 6, 0, 12 | 0, 0, 0, 0, 0, 13 | ])[1...3, 1...2] 14 | 15 | class AnyImageTests : XCTestCase { 16 | func testSlice() { 17 | XCTAssertEqual(slice, ImageSlice(width: 3, height: 2, pixels: [ 18 | 1, 2, 3, 19 | 4, 5, 6, 20 | ])) 21 | } 22 | 23 | func testXRange() { 24 | XCTAssertEqual(image.xRange, 0..<3) 25 | XCTAssertEqual(slice.xRange, 1..<4) 26 | } 27 | 28 | func testYRange() { 29 | XCTAssertEqual(image.yRange, 0..<2) 30 | XCTAssertEqual(slice.yRange, 1..<3) 31 | } 32 | 33 | func testSubscript() { 34 | do { 35 | var a = AnyImage(image) 36 | 37 | XCTAssertEqual(a[0, 0], 1) 38 | XCTAssertEqual(a[1, 0], 2) 39 | XCTAssertEqual(a[2, 0], 3) 40 | XCTAssertEqual(a[0, 1], 4) 41 | XCTAssertEqual(a[1, 1], 5) 42 | XCTAssertEqual(a[2, 1], 6) 43 | 44 | a[1, 0] = 9 45 | 46 | XCTAssertEqual(a[1, 0], 9) 47 | } 48 | 49 | do { 50 | var a = AnyImage(slice) 51 | 52 | XCTAssertEqual(a[1, 1], 1) 53 | XCTAssertEqual(a[2, 1], 2) 54 | XCTAssertEqual(a[3, 1], 3) 55 | XCTAssertEqual(a[1, 2], 4) 56 | XCTAssertEqual(a[2, 2], 5) 57 | XCTAssertEqual(a[3, 2], 6) 58 | 59 | a[2, 1] = 9 60 | 61 | XCTAssertEqual(a[2, 1], 9) 62 | } 63 | } 64 | 65 | func testSubscriptRange() { 66 | do { 67 | let a = AnyImage(Image(width: 5, height: 4, pixels: [ 68 | 0, 0, 0, 0, 0, 69 | 0, 1, 2, 3, 0, 70 | 0, 4, 5, 6, 0, 71 | 0, 0, 0, 0, 0, 72 | ])) 73 | let b: AnyImage = a[1..<4, 1..<3] 74 | 75 | XCTAssertEqual(b, AnyImage(slice)) 76 | 77 | let c = AnyImage(b) 78 | 79 | XCTAssertEqual(c[2..<4, 2...2], AnyImage(width: 2, height: 1, pixels: [5, 6])) 80 | } 81 | } 82 | 83 | func testSequence() { 84 | do { 85 | let a = AnyImage(Image(width: 3, height: 2, pixels: [ 86 | 1, 2, 3, 87 | 4, 5, 6, 88 | ])) 89 | 90 | var iterator = a.makeIterator() 91 | 92 | XCTAssertEqual(iterator.next(), 1) 93 | XCTAssertEqual(iterator.next(), 2) 94 | XCTAssertEqual(iterator.next(), 3) 95 | XCTAssertEqual(iterator.next(), 4) 96 | XCTAssertEqual(iterator.next(), 5) 97 | XCTAssertEqual(iterator.next(), 6) 98 | XCTAssertEqual(iterator.next(), nil) 99 | } 100 | } 101 | 102 | func testCopyOnWrite() { 103 | var a = AnyImage(image) 104 | let b = a 105 | 106 | XCTAssertEqual(a[0, 0], 1) 107 | XCTAssertEqual(b[0, 0], 1) 108 | 109 | a[0, 0] = 9 110 | 111 | XCTAssertEqual(a[0, 0], 9) 112 | XCTAssertEqual(b[0, 0], 1) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/AppKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | #if canImport(AppKit) && canImport(CoreGraphics) 4 | import CoreGraphics 5 | import AppKit 6 | #endif 7 | 8 | class AppKitTests: XCTestCase { 9 | func testInitWithNSImage() { 10 | #if canImport(AppKit) && canImport(CoreGraphics) 11 | do { 12 | let nsImage = NSImage(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png"))! 13 | let image = Image>(nsImage: nsImage) 14 | 15 | XCTAssertEqual(image.width, 2) 16 | XCTAssertEqual(image.height, 2) 17 | 18 | XCTAssertEqual(image[0, 0].red, 255) 19 | XCTAssertEqual(image[0, 0].green, 0) 20 | XCTAssertEqual(image[0, 0].blue, 0) 21 | XCTAssertEqual(image[0, 0].alpha, 64) 22 | 23 | XCTAssertEqual(image[1, 0].red, 0) 24 | XCTAssertEqual(image[1, 0].green, 255) 25 | XCTAssertEqual(image[1, 0].blue, 0) 26 | XCTAssertEqual(image[1, 0].alpha, 127) 27 | 28 | XCTAssertEqual(image[0, 1].red, 0) 29 | XCTAssertEqual(image[0, 1].green, 0) 30 | XCTAssertEqual(image[0, 1].blue, 255) 31 | XCTAssertEqual(image[0, 1].alpha, 191) 32 | 33 | XCTAssertEqual(image[1, 1].red, 255) 34 | XCTAssertEqual(image[1, 1].green, 255) 35 | XCTAssertEqual(image[1, 1].blue, 0) 36 | XCTAssertEqual(image[1, 1].alpha, 255) 37 | } 38 | 39 | do { // `ImageSlice` 40 | let nsImage = NSImage(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png"))! 41 | let slice = ImageSlice>(nsImage: nsImage) 42 | 43 | XCTAssertEqual(slice.width, 2) 44 | XCTAssertEqual(slice.height, 2) 45 | 46 | XCTAssertEqual(slice[0, 0].red, 255) 47 | XCTAssertEqual(slice[0, 0].green, 0) 48 | XCTAssertEqual(slice[0, 0].blue, 0) 49 | XCTAssertEqual(slice[0, 0].alpha, 64) 50 | 51 | XCTAssertEqual(slice[1, 0].red, 0) 52 | XCTAssertEqual(slice[1, 0].green, 255) 53 | XCTAssertEqual(slice[1, 0].blue, 0) 54 | XCTAssertEqual(slice[1, 0].alpha, 127) 55 | 56 | XCTAssertEqual(slice[0, 1].red, 0) 57 | XCTAssertEqual(slice[0, 1].green, 0) 58 | XCTAssertEqual(slice[0, 1].blue, 255) 59 | XCTAssertEqual(slice[0, 1].alpha, 191) 60 | 61 | XCTAssertEqual(slice[1, 1].red, 255) 62 | XCTAssertEqual(slice[1, 1].green, 255) 63 | XCTAssertEqual(slice[1, 1].blue, 0) 64 | XCTAssertEqual(slice[1, 1].alpha, 255) 65 | } 66 | 67 | do { // `PremultipliedRGBA` 68 | let nsImage = NSImage(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png"))! 69 | let image = Image>(nsImage: nsImage) 70 | 71 | XCTAssertEqual(image.width, 2) 72 | XCTAssertEqual(image.height, 2) 73 | 74 | XCTAssertEqual(image[0, 0].red, 64) 75 | XCTAssertEqual(image[0, 0].green, 0) 76 | XCTAssertEqual(image[0, 0].blue, 0) 77 | XCTAssertEqual(image[0, 0].alpha, 64) 78 | 79 | XCTAssertEqual(image[1, 0].red, 0) 80 | XCTAssertEqual(image[1, 0].green, 127) 81 | XCTAssertEqual(image[1, 0].blue, 0) 82 | XCTAssertEqual(image[1, 0].alpha, 127) 83 | 84 | XCTAssertEqual(image[0, 1].red, 0) 85 | XCTAssertEqual(image[0, 1].green, 0) 86 | XCTAssertEqual(image[0, 1].blue, 191) 87 | XCTAssertEqual(image[0, 1].alpha, 191) 88 | 89 | XCTAssertEqual(image[1, 1].red, 255) 90 | XCTAssertEqual(image[1, 1].green, 255) 91 | XCTAssertEqual(image[1, 1].blue, 0) 92 | XCTAssertEqual(image[1, 1].alpha, 255) 93 | } 94 | 95 | do { // With `NSImage` from `CGImage` 96 | let dataProvider = CGDataProvider.init(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")) as CFData)! 97 | let cgImage = CGImage(pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)! 98 | let nsImage = NSImage(cgImage: cgImage, size: .zero) 99 | let image = Image>(nsImage: nsImage) 100 | 101 | XCTAssertEqual(image.width, 2) 102 | XCTAssertEqual(image.height, 2) 103 | 104 | XCTAssertEqual(255, image[0, 0].red) 105 | XCTAssertEqual( 0, image[0, 0].green) 106 | XCTAssertEqual( 0, image[0, 0].blue) 107 | XCTAssertEqual( 64, image[0, 0].alpha) 108 | 109 | XCTAssertEqual( 0, image[1, 0].red) 110 | XCTAssertEqual(255, image[1, 0].green) 111 | XCTAssertEqual( 0, image[1, 0].blue) 112 | XCTAssertEqual(127, image[1, 0].alpha) 113 | 114 | XCTAssertEqual( 0, image[0, 1].red) 115 | XCTAssertEqual( 0, image[0, 1].green) 116 | XCTAssertEqual(255, image[0, 1].blue) 117 | XCTAssertEqual(191, image[0, 1].alpha) 118 | 119 | XCTAssertEqual(255, image[1, 1].red) 120 | XCTAssertEqual(255, image[1, 1].green) 121 | XCTAssertEqual( 0, image[1, 1].blue) 122 | XCTAssertEqual(255, image[1, 1].alpha) 123 | } 124 | 125 | do { // With `NSImage` from `CIImage` 126 | if #available(macOS 10.12, *) { 127 | let ciImage = CIImage(color: CIColor.red).cropped(to: CGRect(x: 0, y: 0, width: 1, height: 1)) 128 | let rep = NSCIImageRep(ciImage: ciImage) 129 | let nsImage = NSImage(size: rep.size) 130 | nsImage.addRepresentation(rep) 131 | let image = Image>(nsImage: nsImage) 132 | 133 | XCTAssertEqual(image.width, 1) 134 | XCTAssertEqual(image.height, 1) 135 | 136 | XCTAssertEqual(255, image[0, 0].red) 137 | XCTAssertEqual( 0, image[0, 0].green) 138 | XCTAssertEqual( 0, image[0, 0].blue) 139 | XCTAssertEqual(255, image[0, 0].alpha) 140 | } 141 | } 142 | 143 | do { // With `NSImage` whose `cgImage` is `nil` 144 | let nsImage = NSImage() 145 | let image = Image>(nsImage: nsImage) 146 | 147 | XCTAssertEqual(image.width, 0) 148 | XCTAssertEqual(image.height, 0) 149 | } 150 | #endif 151 | } 152 | 153 | func testNSImage() { 154 | #if canImport(AppKit) && canImport(CoreGraphics) 155 | do { 156 | let original = Image>(width: 2, height: 2, pixels: [ 157 | RGBA(red: 0, green: 1, blue: 2, alpha: 255), 158 | RGBA(red: 253, green: 254, blue: 255, alpha: 255), 159 | RGBA(red: 10, green: 20, blue: 30, alpha: 102), 160 | RGBA(red: 10, green: 20, blue: 30, alpha: 51), 161 | ]) 162 | let nsImage = original.nsImage 163 | XCTAssertEqual(nsImage.size.width, CGFloat(original.width)) 164 | XCTAssertEqual(nsImage.size.height, CGFloat(original.height)) 165 | 166 | let restored = Image>(nsImage: nsImage) 167 | XCTAssertEqual(restored, original) 168 | } 169 | 170 | do { 171 | let original = Image>(width: 2, height: 2, pixels: [ 172 | PremultipliedRGBA(red: 0, green: 1, blue: 2, alpha: 255), 173 | PremultipliedRGBA(red: 253, green: 254, blue: 255, alpha: 255), 174 | PremultipliedRGBA(red: 10, green: 20, blue: 30, alpha: 102), 175 | PremultipliedRGBA(red: 10, green: 20, blue: 30, alpha: 51), 176 | ]) 177 | let nsImage = original.nsImage 178 | XCTAssertEqual(nsImage.size.width, CGFloat(original.width)) 179 | XCTAssertEqual(nsImage.size.height, CGFloat(original.height)) 180 | 181 | let restored = Image>(nsImage: nsImage) 182 | XCTAssertEqual(restored, original) 183 | } 184 | 185 | do { 186 | let original = Image(width: 2, height: 2, pixels: [0, 1, 127, 255]) 187 | let nsImage = original.nsImage 188 | XCTAssertEqual(nsImage.size.width, CGFloat(original.width)) 189 | XCTAssertEqual(nsImage.size.height, CGFloat(original.height)) 190 | 191 | let restored = Image(nsImage: nsImage) 192 | XCTAssertEqual(restored, original) 193 | } 194 | 195 | do { // `ImageSlice` 196 | let original = ImageSlice>(width: 2, height: 2, pixels: [ 197 | RGBA(red: 0, green: 1, blue: 2, alpha: 255), 198 | RGBA(red: 253, green: 254, blue: 255, alpha: 255), 199 | RGBA(red: 10, green: 20, blue: 30, alpha: 102), 200 | RGBA(red: 10, green: 20, blue: 30, alpha: 51), 201 | ]) 202 | let nsImage = original.nsImage 203 | XCTAssertEqual(nsImage.size.width, CGFloat(original.width)) 204 | XCTAssertEqual(nsImage.size.height, CGFloat(original.height)) 205 | 206 | let restored = ImageSlice>(nsImage: nsImage) 207 | XCTAssertEqual(restored, original) 208 | } 209 | #endif 210 | } 211 | 212 | func testNSImageTiffRepresentation() { 213 | #if canImport(AppKit) && canImport(CoreGraphics) 214 | do { 215 | let original = Image>(width: 2, height: 2, pixels: [ 216 | RGBA(red: 0, green: 1, blue: 2, alpha: 255), 217 | RGBA(red: 253, green: 254, blue: 255, alpha: 255), 218 | RGBA(red: 10, green: 20, blue: 30, alpha: 102), 219 | RGBA(red: 10, green: 20, blue: 30, alpha: 51), 220 | ]) 221 | let nsImage = original.nsImage 222 | let data = nsImage.tiffRepresentation! 223 | 224 | let nsRestored = NSImage(data: data)! 225 | XCTAssertEqual(nsRestored.size.width, CGFloat(original.width)) 226 | XCTAssertEqual(nsRestored.size.height, CGFloat(original.height)) 227 | 228 | let restored = Image>(nsImage: nsRestored) 229 | XCTAssertEqual(restored, original) 230 | } 231 | 232 | do { 233 | let original = Image(width: 2, height: 2, pixels: [0, 1, 127, 255]) 234 | let nsImage = original.nsImage 235 | let data = nsImage.tiffRepresentation! 236 | 237 | let nsRestored = NSImage(data: data)! 238 | XCTAssertEqual(nsRestored.size.width, CGFloat(original.width)) 239 | XCTAssertEqual(nsRestored.size.height, CGFloat(original.height)) 240 | 241 | let restored = Image(nsImage: nsRestored) 242 | XCTAssertEqual(restored, original) 243 | } 244 | #endif 245 | } 246 | 247 | func testInitWithNSImagePerformance() { 248 | #if canImport(AppKit) && canImport(CoreGraphics) 249 | let original = Image>(width: 640, height: 480, pixel: RGBA( 250 | red: .random(in: 0...255), 251 | green: .random(in: 0...255), 252 | blue: .random(in: 0...255), 253 | alpha: 255 254 | )) 255 | let nsImage = original.nsImage 256 | 257 | var restored: Image>! = nil 258 | measure { 259 | restored = Image>(nsImage: nsImage) 260 | } 261 | XCTAssertEqual(restored, original) 262 | #endif 263 | } 264 | 265 | func testNSImagePerformance() { 266 | #if canImport(AppKit) && canImport(CoreGraphics) 267 | let original = Image>(width: 640, height: 480, pixel: RGBA( 268 | red: .random(in: 0...255), 269 | green: .random(in: 0...255), 270 | blue: .random(in: 0...255), 271 | alpha: 255 272 | )) 273 | 274 | var nsImage: NSImage! 275 | measure { 276 | nsImage = original.nsImage 277 | } 278 | let restored = Image>(nsImage: nsImage) 279 | XCTAssertEqual(restored, original) 280 | #endif 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/AppUIKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class AppUIKitTests: XCTestCase { 5 | func testPNGData() { 6 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 7 | do { 8 | let image = Image>(width: 3, height: 2, pixels: [ 9 | RGBA(red: 0, green: 1, blue: 2), 10 | RGBA(red: 3, green: 4, blue: 5), 11 | RGBA(red: 6, green: 7, blue: 8), 12 | RGBA(red: 9, green: 10, blue: 11), 13 | RGBA(red: 12, green: 13, blue: 14), 14 | RGBA(red: 15, green: 16, blue: 17), 15 | ]) 16 | let data = image.pngData()! 17 | let restored = Image>(data: data) 18 | XCTAssertEqual(restored, image) 19 | } 20 | 21 | do { 22 | let image = Image(width: 3, height: 2, pixels: [ 23 | 0, 1, 2, 24 | 3, 4, 5, 25 | ]) 26 | let data = image.pngData()! 27 | let restored = Image(data: data) 28 | XCTAssertEqual(restored, image) 29 | } 30 | #endif 31 | } 32 | 33 | func testJPEGData() { 34 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 35 | do { 36 | let image = Image>(width: 3, height: 2, pixels: [ 37 | RGBA(red: 0.00, green: 0.01, blue: 0.02), 38 | RGBA(red: 0.03, green: 0.04, blue: 0.05), 39 | RGBA(red: 0.06, green: 0.07, blue: 0.08), 40 | RGBA(red: 0.09, green: 0.10, blue: 0.11), 41 | RGBA(red: 0.12, green: 0.13, blue: 0.14), 42 | RGBA(red: 0.15, green: 0.16, blue: 0.17), 43 | ]) 44 | let data = image.jpegData(compressionQuality: 1.0)! 45 | let restored = Image>(data: data)! 46 | XCTAssertEqual(restored, image, accuracy: 0.01) 47 | } 48 | 49 | do { 50 | let image = Image(width: 3, height: 2, pixels: [ 51 | 0.00, 0.01, 0.02, 52 | 0.03, 0.04, 0.05, 53 | ]) 54 | let data = image.jpegData(compressionQuality: 1.0)! 55 | let restored = Image(data: data)! 56 | XCTAssertEqual(restored, image, accuracy: 0.01) 57 | } 58 | #endif 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/AutoreleaseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class AutoreleaseTests: XCTestCase { 5 | func testPNGData() { 6 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 7 | let image = Image(width: 100, height: 100, pixel: 42) 8 | 9 | for _ in 0..<100 { 10 | let _ = image.data(using: .png) 11 | } 12 | 13 | XCTAssertTrue(true) // Break here and check if `CGImage` instances are released. 14 | #endif 15 | } 16 | 17 | func testJPEGData() { 18 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 19 | let image = Image(width: 100, height: 100, pixel: 42) 20 | 21 | for _ in 0..<100 { 22 | let _ = image.data(using: .jpeg(compressionQuality: 0.8)) 23 | } 24 | 25 | XCTAssertTrue(true) // Break here and check if `CGImage` instances are released. 26 | #endif 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ColorLiteralTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class ColorLiteralTests: XCTestCase { 5 | func testRGBInitWithColorLiteral() { 6 | do { // `UInt8` 7 | let red: RGB = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 8 | XCTAssertEqual(red, RGB(red: 255, green: 0, blue: 0)) 9 | 10 | let color: RGB = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 11 | XCTAssertEqual(color, RGB(red: 40, green: 81, blue: 122)) 12 | } 13 | 14 | do { // `UInt16` 15 | let red: RGB = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 16 | XCTAssertEqual(red, RGB(red: 65535, green: 0, blue: 0)) 17 | 18 | let color: RGB = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 19 | XCTAssertEqual(color, RGB(red: 10485, green: 20971, blue: 31456)) 20 | } 21 | 22 | do { // `Float` 23 | let red: RGB = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 24 | XCTAssertEqual(red, RGB(red: 1.0, green: 0.0, blue: 0.0)) 25 | 26 | let color: RGB = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 27 | XCTAssertEqual(color, RGB(red: 0.16, green: 0.32, blue: 0.48), accuracy: 1.0e-5) 28 | } 29 | 30 | do { // `Double` 31 | let red: RGB = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 32 | XCTAssertEqual(red, RGB(red: 1.0, green: 0.0, blue: 0.0)) 33 | 34 | let color: RGB = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 35 | XCTAssertEqual(color, RGB(red: 0.16, green: 0.32, blue: 0.48), accuracy: 1.0e-5) 36 | } 37 | } 38 | 39 | func testRGBAInitWithColorLiteral() { 40 | do { // `UInt8` 41 | let red: RGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 42 | XCTAssertEqual(red, RGBA(red: 255, green: 0, blue: 0, alpha: 255)) 43 | 44 | let color: RGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 45 | XCTAssertEqual(color, RGBA(red: 51, green: 102, blue: 153, alpha: 204)) 46 | } 47 | 48 | do { // `UInt16` 49 | let red: RGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 50 | XCTAssertEqual(red, RGBA(red: 65535, green: 0, blue: 0, alpha: 65535)) 51 | 52 | let color: RGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 53 | XCTAssertEqual(color, RGBA(red: 13107, green: 26214, blue: 39321, alpha: 52428)) 54 | } 55 | 56 | do { // `Float` 57 | let red: RGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 58 | XCTAssertEqual(red, RGBA(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)) 59 | 60 | let color: RGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 61 | XCTAssertEqual(color, RGBA(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8), accuracy: 1.0e-5) 62 | } 63 | 64 | do { // `Double` 65 | let red: RGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 66 | XCTAssertEqual(red, RGBA(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)) 67 | 68 | let color: RGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 69 | XCTAssertEqual(color, RGBA(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8), accuracy: 1.0e-5) 70 | } 71 | } 72 | 73 | func testPremultipliedRGBAInitWithColorLiteral() { 74 | do { // `UInt8` 75 | let red: PremultipliedRGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 76 | XCTAssertEqual(red, PremultipliedRGBA(red: 255, green: 0, blue: 0, alpha: 255)) 77 | 78 | let color: PremultipliedRGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 79 | XCTAssertEqual(color, PremultipliedRGBA(red: 40, green: 81, blue: 122, alpha: 204)) 80 | } 81 | 82 | do { // `UInt16` 83 | let red: PremultipliedRGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 84 | XCTAssertEqual(red, PremultipliedRGBA(red: 65535, green: 0, blue: 0, alpha: 65535)) 85 | 86 | let color: PremultipliedRGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 87 | XCTAssertEqual(color, PremultipliedRGBA(red: 10485, green: 20971, blue: 31456, alpha: 52428)) 88 | } 89 | 90 | do { // `Float` 91 | let red: PremultipliedRGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 92 | XCTAssertEqual(red, PremultipliedRGBA(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)) 93 | 94 | let color: PremultipliedRGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 95 | XCTAssertEqual(color, PremultipliedRGBA(red: 0.16, green: 0.32, blue: 0.48, alpha: 0.8), accuracy: 1.0e-5) 96 | } 97 | 98 | do { // `Double` 99 | let red: PremultipliedRGBA = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1) 100 | XCTAssertEqual(red, PremultipliedRGBA(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)) 101 | 102 | let color: PremultipliedRGBA = #colorLiteral(red: 0.2, green: 0.4, blue: 0.6, alpha: 0.8) 103 | XCTAssertEqual(color, PremultipliedRGBA(red: 0.16, green: 0.32, blue: 0.48, alpha: 0.8), accuracy: 1.0e-5) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ConvolutionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class ConvolutionTests: XCTestCase { 5 | func testConvoluted() { 6 | do { 7 | let image = Image(width: 2, height: 2, pixels: [ 8 | 1, 2, 9 | 3, 4, 10 | ]) 11 | 12 | let kernel = Image(width: 5, height: 5, pixels: [ 13 | 2, 1, 1, 1, 2, 14 | 1, 1, 1, 1, 1, 15 | 1, 1, 1, 1, 1, 16 | 1, 1, 1, 1, 1, 17 | 2, 1, 1, 1, 2, 18 | ]) 19 | 20 | let convoluted = image.convoluted(with: kernel) 21 | 22 | XCTAssertEqual(convoluted[0, 0], 65) 23 | XCTAssertEqual(convoluted[1, 0], 70) 24 | XCTAssertEqual(convoluted[0, 1], 75) 25 | XCTAssertEqual(convoluted[1, 1], 80) 26 | } 27 | 28 | do { 29 | let image = Image(width: 2, height: 2, pixels: [ 30 | 1, 2, 31 | 3, 4, 32 | ]) 33 | 34 | let kernel = Image(width: 5, height: 5, pixels: [ 35 | 1, 1, 1, 1, 1, 36 | 1, 2, 3, 4, 1, 37 | 1, 5, 6, 7, 1, 38 | 1, 8, 9, 10, 1, 39 | 1, 1, 1, 1, 1, 40 | ]) 41 | 42 | let convoluted = image.convoluted(with: kernel, extrapolatedBy: .constant(0)) 43 | 44 | XCTAssertEqual(convoluted[0, 0], 87) 45 | XCTAssertEqual(convoluted[1, 0], 77) 46 | XCTAssertEqual(convoluted[0, 1], 57) 47 | XCTAssertEqual(convoluted[1, 1], 47) 48 | } 49 | 50 | do { 51 | let image = Image(width: 2, height: 2, pixels: [ 52 | 1, 2, 53 | 3, 4, 54 | ]) 55 | 56 | let kernel = Image(width: 5, height: 5, pixels: [ 57 | 2, 1, 1, 1, 2, 58 | 1, 1, 1, 1, 1, 59 | 1, 1, 1, 1, 1, 60 | 1, 1, 1, 1, 1, 61 | 2, 1, 1, 1, 2, 62 | ]) 63 | 64 | let convoluted = image.convoluted(with: kernel, extrapolatedBy: .edge) 65 | 66 | XCTAssertEqual(convoluted[0, 0], 65) 67 | XCTAssertEqual(convoluted[1, 0], 70) 68 | XCTAssertEqual(convoluted[0, 1], 75) 69 | XCTAssertEqual(convoluted[1, 1], 80) 70 | } 71 | 72 | do { 73 | let image = Image(width: 2, height: 2, pixels: [ 74 | 1, 2, 75 | 3, 4, 76 | ]) 77 | 78 | let kernel = Image(width: 5, height: 5, pixels: [ 79 | 2, 1, 1, 1, 2, 80 | 1, 1, 1, 1, 1, 81 | 1, 1, 1, 1, 1, 82 | 1, 1, 1, 1, 1, 83 | 2, 1, 1, 1, 2, 84 | ]) 85 | 86 | let convoluted = image.convoluted(with: kernel, extrapolatedBy: .repeat) 87 | 88 | XCTAssertEqual(convoluted[0, 0], 59) 89 | XCTAssertEqual(convoluted[1, 0], 68) 90 | XCTAssertEqual(convoluted[0, 1], 77) 91 | XCTAssertEqual(convoluted[1, 1], 86) 92 | } 93 | 94 | do { 95 | let image = Image(width: 2, height: 2, pixels: [1, 2, 3, 4]) 96 | 97 | let kernel = Image(width: 5, height: 5, pixels: [ 98 | 1, 1, 1, 1, 1, 99 | 1, 1, 1, 1, 1, 100 | 1, 1, 1, 1, 1, 101 | 1, 1, 1, 1, 1, 102 | 1, 1, 1, 1, 1, 103 | ]) 104 | 105 | let convoluted = image.convoluted(with: kernel, extrapolatedBy: .reflection) 106 | 107 | XCTAssertEqual(convoluted[0, 0], 70) 108 | XCTAssertEqual(convoluted[1, 0], 65) 109 | XCTAssertEqual(convoluted[0, 1], 60) 110 | XCTAssertEqual(convoluted[1, 1], 55) 111 | } 112 | 113 | do { // `kernel` of `ImageSlice` 114 | let image = Image(width: 3, height: 2, pixels: [ 115 | 1, 2, 3, 116 | 4, 5, 6, 117 | ]) 118 | let kernel0 = Image(width: 5, height: 5, pixels: [ 119 | 99, 99, 99, 99, 99, 120 | 99, 1, 2, 3, 99, 121 | 99, 4, 5, 6, 99, 122 | 99, 7, 8, 9, 99, 123 | 99, 99, 99, 99, 99, 124 | ]) 125 | let kernel: ImageSlice = kernel0[1...3, 1...3] 126 | XCTAssertEqual(image.convoluted(with: kernel, extrapolatedBy: .constant(0)), Image(width: 3, height: 2, pixels: [ 127 | 94, 154, 106, 128 | 58, 91, 58, 129 | ])) 130 | } 131 | 132 | do { // `convoluted` for `ImageSlice` 133 | let image = Image(width: 4, height: 4, pixels: [ 134 | 99, 99, 99, 99, 135 | 99, 1, 2, 99, 136 | 99, 3, 4, 99, 137 | 99, 99, 99, 99, 138 | ]) 139 | let slice: ImageSlice = image[1...2, 1...2] 140 | let kernel = Image(width: 5, height: 5, pixels: [ 141 | 2, 1, 1, 1, 2, 142 | 1, 1, 1, 1, 1, 143 | 1, 1, 1, 1, 1, 144 | 1, 1, 1, 1, 1, 145 | 2, 1, 1, 1, 2, 146 | ]) 147 | XCTAssertEqual(slice.convoluted(with: kernel), Image(width: 2, height: 2, pixels: [ 148 | 65, 70, 149 | 75, 80, 150 | ])) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/HigherOrderFunctionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class HigherOrderFunctionsTests : XCTestCase { 5 | func testMap() { 6 | do { 7 | let image = Image(width: 3, height: 2, pixels: [ 8 | 1, 2, 3, 9 | 4, 5, 6, 10 | ]) 11 | // Assigning the value to `result` to check if its type can be inferred though `Sequence` also has `map` 12 | let result = image.map { $0 + 1 } 13 | XCTAssertEqual(result, Image(width: 3, height: 2, pixels: [ 14 | 2, 3, 4, 15 | 5, 6, 7, 16 | ])) 17 | } 18 | 19 | do { // rethrows 20 | let image = Image(width: 3, height: 2, pixels: [ 21 | 1, 2, 3, 22 | 4, 5, 6, 23 | ]) 24 | do { 25 | // Assigning the value to `result` to check if its type can be inferred though `Sequence` also has `map` 26 | let result = try image.map { pixel -> UInt8 in 27 | guard pixel < 99 else { throw GeneralError() } 28 | return pixel + 1 29 | } 30 | XCTAssertEqual(result, Image(width: 3, height: 2, pixels: [ 31 | 2, 3, 4, 32 | 5, 6, 7, 33 | ])) 34 | } catch _ { 35 | XCTFail() 36 | } 37 | } 38 | 39 | do { // ImageSlice 40 | let image = Image(width: 5, height: 4, pixels: [ 41 | 0, 0, 0, 0, 0, 42 | 0, 1, 2, 3, 0, 43 | 0, 4, 5, 6, 0, 44 | 0, 0, 0, 0, 0, 45 | ]) 46 | let slice: ImageSlice = image[1...3, 1...2] 47 | // Assigning the value to `result` to check if its type can be inferred though `Sequence` also has `map` 48 | let result = slice.map { $0 + 1 } 49 | XCTAssertEqual(result, Image(width: 3, height: 2, pixels: [ 50 | 2, 3, 4, 51 | 5, 6, 7, 52 | ])) 53 | } 54 | 55 | do { // Sequence 56 | let image = Image(width: 3, height: 2, pixels: [ 57 | 1, 2, 3, 58 | 4, 5, 6, 59 | ]) 60 | let result: [UInt8] = image.map { $0 + 1 } 61 | XCTAssertEqual(result, [2, 3, 4, 5, 6, 7]) 62 | } 63 | } 64 | 65 | func testUpdate() { 66 | do { 67 | var image = Image(width: 3, height: 2, pixels: [ 68 | 1, 2, 3, 69 | 4, 5, 6, 70 | ]) 71 | image._update { $0 *= 2 } 72 | XCTAssertEqual(image, Image(width: 3, height: 2, pixels: [ 73 | 2, 4, 6, 74 | 8, 10, 12, 75 | ])) 76 | } 77 | 78 | do { 79 | let image = Image(width: 5, height: 4, pixels: [ 80 | 0, 0, 0, 0, 0, 81 | 0, 1, 2, 3, 0, 82 | 0, 4, 5, 6, 0, 83 | 0, 0, 0, 0, 0, 84 | ]) 85 | var slice: ImageSlice = image[1...3, 1...2] 86 | slice._update { $0 *= 2 } 87 | XCTAssertEqual(slice, ImageSlice(width: 3, height: 2, pixels: [ 88 | 2, 4, 6, 89 | 8, 10, 12, 90 | ])) 91 | } 92 | 93 | do { // Shared `Image` instances 94 | var image1 = Image(width: 3, height: 2, pixels: [ 95 | 1, 2, 3, 96 | 4, 5, 6, 97 | ]) 98 | let image2 = image1 99 | image1._update { $0 *= 2 } 100 | XCTAssertEqual(image1, Image(width: 3, height: 2, pixels: [ 101 | 2, 4, 6, 102 | 8, 10, 12, 103 | ])) 104 | XCTAssertEqual(image2, Image(width: 3, height: 2, pixels: [ 105 | 1, 2, 3, 106 | 4, 5, 6, 107 | ])) 108 | } 109 | 110 | do { // Shared reference type instances 111 | class Foo { 112 | private let deinitBody: () -> Void 113 | init(deinitBody: @escaping () -> Void) { 114 | self.deinitBody = deinitBody 115 | } 116 | deinit { 117 | deinitBody() 118 | } 119 | } 120 | 121 | var flags = 0b0 122 | var image = Image(width: 1, height: 1, pixels: [ 123 | Foo { 124 | flags |= 0b1 125 | } 126 | ]) 127 | XCTAssertEqual(flags, 0b0) 128 | image._update { 129 | $0 = Foo { 130 | flags |= 0b10 131 | } 132 | } 133 | XCTAssertEqual(flags, 0b1) 134 | image._update { 135 | $0 = Foo { 136 | flags |= 0b100 137 | } 138 | } 139 | XCTAssertEqual(flags, 0b11) 140 | image[0, 0] = Foo { 141 | flags |= 0b1000 142 | } 143 | XCTAssertEqual(flags, 0b111) 144 | 145 | XCTAssertEqual(image.count, 1) // to prevent `image` being released 146 | } 147 | } 148 | 149 | func testMapPerformance() { 150 | let image = Image(width: 1024, height: 1024, pixels: (1...(1024 * 1024)).map { _ in Int.random(in: .min ... (.max - 1)) }) 151 | var results: [Image] = [] 152 | measure { 153 | results.append(image.map { $0 + 1 }) 154 | } 155 | let result = results.randomElement()! 156 | XCTAssertEqual(result.max()!, image.max()! + 1) 157 | } 158 | 159 | func testUpdatePerformance() { 160 | var image = Image(width: 1024, height: 1024, pixels: 1...(1024 * 1024)) 161 | measure { 162 | image._update { $0 += 1 } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ImageOperatorsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class ImageOperatorsTests: XCTestCase { 5 | func testAdd() { 6 | do { 7 | let a = Image>(width: 2, height: 1, pixels: [ 8 | RGBA(red: 1, green: 2, blue: -3, alpha: -4), 9 | RGBA(red: -1, green: -2, blue: 3, alpha: 4), 10 | ]) 11 | let b = Image>(width: 2, height: 1, pixels: [ 12 | RGBA(red: 5, green: -6, blue: 7, alpha: -8), 13 | RGBA(red: -5, green: 6, blue: -7, alpha: 8), 14 | ]) 15 | XCTAssertEqual(a + b, Image>(width: 2, height: 1, pixels: [ 16 | RGBA(red: 6, green: -4, blue: 4, alpha: -12), 17 | RGBA(red: -6, green: 4, blue: -4, alpha: 12), 18 | ])) 19 | } 20 | 21 | do { 22 | let a = Image(width: 3, height: 2, pixels: [ 23 | 1, 2, 3, 24 | -4, -5, -6, 25 | ]) 26 | let b = Image(width: 3, height: 2, pixels: [ 27 | 7, -8, 9, 28 | -10, 11, -12, 29 | ]) 30 | XCTAssertEqual(a + b, Image(width: 3, height: 2, pixels: [ 31 | 8, -6, 12, 32 | -14, 6, -18 33 | ])) 34 | } 35 | 36 | do { 37 | let image = Image(width: 5, height: 4, pixels: [ 38 | 99, 99, 99, 99, 99, 39 | 99, 1, 2, 3, 99, 40 | 99, -4, -5, -6, 99, 41 | 99, 99, 99, 99, 99, 42 | ]) 43 | let a: ImageSlice = image[1...3, 1...2] 44 | let b = Image(width: 3, height: 2, pixels: [ 45 | 7, -8, 9, 46 | -10, 11, -12, 47 | ]) 48 | XCTAssertEqual(a + b, Image(width: 3, height: 2, pixels: [ 49 | 8, -6, 12, 50 | -14, 6, -18 51 | ])) 52 | XCTAssertEqual(b + a, Image(width: 3, height: 2, pixels: [ 53 | 8, -6, 12, 54 | -14, 6, -18 55 | ])) 56 | XCTAssertEqual(a + a, Image(width: 3, height: 2, pixels: [ 57 | 2, 4, 6, 58 | -8, -10, -12, 59 | ])) 60 | } 61 | } 62 | 63 | func testAnd() { 64 | do { 65 | let a = Image(width: 2, height: 2, pixels: [ 66 | true, true, 67 | false, false, 68 | ]) 69 | let b = Image(width: 2, height: 2, pixels: [ 70 | true, false, 71 | true, false, 72 | ]) 73 | XCTAssertEqual(a && b, Image(width: 2, height: 2, pixels: [ 74 | true, false, 75 | false, false, 76 | ])) 77 | } 78 | 79 | do { 80 | let image = Image(width: 4, height: 4, pixels: [ 81 | false, false, false, false, 82 | false, true, true, false, 83 | false, false, false, false, 84 | false, false, false, false, 85 | ]) 86 | let a: ImageSlice = image[1...2, 1...2] 87 | let b = Image(width: 2, height: 2, pixels: [ 88 | true, false, 89 | true, false, 90 | ]) 91 | XCTAssertEqual(a && b, Image(width: 2, height: 2, pixels: [ 92 | true, false, 93 | false, false, 94 | ])) 95 | XCTAssertEqual(b && a, Image(width: 2, height: 2, pixels: [ 96 | true, false, 97 | false, false, 98 | ])) 99 | XCTAssertEqual(a && a, Image(width: 2, height: 2, pixels: [ 100 | true, true, 101 | false, false, 102 | ])) 103 | } 104 | } 105 | 106 | func testAddAsign() { 107 | do { 108 | var a = Image(width: 3, height: 2, pixels: [ 109 | 1, 2, 3, 110 | -4, -5, -6, 111 | ]) 112 | let b = Image(width: 3, height: 2, pixels: [ 113 | 7, -8, 9, 114 | -10, 11, -12, 115 | ]) 116 | a += b 117 | XCTAssertEqual(a, Image(width: 3, height: 2, pixels: [ 118 | 8, -6, 12, 119 | -14, 6, -18 120 | ])) 121 | } 122 | 123 | do { 124 | let image = Image(width: 5, height: 4, pixels: [ 125 | 99, 99, 99, 99, 99, 126 | 99, 1, 2, 3, 99, 127 | 99, -4, -5, -6, 99, 128 | 99, 99, 99, 99, 99, 129 | ]) 130 | do { 131 | var a: ImageSlice = image[1...3, 1...2] 132 | let b = Image(width: 3, height: 2, pixels: [ 133 | 7, -8, 9, 134 | -10, 11, -12, 135 | ]) 136 | a += b 137 | XCTAssertEqual(a, ImageSlice(width: 3, height: 2, pixels: [ 138 | 8, -6, 12, 139 | -14, 6, -18 140 | ])) 141 | } 142 | do { 143 | let a: ImageSlice = image[1...3, 1...2] 144 | var b = Image(width: 3, height: 2, pixels: [ 145 | 7, -8, 9, 146 | -10, 11, -12, 147 | ]) 148 | b += a 149 | XCTAssertEqual(b, Image(width: 3, height: 2, pixels: [ 150 | 8, -6, 12, 151 | -14, 6, -18 152 | ])) 153 | } 154 | do { 155 | var a: ImageSlice = image[1...3, 1...2] 156 | a += a 157 | XCTAssertEqual(a, ImageSlice(width: 3, height: 2, pixels: [ 158 | 2, 4, 6, 159 | -8, -10, -12, 160 | ])) 161 | } 162 | } 163 | } 164 | 165 | func testSubtractAsign() { 166 | var a = Image(width: 3, height: 2, pixels: [ 167 | 1, 2, 3, 168 | -4, -5, -6, 169 | ]) 170 | let b = Image(width: 3, height: 2, pixels: [ 171 | 7, -8, 9, 172 | -10, 11, -12, 173 | ]) 174 | a -= b 175 | XCTAssertEqual(a, Image(width: 3, height: 2, pixels: [ 176 | -6, 10, -6, 177 | 6, -16, 6, 178 | ])) 179 | } 180 | 181 | func testIsEqual() { 182 | do { 183 | let a = Image(width: 1, height: 2, pixels: [3, 4]) 184 | let b = Image(width: 1, height: 2, pixels: [3, 4]) 185 | XCTAssertEqual(a == b, true) 186 | } 187 | 188 | do { 189 | let a = Image(width: 1, height: 2, pixels: [3, 4]) 190 | let b = Image(width: 1, height: 2, pixels: [3, 9]) 191 | XCTAssertEqual(a == b, false) 192 | } 193 | 194 | do { 195 | let a = Image(width: 1, height: 2, pixels: [3, 4]) 196 | let b = Image(width: 2, height: 1, pixels: [3, 4]) 197 | XCTAssertEqual(a == b, false) 198 | } 199 | 200 | do { 201 | let image = Image(width: 3, height: 4, pixels: [ 202 | 0, 0, 0, 203 | 0, 3, 0, 204 | 0, 4, 0, 205 | 0, 0, 0, 206 | ]) 207 | let a: ImageSlice = image[1...1, 1...2] 208 | do { 209 | let b = Image(width: 1, height: 2, pixels: [3, 4]) 210 | XCTAssertEqual(a == b, true) 211 | XCTAssertEqual(b == a, true) 212 | XCTAssertEqual(a == a, true) 213 | } 214 | do { 215 | let b = Image(width: 1, height: 2, pixels: [3, 9]) 216 | XCTAssertEqual(a == b, false) 217 | XCTAssertEqual(b == a, false) 218 | } 219 | do { 220 | let b = Image(width: 2, height: 1, pixels: [3, 4]) 221 | XCTAssertEqual(a == b, false) 222 | XCTAssertEqual(b == a, false) 223 | } 224 | } 225 | } 226 | 227 | func testIsNotEqual() { 228 | do { 229 | let a = Image(width: 1, height: 2, pixels: [3, 4]) 230 | let b = Image(width: 1, height: 2, pixels: [3, 4]) 231 | XCTAssertEqual(a != b, false) 232 | } 233 | 234 | do { 235 | let a = Image(width: 1, height: 2, pixels: [3, 4]) 236 | let b = Image(width: 1, height: 2, pixels: [3, 9]) 237 | XCTAssertEqual(a != b, true) 238 | } 239 | 240 | do { 241 | let a = Image(width: 2, height: 1, pixels: [3, 4]) 242 | let b = Image(width: 1, height: 2, pixels: [3, 4]) 243 | XCTAssertEqual(a != b, true) 244 | } 245 | } 246 | 247 | func testNegate() { 248 | do { 249 | let a = Image(width: 3, height: 2, pixels: [ 250 | 1, 2, 3, 251 | -4, -5, -6, 252 | ]) 253 | XCTAssertEqual(-a, Image(width: 3, height: 2, pixels: [ 254 | -1, -2, -3, 255 | 4, 5, 6, 256 | ])) 257 | } 258 | 259 | do { 260 | let image = Image(width: 5, height: 4, pixels: [ 261 | 99, 99, 99, 99, 99, 262 | 99, 1, 2, 3, 99, 263 | 99, -4, -5, -6, 99, 264 | 99, 99, 99, 99, 99, 265 | ]) 266 | let a: ImageSlice = image[1...3, 1...2] 267 | XCTAssertEqual(-a, Image(width: 3, height: 2, pixels: [ 268 | -1, -2, -3, 269 | 4, 5, 6, 270 | ])) 271 | } 272 | } 273 | 274 | func testNot() { 275 | let a = Image(width: 2, height: 2, pixels: [ 276 | true, true, 277 | false, false, 278 | ]) 279 | XCTAssertEqual(!a, Image(width: 2, height: 2, pixels: [ 280 | false, false, 281 | true, true, 282 | ])) 283 | } 284 | } 285 | 286 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ImageProtocolTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | private let image = Image(width: 3, height: 2, pixels: [ 5 | 1, 2, 3, 6 | 4, 5, 6, 7 | ]) 8 | private let slice = Image(width: 5, height: 4, pixels: [ 9 | 0, 0, 0, 0, 0, 10 | 1, 2, 3, 4, 5, 11 | 6, 7, 8, 9, 0, 12 | 0, 0, 0, 0, 0, 13 | ])[1...3, 1...2] 14 | 15 | class ImageProtocolTests : XCTestCase { 16 | func testSlice() { 17 | XCTAssertEqual(slice, ImageSlice(width: 3, height: 2, pixels: [ 18 | 2, 3, 4, 19 | 7, 8, 9, 20 | ])) 21 | } 22 | 23 | func testInitWithPixelAt() { 24 | do { 25 | let image = Image(width: 3, height: 2) { (x: Int, y: Int) -> Int in 26 | (y + 1) * 10 + (x + 1) 27 | } 28 | XCTAssertEqual(image, Image(width: 3, height: 2, pixels: [ 29 | 11, 12, 13, 30 | 21, 22, 23, 31 | ])) 32 | } 33 | } 34 | 35 | func testTransposed() { 36 | do { 37 | XCTAssertEqual(image.transposed(), Image(width: 2, height: 3, pixels: [ 38 | 1, 4, 39 | 2, 5, 40 | 3, 6, 41 | ])) 42 | } 43 | 44 | do { 45 | XCTAssertEqual(slice.transposed(), Image(width: 2, height: 3, pixels: [ 46 | 2, 7, 47 | 3, 8, 48 | 4, 9, 49 | ])) 50 | } 51 | } 52 | 53 | func testXReversed() { 54 | do { 55 | XCTAssertEqual(image.xReversed(), Image(width: 3, height: 2, pixels: [ 56 | 3, 2, 1, 57 | 6, 5, 4, 58 | ])) 59 | } 60 | 61 | do { 62 | XCTAssertEqual(slice.xReversed(), Image(width: 3, height: 2, pixels: [ 63 | 4, 3, 2, 64 | 9, 8, 7, 65 | ])) 66 | } 67 | } 68 | 69 | func testYReversed() { 70 | do { 71 | XCTAssertEqual(image.yReversed(), Image(width: 3, height: 2, pixels: [ 72 | 4, 5, 6, 73 | 1, 2, 3, 74 | ])) 75 | } 76 | 77 | do { 78 | XCTAssertEqual(slice.yReversed(), Image(width: 3, height: 2, pixels: [ 79 | 7, 8, 9, 80 | 2, 3, 4, 81 | ])) 82 | } 83 | } 84 | 85 | func testRotated() { 86 | do { 87 | XCTAssertEqual(image.rotated(byDegrees: 90), Image(width: 2, height: 3, pixels: [ 88 | 4, 1, 89 | 5, 2, 90 | 6, 3, 91 | ])) 92 | 93 | for times in [-8, -4, 0, 4, 8] { 94 | XCTAssertEqual(image.rotated(byDegrees: 90 * times), Image(width: 3, height: 2, pixels: [ 95 | 1, 2, 3, 96 | 4, 5, 6, 97 | ])) 98 | } 99 | 100 | for times in [-7, -3, 1, 5] { 101 | XCTAssertEqual(image.rotated(byDegrees: 90 * times), Image(width: 2, height: 3, pixels: [ 102 | 4, 1, 103 | 5, 2, 104 | 6, 3, 105 | ])) 106 | } 107 | 108 | for times in [-6, -2, 2, 6] { 109 | XCTAssertEqual(image.rotated(byDegrees: 90 * times), Image(width: 3, height: 2, pixels: [ 110 | 6, 5, 4, 111 | 3, 2, 1, 112 | ])) 113 | } 114 | 115 | for times in [-5, -1, 3, 7] { 116 | XCTAssertEqual(image.rotated(byDegrees: 90 * times), Image(width: 2, height: 3, pixels: [ 117 | 3, 6, 118 | 2, 5, 119 | 1, 4, 120 | ])) 121 | } 122 | } 123 | 124 | do { 125 | XCTAssertEqual(slice.rotated(byDegrees: 90), Image(width: 2, height: 3, pixels: [ 126 | 7, 2, 127 | 8, 3, 128 | 9, 4, 129 | ])) 130 | 131 | for times in [-8, -4, 0, 4, 8] { 132 | XCTAssertEqual(slice.rotated(byDegrees: 90 * times), Image(width: 3, height: 2, pixels: [ 133 | 2, 3, 4, 134 | 7, 8, 9, 135 | ])) 136 | } 137 | 138 | for times in [-7, -3, 1, 5] { 139 | XCTAssertEqual(slice.rotated(byDegrees: 90 * times), Image(width: 2, height: 3, pixels: [ 140 | 7, 2, 141 | 8, 3, 142 | 9, 4, 143 | ])) 144 | } 145 | 146 | for times in [-6, -2, 2, 6] { 147 | XCTAssertEqual(slice.rotated(byDegrees: 90 * times), Image(width: 3, height: 2, pixels: [ 148 | 9, 8, 7, 149 | 4, 3, 2, 150 | ])) 151 | } 152 | 153 | for times in [-5, -1, 3, 7] { 154 | XCTAssertEqual(slice.rotated(byDegrees: 90 * times), Image(width: 2, height: 3, pixels: [ 155 | 4, 9, 156 | 3, 8, 157 | 2, 7, 158 | ])) 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ImageSliceTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class ImageSliceTests: XCTestCase { 5 | func testInit() { 6 | do { 7 | let slice = ImageSlice(width: 3, height: 2, pixels: [ 8 | 1, 2, 3, 9 | 4, 5, 6, 10 | ]) 11 | XCTAssertEqual(slice.width, 3) 12 | XCTAssertEqual(slice.height, 2) 13 | XCTAssertEqual(slice.xRange, 0..<3) 14 | XCTAssertEqual(slice.yRange, 0..<2) 15 | XCTAssertEqual(slice[0, 0], 1) 16 | XCTAssertEqual(slice[1, 0], 2) 17 | XCTAssertEqual(slice[2, 0], 3) 18 | XCTAssertEqual(slice[0, 1], 4) 19 | XCTAssertEqual(slice[1, 1], 5) 20 | XCTAssertEqual(slice[2, 1], 6) 21 | } 22 | 23 | do { 24 | let image = Image(width: 3, height: 2, pixels: [ 25 | 1, 2, 3, 26 | 4, 5, 6, 27 | ]) 28 | let slice = ImageSlice(image) 29 | XCTAssertEqual(slice.width, 3) 30 | XCTAssertEqual(slice.height, 2) 31 | XCTAssertEqual(slice.xRange, 0..<3) 32 | XCTAssertEqual(slice.yRange, 0..<2) 33 | XCTAssertEqual(slice[0, 0], 1) 34 | XCTAssertEqual(slice[1, 0], 2) 35 | XCTAssertEqual(slice[2, 0], 3) 36 | XCTAssertEqual(slice[0, 1], 4) 37 | XCTAssertEqual(slice[1, 1], 5) 38 | XCTAssertEqual(slice[2, 1], 6) 39 | } 40 | } 41 | 42 | func testSequence() { 43 | do { 44 | let original = Image(width: 5, height: 4, pixels: [ 45 | 0, 0, 0, 0, 0, 46 | 0, 1, 2, 3, 0, 47 | 0, 4, 5, 6, 0, 48 | 0, 0, 0, 0, 0, 49 | ]) 50 | let slice: ImageSlice = original[1...3, 1...2] 51 | 52 | var iterator = slice.makeIterator() 53 | 54 | XCTAssertEqual(iterator.next(), 1) 55 | XCTAssertEqual(iterator.next(), 2) 56 | XCTAssertEqual(iterator.next(), 3) 57 | XCTAssertEqual(iterator.next(), 4) 58 | XCTAssertEqual(iterator.next(), 5) 59 | XCTAssertEqual(iterator.next(), 6) 60 | XCTAssertNil(iterator.next()) 61 | } 62 | } 63 | 64 | func testSubscriptRange() { 65 | do { 66 | let original = Image(width: 5, height: 4, pixels: [ 67 | 0, 0, 0, 0, 0, 68 | 0, 1, 2, 3, 0, 69 | 0, 4, 5, 6, 0, 70 | 0, 0, 0, 0, 0, 71 | ]) 72 | let slice: ImageSlice = original[1...3, 1...2] 73 | 74 | do { 75 | let slice2: ImageSlice = slice[1...1, 1...2] 76 | 77 | XCTAssertEqual(slice2.width, 1) 78 | XCTAssertEqual(slice2.height, 2) 79 | 80 | XCTAssertEqual(slice[1, 1], 1) 81 | XCTAssertEqual(slice[1, 2], 4) 82 | } 83 | 84 | do { 85 | let slice2: ImageSlice = slice[2...3, 2...2] 86 | 87 | XCTAssertEqual(slice2.width, 2) 88 | XCTAssertEqual(slice2.height, 1) 89 | 90 | XCTAssertEqual(slice[2, 2], 5) 91 | XCTAssertEqual(slice[3, 2], 6) 92 | } 93 | } 94 | 95 | do { 96 | let original = Image(width: 4, height: 5, pixels: [ 97 | 0, 0, 0, 0, 98 | 0, 1, 2, 0, 99 | 0, 3, 4, 0, 100 | 0, 5, 6, 0, 101 | 0, 0, 0, 0, 102 | ]) 103 | let slice: ImageSlice = original[1...2, 1...3] 104 | 105 | do { 106 | let slice2: ImageSlice = slice[1...2, 1...1] 107 | 108 | XCTAssertEqual(slice2.width, 2) 109 | XCTAssertEqual(slice2.height, 1) 110 | 111 | XCTAssertEqual(slice[1, 1], 1) 112 | XCTAssertEqual(slice[2, 1], 2) 113 | } 114 | 115 | do { 116 | let slice2: ImageSlice = slice[2...2, 2...3] 117 | 118 | XCTAssertEqual(slice2.width, 1) 119 | XCTAssertEqual(slice2.height, 2) 120 | 121 | XCTAssertEqual(slice[2, 2], 4) 122 | XCTAssertEqual(slice[2, 3], 6) 123 | } 124 | } 125 | } 126 | 127 | func testPixel() { 128 | let image = Image(width: 4, height: 3, pixels: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])[1...2, 1...1] 129 | 130 | XCTAssertNil(image.pixelAt(x: 0, y: 0)) 131 | XCTAssertNil(image.pixelAt(x: 1, y: 0)) 132 | XCTAssertNil(image.pixelAt(x: 2, y: 0)) 133 | XCTAssertNil(image.pixelAt(x: 3, y: 0)) 134 | XCTAssertNil(image.pixelAt(x: 0, y: 1)) 135 | XCTAssertEqual(5, image.pixelAt(x: 1, y: 1)!) 136 | XCTAssertEqual(6, image.pixelAt(x: 2, y: 1)!) 137 | XCTAssertNil(image.pixelAt(x: 3, y: 1)) 138 | XCTAssertNil(image.pixelAt(x: 0, y: 2)) 139 | XCTAssertNil(image.pixelAt(x: 1, y: 2)) 140 | XCTAssertNil(image.pixelAt(x: 2, y: 2)) 141 | XCTAssertNil(image.pixelAt(x: 3, y: 2)) 142 | 143 | XCTAssertNil(image.pixelAt(x: -1, y: 0)) 144 | XCTAssertNil(image.pixelAt(x: 4, y: 0)) 145 | XCTAssertNil(image.pixelAt(x: 0, y: -1)) 146 | XCTAssertNil(image.pixelAt(x: 0, y: 3)) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ImageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | #if canImport(UIKit) 4 | import UIKit 5 | #endif 6 | #if canImport(AppKit) 7 | import AppKit 8 | #endif 9 | 10 | class ImageTests: XCTestCase { 11 | func testInitNamed() { 12 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 13 | do { 14 | let image = Image>(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")))! 15 | 16 | XCTAssertEqual(255, image[0, 0].red) 17 | XCTAssertEqual( 0, image[0, 0].green) 18 | XCTAssertEqual( 0, image[0, 0].blue) 19 | XCTAssertEqual( 64, image[0, 0].alpha) 20 | 21 | XCTAssertEqual( 0, image[1, 0].red) 22 | XCTAssertEqual(255, image[1, 0].green) 23 | XCTAssertEqual( 0, image[1, 0].blue) 24 | XCTAssertEqual(127, image[1, 0].alpha) 25 | 26 | XCTAssertEqual( 0, image[0, 1].red) 27 | XCTAssertEqual( 0, image[0, 1].green) 28 | XCTAssertEqual(255, image[0, 1].blue) 29 | XCTAssertEqual(191, image[0, 1].alpha) 30 | 31 | XCTAssertEqual(255, image[1, 1].red) 32 | XCTAssertEqual(255, image[1, 1].green) 33 | XCTAssertEqual( 0, image[1, 1].blue) 34 | XCTAssertEqual(255, image[1, 1].alpha) 35 | } 36 | #endif 37 | } 38 | 39 | func testInitWithImageSlice() { 40 | let original = Image(width: 5, height: 4, pixels: [ 41 | 0, 0, 0, 0, 0, 42 | 0, 1, 2, 3, 0, 43 | 0, 4, 5, 6, 0, 44 | 0, 0, 0, 0, 0, 45 | ]) 46 | let slice: ImageSlice = original[1...3, 1...2] 47 | let image = Image(slice) 48 | XCTAssertEqual(image, Image(width: 3, height: 2, pixels: [ 49 | 1, 2, 3, 50 | 4, 5, 6, 51 | ])) 52 | } 53 | 54 | func testSequence() { 55 | let image = Image(width: 3, height: 2, pixels: [ 56 | 1, 2, 3, 57 | 4, 5, 6, 58 | ]) 59 | 60 | var iterator = image.makeIterator() 61 | 62 | XCTAssertEqual(iterator.next(), 1) 63 | XCTAssertEqual(iterator.next(), 2) 64 | XCTAssertEqual(iterator.next(), 3) 65 | XCTAssertEqual(iterator.next(), 4) 66 | XCTAssertEqual(iterator.next(), 5) 67 | XCTAssertEqual(iterator.next(), 6) 68 | XCTAssertNil(iterator.next()) 69 | } 70 | 71 | func testSubscriptGet() { 72 | let image = Image(width: 3, height: 2, pixels: [ 73 | 1, 2, 3, 74 | 4, 5, 6, 75 | ]) 76 | 77 | XCTAssertEqual(image[0, 0], 1) 78 | XCTAssertEqual(image[1, 0], 2) 79 | XCTAssertEqual(image[2, 0], 3) 80 | 81 | XCTAssertEqual(image[0, 1], 4) 82 | XCTAssertEqual(image[1, 1], 5) 83 | XCTAssertEqual(image[2, 1], 6) 84 | } 85 | 86 | func testSubscriptSet() { 87 | var image = Image(width: 3, height: 2, pixels: [ 88 | 1, 2, 3, 89 | 4, 5, 6, 90 | ]) 91 | let original = image 92 | 93 | image[0, 0] = 11 94 | image[1, 0] = 12 95 | image[2, 0] = 13 96 | image[0, 1] = 14 97 | image[1, 1] = 15 98 | image[2, 1] = 16 99 | 100 | XCTAssertEqual(image[0, 0], 11) 101 | XCTAssertEqual(image[1, 0], 12) 102 | XCTAssertEqual(image[2, 0], 13) 103 | 104 | XCTAssertEqual(image[0, 1], 14) 105 | XCTAssertEqual(image[1, 1], 15) 106 | XCTAssertEqual(image[2, 1], 16) 107 | 108 | XCTAssertEqual(original[0, 0], 1) 109 | XCTAssertEqual(original[1, 0], 2) 110 | XCTAssertEqual(original[2, 0], 3) 111 | 112 | XCTAssertEqual(original[0, 1], 4) 113 | XCTAssertEqual(original[1, 1], 5) 114 | XCTAssertEqual(original[2, 1], 6) 115 | } 116 | 117 | func testSubscriptRange() { 118 | let image = Image(width: 5, height: 4, pixels: [ 119 | 0, 0, 0, 0, 0, 120 | 0, 1, 2, 3, 0, 121 | 0, 4, 5, 6, 0, 122 | 0, 0, 0, 0, 0, 123 | ]) 124 | let slice: ImageSlice = image[1...3, 1...2] 125 | 126 | XCTAssertEqual(slice.width, 3) 127 | XCTAssertEqual(slice.height, 2) 128 | 129 | XCTAssertEqual(slice[1, 1], 1) 130 | XCTAssertEqual(slice[2, 1], 2) 131 | XCTAssertEqual(slice[3, 1], 3) 132 | 133 | XCTAssertEqual(slice[1, 2], 4) 134 | XCTAssertEqual(slice[2, 2], 5) 135 | XCTAssertEqual(slice[3, 2], 6) 136 | } 137 | 138 | func testPixel() { 139 | let image = Image(width: 3, height: 2, pixels: [ 140 | 1, 2, 3, 141 | 4, 5, 6, 142 | ]) 143 | 144 | XCTAssertEqual(image.pixelAt(x: 0, y: 0), 1) 145 | XCTAssertEqual(image.pixelAt(x: 1, y: 0), 2) 146 | XCTAssertEqual(image.pixelAt(x: 2, y: 0), 3) 147 | 148 | XCTAssertEqual(image.pixelAt(x: 0, y: 1), 4) 149 | XCTAssertEqual(image.pixelAt(x: 1, y: 1), 5) 150 | XCTAssertEqual(image.pixelAt(x: 2, y: 1), 6) 151 | 152 | XCTAssertNil(image.pixelAt(x: -1, y: 0)) 153 | XCTAssertNil(image.pixelAt(x: 3, y: 0)) 154 | XCTAssertNil(image.pixelAt(x: 0, y: -1)) 155 | XCTAssertNil(image.pixelAt(x: 0, y: 2)) 156 | } 157 | 158 | func testWithUnsafeBufferPointer() { 159 | let image = Image(width: 3, height: 2, pixels: [ 160 | 1, 2, 3, 161 | 4, 5, 6, 162 | ]) 163 | 164 | image.withUnsafeBufferPointer { p in 165 | XCTAssertEqual(p.count, 6) 166 | 167 | XCTAssertEqual(p[0], 1) 168 | XCTAssertEqual(p[1], 2) 169 | XCTAssertEqual(p[2], 3) 170 | XCTAssertEqual(p[3], 4) 171 | XCTAssertEqual(p[4], 5) 172 | XCTAssertEqual(p[5], 6) 173 | } 174 | } 175 | 176 | func testWithUnsafeMutableBufferPointer() { 177 | var image = Image(width: 3, height: 2, pixels: [ 178 | 1, 2, 3, 179 | 4, 5, 6, 180 | ]) 181 | 182 | image.withUnsafeMutableBufferPointer { p in 183 | XCTAssertEqual(p.count, 6) 184 | 185 | XCTAssertEqual(p[0], 1) 186 | XCTAssertEqual(p[1], 2) 187 | XCTAssertEqual(p[2], 3) 188 | XCTAssertEqual(p[3], 4) 189 | XCTAssertEqual(p[4], 5) 190 | XCTAssertEqual(p[5], 6) 191 | 192 | p[0] += 10 193 | p[1] += 10 194 | p[2] += 10 195 | p[3] += 10 196 | p[4] += 10 197 | p[5] += 10 198 | } 199 | 200 | XCTAssertEqual(image.pixelAt(x: 0, y: 0), 11) 201 | XCTAssertEqual(image.pixelAt(x: 1, y: 0), 12) 202 | XCTAssertEqual(image.pixelAt(x: 2, y: 0), 13) 203 | 204 | XCTAssertEqual(image.pixelAt(x: 0, y: 1), 14) 205 | XCTAssertEqual(image.pixelAt(x: 1, y: 1), 15) 206 | XCTAssertEqual(image.pixelAt(x: 2, y: 1), 16) 207 | } 208 | 209 | func testWithUnsafeBytes() { 210 | let image = Image(width: 3, height: 2, pixels: [ 211 | 1, 2, 3, 212 | 4, 5, 6, 213 | ]) 214 | 215 | image.withUnsafeBytes { p in 216 | XCTAssertEqual(p.count, 6) 217 | 218 | XCTAssertEqual(p[0], 1) 219 | XCTAssertEqual(p[1], 2) 220 | XCTAssertEqual(p[2], 3) 221 | XCTAssertEqual(p[3], 4) 222 | XCTAssertEqual(p[4], 5) 223 | XCTAssertEqual(p[5], 6) 224 | } 225 | } 226 | 227 | func testWithUnsafeMutableBytes() { 228 | var image = Image(width: 3, height: 2, pixels: [ 229 | 1, 2, 3, 230 | 4, 5, 6, 231 | ]) 232 | 233 | image.withUnsafeMutableBytes { p in 234 | XCTAssertEqual(p.count, 6) 235 | 236 | XCTAssertEqual(p[0], 1) 237 | XCTAssertEqual(p[1], 2) 238 | XCTAssertEqual(p[2], 3) 239 | XCTAssertEqual(p[3], 4) 240 | XCTAssertEqual(p[4], 5) 241 | XCTAssertEqual(p[5], 6) 242 | 243 | p[0] += 10 244 | p[1] += 10 245 | p[2] += 10 246 | p[3] += 10 247 | p[4] += 10 248 | p[5] += 10 249 | } 250 | 251 | XCTAssertEqual(image.pixelAt(x: 0, y: 0), 11) 252 | XCTAssertEqual(image.pixelAt(x: 1, y: 0), 12) 253 | XCTAssertEqual(image.pixelAt(x: 2, y: 0), 13) 254 | 255 | XCTAssertEqual(image.pixelAt(x: 0, y: 1), 14) 256 | XCTAssertEqual(image.pixelAt(x: 1, y: 1), 15) 257 | XCTAssertEqual(image.pixelAt(x: 2, y: 1), 16) 258 | } 259 | 260 | func testCopyOnWritePerformanceOfCopy() { // Fast 261 | let image = Image>(width: 8192, height: 8192, pixel: RGBA.clear) 262 | measure { 263 | let copy = image 264 | XCTAssertEqual(8192 * 8192, copy.count) 265 | } 266 | } 267 | 268 | func testCopyOnWritePerformanceOfUpdate() { // Fast 269 | var image = Image>(width: 8192, height: 8192, pixel: RGBA.clear) 270 | measure { 271 | image[0, 0] = .white 272 | XCTAssertEqual(8192 * 8192, image.count) 273 | } 274 | } 275 | 276 | func testCopyPerformance() { // Slow 277 | let image = Image>(width: 8192, height: 8192, pixel: RGBA.clear) 278 | measure { 279 | var copy = image 280 | copy[0, 0] = .white 281 | XCTAssertEqual(8192 * 8192, copy.count) 282 | } 283 | } 284 | 285 | func testData() { 286 | #if (canImport(AppKit) || canImport(UIKit)) && canImport(CoreGraphics) 287 | do { 288 | let image = Image(width: 3, height: 2, pixels: [ 289 | 1, 2, 3, 290 | 4, 5, 6, 291 | ]) 292 | guard let data: Data = image.data(using: .png) else { 293 | XCTFail() 294 | fatalError() 295 | } 296 | guard let restored = Image(data: data) else { 297 | XCTFail() 298 | fatalError() 299 | } 300 | XCTAssertEqual(restored, image) 301 | } 302 | 303 | do { 304 | let image = Image(width: 3, height: 2, pixels: [ 305 | 0.1, 0.2, 0.3, 306 | 0.4, 0.5, 0.6 307 | ]) 308 | guard let data: Data = image.data(using: .jpeg(compressionQuality: 0.95)) else { 309 | XCTFail() 310 | fatalError() 311 | } 312 | guard let restored = Image(data: data) else { 313 | XCTFail() 314 | fatalError() 315 | } 316 | XCTAssertEqual(restored, image, accuracy: 0.01) 317 | } 318 | 319 | do { 320 | let image = Image(width: 0, height: 0, pixels: []) 321 | XCTAssertNil(image.data(using: .png)) 322 | } 323 | 324 | do { 325 | let image = Image(width: 0, height: 42, pixels: []) 326 | XCTAssertNil(image.data(using: .png)) 327 | } 328 | 329 | do { 330 | let image = Image(width: 42, height: 0, pixels: []) 331 | XCTAssertNil(image.data(using: .png)) 332 | } 333 | #endif 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/PremultipliedRGBATests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class PremultipliedRGBATests: XCTestCase { 5 | func testInit() { 6 | do { 7 | let r = PremultipliedRGBA(RGBA(red: 255, green: 255, blue: 255, alpha: 123)) 8 | XCTAssertEqual(r, PremultipliedRGBA(red: 123, green: 123, blue: 123, alpha: 123)) 9 | } 10 | 11 | do { 12 | let r = PremultipliedRGBA(RGBA(red: 50, green: 100, blue: 200, alpha: 127)) 13 | XCTAssertEqual(r, PremultipliedRGBA(red: 24, green: 49, blue: 99, alpha: 127)) 14 | } 15 | 16 | do { 17 | let r = PremultipliedRGBA(RGBA(red: 0.5, green: 0.25, blue: 0.125, alpha: 0.5)) 18 | XCTAssertEqual(r, PremultipliedRGBA(red: 0.25, green: 0.125, blue: 0.0625, alpha: 0.5)) 19 | } 20 | 21 | do { 22 | let r = PremultipliedRGBA(0xCCDDEEFF) 23 | XCTAssertEqual(r, PremultipliedRGBA(red: 204, green: 221, blue: 238, alpha: 255)) 24 | } 25 | } 26 | 27 | func testInitWithRGB() { 28 | do { 29 | let a = RGB(red: 1, green: 2, blue: 254) 30 | let r = PremultipliedRGBA(a) 31 | XCTAssertEqual(r, PremultipliedRGBA(red: 1, green: 2, blue: 254, alpha: 255)) 32 | } 33 | 34 | do { 35 | let a = RGB(red: 0.2, green: 0.4, blue: 0.6) 36 | let r = PremultipliedRGBA(a) 37 | XCTAssertEqual(r, PremultipliedRGBA(red: 0.2, green: 0.4, blue: 0.6, alpha: 1.0)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/RGBAOperatorsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class RGBAOperatorsTests: XCTestCase { 5 | func testAdd() { 6 | let a = RGBA(red: 1, green: 2, blue: -3, alpha: -4) 7 | let b = RGBA(red: 5, green: -6, blue: 7, alpha: -8) 8 | XCTAssertEqual(a + b, RGBA(red: 6, green: -4, blue: 4, alpha: -12)) 9 | } 10 | 11 | func testAnd() { 12 | let a = RGBA(red: true, green: false, blue: false, alpha: true) 13 | let b = RGBA(red: true, green: true, blue: false, alpha: false) 14 | XCTAssertEqual(a && b, RGBA(red: true, green: false, blue: false, alpha: false)) 15 | } 16 | 17 | func testAddAsign() { 18 | var a = RGBA(red: 1, green: 2, blue: -3, alpha: -4) 19 | let b = RGBA(red: 5, green: -6, blue: 7, alpha: -8) 20 | a += b 21 | XCTAssertEqual(a, RGBA(red: 6, green: -4, blue: 4, alpha: -12)) 22 | } 23 | 24 | func testSubtractAsign() { 25 | var a = RGBA(red: 1, green: 2, blue: -3, alpha: -4) 26 | let b = RGBA(red: 5, green: -6, blue: 7, alpha: -8) 27 | a -= b 28 | XCTAssertEqual(a, RGBA(red: -4, green: 8, blue: -10, alpha: 4)) 29 | } 30 | 31 | func testNegate() { 32 | let a = RGBA(red: 1, green: 2, blue: -3, alpha: -4) 33 | XCTAssertEqual(-a, RGBA(red: -1, green: -2, blue: 3, alpha: 4)) 34 | } 35 | 36 | func testNot() { 37 | let a = RGBA(red: true, green: false, blue: false, alpha: true) 38 | XCTAssertEqual(!a, RGBA(red: false, green: true, blue: true, alpha: false)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/ResizingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | class ResizingTests : XCTestCase { 5 | func testResized() { 6 | do { 7 | let image = Image(width: 3, height: 2, pixels: [ 8 | 1, 2, 3, 9 | 4, 5, 6, 10 | ]) 11 | XCTAssertEqual(image.resizedTo(width: 6, height: 4), Image(width: 6, height: 4, pixels: [ 12 | 1.00, 1.25, 1.75, 2.25, 2.75, 3.00, 13 | 1.75, 2.00, 2.50, 3.00, 3.50, 3.75, 14 | 3.25, 3.50, 4.00, 4.50, 5.00, 5.25, 15 | 4.00, 4.25, 4.75, 5.25, 5.75, 6.00, 16 | ]), accuracy: 1.0e-10) 17 | } 18 | 19 | do { // `ImageSlice` 20 | let image = Image(width: 5, height: 4, pixels: [ 21 | 0, 0, 0, 0, 0, 22 | 0, 1, 2, 3, 0, 23 | 0, 4, 5, 6, 0, 24 | 0, 0, 0, 0, 0, 25 | ]) 26 | let slice: ImageSlice = image[1...3, 1...2] 27 | XCTAssertEqual(slice.resizedTo(width: 6, height: 4), Image(width: 6, height: 4, pixels: [ 28 | 1.00, 1.25, 1.75, 2.25, 2.75, 3.00, 29 | 1.75, 2.00, 2.50, 3.00, 3.50, 3.75, 30 | 3.25, 3.50, 4.00, 4.50, 5.00, 5.25, 31 | 4.00, 4.25, 4.75, 5.25, 5.75, 6.00, 32 | ]), accuracy: 1.0e-10) 33 | } 34 | 35 | do { 36 | let image = Image(width: 3, height: 2, pixels: [ 37 | 1, 2, 3, 38 | 4, 5, 6, 39 | ]) 40 | XCTAssertEqual(image.resizedTo(width: 6, height: 4, interpolatedBy: .bilinear), Image(width: 6, height: 4, pixels: [ 41 | 1.00, 1.25, 1.75, 2.25, 2.75, 3.00, 42 | 1.75, 2.00, 2.50, 3.00, 3.50, 3.75, 43 | 3.25, 3.50, 4.00, 4.50, 5.00, 5.25, 44 | 4.00, 4.25, 4.75, 5.25, 5.75, 6.00, 45 | ]), accuracy: 1.0e-10) 46 | } 47 | 48 | do { 49 | let image = Image(width: 3, height: 2, pixels: [ 50 | 1, 2, 3, 51 | 4, 5, 6, 52 | ]) 53 | XCTAssertEqual(image.resizedTo(width: 6, height: 4, interpolatedBy: .nearestNeighbor), Image(width: 6, height: 4, pixels: [ 54 | 1, 1, 2, 2, 3, 3, 55 | 1, 1, 2, 2, 3, 3, 56 | 4, 4, 5, 5, 6, 6, 57 | 4, 4, 5, 5, 6, 6, 58 | ])) 59 | } 60 | 61 | do { 62 | let image = Image(width: 6, height: 4, pixels: [ 63 | 1, 1, 2, 2, 3, 3, 64 | 1, 1, 2, 2, 3, 3, 65 | 4, 4, 5, 5, 6, 6, 66 | 4, 4, 5, 5, 6, 6, 67 | ]) 68 | XCTAssertEqual(image.resizedTo(width: 3, height: 2, interpolatedBy: .nearestNeighbor), Image(width: 3, height: 2, pixels: [ 69 | 1, 2, 3, 70 | 4, 5, 6, 71 | ])) 72 | } 73 | 74 | do { // To check anti-aliasing is applied 75 | let image = Image(width: 6, height: 3, pixels: [ 76 | 1, 8, 5, 4, 5, 3, 77 | 3, 2, 6, 7, 8, 9, 78 | 4, 7, 9, 1, 6, 2, 79 | ]) 80 | XCTAssertEqual(image.resizedTo(width: 2, height: 1, interpolatedBy: .bilinear), Image(width: 2, height: 1, pixels: [ 81 | 5, 5, 82 | ]), accuracy: 1.0e-10) 83 | } 84 | 85 | do { // To check anti-aliasing is not applied for `.nearestNeighbor` 86 | let image = Image(width: 6, height: 3, pixels: [ 87 | 1, 8, 5, 4, 5, 3, 88 | 3, 2, 6, 7, 8, 9, 89 | 4, 7, 9, 1, 6, 2, 90 | ]) 91 | XCTAssertEqual(image.resizedTo(width: 2, height: 1, interpolatedBy: .nearestNeighbor), Image(width: 2, height: 1, pixels: [ 92 | 2, 8, 93 | ]), accuracy: 1.0e-10) 94 | } 95 | 96 | do { // Interpolation 97 | let image = Image(width: 3, height: 3, pixels: [ 98 | 1, 2, 3, 99 | 4, 5, 6, 100 | 7, 8, 9, 101 | ]) 102 | XCTAssertEqual(image.resizedTo(width: 2, height: 2), Image(width: 2, height: 2, pixels: [ 103 | 2.0, 3.5, 104 | 6.5, 8.0, 105 | ]), accuracy: 1.0e-10) 106 | } 107 | 108 | do { // Non-numeric Pixels 109 | let image = Image(width: 3, height: 2, pixels: [ 110 | true, false, true, 111 | false, true, false, 112 | ]) 113 | XCTAssertEqual(image.resizedTo(width: 6, height: 4), Image(width: 6, height: 4, pixels: [ 114 | true, true, false, false, true, true, 115 | true, true, false, false, true, true, 116 | false, false, true, true, false, false, 117 | false, false, true, true, false, false, 118 | ])) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/RotationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | private let accuracy = 1.0e-10 5 | 6 | class RotationTests : XCTestCase { 7 | func testRotated() { 8 | do { 9 | let image = Image(width: 2, height: 2, pixels: [ 10 | 1, 2, 11 | 3, 4, 12 | ]) 13 | XCTAssertEqual(image.rotated(by: .pi / 4, interpolatedBy: .bilinear, extrapolatedBy: .edge), Image(width: 3, height: 3, pixels: [ 14 | 2.0, 1.0, 1.5, 15 | 3.0, 2.5, 2.0, 16 | 3.5, 4.0, 3.0, 17 | ]), accuracy: accuracy) 18 | } 19 | 20 | do { 21 | let image = Image(width: 4, height: 4, pixels: [ 22 | 0, 0, 0, 0, 23 | 0, 1, 2, 0, 24 | 0, 3, 4, 0, 25 | 0, 0, 0, 0, 26 | ]) 27 | let slice = image[1...2, 1...2] 28 | XCTAssertEqual(slice.rotated(by: .pi / 4, interpolatedBy: .bilinear, extrapolatedBy: .edge), Image(width: 3, height: 3, pixels: [ 29 | 2.0, 1.0, 1.5, 30 | 3.0, 2.5, 2.0, 31 | 3.5, 4.0, 3.0, 32 | ]), accuracy: accuracy) 33 | } 34 | 35 | do { 36 | let image = Image(width: 2, height: 2, pixels: [ 37 | true, false, 38 | false, true, 39 | ]) 40 | XCTAssertEqual(image.rotated(by: .pi / 4), Image(width: 3, height: 3, pixels: [ 41 | false, true, false, 42 | false, true, false, 43 | false, true, false, 44 | ])) 45 | } 46 | 47 | do { 48 | let image = Image(width: 2, height: 2, pixels: [ 49 | true, false, 50 | false, true, 51 | ]) 52 | XCTAssertEqual(image.rotated(byDegrees: 45), Image(width: 3, height: 3, pixels: [ 53 | false, true, false, 54 | false, true, false, 55 | false, true, false, 56 | ])) 57 | } 58 | 59 | do { 60 | let image = Image(width: 2, height: 2, pixels: [ 61 | 2, nil, 62 | nil, 3, 63 | ]) 64 | XCTAssertEqual(image.rotated(byDegrees: 90), Image(width: 2, height: 2, pixels: [ 65 | nil, 2, 66 | 3, nil, 67 | ])) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/SwiftImageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | #if canImport(UIKit) 3 | import UIKit 4 | #endif 5 | #if canImport(AppKit) 6 | import AppKit 7 | #endif 8 | #if canImport(CoreGraphics) 9 | import CoreGraphics 10 | #endif 11 | import SwiftImage 12 | 13 | class SwiftImageSample: XCTestCase { 14 | func testSample() { 15 | /**/ #if canImport(UIKit) || canImport(AppKit) 16 | /**/ let x = 0 17 | /**/ let y = 0 18 | /**/ #if canImport(UIKit) 19 | /**/ let imageView: UIImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 20 | /**/ imageView.image = Image>(width: 1, height: 1, pixel: .black).uiImage 21 | /**/ #endif 22 | 23 | /**/ if never() { 24 | var image = Image>(named: "ImageName")! 25 | /**/ image[0, 0] = image[0, 0] 26 | /**/ } 27 | /**/ var image = Image>(width: 1, height: 1, pixel: .black) 28 | 29 | let pixel: RGBA = image[x, y] 30 | image[x, y] = RGBA(red: 255, green: 0, blue: 0, alpha: 127) 31 | image[x, y] = RGBA(0xFF00007F) // red: 255, green: 0, blue: 0, alpha: 127 32 | 33 | // Iterates over all pixels 34 | for pixel in image { 35 | // ... 36 | /**/ _ = pixel.description 37 | } 38 | 39 | // Image processing (e.g. binarizations) 40 | let binarized: Image = image.map { $0.gray >= 127 } 41 | 42 | // From/to `UIImage` 43 | /**/ #if canImport(UIKit) 44 | image = Image>(uiImage: imageView.image!) 45 | imageView.image = image.uiImage 46 | /**/ #endif 47 | 48 | /**/ _ = pixel 49 | /**/ _ = binarized[0, 0] 50 | /**/ #endif 51 | } 52 | 53 | func testIntroduction() { 54 | do { 55 | /**/ if never() { 56 | var image: Image = Image(width: 640, height: 480, pixels: [255, 248, /* ... */]) 57 | 58 | /**/ image[0, 0] = 0 59 | /**/ } 60 | /**/ var image: Image = Image(width: 1, height: 1, pixel: 0) 61 | /**/ let x = 0 62 | /**/ let y = 0 63 | let pixel: UInt8 = image[x, y] 64 | image[x, y] = 255 65 | /**/ _ = pixel 66 | 67 | let width: Int = image.width // 640 68 | let height: Int = image.height // 480 69 | /**/ _ = width 70 | /**/ _ = height 71 | } 72 | 73 | do { 74 | let image: Image> = // ... 75 | /**/ Image>(width: 1, height: 1, pixel: RGBA(red: 42, green: 42, blue: 42)) 76 | let grayscale: Image = image.map { $0.gray } 77 | /**/ XCTAssertEqual(grayscale[0, 0], 42) 78 | } 79 | 80 | do { 81 | /**/ let image = Image(width: 1, height: 1, pixel: 0) 82 | /**/ let x = 0, y = 0 83 | var another: Image = image // Not copied here because of copy-on-write 84 | another[x, y] = 255 // Copied here lazily 85 | /**/ XCTAssertFalse( 86 | another[x, y] == image[x, y] // false: Instances are never shared 87 | /**/ ) 88 | } 89 | } 90 | 91 | func testInitialization() async { 92 | #if canImport(UIKit) 93 | do { 94 | /**/ if never() { 95 | let image = Image>(named: "ImageName")! 96 | /**/ _ = image.count 97 | /**/ } 98 | } 99 | do { 100 | /**/ if never() { 101 | let image = Image>(contentsOfFile: "path/to/file")! 102 | /**/ _ = image.count 103 | /**/ } 104 | } 105 | do { 106 | /**/ if never() { 107 | let image = Image>(data: Data(/* ... */))! 108 | /**/ _ = image.count 109 | /**/ } 110 | } 111 | await MainActor.run { 112 | /**/ let imageView: UIImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 113 | /**/ imageView.image = Image>(width: 1, height: 1, pixel: RGBA.black).uiImage 114 | let image = Image>(uiImage: imageView.image!) // from a UIImage 115 | /**/ _ = image.count 116 | } 117 | #endif 118 | #if canImport(AppKit) 119 | await MainActor.run { 120 | /**/ let imageView: NSImageView = NSImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 121 | /**/ imageView.image = Image>(width: 1, height: 1, pixel: RGBA.black).nsImage 122 | let image = Image>(nsImage: imageView.image!) // from a NSImage 123 | /**/ _ = image.count 124 | } 125 | #endif 126 | #if canImport(CoreGraphics) 127 | do { 128 | let cgImage: CGImage = Image>(width: 1, height: 1, pixel: RGBA.black).cgImage 129 | let image = Image>(cgImage: cgImage) // from a CGImage 130 | /**/ XCTAssertEqual(image.count, 1) 131 | } 132 | #endif 133 | do { 134 | /**/ let pixels = [RGBA](repeating: .black, count: 640 * 480) 135 | let image = Image>(width: 640, height: 480, pixels: pixels) // from a pixel array 136 | /**/ _ = image.count 137 | } 138 | do { 139 | let image = Image>(width: 640, height: 480, pixel: .black) // a black RGBA image 140 | /**/ _ = image.count 141 | } 142 | do { 143 | let image = Image(width: 640, height: 480, pixel: 0) // a black grayscale image 144 | /**/ _ = image.count 145 | } 146 | do { 147 | let image = Image(width: 640, height: 480, pixel: false) // a black binary image 148 | /**/ _ = image.count 149 | } 150 | } 151 | 152 | func testAccessToAPixel() { 153 | /**/ let x = 0 154 | /**/ let y = 0 155 | /**/ var image = Image>(width: 1, height: 1, pixel: .red) 156 | 157 | do { 158 | // Gets a pixel by subscripts 159 | let pixel = image[x, y] 160 | /**/ _ = pixel 161 | } 162 | 163 | ///////////////////////////////// 164 | 165 | // Sets a pixel by subscripts 166 | image[x, y] = RGBA(0xFF0000FF) 167 | image[x, y].alpha = 127 168 | 169 | ///////////////////////////////// 170 | 171 | // Safe get for a pixel 172 | if let pixel = image.pixelAt(x: x, y: y) { 173 | print(pixel.red) 174 | print(pixel.green) 175 | print(pixel.blue) 176 | print(pixel.alpha) 177 | 178 | print(pixel.gray) // (red + green + blue) / 3 179 | print(pixel) // formatted like "#FF0000FF" 180 | } else { 181 | // `pixel` is safe: `nil` is returned when out of bounds 182 | print("Out of bounds") 183 | } 184 | } 185 | 186 | func testRotation() { 187 | do { 188 | /**/ let image = Image(width: 1, height: 1, pixel: 0) 189 | let result = image.rotated(by: .pi) // Rotated clockwise by π 190 | /**/ _ = result.count 191 | } 192 | do { 193 | /**/ let image = Image(width: 1, height: 1, pixel: 0) 194 | let result = image.rotated(byDegrees: 180) // Rotated clockwise by 180 degrees 195 | /**/ _ = result.count 196 | } 197 | do { 198 | /**/ let image = Image>(width: 1, height: 1, pixel: .red) 199 | // Rotated clockwise by π / 4 and fill the background with red 200 | let result = image.rotated(by: .pi / 4, extrapolatedBy: .constant(.red)) 201 | /**/ _ = result.count 202 | } 203 | } 204 | 205 | func testResizing() { 206 | do { 207 | /**/ let image = Image(width: 1, height: 1, pixel: 0) 208 | let result = image.resizedTo(width: 320, height: 240) 209 | /**/ _ = result.count 210 | } 211 | 212 | do { 213 | /**/ let image = Image(width: 1, height: 1, pixel: 0) 214 | let result = image.resizedTo(width: 320, height: 240, interpolatedBy: .nearestNeighbor) 215 | /**/ _ = result.count 216 | } 217 | } 218 | 219 | func testCropping() { 220 | /**/ let image = Image>(width: 128, height: 128, pixel: .red) 221 | 222 | let slice: ImageSlice> = image[32..<64, 32..<64] // No copying costs 223 | let cropped = Image>(slice) // Copying is executed here 224 | 225 | /**/ _ = cropped.count 226 | } 227 | func testWithUIImage() async { 228 | #if canImport(UIKit) 229 | await MainActor.run { 230 | /**/ if never() { 231 | /**/ let imageView = UIImageView() 232 | 233 | // From `UIImage` 234 | let image = Image>(uiImage: imageView.image!) 235 | 236 | // To `UIImage` 237 | imageView.image = image.uiImage 238 | /**/ } 239 | } 240 | #endif 241 | } 242 | func testWithNSImage() async { 243 | #if canImport(AppKit) 244 | await MainActor.run { 245 | /**/ if never() { 246 | /**/ let imageView = NSImageView() 247 | 248 | // From `NSImage` 249 | let image = Image>(nsImage: imageView.image!) 250 | 251 | // To `NSImage` 252 | imageView.image = image.nsImage 253 | /**/ } 254 | } 255 | #endif 256 | } 257 | func testWithCoreGraphics() { 258 | #if canImport(UIKit) 259 | /**/ if never() { 260 | /**/ let imageView = UIImageView() 261 | 262 | // Drawing on images with CoreGraphics 263 | var image = Image>(uiImage: imageView.image!) 264 | image.withCGContext { context in 265 | context.setLineWidth(1) 266 | context.setStrokeColor(UIColor.red.cgColor) 267 | context.move(to: CGPoint(x: -1, y: -1)) 268 | context.addLine(to: CGPoint(x: 640, y: 480)) 269 | context.strokePath() 270 | } 271 | imageView.image = image.uiImage 272 | /**/ } 273 | #endif 274 | } 275 | } 276 | 277 | private func never() -> Bool { 278 | return false 279 | } 280 | 281 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/Test2x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/swift-image/36c8a282c6cc75d8f3a6f25057f093f950dced83/Tests/SwiftImageTests/Test2x1.png -------------------------------------------------------------------------------- /Tests/SwiftImageTests/Test2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/swift-image/36c8a282c6cc75d8f3a6f25057f093f950dced83/Tests/SwiftImageTests/Test2x2.png -------------------------------------------------------------------------------- /Tests/SwiftImageTests/Test4x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/swift-image/36c8a282c6cc75d8f3a6f25057f093f950dced83/Tests/SwiftImageTests/Test4x4.png -------------------------------------------------------------------------------- /Tests/SwiftImageTests/UIKitTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) && canImport(CoreGraphics) 2 | import XCTest 3 | import SwiftImage 4 | import CoreGraphics 5 | import UIKit 6 | 7 | class UIKitTests: XCTestCase { 8 | func testInitWithUIImage() { 9 | do { 10 | let uiImage = UIImage(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")))! 11 | let image = Image>(uiImage: uiImage) 12 | 13 | XCTAssertEqual(image.width, 2) 14 | XCTAssertEqual(image.height, 2) 15 | 16 | XCTAssertEqual(image[0, 0].red, 255) 17 | XCTAssertEqual(image[0, 0].green, 0) 18 | XCTAssertEqual(image[0, 0].blue, 0) 19 | XCTAssertEqual(image[0, 0].alpha, 64) 20 | 21 | XCTAssertEqual(image[1, 0].red, 0) 22 | XCTAssertEqual(image[1, 0].green, 255) 23 | XCTAssertEqual(image[1, 0].blue, 0) 24 | XCTAssertEqual(image[1, 0].alpha, 127) 25 | 26 | XCTAssertEqual(image[0, 1].red, 0) 27 | XCTAssertEqual(image[0, 1].green, 0) 28 | XCTAssertEqual(image[0, 1].blue, 255) 29 | XCTAssertEqual(image[0, 1].alpha, 191) 30 | 31 | XCTAssertEqual(image[1, 1].red, 255) 32 | XCTAssertEqual(image[1, 1].green, 255) 33 | XCTAssertEqual(image[1, 1].blue, 0) 34 | XCTAssertEqual(image[1, 1].alpha, 255) 35 | } 36 | 37 | do { // ImageSlice 38 | let uiImage = UIImage(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")))! 39 | let slice = ImageSlice>(uiImage: uiImage) 40 | 41 | XCTAssertEqual(slice.width, 2) 42 | XCTAssertEqual(slice.height, 2) 43 | 44 | XCTAssertEqual(slice[0, 0].red, 255) 45 | XCTAssertEqual(slice[0, 0].green, 0) 46 | XCTAssertEqual(slice[0, 0].blue, 0) 47 | XCTAssertEqual(slice[0, 0].alpha, 64) 48 | 49 | XCTAssertEqual(slice[1, 0].red, 0) 50 | XCTAssertEqual(slice[1, 0].green, 255) 51 | XCTAssertEqual(slice[1, 0].blue, 0) 52 | XCTAssertEqual(slice[1, 0].alpha, 127) 53 | 54 | XCTAssertEqual(slice[0, 1].red, 0) 55 | XCTAssertEqual(slice[0, 1].green, 0) 56 | XCTAssertEqual(slice[0, 1].blue, 255) 57 | XCTAssertEqual(slice[0, 1].alpha, 191) 58 | 59 | XCTAssertEqual(slice[1, 1].red, 255) 60 | XCTAssertEqual(slice[1, 1].green, 255) 61 | XCTAssertEqual(slice[1, 1].blue, 0) 62 | XCTAssertEqual(slice[1, 1].alpha, 255) 63 | } 64 | 65 | do { // `PremultipliedRGBA` 66 | let uiImage = UIImage(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")))! 67 | let image = Image>(uiImage: uiImage) 68 | 69 | XCTAssertEqual(image.width, 2) 70 | XCTAssertEqual(image.height, 2) 71 | 72 | XCTAssertEqual(image[0, 0].red, 64) 73 | XCTAssertEqual(image[0, 0].green, 0) 74 | XCTAssertEqual(image[0, 0].blue, 0) 75 | XCTAssertEqual(image[0, 0].alpha, 64) 76 | 77 | XCTAssertEqual(image[1, 0].red, 0) 78 | XCTAssertEqual(image[1, 0].green, 127) 79 | XCTAssertEqual(image[1, 0].blue, 0) 80 | XCTAssertEqual(image[1, 0].alpha, 127) 81 | 82 | XCTAssertEqual(image[0, 1].red, 0) 83 | XCTAssertEqual(image[0, 1].green, 0) 84 | XCTAssertEqual(image[0, 1].blue, 191) 85 | XCTAssertEqual(image[0, 1].alpha, 191) 86 | 87 | XCTAssertEqual(image[1, 1].red, 255) 88 | XCTAssertEqual(image[1, 1].green, 255) 89 | XCTAssertEqual(image[1, 1].blue, 0) 90 | XCTAssertEqual(image[1, 1].alpha, 255) 91 | } 92 | 93 | do { // With `UIImage` from `CGImage` 94 | let dataProvider = CGDataProvider.init(data: try! Data(contentsOf: URL(fileURLWithPath: (#file as NSString).deletingLastPathComponent).appendingPathComponent("Test2x2.png")) as CFData)! 95 | let cgImage = CGImage(pngDataProviderSource: dataProvider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)! 96 | let uiImage = UIImage(cgImage: cgImage) 97 | let image = Image>(uiImage: uiImage) 98 | 99 | XCTAssertEqual(image.width, 2) 100 | XCTAssertEqual(image.height, 2) 101 | 102 | XCTAssertEqual(255, image[0, 0].red) 103 | XCTAssertEqual( 0, image[0, 0].green) 104 | XCTAssertEqual( 0, image[0, 0].blue) 105 | XCTAssertEqual( 64, image[0, 0].alpha) 106 | 107 | XCTAssertEqual( 0, image[1, 0].red) 108 | XCTAssertEqual(255, image[1, 0].green) 109 | XCTAssertEqual( 0, image[1, 0].blue) 110 | XCTAssertEqual(127, image[1, 0].alpha) 111 | 112 | XCTAssertEqual( 0, image[0, 1].red) 113 | XCTAssertEqual( 0, image[0, 1].green) 114 | XCTAssertEqual(255, image[0, 1].blue) 115 | XCTAssertEqual(191, image[0, 1].alpha) 116 | 117 | XCTAssertEqual(255, image[1, 1].red) 118 | XCTAssertEqual(255, image[1, 1].green) 119 | XCTAssertEqual( 0, image[1, 1].blue) 120 | XCTAssertEqual(255, image[1, 1].alpha) 121 | } 122 | 123 | #if os(iOS) || os(tvOS) 124 | do { // With `UIImage` from `CIImage` 125 | let ciImage = CIImage(color: CIColor.red).cropped(to: CGRect(x: 0, y: 0, width: 1, height: 1)) 126 | let uiImage = UIImage(ciImage: ciImage) 127 | let image = Image>(uiImage: uiImage) 128 | 129 | XCTAssertEqual(image.width, 1) 130 | XCTAssertEqual(image.height, 1) 131 | 132 | XCTAssertEqual(255, image[0, 0].red) 133 | XCTAssertEqual( 0, image[0, 0].green) 134 | XCTAssertEqual( 0, image[0, 0].blue) 135 | XCTAssertEqual(255, image[0, 0].alpha) 136 | } 137 | #endif 138 | 139 | do { // With `UIImage` whose `cgImage` and `ciImage` are `nil` 140 | let uiImage = UIImage() 141 | let image = Image>(uiImage: uiImage) 142 | XCTAssertEqual(image.width, 0) 143 | XCTAssertEqual(image.height, 0) 144 | } 145 | } 146 | 147 | func testUIImage() { 148 | do { 149 | let original = Image>(width: 2, height: 2, pixels: [ 150 | RGBA(red: 0, green: 1, blue: 2, alpha: 255), 151 | RGBA(red: 253, green: 254, blue: 255, alpha: 255), 152 | RGBA(red: 10, green: 20, blue: 30, alpha: 102), 153 | RGBA(red: 10, green: 20, blue: 30, alpha: 51), 154 | ]) 155 | let uiImage = original.uiImage 156 | XCTAssertEqual(uiImage.size.width, CGFloat(original.width)) 157 | XCTAssertEqual(uiImage.size.height, CGFloat(original.height)) 158 | 159 | let restored = Image>(uiImage: uiImage) 160 | XCTAssertEqual(restored, original) 161 | } 162 | 163 | do { 164 | let original = Image>(width: 2, height: 2, pixels: [ 165 | PremultipliedRGBA(red: 0, green: 1, blue: 2, alpha: 255), 166 | PremultipliedRGBA(red: 253, green: 254, blue: 255, alpha: 255), 167 | PremultipliedRGBA(red: 10, green: 20, blue: 30, alpha: 102), 168 | PremultipliedRGBA(red: 10, green: 20, blue: 30, alpha: 51), 169 | ]) 170 | let uiImage = original.uiImage 171 | XCTAssertEqual(uiImage.size.width, CGFloat(original.width)) 172 | XCTAssertEqual(uiImage.size.height, CGFloat(original.height)) 173 | 174 | let restored = Image>(uiImage: uiImage) 175 | XCTAssertEqual(restored, original) 176 | } 177 | 178 | do { 179 | let original = Image(width: 2, height: 2, pixels: [0, 1, 127, 255]) 180 | let uiImage = original.uiImage 181 | XCTAssertEqual(uiImage.size.width, CGFloat(original.width)) 182 | XCTAssertEqual(uiImage.size.height, CGFloat(original.height)) 183 | 184 | let restored = Image(uiImage: uiImage) 185 | XCTAssertEqual(restored, original) 186 | } 187 | 188 | do { // `ImageSlice` 189 | let original = ImageSlice>(width: 2, height: 2, pixels: [ 190 | RGBA(red: 0, green: 1, blue: 2, alpha: 255), 191 | RGBA(red: 253, green: 254, blue: 255, alpha: 255), 192 | RGBA(red: 10, green: 20, blue: 30, alpha: 102), 193 | RGBA(red: 10, green: 20, blue: 30, alpha: 51), 194 | ]) 195 | let uiImage = original.uiImage 196 | XCTAssertEqual(uiImage.size.width, CGFloat(original.width)) 197 | XCTAssertEqual(uiImage.size.height, CGFloat(original.height)) 198 | 199 | let restored = ImageSlice>(uiImage: uiImage) 200 | XCTAssertEqual(restored, original) 201 | } 202 | } 203 | 204 | func testInitWithUIImagePerformance() { 205 | let original = Image>(width: 640, height: 480, pixel: RGBA( 206 | red: .random(in: 0...255), 207 | green: .random(in: 0...255), 208 | blue: .random(in: 0...255), 209 | alpha: 255 210 | )) 211 | let uiImage = original.uiImage 212 | 213 | var restored: Image>! = nil 214 | measure { 215 | restored = Image>(uiImage: uiImage) 216 | } 217 | XCTAssertEqual(restored, original) 218 | } 219 | 220 | func testUIImagePerformance() { 221 | let original = Image>(width: 640, height: 480, pixel: RGBA( 222 | red: .random(in: 0...255), 223 | green: .random(in: 0...255), 224 | blue: .random(in: 0...255), 225 | alpha: 255 226 | )) 227 | 228 | var uiImage: UIImage! 229 | measure { 230 | uiImage = original.uiImage 231 | } 232 | let restored = Image>(uiImage: uiImage) 233 | XCTAssertEqual(restored, original) 234 | } 235 | } 236 | #endif 237 | -------------------------------------------------------------------------------- /Tests/SwiftImageTests/Util.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftImage 3 | 4 | internal func XCTAssertEqual( 5 | _ expression1: @autoclosure () throws -> I, 6 | _ expression2: @autoclosure () throws -> I, 7 | accuracy: F, 8 | _ message: @autoclosure () -> String = "", 9 | file: StaticString = #file, 10 | line: UInt = #line 11 | ) where I.Pixel == F { 12 | do { 13 | let e1 = try expression1() 14 | let e2 = try expression2() 15 | let m = message() 16 | 17 | XCTAssertEqual(e1.width, e2.width, m == "" ? "width" : "\(m) (width)", file: file, line: line) 18 | XCTAssertEqual(e1.height, e2.height, m == "" ? "height" : "\(m) (height)", file: file, line: line) 19 | 20 | for (y1, y2) in zip(e1.yRange, e2.yRange) { 21 | for (x1, x2) in zip(e1.xRange, e2.xRange) { 22 | XCTAssertEqual(e1[x1, y1], e2[x2, y2], accuracy: accuracy, m == "" ? "[\(x1), \(y1)], [\(x2), \(y2)]" : "\(m) ([\(x1), \(y1)], [\(x2), \(y2)])", file: file, line: line) 23 | } 24 | } 25 | } catch let error { 26 | XCTAssertThrowsError(error, message(), file: file, line: line) 27 | } 28 | } 29 | 30 | internal func XCTAssertEqual( 31 | _ expression1: @autoclosure () throws -> I, 32 | _ expression2: @autoclosure () throws -> I, 33 | accuracy: F, 34 | _ message: @autoclosure () -> String = "", 35 | file: StaticString = #file, 36 | line: UInt = #line 37 | ) where I.Pixel == RGBA { 38 | do { 39 | let e1 = try expression1() 40 | let e2 = try expression2() 41 | let m = message() 42 | 43 | XCTAssertEqual(e1.width, e2.width, m == "" ? "width" : "\(m) (width)", file: file, line: line) 44 | XCTAssertEqual(e1.height, e2.height, m == "" ? "height" : "\(m) (height)", file: file, line: line) 45 | 46 | for (y1, y2) in zip(e1.yRange, e2.yRange) { 47 | for (x1, x2) in zip(e1.xRange, e2.xRange) { 48 | XCTAssertEqual(e1[x1, y1], e2[x2, y2], accuracy: accuracy, m == "" ? "[\(x1), \(y1)], [\(x2), \(y2)]" : "\(m) ([\(x1), \(y1)], [\(x2), \(y2)])", file: file, line: line) 49 | } 50 | } 51 | } catch let error { 52 | XCTAssertThrowsError(error, message(), file: file, line: line) 53 | } 54 | } 55 | 56 | internal func XCTAssertEqual( 57 | _ expression1: @autoclosure () throws -> RGB, 58 | _ expression2: @autoclosure () throws -> RGB, 59 | accuracy: F, 60 | _ message: @autoclosure () -> String = "", 61 | file: StaticString = #file, 62 | line: UInt = #line 63 | ) { 64 | do { 65 | let e1 = try expression1() 66 | let e2 = try expression2() 67 | let m = message() 68 | 69 | XCTAssertEqual(e1.red, e2.red, accuracy: accuracy, m, file: file, line: line) 70 | XCTAssertEqual(e1.green, e2.green, accuracy: accuracy, m, file: file, line: line) 71 | XCTAssertEqual(e1.blue, e2.blue, accuracy: accuracy, m, file: file, line: line) 72 | } catch let error { 73 | XCTAssertThrowsError(error, message(), file: file, line: line) 74 | } 75 | } 76 | 77 | internal func XCTAssertEqual( 78 | _ expression1: @autoclosure () throws -> RGBA, 79 | _ expression2: @autoclosure () throws -> RGBA, 80 | accuracy: F, 81 | _ message: @autoclosure () -> String = "", 82 | file: StaticString = #file, 83 | line: UInt = #line 84 | ) { 85 | do { 86 | let e1 = try expression1() 87 | let e2 = try expression2() 88 | let m = message() 89 | 90 | XCTAssertEqual(e1.red, e2.red, accuracy: accuracy, m, file: file, line: line) 91 | XCTAssertEqual(e1.green, e2.green, accuracy: accuracy, m, file: file, line: line) 92 | XCTAssertEqual(e1.blue, e2.blue, accuracy: accuracy, m, file: file, line: line) 93 | XCTAssertEqual(e1.alpha, e2.alpha, accuracy: accuracy, m, file: file, line: line) 94 | } catch let error { 95 | XCTAssertThrowsError(error, message(), file: file, line: line) 96 | } 97 | } 98 | 99 | internal func XCTAssertEqual( 100 | _ expression1: @autoclosure () throws -> PremultipliedRGBA, 101 | _ expression2: @autoclosure () throws -> PremultipliedRGBA, 102 | accuracy: F, 103 | _ message: @autoclosure () -> String = "", 104 | file: StaticString = #file, 105 | line: UInt = #line 106 | ) { 107 | do { 108 | let e1 = try expression1() 109 | let e2 = try expression2() 110 | let m = message() 111 | 112 | XCTAssertEqual(e1.red, e2.red, accuracy: accuracy, m, file: file, line: line) 113 | XCTAssertEqual(e1.green, e2.green, accuracy: accuracy, m, file: file, line: line) 114 | XCTAssertEqual(e1.blue, e2.blue, accuracy: accuracy, m, file: file, line: line) 115 | XCTAssertEqual(e1.alpha, e2.alpha, accuracy: accuracy, m, file: file, line: line) 116 | } catch let error { 117 | XCTAssertThrowsError(error, message(), file: file, line: line) 118 | } 119 | } 120 | 121 | internal struct GeneralError : Error { 122 | let message: String? = nil 123 | } 124 | --------------------------------------------------------------------------------