├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── TextureMap │ ├── Extensions │ ├── CGSize.swift │ ├── CVPixelBuffer.swift │ └── MTLTexture.swift │ ├── Texture │ ├── Array+MTLTexture.swift │ └── MTLTexture+sample.swift │ ├── TextureMap.swift │ ├── TexureMap+Raw.swift │ └── Types │ ├── TMAxis.swift │ ├── TMBits.swift │ ├── TMColor.swift │ ├── TMColorSpace.swift │ ├── TMError.swift │ ├── TMImage.swift │ └── TMSendableImage.swift └── Tests └── TextureMapTests └── TextureMapTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anton Heestand 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: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TextureMap", 7 | platforms: [ 8 | .iOS(.v16), 9 | .tvOS(.v16), 10 | .macOS(.v13), 11 | .visionOS(.v1) 12 | ], 13 | products: [ 14 | .library( 15 | name: "TextureMap", 16 | targets: ["TextureMap"]), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "TextureMap", 21 | dependencies: []), 22 | .testTarget( 23 | name: "TextureMapTests", 24 | dependencies: ["TextureMap"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextureMap 2 | 3 | ### Overview 4 | 5 | **Texture Map** is a Swift 6 package for working with images and textures. Covert images or raw data between various formats. Made for iOS, macOS, and visionOS. Powered by Metal. 6 | 7 | --- 8 | 9 | ### Features 10 | 11 | 1. **Cross-Platform Support**: 12 | - Provides seamless support for macOS and iOS, utilizing `NSImage` and `UIImage` interchangeably. 13 | 2. **High Color Bit Support** 14 | - Work with `8 bit`, `16 bit` and `32 bit` graphics. 15 | 3. **Image Formats**: 16 | - Converts between `UIImage`/`NSImage`, `CGImage`, `CIImage`, `CVPixelBuffer`, `CMSampleBuffer` and `MTLTexture`. 17 | - Cross-platofrm support for getting `TIFF`, `PNG` and `JPG` data. 18 | 4. **Metal Texture Utilities**: 19 | - Create empty textures with specified pixel formats and dimensions. 20 | - Support for 2D, 3D, and array textures. 21 | - Texture copying and sampling. 22 | 5. **Raw Data Operations**: 23 | - Extract normalized or raw texture data as `UInt8`, `Float16`, or `Float32`. 24 | - Create textures from raw data arrays. 25 | 26 | --- 27 | 28 | ### Installation 29 | 30 | Add **Texture Map** to your project by integrating it as a Swift package. Use the repository URL: 31 | 32 | ```swift 33 | dependencies: [ 34 | .package(url: "https://github.com/heestand-xyz/TextureMap", from: "2.0.0") 35 | ] 36 | ``` 37 | 38 | --- 39 | 40 | ### Requirements 41 | 42 | - **Platforms**: 43 | - iOS 16.0+ 44 | - macOS 13.0+ 45 | - visionOS 1.0+ 46 | 47 | --- 48 | 49 | ### Usage 50 | 51 | #### Convert Image to Texture 52 | ```swift 53 | import TextureMap 54 | 55 | let image: UIImage = UIImage(named: "Example")! 56 | let texture: MTLTexture = try TextureMap.texture(image: image) 57 | ``` 58 | 59 | #### Extract Raw Data from Texture 60 | ```swift 61 | let rawChannels: [UInt8] = try TextureMap.raw8(texture: texture) 62 | ``` 63 | 64 | #### Convert Texture to Image 65 | ```swift 66 | let outputImage: UIImage = try await texture.image(colorSpace: .sRGB, bits: ._8) 67 | ``` 68 | 69 | #### Copy a Metal Texture 70 | ```swift 71 | let originalTexture: MTLTexture = ... // Your Metal texture 72 | 73 | do { 74 | let copiedTexture: MTLTexture = try await originalTexture.copy() 75 | print("Copied texture: \(copiedTexture)") 76 | } catch { 77 | print("Error copying texture: \(error)") 78 | } 79 | ``` 80 | 81 | #### Create a Texture from Raw Normalized Data 82 | ```swift 83 | let rawTexture: MTLTexture = ... 84 | let bits: TMBits = ._8 85 | 86 | do { 87 | let normalizedRawData: [CGFloat] = try await TextureMap.rawNormalized(texture: rawTexture, bits: bits) 88 | print("Normalized raw data: \(normalizedRawData)") 89 | } catch { 90 | print("Error extracting raw data: \(error)") 91 | } 92 | ``` 93 | 94 | #### Convert Texture Color Space 95 | ```swift 96 | let inputTexture: MTLTexture = ... 97 | let fromColorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB)! 98 | let toColorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.displayP3)! 99 | 100 | do { 101 | let convertedTexture: MTLTexture = try await inputTexture.convertColorSpace(from: fromColorSpace, to: toColorSpace) 102 | print("Converted texture: \(convertedTexture)") 103 | } catch { 104 | print("Error converting texture color space: \(error)") 105 | } 106 | ``` 107 | 108 | --- 109 | 110 | ### Color Spaces 111 | 112 | - **`sRGB`**: Standard RGB space. 113 | - **`Display P3`**: Extended gamut for HDR content. 114 | - **`XDR`**: For high bit graphics displayed on XDR compatible displays. 115 | 116 | --- 117 | 118 | ### Contributing 119 | 120 | Feel free to contribute by submitting pull requests or reporting issues. 121 | 122 | --- 123 | 124 | ### License 125 | 126 | This library is available under the MIT License. 127 | 128 | --- 129 | ### Acknowledgments 130 | 131 | Developed by [Anton Heestand](http://heestand.xyz) 132 | -------------------------------------------------------------------------------- /Sources/TextureMap/Extensions/CGSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2021-10-24. 3 | // 4 | 5 | import MetalKit 6 | import CoreGraphics 7 | 8 | extension MTLTexture { 9 | 10 | public var size: CGSize { 11 | CGSize(width: width, height: height) 12 | } 13 | } 14 | 15 | 16 | extension CGImage { 17 | 18 | public var size: CGSize { 19 | CGSize(width: width, height: height) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/TextureMap/Extensions/CVPixelBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-27. 3 | // 4 | 5 | import Metal 6 | import CoreVideo 7 | import VideoToolbox 8 | 9 | extension CVPixelBuffer { 10 | 11 | public func texture() throws -> MTLTexture { 12 | 13 | var cgImage: CGImage! 14 | 15 | VTCreateCGImageFromCVPixelBuffer(self, options: nil, imageOut: &cgImage) 16 | 17 | return try TextureMap.texture(cgImage: cgImage) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/TextureMap/Extensions/MTLTexture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-01. 3 | // 4 | 5 | import Spatial 6 | import CoreGraphics 7 | import Metal 8 | import MetalPerformanceShaders 9 | 10 | // MARK: - Image 11 | 12 | extension MTLTexture { 13 | 14 | public func image(colorSpace: TMColorSpace, bits: TMBits) async throws -> TMImage { 15 | try TextureMap.image(texture: self, colorSpace: colorSpace, bits: bits) 16 | } 17 | } 18 | 19 | // MARK: - Color Space 20 | 21 | extension MTLTexture { 22 | 23 | public func convertColorSpace(from fromColorSpace: CGColorSpace, to toColorSpace: CGColorSpace) async throws -> MTLTexture { 24 | 25 | let conversionInfo = CGColorConversionInfo(src: fromColorSpace, dst: toColorSpace) 26 | 27 | let conversion = MPSImageConversion(device: device, 28 | srcAlpha: .premultiplied, 29 | destAlpha: .premultiplied, 30 | backgroundColor: nil, 31 | conversionInfo: conversionInfo) 32 | 33 | guard let commandQueue = TextureMap.metalDevice.makeCommandQueue() else { 34 | throw TMError.makeCommandQueueFailed 35 | } 36 | 37 | guard let commandBuffer: MTLCommandBuffer = commandQueue.makeCommandBuffer() else { 38 | throw TMError.makeCommandBufferFailed 39 | } 40 | 41 | let resolution = CGSize(width: width, height: height) 42 | let bits = try TMBits(texture: self) 43 | let targetTexture: MTLTexture = try .empty(resolution: resolution, bits: bits, usage: .write) 44 | 45 | conversion.encode(commandBuffer: commandBuffer, sourceTexture: self, destinationTexture: targetTexture) 46 | 47 | let _: Void = await withCheckedContinuation { continuation in 48 | 49 | commandBuffer.addCompletedHandler { _ in 50 | 51 | continuation.resume() 52 | } 53 | 54 | commandBuffer.commit() 55 | } 56 | 57 | return targetTexture 58 | } 59 | } 60 | 61 | // MARK: Empty Texture 62 | 63 | public enum TextureUsage { 64 | case renderTarget 65 | case write 66 | var textureUsage: MTLTextureUsage { 67 | switch self { 68 | case .renderTarget: 69 | return MTLTextureUsage(rawValue: MTLTextureUsage.renderTarget.rawValue | MTLTextureUsage.shaderRead.rawValue) 70 | case .write: 71 | return MTLTextureUsage(rawValue: MTLTextureUsage.shaderWrite.rawValue | MTLTextureUsage.shaderRead.rawValue) 72 | } 73 | } 74 | } 75 | 76 | extension MTLTexture where Self == MTLTexture { 77 | 78 | // 2D 79 | public static func empty(resolution: CGSize, bits: TMBits, sampleCount: Int = 1, swapRedAndBlue: Bool = false, usage: TextureUsage = .renderTarget) throws -> MTLTexture { 80 | 81 | guard resolution.width >= 1, 82 | resolution.height >= 1 else { 83 | throw TMError.resolutionZero 84 | } 85 | 86 | guard resolution.width <= 16_384, 87 | resolution.height <= 16_384 else { 88 | throw TMError.resolutionTooHigh(maximum: 16_384) 89 | } 90 | 91 | let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: bits.metalPixelFormat(swapRedAndBlue: swapRedAndBlue), width: Int(resolution.width), height: Int(resolution.height), mipmapped: sampleCount == 1) 92 | 93 | descriptor.usage = usage.textureUsage 94 | descriptor.textureType = sampleCount > 1 ? .type2DMultisample : .type2D 95 | descriptor.sampleCount = sampleCount 96 | 97 | guard let texture = TextureMap.metalDevice.makeTexture(descriptor: descriptor) else { 98 | throw TMError.makeTextureFailed 99 | } 100 | 101 | return texture 102 | } 103 | 104 | // 3D 105 | public static func empty3d(resolution: Size3D, bits: TMBits, usage: TextureUsage) throws -> MTLTexture { 106 | 107 | let width = Int(resolution.width) 108 | let height = Int(resolution.height) 109 | let depth = Int(resolution.depth) 110 | 111 | guard width > 0 && height > 0 && depth > 0 else { 112 | throw TMError.resolutionZero 113 | } 114 | 115 | let maximum = 2048 116 | guard width <= maximum && height <= maximum && depth <= maximum else { 117 | throw TMError.resolutionTooHigh(maximum: maximum) 118 | } 119 | 120 | let descriptor = MTLTextureDescriptor() 121 | descriptor.pixelFormat = bits.metalPixelFormat() 122 | descriptor.textureType = .type3D 123 | descriptor.width = width 124 | descriptor.height = height 125 | descriptor.depth = depth 126 | descriptor.usage = usage.textureUsage 127 | 128 | guard let texture = TextureMap.metalDevice.makeTexture(descriptor: descriptor) else { 129 | throw TMError.makeTextureFailed 130 | } 131 | 132 | return texture 133 | } 134 | } 135 | 136 | // MARK: - Copy 137 | 138 | extension MTLTexture { 139 | 140 | public func copy() async throws -> MTLTexture { 141 | 142 | let bits = try TMBits(texture: self) 143 | 144 | let textureCopy: MTLTexture = try .empty(resolution: CGSize(width: width, height: height), bits: bits) 145 | 146 | guard let commandQueue = TextureMap.metalDevice.makeCommandQueue() else { 147 | throw TMError.makeCommandQueueFailed 148 | } 149 | 150 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 151 | throw TMError.makeCommandBufferFailed 152 | } 153 | 154 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 155 | throw TMError.makeBlitCommandEncoderFailed 156 | } 157 | 158 | blitEncoder.copy(from: self, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), sourceSize: MTLSize(width: width, height: height, depth: 1), to: textureCopy, destinationSlice: 0, destinationLevel: 0, destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) 159 | 160 | blitEncoder.endEncoding() 161 | 162 | let _: Void = await withCheckedContinuation { continuation in 163 | 164 | commandBuffer.addCompletedHandler { _ in 165 | 166 | continuation.resume() 167 | } 168 | 169 | commandBuffer.commit() 170 | } 171 | 172 | return textureCopy 173 | } 174 | 175 | public func copy(to texture: MTLTexture) async throws { 176 | 177 | guard let commandQueue = TextureMap.metalDevice.makeCommandQueue() else { 178 | throw TMError.makeCommandQueueFailed 179 | } 180 | 181 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 182 | throw TMError.makeCommandBufferFailed 183 | } 184 | 185 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 186 | throw TMError.makeBlitCommandEncoderFailed 187 | } 188 | 189 | blitEncoder.copy(from: self, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), sourceSize: MTLSize(width: width, height: height, depth: 1), to: texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) 190 | 191 | blitEncoder.endEncoding() 192 | 193 | let _: Void = await withCheckedContinuation { continuation in 194 | 195 | commandBuffer.addCompletedHandler { _ in 196 | 197 | continuation.resume() 198 | } 199 | 200 | commandBuffer.commit() 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Sources/TextureMap/Texture/Array+MTLTexture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-12. 3 | // 4 | 5 | import Foundation 6 | import Metal 7 | 8 | public enum TMTextureArrayType { 9 | 10 | case type3D 11 | case typeArray 12 | 13 | var textureType: MTLTextureType { 14 | switch self { 15 | case .type3D: 16 | return .type3D 17 | case .typeArray: 18 | return .type2DArray 19 | } 20 | } 21 | } 22 | 23 | enum TMTextureArrayError: LocalizedError { 24 | 25 | case empty 26 | case differentResolutions 27 | case differentBits 28 | case differentPixelFormat 29 | case makeCommandQueueFailed 30 | case makeCommandBufferFailed 31 | case makeBlitCommandEncoderFailed 32 | case makeTextureFailed 33 | 34 | var errorDescription: String? { 35 | switch self { 36 | case .empty: 37 | return "Texture Map - Texture Array - Empty" 38 | case .differentResolutions: 39 | return "Texture Map - Texture Array - Different Resolutions" 40 | case .differentBits: 41 | return "Texture Map - Texture Array - Different Bits" 42 | case .differentPixelFormat: 43 | return "Texture Map - Texture Array - Different Pixel Format" 44 | case .makeCommandQueueFailed: 45 | return "Texture Map - Texture Array - Make Command Queue Failed" 46 | case .makeCommandBufferFailed: 47 | return "Texture Map - Texture Array - Make Command Buffer Failed" 48 | case .makeBlitCommandEncoderFailed: 49 | return "Texture Map - Texture Array - Make Blit Command Encoder Failed" 50 | case .makeTextureFailed: 51 | return "Texture Map - Texture Array - Make Texture Failed" 52 | } 53 | } 54 | } 55 | 56 | public extension Array where Element == MTLTexture { 57 | 58 | func texture(type: TMTextureArrayType) async throws -> MTLTexture { 59 | 60 | guard !isEmpty else { 61 | throw TMTextureArrayError.empty 62 | } 63 | 64 | let width = first!.width 65 | let height = first!.height 66 | guard filter({ texture -> Bool in 67 | texture.width == width && texture.height == height 68 | }).count == count else { 69 | throw TMTextureArrayError.differentResolutions 70 | } 71 | 72 | let depth = first!.depth 73 | guard filter({ texture -> Bool in 74 | texture.depth == depth 75 | }).count == count else { 76 | throw TMTextureArrayError.differentBits 77 | } 78 | 79 | let pixelFormat = first!.pixelFormat 80 | guard filter({ texture -> Bool in 81 | texture.pixelFormat == pixelFormat 82 | }).count == count else { 83 | throw TMTextureArrayError.differentPixelFormat 84 | } 85 | 86 | let descriptor = MTLTextureDescriptor() 87 | descriptor.pixelFormat = first!.pixelFormat 88 | descriptor.textureType = type.textureType 89 | descriptor.width = width 90 | descriptor.height = height 91 | descriptor.mipmapLevelCount = first?.mipmapLevelCount ?? 1 92 | switch type { 93 | case .type3D: 94 | descriptor.depth = count 95 | case .typeArray: 96 | descriptor.arrayLength = count 97 | } 98 | 99 | guard let commandQueue: MTLCommandQueue = TextureMap.metalDevice.makeCommandQueue() else { 100 | throw TMTextureArrayError.makeCommandQueueFailed 101 | } 102 | 103 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 104 | throw TMTextureArrayError.makeCommandBufferFailed 105 | } 106 | 107 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 108 | throw TMTextureArrayError.makeBlitCommandEncoderFailed 109 | } 110 | 111 | guard let multiTexture = TextureMap.metalDevice.makeTexture(descriptor: descriptor) else { 112 | throw TMTextureArrayError.makeTextureFailed 113 | } 114 | 115 | for (i, texture) in enumerated() { 116 | blitEncoder.copy(from: texture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), sourceSize: MTLSize(width: texture.width, height: texture.height, depth: 1), to: multiTexture, destinationSlice: type == .type3D ? 0 : i, destinationLevel: 0, destinationOrigin: MTLOrigin(x: 0, y: 0, z: type == .type3D ? i : 0)) 117 | } 118 | 119 | blitEncoder.endEncoding() 120 | 121 | let _: Void = await withCheckedContinuation { continuation in 122 | commandBuffer.addCompletedHandler { _ in 123 | continuation.resume() 124 | } 125 | commandBuffer.commit() 126 | } 127 | 128 | return multiTexture 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/TextureMap/Texture/MTLTexture+sample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-11. 3 | // 4 | 5 | import Metal 6 | import CoreGraphics 7 | 8 | enum TMTextureSampleError: LocalizedError { 9 | 10 | case indexOutOfBounds 11 | case makeCommandQueueFailed 12 | case makeCommandBufferFailed 13 | case makeBlitCommandEncoderFailed 14 | 15 | public var errorDescription: String? { 16 | switch self { 17 | case .indexOutOfBounds: 18 | return "Texture Map - Sample - Index Out of Bounds" 19 | case .makeCommandQueueFailed: 20 | return "Texture Map - Sample - Make Command Queue Failed" 21 | case .makeCommandBufferFailed: 22 | return "Texture Map - Sample - Make Command Buffer Failed" 23 | case .makeBlitCommandEncoderFailed: 24 | return "Texture Map - Sample - Make Blit Command Encoder Failed" 25 | } 26 | } 27 | } 28 | 29 | public extension MTLTexture { 30 | 31 | func sample3d(index: Int, axis: TMAxis, bits: TMBits) async throws -> MTLTexture { 32 | 33 | let length: Int = { 34 | switch axis { 35 | case .x: 36 | return width 37 | case .y: 38 | return height 39 | case .z: 40 | return depth 41 | } 42 | }() 43 | 44 | guard index >= 0 && index < length else { 45 | throw TMTextureSampleError.indexOutOfBounds 46 | } 47 | 48 | let resolution: CGSize = CGSize(width: axis == .x ? depth : width, 49 | height: axis == .y ? depth : height) 50 | 51 | guard let commandQueue: MTLCommandQueue = TextureMap.metalDevice.makeCommandQueue() else { 52 | throw TMTextureSampleError.makeCommandQueueFailed 53 | } 54 | 55 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 56 | throw TMTextureSampleError.makeCommandBufferFailed 57 | } 58 | 59 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 60 | throw TMTextureSampleError.makeBlitCommandEncoderFailed 61 | } 62 | 63 | let targetTexture: MTLTexture = try .empty(resolution: resolution, bits: bits) 64 | 65 | let sourceOrigin = MTLOrigin(x: axis == .x ? index : 0, 66 | y: axis == .y ? index : 0, 67 | z: axis == .z ? index : 0) 68 | 69 | let sourceSize = MTLSize(width: axis == .x ? 1 : width, 70 | height: axis == .y ? 1 : height, 71 | depth: axis == .z ? 1 : depth) 72 | 73 | let destinationOrigin = MTLOrigin(x: 0, y: 0, z: 0) 74 | 75 | blitEncoder.copy(from: self, 76 | sourceSlice: 0, 77 | sourceLevel: 0, 78 | sourceOrigin: sourceOrigin, 79 | sourceSize: sourceSize, 80 | to: targetTexture, 81 | destinationSlice: 0, 82 | destinationLevel: 0, 83 | destinationOrigin: destinationOrigin) 84 | 85 | blitEncoder.endEncoding() 86 | 87 | let _: Void = await withCheckedContinuation { continuation in 88 | commandBuffer.addCompletedHandler { _ in 89 | continuation.resume() 90 | } 91 | commandBuffer.commit() 92 | } 93 | 94 | return targetTexture 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/TextureMap/TextureMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2021-10-15. 3 | // 4 | 5 | import Foundation 6 | @preconcurrency import VideoToolbox 7 | import MetalKit 8 | 9 | public struct TextureMap { 10 | 11 | static let metalDevice: MTLDevice = { 12 | guard let metalDevice = MTLCreateSystemDefaultDevice() else { 13 | fatalError("TextureMap: Default metal device not found.") 14 | } 15 | return metalDevice 16 | }() 17 | } 18 | 19 | // MARK: Texture 20 | 21 | public extension TextureMap { 22 | 23 | enum TextureError: LocalizedError { 24 | 25 | case noImageDataFound 26 | case vtCreateCGImageFromCVPixelBufferFailed 27 | case cmSampleBufferGetImageBufferFailed 28 | case failedToReadCIImageFromURL 29 | case vcMetalTextureCacheCouldNotBeCreated 30 | case cvMetalTextureCacheCreateTextureFromImageFailed 31 | case cvMetalTextureGetTextureFailed 32 | case imageToTextureConversionFailed 33 | case cgContextFailedToCreate 34 | case makeOfCGImageFailed 35 | 36 | public var errorDescription: String? { 37 | switch self { 38 | case .noImageDataFound: 39 | return "TextureMap - Texture - No Image Data Found" 40 | case .vtCreateCGImageFromCVPixelBufferFailed: 41 | return "TextureMap - Texture - VT Create CGImage from CVPixelBuffer Failed" 42 | case .cmSampleBufferGetImageBufferFailed: 43 | return "TextureMap - Texture - CMSampleBuffer Get Image Buffer Failed" 44 | case .failedToReadCIImageFromURL: 45 | return "TextureMap - Failed to Read CIImage from URL" 46 | case .vcMetalTextureCacheCouldNotBeCreated: 47 | return "TextureMap - CV Metal Texture Cache Could Not be Created" 48 | case .cvMetalTextureCacheCreateTextureFromImageFailed: 49 | return "TextureMap - CV Metal Texture Cache Create Texture from Image Failed" 50 | case .cvMetalTextureGetTextureFailed: 51 | return "TextureMap - CV Metal Texture Get Texture Failed" 52 | case .imageToTextureConversionFailed: 53 | return "TextureMap - Image to Texture Conversion Failed" 54 | case .cgContextFailedToCreate: 55 | return "TextureMap - CG Context Failed to Create" 56 | case .makeOfCGImageFailed: 57 | return "TextureMap - Make of CG Image Failed" 58 | } 59 | } 60 | } 61 | 62 | static func texture(image: TMImage) throws -> MTLTexture { 63 | 64 | guard let data = image.tiffData() else { 65 | throw TextureError.noImageDataFound 66 | } 67 | 68 | do { 69 | let loader = MTKTextureLoader(device: metalDevice) 70 | let texture: MTLTexture = try loader.newTexture(data: data, options: nil) 71 | return texture 72 | } catch { 73 | print("TextureMap - Texture Conversion Failed - Reverting to Backup Method") 74 | let bits = try TMBits(image: image) 75 | let colorSpace = try TMColorSpace(image: image) 76 | let ciImage: CIImage = try ciImage(image: image) 77 | let width = Int(ciImage.extent.width) 78 | let height = Int(ciImage.extent.height) 79 | let bounds = CGRect(x: 0, y: 0, width: width, height: height) 80 | let ciContext = CIContext(mtlDevice: metalDevice) 81 | let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( 82 | pixelFormat: bits.metalPixelFormat(), 83 | width: width, 84 | height: height, 85 | mipmapped: false 86 | ) 87 | textureDescriptor.usage = [.shaderRead, .shaderWrite] 88 | guard let texture = metalDevice.makeTexture(descriptor: textureDescriptor) else { 89 | throw TextureError.imageToTextureConversionFailed 90 | } 91 | ciContext.render(ciImage, to: texture, commandBuffer: nil, bounds: bounds, colorSpace: colorSpace.cgColorSpace) 92 | return texture 93 | } 94 | } 95 | 96 | static func texture(cgImage: CGImage) throws -> MTLTexture { 97 | 98 | let loader = MTKTextureLoader(device: metalDevice) 99 | 100 | let texture: MTLTexture = try loader.newTexture(cgImage: cgImage, options: nil) 101 | 102 | return texture 103 | } 104 | 105 | static func textureViaContext(cgImage: CGImage) throws -> MTLTexture { 106 | 107 | let width = cgImage.width 108 | let height = cgImage.height 109 | 110 | let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( 111 | pixelFormat: .rgba8Unorm, 112 | width: width, 113 | height: height, 114 | mipmapped: false 115 | ) 116 | guard let texture = Self.metalDevice.makeTexture(descriptor: textureDescriptor) else { 117 | throw TextureError.imageToTextureConversionFailed 118 | } 119 | 120 | let bytesPerPixel = 4 121 | let bytesPerRow = bytesPerPixel * width 122 | let imageData = UnsafeMutablePointer.allocate( 123 | capacity: width * height * bytesPerPixel 124 | ) 125 | 126 | let colorSpace = CGColorSpaceCreateDeviceRGB() 127 | let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue 128 | guard let context = CGContext( 129 | data: imageData, 130 | width: width, 131 | height: height, 132 | bitsPerComponent: 8, 133 | bytesPerRow: bytesPerRow, 134 | space: colorSpace, 135 | bitmapInfo: bitmapInfo 136 | ) else { 137 | throw TextureError.imageToTextureConversionFailed 138 | } 139 | 140 | context.draw( 141 | cgImage, 142 | in: CGRect( 143 | x: 0, 144 | y: 0, 145 | width: width, 146 | height: height 147 | ) 148 | ) 149 | 150 | let region = MTLRegionMake2D(0, 0, width, height) 151 | texture.replace( 152 | region: region, 153 | mipmapLevel: 0, 154 | withBytes: imageData, 155 | bytesPerRow: bytesPerRow 156 | ) 157 | 158 | return texture 159 | } 160 | 161 | static func texture(ciImage: CIImage, colorSpace: TMColorSpace? = nil, bits: TMBits? = nil) throws -> MTLTexture { 162 | 163 | let cgImage: CGImage = try cgImage(ciImage: ciImage, colorSpace: colorSpace, bits: bits) 164 | 165 | return try texture(cgImage: cgImage) 166 | } 167 | 168 | #if os(macOS) 169 | static func texture(bitmap: NSBitmapImageRep) throws -> MTLTexture { 170 | 171 | guard let data: UnsafeMutablePointer = bitmap.bitmapData else { 172 | throw TMError.bitmapDataNotFound 173 | } 174 | 175 | let texture: MTLTexture = try .empty(resolution: bitmap.size, bits: ._8) 176 | 177 | let region = MTLRegionMake2D(0, 0, bitmap.pixelsWide, bitmap.pixelsHigh) 178 | 179 | texture.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bitmap.bytesPerRow) 180 | 181 | return texture 182 | } 183 | #endif 184 | 185 | static func texture(sampleBuffer: CMSampleBuffer, planeIndex: Int = 0) throws -> MTLTexture { 186 | guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) 187 | else { throw TextureError.cmSampleBufferGetImageBufferFailed } 188 | return try texture(pixelBuffer: pixelBuffer, planeIndex: planeIndex) 189 | } 190 | 191 | static func texture(pixelBuffer: CVPixelBuffer, planeIndex: Int = 0) throws -> MTLTexture { 192 | 193 | var textureCache: CVMetalTextureCache! 194 | CVMetalTextureCacheCreate(nil, nil, metalDevice, nil, &textureCache) 195 | if textureCache == nil { 196 | throw TextureError.vcMetalTextureCacheCouldNotBeCreated 197 | } 198 | 199 | var metalTexture: CVMetalTexture! 200 | var width = CVPixelBufferGetWidth(pixelBuffer) 201 | var height = CVPixelBufferGetHeight(pixelBuffer) 202 | let osType: OSType = CVPixelBufferGetPixelFormatType(pixelBuffer) 203 | let isGrayscale: Bool = osType == OSType(1278226488) 204 | let isPRGB: Bool = osType == OSType(1278226534) 205 | let isVUV: Bool = osType == OSType(875704438) 206 | let format: MTLPixelFormat 207 | if isVUV { 208 | format = planeIndex == 1 ? .rg8Unorm : .r8Unorm 209 | if planeIndex == 1 { 210 | width /= 2 211 | height /= 2 212 | } 213 | } else { 214 | format = isPRGB ? .rg16Float : isGrayscale ? .r8Unorm : .bgra8Unorm 215 | } 216 | CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, format, width, height, planeIndex, &metalTexture) 217 | if metalTexture == nil { 218 | throw TextureError.cvMetalTextureCacheCreateTextureFromImageFailed 219 | } 220 | 221 | let texture: MTLTexture! = CVMetalTextureGetTexture(metalTexture) 222 | if texture == nil { 223 | throw TextureError.cvMetalTextureGetTextureFailed 224 | } 225 | 226 | return texture 227 | } 228 | } 229 | 230 | // MARK: Image 231 | 232 | public extension TextureMap { 233 | 234 | static func image(texture: MTLTexture, colorSpace: TMColorSpace, bits: TMBits) throws -> TMImage { 235 | 236 | let ciImage: CIImage = try ciImage(texture: texture, colorSpace: colorSpace) 237 | 238 | let cgImage: CGImage = try cgImage(ciImage: ciImage, colorSpace: colorSpace, bits: bits) 239 | 240 | return try image(cgImage: cgImage) 241 | } 242 | 243 | static func image(cgImage: CGImage) throws -> TMImage { 244 | #if os(macOS) 245 | return NSImage(cgImage: cgImage, size: cgImage.size) 246 | #else 247 | return UIImage(cgImage: cgImage) 248 | #endif 249 | } 250 | 251 | static func image(ciImage: CIImage) throws -> TMImage { 252 | #if os(macOS) 253 | let rep = NSCIImageRep(ciImage: ciImage) 254 | let nsImage = NSImage(size: rep.size) 255 | nsImage.addRepresentation(rep) 256 | return nsImage 257 | #else 258 | return UIImage(ciImage: ciImage) 259 | #endif 260 | } 261 | 262 | static func write(image: TMImage, to url: URL, bits: TMBits, colorSpace: TMColorSpace) throws { 263 | let ciImage: CIImage = try ciImage(image: image) 264 | try write(ciImage: ciImage, to: url, bits: bits, colorSpace: colorSpace) 265 | } 266 | 267 | static func readImage(from url: URL, xdr: Bool = false) throws -> TMImage { 268 | let ciImage: CIImage = try readImage(from: url, xdr: xdr) 269 | return try image(ciImage: ciImage) 270 | } 271 | } 272 | 273 | // MARK: CIImage 274 | 275 | public extension TextureMap { 276 | 277 | static func ciImage(texture: MTLTexture, colorSpace: TMColorSpace) throws -> CIImage { 278 | 279 | var options: [CIImageOption : Any] = [:] 280 | options[.colorSpace] = colorSpace.cgColorSpace 281 | if colorSpace == .xdr { 282 | if #available(iOS 17.0, tvOS 17.0, macOS 14.0, *) { 283 | options[.expandToHDR] = true 284 | options[.colorSpace] = TMColorSpace.sRGB.cgColorSpace 285 | } 286 | } 287 | 288 | guard let ciImage = CIImage(mtlTexture: texture, options: options) else { 289 | throw TMError.createCIImageFailed 290 | } 291 | 292 | return ciImage 293 | } 294 | 295 | static func ciImage(cgImage: CGImage) -> CIImage { 296 | CIImage(cgImage: cgImage) 297 | } 298 | 299 | static func ciImage(image: TMImage) throws -> CIImage { 300 | #if os(macOS) 301 | guard let data = image.tiffRepresentation else { 302 | throw TMError.tiffRepresentationNotFound 303 | } 304 | guard let ciImage = CIImage(data: data) else { 305 | throw TMError.createCIImageFailed 306 | } 307 | return ciImage 308 | #else 309 | guard let ciImage = CIImage(image: image) else { 310 | throw TMError.createCIImageFailed 311 | } 312 | return ciImage 313 | #endif 314 | } 315 | 316 | static func write(ciImage: CIImage, to url: URL, bits: TMBits, colorSpace: TMColorSpace) throws { 317 | 318 | let context = CIContext(options: nil) 319 | 320 | try context.writePNGRepresentation( 321 | of: ciImage, 322 | to: url, 323 | format: bits.ciFormat, 324 | colorSpace: colorSpace.cgColorSpace, 325 | options: [:]) 326 | } 327 | 328 | static func readImage(from url: URL, xdr: Bool = false) throws -> CIImage { 329 | var options: [CIImageOption: Any] = [:] 330 | if #available(iOS 17.0, tvOS 17.0, macOS 14.0, *) { 331 | options[.expandToHDR] = xdr 332 | } 333 | guard let ciImage = CIImage(contentsOf: url, 334 | options: options) else { 335 | throw TextureError.failedToReadCIImageFromURL 336 | } 337 | return ciImage 338 | } 339 | } 340 | 341 | // MARK: CGImage 342 | 343 | public extension TextureMap { 344 | 345 | static func cgImage(texture: MTLTexture, colorSpace: TMColorSpace, bits: TMBits) throws -> CGImage { 346 | 347 | let ciImage = try ciImage(texture: texture, colorSpace: colorSpace) 348 | 349 | return try cgImage(ciImage: ciImage, colorSpace: colorSpace, bits: bits) 350 | } 351 | 352 | static func copyCGImage(texture: MTLTexture) throws -> CGImage { 353 | 354 | let bits = try TMBits(texture: texture) 355 | let width: Int = texture.width 356 | let height: Int = texture.height 357 | let rowBytes: Int = texture.width * 4 * (bits.rawValue / 8) 358 | let dataSize: Int = rowBytes * height 359 | var data = [UInt8](repeating: 0, count: dataSize) 360 | 361 | let region = MTLRegionMake2D(0, 0, width, height) 362 | texture.getBytes(&data, bytesPerRow: rowBytes, from: region, mipmapLevel: 0) 363 | 364 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 365 | let colorSpace = CGColorSpaceCreateDeviceRGB() 366 | guard let context = CGContext(data: &data, 367 | width: width, 368 | height: height, 369 | bitsPerComponent: bits.rawValue, 370 | bytesPerRow: rowBytes, 371 | space: colorSpace, 372 | bitmapInfo: bitmapInfo.rawValue) else { 373 | throw TextureError.cgContextFailedToCreate 374 | } 375 | 376 | guard let cgImage: CGImage = context.makeImage() else { 377 | throw TextureError.makeOfCGImageFailed 378 | } 379 | 380 | return cgImage 381 | } 382 | 383 | static func cgImage(ciImage: CIImage, colorSpace: TMColorSpace? = nil, bits: TMBits? = nil) throws -> CGImage { 384 | 385 | let bits: TMBits = try bits ?? TMBits(ciImage: ciImage) 386 | 387 | guard let cgColorSpace: CGColorSpace = colorSpace?.coloredCGColorSpace ?? ciImage.colorSpace else { 388 | throw TMError.ciImageColorSpaceNotFound 389 | } 390 | 391 | let context = CIContext(options: nil) 392 | 393 | guard let cgImage: CGImage = context.createCGImage(ciImage, 394 | from: ciImage.extent, 395 | format: bits.ciFormat, 396 | colorSpace: cgColorSpace) else { 397 | throw TMError.createCGImageFailed 398 | } 399 | 400 | return cgImage 401 | } 402 | 403 | static func cgImage(image: TMImage) throws -> CGImage { 404 | #if os(macOS) 405 | var imageRect = CGRect(origin: .zero, size: image.size) 406 | 407 | guard let cgImage: CGImage = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else { 408 | throw TMError.cgImageNotFound 409 | } 410 | 411 | return cgImage 412 | #else 413 | guard let cgImage: CGImage = image.cgImage else { 414 | throw TMError.cgImageNotFound 415 | } 416 | 417 | return cgImage 418 | #endif 419 | } 420 | 421 | static func cgImage(pixelBuffer: CVPixelBuffer) throws -> CGImage { 422 | var cgImage: CGImage! 423 | VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage) 424 | if cgImage == nil { 425 | throw TextureError.vtCreateCGImageFromCVPixelBufferFailed 426 | } 427 | return cgImage 428 | } 429 | } 430 | 431 | // MARK: CVPixelBuffer 432 | 433 | extension TextureMap { 434 | 435 | enum PixelBufferError: LocalizedError { 436 | 437 | case cvPixelBufferCreateFailed 438 | case cvPixelBufferLockBaseAddressFailed 439 | case cgContextFailed 440 | 441 | var errorDescription: String? { 442 | switch self { 443 | case .cvPixelBufferCreateFailed: 444 | return "TextureMap - Pixel Buffer - Create Failed" 445 | case .cvPixelBufferLockBaseAddressFailed: 446 | return "TextureMap - Pixel Buffer - Lock Base Address Failed" 447 | case .cgContextFailed: 448 | return "TextureMap - Pixel Buffer - Context Failed" 449 | } 450 | } 451 | } 452 | 453 | public static func pixelBuffer(texture: MTLTexture, colorSpace: TMColorSpace) throws -> CVPixelBuffer { 454 | 455 | let bits = try TMBits(texture: texture) 456 | 457 | let cgImage: CGImage = try cgImage(texture: texture, colorSpace: colorSpace, bits: bits) 458 | 459 | let pixelBuffer: CVPixelBuffer = try pixelBuffer(cgImage: cgImage, colorSpace: colorSpace, bits: bits) 460 | 461 | return pixelBuffer 462 | } 463 | 464 | public static func pixelBuffer(cgImage: CGImage, colorSpace: TMColorSpace, bits: TMBits) throws -> CVPixelBuffer { 465 | 466 | var optionalPixelBuffer: CVPixelBuffer? 467 | 468 | let attributes: [CFString: Any] = [ 469 | kCVPixelBufferPixelFormatTypeKey: Int(bits.osType) as CFNumber, 470 | kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue!, 471 | kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue!, 472 | kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue!, 473 | ] 474 | 475 | let status = CVPixelBufferCreate(kCFAllocatorDefault, 476 | cgImage.width, 477 | cgImage.height, 478 | bits.osType, 479 | attributes as CFDictionary, 480 | &optionalPixelBuffer) 481 | 482 | guard status == kCVReturnSuccess, let pixelBuffer = optionalPixelBuffer else { 483 | throw PixelBufferError.cvPixelBufferCreateFailed 484 | } 485 | 486 | let flags = CVPixelBufferLockFlags(rawValue: 0) 487 | guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(pixelBuffer, flags) else { 488 | throw PixelBufferError.cvPixelBufferLockBaseAddressFailed 489 | } 490 | defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, flags) } 491 | 492 | guard let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer), 493 | width: cgImage.width, 494 | height: cgImage.height, 495 | bitsPerComponent: bits.rawValue, 496 | bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), 497 | space: colorSpace.cgColorSpace, 498 | bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else { 499 | throw PixelBufferError.cgContextFailed 500 | } 501 | 502 | context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)) 503 | 504 | return pixelBuffer 505 | } 506 | } 507 | 508 | // MARK: CMSampleBuffer 509 | 510 | 511 | extension TextureMap { 512 | 513 | enum SampleBufferError: LocalizedError { 514 | 515 | case failedToCreateSampleBuffer(OSStatus) 516 | 517 | var errorDescription: String? { 518 | switch self { 519 | case .failedToCreateSampleBuffer(let osStatus): 520 | return "TextureMap - Sample Buffer - Failed to Create (OSStatus: \(osStatus))" 521 | } 522 | } 523 | } 524 | 525 | public static func sampleBuffer(texture: MTLTexture, colorSpace: TMColorSpace) throws -> CMSampleBuffer { 526 | 527 | let pixelBuffer: CVPixelBuffer = try pixelBuffer(texture: texture, colorSpace: colorSpace) 528 | 529 | var sampleBuffer: CMSampleBuffer? 530 | 531 | var timingInfo = CMSampleTimingInfo() 532 | var formatDescription: CMFormatDescription? = nil 533 | CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatDescription) 534 | 535 | let osStatus: OSStatus = CMSampleBufferCreateReadyWithImageBuffer( 536 | allocator: kCFAllocatorDefault, 537 | imageBuffer: pixelBuffer, 538 | formatDescription: formatDescription!, 539 | sampleTiming: &timingInfo, 540 | sampleBufferOut: &sampleBuffer 541 | ) 542 | 543 | guard let sampleBuffer else { 544 | throw SampleBufferError.failedToCreateSampleBuffer(osStatus) 545 | } 546 | 547 | return sampleBuffer 548 | } 549 | } 550 | 551 | // MARK: - CGImageSource 552 | 553 | extension TextureMap { 554 | enum ImageSourceError: String, LocalizedError { 555 | case imageSourceCreateFailed 556 | case imageNotFound 557 | case dataProviderNotFound 558 | case unsupportedAssetFile 559 | case dataNotFound 560 | case finalizationFailed 561 | var errorDescription: String? { 562 | switch self { 563 | case .imageSourceCreateFailed: 564 | "Image source create failed." 565 | case .imageNotFound: 566 | "Image not found." 567 | case .dataProviderNotFound: 568 | "Data provider not found." 569 | case .unsupportedAssetFile: 570 | "Unsupported asset file." 571 | case .dataNotFound: 572 | "Data not found." 573 | case .finalizationFailed: 574 | "Finalization failed." 575 | } 576 | } 577 | } 578 | 579 | public static func cgImageSource(cgImage: CGImage) throws -> CGImageSource { 580 | guard let dataProvider: CGDataProvider = cgImage.dataProvider else { 581 | throw ImageSourceError.dataProviderNotFound 582 | } 583 | guard let imageSource: CGImageSource = CGImageSourceCreateWithDataProvider(dataProvider, nil) else { 584 | throw ImageSourceError.imageSourceCreateFailed 585 | } 586 | return imageSource 587 | } 588 | 589 | public static func cgImageSource(url: URL) throws -> CGImageSource { 590 | guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { 591 | throw ImageSourceError.imageSourceCreateFailed 592 | } 593 | return imageSource 594 | } 595 | 596 | public static func cgImageSource(data: Data) throws -> CGImageSource { 597 | guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { 598 | throw ImageSourceError.imageSourceCreateFailed 599 | } 600 | return imageSource 601 | } 602 | } 603 | 604 | -------------------------------------------------------------------------------- /Sources/TextureMap/TexureMap+Raw.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-02. 3 | // 4 | 5 | import Foundation 6 | import Spatial 7 | import CoreGraphics 8 | import MetalKit 9 | 10 | public extension TextureMap { 11 | 12 | enum TMRawError: LocalizedError { 13 | 14 | case badResolution 15 | case unsupportedBits(TMBits) 16 | case unsupportedOS 17 | case unsupportedOSVersion 18 | case failedToMakeCommandBuffer 19 | case failedToMakeBuffer 20 | case failedToMakeCommandEncoder 21 | 22 | public var errorDescription: String? { 23 | switch self { 24 | case .badResolution: 25 | return "Texture Map - Raw - Bad Resolution" 26 | case .unsupportedBits(let bits): 27 | return "Texture Map - Raw - Unsupported Bits (\(bits.rawValue))" 28 | case .unsupportedOS: 29 | return "Texture Map - Raw - Unsupported OS" 30 | case .unsupportedOSVersion: 31 | return "Texture Map - Raw - Unsupported OS Version" 32 | case .failedToMakeCommandBuffer: 33 | return "Texture Map - Raw - Failed to Make Command Buffer" 34 | case .failedToMakeBuffer: 35 | return "Texture Map - Raw - Failed to Make Buffer" 36 | case .failedToMakeCommandEncoder: 37 | return "Texture Map - Raw - Failed to Make Command Encoder" 38 | } 39 | } 40 | } 41 | } 42 | 43 | // MARK: - Raw to Texture 44 | 45 | public extension TextureMap { 46 | 47 | /// 2D 48 | static func texture(channels: [UInt8], resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 49 | let count: Int = channels.count 50 | guard count == Int(resolution.width) * Int(resolution.height) * 4 else { 51 | throw TMRawError.badResolution 52 | } 53 | var channels: [UInt8] = channels 54 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 55 | pointer.initialize(from: &channels, count: count) 56 | return try texture(raw: pointer, resolution: resolution, on: device) 57 | } 58 | 59 | /// 3D 60 | static func texture3d(channels: [UInt8], resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 61 | let count: Int = channels.count 62 | guard count == Int(resolution.width) * Int(resolution.height) * Int(resolution.depth) * 4 else { 63 | throw TMRawError.badResolution 64 | } 65 | var channels: [UInt8] = channels 66 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 67 | pointer.initialize(from: &channels, count: count) 68 | return try texture3d(raw: pointer, resolution: resolution, on: device) 69 | } 70 | 71 | /// 2D 72 | static func texture(raw: UnsafePointer, resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 73 | guard resolution.width > 0 && resolution.height > 0 else { 74 | throw TMRawError.badResolution 75 | } 76 | let bytesPerRow: Int = Int(resolution.width) * 4 77 | let capacity: Int = bytesPerRow * Int(resolution.height) 78 | let texture: MTLTexture = try .empty(resolution: resolution, bits: ._8) 79 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 80 | size: MTLSize(width: Int(resolution.width), 81 | height: Int(resolution.height), 82 | depth: 1)) 83 | raw.withMemoryRebound(to: UInt8.self, capacity: capacity) { rawPointer in 84 | texture.replace(region: region, mipmapLevel: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow) 85 | } 86 | return texture 87 | } 88 | 89 | /// 3D 90 | static func texture3d(raw: UnsafePointer, resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 91 | guard resolution.width > 0 && resolution.height > 0 && resolution.depth > 0 else { 92 | throw TMRawError.badResolution 93 | } 94 | let bytesPerRow: Int = Int(resolution.width) * 4 95 | let bytesPerImage: Int = Int(resolution.width) * Int(resolution.height) * 4 96 | let capacity: Int = bytesPerImage * Int(resolution.depth) 97 | let texture: MTLTexture = try .empty3d(resolution: resolution, bits: ._8, usage: .write) 98 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 99 | size: MTLSize(width: Int(resolution.width), 100 | height: Int(resolution.height), 101 | depth: Int(resolution.depth))) 102 | raw.withMemoryRebound(to: UInt8.self, capacity: capacity) { rawPointer in 103 | texture.replace(region: region, mipmapLevel: 0, slice: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage) 104 | } 105 | return texture 106 | } 107 | 108 | #if !os(macOS) 109 | 110 | /// 2D 111 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 112 | static func texture(channels: [Float16], resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 113 | let count: Int = channels.count 114 | guard count == Int(resolution.width) * Int(resolution.height) * 4 else { 115 | throw TMRawError.badResolution 116 | } 117 | var channels: [Float16] = channels 118 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 119 | pointer.initialize(from: &channels, count: count) 120 | return try texture(raw: pointer, resolution: resolution, on: device) 121 | } 122 | 123 | /// 3D 124 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 125 | static func texture3d(channels: [Float16], resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 126 | let count: Int = channels.count 127 | guard count == Int(resolution.width) * Int(resolution.height) * Int(resolution.depth) * 4 else { 128 | throw TMRawError.badResolution 129 | } 130 | var channels: [Float16] = channels 131 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 132 | pointer.initialize(from: &channels, count: count) 133 | return try texture3d(raw: pointer, resolution: resolution, on: device) 134 | } 135 | 136 | /// 2D 137 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 138 | static func texture(raw: UnsafePointer, resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 139 | let bytesPerRow: Int = Int(resolution.width) * 4 * 2 140 | let capacity: Int = bytesPerRow * Int(resolution.height) 141 | let texture: MTLTexture = try .empty(resolution: resolution, bits: ._16) 142 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 143 | size: MTLSize(width: Int(resolution.width), 144 | height: Int(resolution.height), 145 | depth: 1)) 146 | raw.withMemoryRebound(to: Float16.self, capacity: capacity) { rawPointer in 147 | texture.replace(region: region, mipmapLevel: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow) 148 | } 149 | return texture 150 | } 151 | 152 | /// 3D 153 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 154 | static func texture3d(raw: UnsafePointer, resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 155 | let bytesPerRow: Int = Int(resolution.width) * 4 * 2 156 | let bytesPerImage: Int = Int(resolution.width) * Int(resolution.height) * 4 * 2 157 | let capacity: Int = bytesPerImage * Int(resolution.depth) 158 | let texture: MTLTexture = try .empty3d(resolution: resolution, bits: ._8, usage: .write) 159 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 160 | size: MTLSize(width: Int(resolution.width), 161 | height: Int(resolution.height), 162 | depth: Int(resolution.depth))) 163 | raw.withMemoryRebound(to: Float16.self, capacity: capacity) { rawPointer in 164 | texture.replace(region: region, mipmapLevel: 0, slice: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage) 165 | } 166 | return texture 167 | } 168 | 169 | #endif 170 | 171 | /// 2D 172 | static func texture(channels: [Float], resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 173 | let count: Int = channels.count 174 | guard count == Int(resolution.width) * Int(resolution.height) * 4 else { 175 | throw TMRawError.badResolution 176 | } 177 | var channels: [Float] = channels 178 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 179 | pointer.initialize(from: &channels, count: count) 180 | return try texture(raw: pointer, resolution: resolution, on: device) 181 | } 182 | 183 | /// 3D 184 | static func texture3d(channels: [Float], resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 185 | let count: Int = channels.count 186 | guard count == Int(resolution.width) * Int(resolution.height) * Int(resolution.depth) * 4 else { 187 | throw TMRawError.badResolution 188 | } 189 | var channels: [Float] = channels 190 | let pointer = UnsafeMutablePointer.allocate(capacity: count) 191 | pointer.initialize(from: &channels, count: count) 192 | return try texture3d(raw: pointer, resolution: resolution, on: device) 193 | } 194 | 195 | /// 2D 196 | static func texture(raw: UnsafePointer, resolution: CGSize, on device: MTLDevice) throws -> MTLTexture { 197 | let bytesPerRow: Int = Int(resolution.width) * 4 * 4 198 | let capacity: Int = bytesPerRow * Int(resolution.height) 199 | let texture: MTLTexture = try .empty(resolution: resolution, bits: ._32) 200 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 201 | size: MTLSize(width: Int(resolution.width), 202 | height: Int(resolution.height), 203 | depth: 1)) 204 | raw.withMemoryRebound(to: Float.self, capacity: capacity) { rawPointer in 205 | texture.replace(region: region, mipmapLevel: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow) 206 | } 207 | return texture 208 | } 209 | 210 | /// 3D 211 | static func texture3d(raw: UnsafePointer, resolution: Size3D, on device: MTLDevice) throws -> MTLTexture { 212 | let bytesPerRow: Int = Int(resolution.width) * 4 * 4 213 | let bytesPerImage: Int = Int(resolution.width) * Int(resolution.height) * 4 * 4 214 | let capacity: Int = bytesPerImage * Int(resolution.depth) 215 | let texture: MTLTexture = try .empty3d(resolution: resolution, bits: ._32, usage: .write) 216 | let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), 217 | size: MTLSize(width: Int(resolution.width), 218 | height: Int(resolution.height), 219 | depth: Int(resolution.depth))) 220 | raw.withMemoryRebound(to: Float.self, capacity: capacity) { rawPointer in 221 | texture.replace(region: region, mipmapLevel: 0, slice: 0, withBytes: rawPointer, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage) 222 | } 223 | return texture 224 | } 225 | } 226 | 227 | // MARK: - Texture to Raw 228 | 229 | public extension TextureMap { 230 | 231 | /// 2D 232 | static func raw8(texture: MTLTexture) throws -> [UInt8] { 233 | let bits = try TMBits(texture: texture) 234 | guard bits == ._8 else { 235 | throw TMRawError.unsupportedBits(bits) 236 | } 237 | let region = MTLRegionMake2D(0, 0, texture.width, texture.height) 238 | var raw = Array(repeating: 0, count: texture.width * texture.height * 4) 239 | raw.withUnsafeMutableBytes { 240 | let bytesPerRow = MemoryLayout.size * texture.width * 4 241 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) 242 | } 243 | return raw 244 | } 245 | 246 | /// 2D 247 | static func rawCopy8(texture: MTLTexture, on metalDevice: MTLDevice, in commandQueue: MTLCommandQueue) throws -> [UInt8] { 248 | let bits = try TMBits(texture: texture) 249 | guard bits == ._8 else { 250 | throw TMRawError.unsupportedBits(bits) 251 | } 252 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 253 | throw TMRawError.failedToMakeCommandBuffer 254 | } 255 | let bytesPerTexture = MemoryLayout.size * texture.width * texture.height * 4 256 | let bytesPerRow = MemoryLayout.size * texture.width * 4 257 | guard let imageBuffer = metalDevice.makeBuffer(length: bytesPerTexture, options: []) else { 258 | throw TMRawError.failedToMakeBuffer 259 | } 260 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 261 | throw TMRawError.failedToMakeCommandEncoder 262 | } 263 | blitEncoder.copy(from: texture, 264 | sourceSlice: 0, 265 | sourceLevel: 0, 266 | sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), 267 | sourceSize: MTLSize(width: texture.width, height: texture.height, depth: 1), 268 | to: imageBuffer, 269 | destinationOffset: 0, 270 | destinationBytesPerRow: bytesPerRow, 271 | destinationBytesPerImage: 0) 272 | blitEncoder.endEncoding() 273 | commandBuffer.commit() 274 | commandBuffer.waitUntilCompleted() 275 | var raw = Array(repeating: 0, count: texture.width * texture.height * 4) 276 | memcpy(&raw, imageBuffer.contents(), imageBuffer.length) 277 | return raw 278 | } 279 | 280 | /// 3D 281 | static func raw3d8(texture: MTLTexture) throws -> [UInt8] { 282 | let bits = try TMBits(texture: texture) 283 | guard bits == ._8 else { 284 | throw TMRawError.unsupportedBits(bits) 285 | } 286 | let region = MTLRegionMake3D(0, 0, 0, texture.width, texture.height, texture.depth) 287 | var raw = Array(repeating: 0, count: texture.width * texture.height * texture.depth * 4) 288 | raw.withUnsafeMutableBytes { 289 | let bytesPerRow = MemoryLayout.size * texture.width * 4 290 | let bytesPerImage = MemoryLayout.size * texture.width * texture.height * 4 291 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage, from: region, mipmapLevel: 0, slice: 0) 292 | } 293 | return raw 294 | } 295 | 296 | /// 3D 297 | static func rawCopy3d8(texture: MTLTexture, on metalDevice: MTLDevice, in commandQueue: MTLCommandQueue) throws -> [UInt8] { 298 | let bits = try TMBits(texture: texture) 299 | guard bits == ._8 else { 300 | throw TMRawError.unsupportedBits(bits) 301 | } 302 | guard let commandBuffer = commandQueue.makeCommandBuffer() else { 303 | throw TMRawError.failedToMakeCommandBuffer 304 | } 305 | let bytesPerTexture = MemoryLayout.size * texture.width * texture.height * texture.depth * 4 306 | let bytesPerGrid = MemoryLayout.size * texture.width * texture.height * 4 307 | let bytesPerRow = MemoryLayout.size * texture.width * 4 308 | guard let imageBuffer = metalDevice.makeBuffer(length: bytesPerTexture, options: []) else { 309 | throw TMRawError.failedToMakeBuffer 310 | } 311 | guard let blitEncoder = commandBuffer.makeBlitCommandEncoder() else { 312 | throw TMRawError.failedToMakeCommandEncoder 313 | } 314 | blitEncoder.copy(from: texture, 315 | sourceSlice: 0, 316 | sourceLevel: 0, 317 | sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), 318 | sourceSize: MTLSize(width: texture.width, 319 | height: texture.height, 320 | depth: texture.depth), 321 | to: imageBuffer, 322 | destinationOffset: 0, 323 | destinationBytesPerRow: bytesPerRow, 324 | destinationBytesPerImage: bytesPerGrid) 325 | blitEncoder.endEncoding() 326 | commandBuffer.commit() 327 | commandBuffer.waitUntilCompleted() 328 | var raw = Array(repeating: 0, count: texture.width * texture.height * texture.depth * 4) 329 | memcpy(&raw, imageBuffer.contents(), imageBuffer.length) 330 | return raw 331 | } 332 | 333 | #if !os(macOS) 334 | 335 | /// 2D 336 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 337 | static func raw16(texture: MTLTexture) throws -> [Float16] { 338 | let bits = try TMBits(texture: texture) 339 | guard bits == ._16 else { 340 | throw TMRawError.unsupportedBits(bits) 341 | } 342 | let region = MTLRegionMake2D(0, 0, texture.width, texture.height) 343 | var raw = Array(repeating: -1.0, count: texture.width * texture.height * 4) 344 | raw.withUnsafeMutableBytes { 345 | let bytesPerRow = MemoryLayout.size * texture.width * 4 346 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) 347 | } 348 | return raw 349 | } 350 | 351 | /// 3D 352 | @available(iOS 14.0, tvOS 14.0, macOS 11.0, *) 353 | static func raw3d16(texture: MTLTexture) throws -> [Float16] { 354 | let bits = try TMBits(texture: texture) 355 | guard bits == ._16 else { 356 | throw TMRawError.unsupportedBits(bits) 357 | } 358 | let region = MTLRegionMake3D(0, 0, 0, texture.width, texture.height, texture.depth) 359 | var raw = Array(repeating: -1.0, count: texture.width * texture.height * texture.depth * 4) 360 | raw.withUnsafeMutableBytes { 361 | let bytesPerRow = MemoryLayout.size * texture.width * 4 362 | let bytesPerImage = MemoryLayout.size * texture.width * texture.height * 4 363 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage, from: region, mipmapLevel: 0, slice: 0) 364 | } 365 | return raw 366 | } 367 | 368 | #endif 369 | 370 | /// 2D 371 | static func raw32(texture: MTLTexture) throws -> [Float] { 372 | let bits = try TMBits(texture: texture) 373 | guard bits == ._32 else { 374 | throw TMRawError.unsupportedBits(bits) 375 | } 376 | let region = MTLRegionMake2D(0, 0, texture.width, texture.height) 377 | var raw = Array(repeating: -1.0, count: texture.width * texture.height * 4) 378 | raw.withUnsafeMutableBytes { 379 | let bytesPerRow = MemoryLayout.size * texture.width * 4 380 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, from: region, mipmapLevel: 0) 381 | } 382 | return raw 383 | } 384 | 385 | /// 3D 386 | static func raw3d32(texture: MTLTexture) throws -> [Float] { 387 | let bits = try TMBits(texture: texture) 388 | guard bits == ._32 else { 389 | throw TMRawError.unsupportedBits(bits) 390 | } 391 | let region = MTLRegionMake3D(0, 0, 0, texture.width, texture.height, texture.depth) 392 | var raw = Array(repeating: -1.0, count: texture.width * texture.height * texture.depth * 4) 393 | raw.withUnsafeMutableBytes { 394 | let bytesPerRow = MemoryLayout.size * texture.width * 4 395 | let bytesPerImage = MemoryLayout.size * texture.width * texture.height * 4 396 | texture.getBytes($0.baseAddress!, bytesPerRow: bytesPerRow, bytesPerImage: bytesPerImage, from: region, mipmapLevel: 0, slice: 0) 397 | } 398 | return raw 399 | } 400 | 401 | /// 2D 402 | static func rawNormalized(texture: MTLTexture, bits: TMBits) async throws -> [CGFloat] { 403 | 404 | try await withCheckedThrowingContinuation { continuation in 405 | 406 | do { 407 | 408 | let channels = try rawNormalized(texture: texture, bits: bits) 409 | 410 | continuation.resume(returning: channels) 411 | 412 | } catch { 413 | 414 | continuation.resume(throwing: error) 415 | } 416 | } 417 | } 418 | 419 | /// 2D 420 | static func rawNormalized(texture: MTLTexture, bits: TMBits) throws -> [CGFloat] { 421 | let raw: [CGFloat] 422 | switch bits { 423 | case ._8: 424 | raw = try raw8(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) / (pow(2, 8) - 1) }) 425 | case ._16: 426 | #if !os(macOS) 427 | if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) { 428 | raw = try raw16(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) }) 429 | } else { 430 | throw TMRawError.unsupportedOSVersion 431 | } 432 | #else 433 | throw TMRawError.unsupportedOS 434 | #endif 435 | case ._32: 436 | raw = try raw32(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) }) 437 | } 438 | return raw 439 | } 440 | 441 | /// 2D 442 | static func rawNormalizedCopy(texture: MTLTexture, bits: TMBits, on metalDevice: MTLDevice, in commandQueue: MTLCommandQueue) throws -> [CGFloat] { 443 | let raw: [CGFloat] 444 | switch bits { 445 | case ._8: 446 | raw = try rawCopy8(texture: texture, on: metalDevice, in: commandQueue).map({ chan -> CGFloat in 447 | return CGFloat(chan) / (pow(2, 8) - 1) 448 | }) 449 | default: 450 | throw TMRawError.unsupportedBits(bits) 451 | } 452 | return raw 453 | } 454 | 455 | /// 3D 456 | static func rawNormalized3d(texture: MTLTexture, bits: TMBits) async throws -> [CGFloat] { 457 | 458 | try await withCheckedThrowingContinuation { continuation in 459 | 460 | do { 461 | 462 | let channels = try rawNormalized3d(texture: texture, bits: bits) 463 | 464 | continuation.resume(returning: channels) 465 | 466 | } catch { 467 | 468 | continuation.resume(throwing: error) 469 | } 470 | } 471 | } 472 | 473 | /// 3D 474 | static func rawNormalized3d(texture: MTLTexture, bits: TMBits) throws -> [CGFloat] { 475 | let raw: [CGFloat] 476 | switch bits { 477 | case ._8: 478 | raw = try raw3d8(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) / (pow(2, 8) - 1) }) 479 | case ._16: 480 | #if !os(macOS) 481 | if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) { 482 | raw = try raw3d16(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) }) 483 | } else { 484 | throw TMRawError.unsupportedOSVersion 485 | } 486 | #else 487 | throw TMRawError.unsupportedOS 488 | #endif 489 | case ._32: 490 | raw = try raw3d32(texture: texture).map({ chan -> CGFloat in return CGFloat(chan) }) 491 | } 492 | return raw 493 | } 494 | 495 | /// 3D 496 | static func rawNormalizedCopy3d(texture: MTLTexture, bits: TMBits, on metalDevice: MTLDevice, in commandQueue: MTLCommandQueue) throws -> [CGFloat] { 497 | let raw: [CGFloat] 498 | switch bits { 499 | case ._8: 500 | raw = try rawCopy3d8(texture: texture, on: metalDevice, in: commandQueue).map({ chan -> CGFloat in return CGFloat(chan) / (pow(2, 8) - 1) }) 501 | // case ._16: 502 | // raw = try rawCopy3d16(texture: texture, on: metalDevice, in: commandQueue).map({ chan -> CGFloat in return CGFloat(chan) }) 503 | default: 504 | throw TMRawError.unsupportedBits(bits) 505 | } 506 | return raw 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMAxis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-11. 3 | // 4 | 5 | public enum TMAxis { 6 | case x 7 | case y 8 | case z 9 | } 10 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMBits.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2021-10-15. 3 | // 4 | 5 | import Foundation 6 | import MetalKit 7 | 8 | public enum TMBits: Int, Codable, CaseIterable, Comparable, Sendable { 9 | 10 | case _8 = 8 11 | case _16 = 16 12 | case _32 = 32 13 | 14 | public static func < (lhs: TMBits, rhs: TMBits) -> Bool { 15 | lhs.rawValue < rhs.rawValue 16 | } 17 | } 18 | 19 | // MARK: Format 20 | 21 | public extension TMBits { 22 | 23 | /// Metal Pixel Format 24 | /// - Parameter swapRedAndBlue: RGBA when `false`, BGRA when `true` *(8 bit only)* 25 | func metalPixelFormat(swapRedAndBlue: Bool = false) -> MTLPixelFormat { 26 | switch self { 27 | case ._8: return swapRedAndBlue ? .bgra8Unorm : .rgba8Unorm 28 | case ._16: return .rgba16Float 29 | case ._32: return .rgba32Float 30 | } 31 | } 32 | 33 | var ciFormat: CIFormat { 34 | switch self { 35 | case ._8: return .RGBA8 36 | case ._16: return .RGBAh 37 | case ._32: return .RGBAf 38 | } 39 | } 40 | 41 | var osType: OSType { 42 | switch self { 43 | case ._8: return kCVPixelFormatType_32BGRA 44 | case ._16: return kCVPixelFormatType_64RGBAHalf 45 | case ._32: return kCVPixelFormatType_128RGBAFloat 46 | } 47 | } 48 | 49 | } 50 | 51 | // MARK: Init 52 | 53 | public extension TMBits { 54 | 55 | init(metalPixelFormat: MTLPixelFormat) throws { 56 | 57 | var bits: Self? 58 | 59 | for currentBits in Self.allCases { 60 | 61 | if currentBits.metalPixelFormat(swapRedAndBlue: false) == metalPixelFormat { 62 | bits = currentBits 63 | } else if currentBits.metalPixelFormat(swapRedAndBlue: true) == metalPixelFormat { 64 | bits = currentBits 65 | } 66 | } 67 | 68 | if bits == nil { 69 | if metalPixelFormat == .bgra8Unorm_srgb { 70 | bits = ._8 71 | } else if [.r32Float, .rg32Float].contains(metalPixelFormat) { 72 | bits = ._32 73 | } else if [.r16Float, .rg16Float, .rgba16Unorm].contains(metalPixelFormat) { 74 | bits = ._16 75 | } else if metalPixelFormat == .r8Unorm { 76 | bits = ._8 77 | } else if metalPixelFormat == .rg8Unorm { 78 | bits = ._8 79 | } 80 | } 81 | 82 | if let bits: Self = bits { 83 | self = bits 84 | } else { 85 | throw TMBitsError.metalPixelFormatNotSupported(metalPixelFormat) 86 | } 87 | } 88 | 89 | init(texture: MTLTexture) throws { 90 | self = try Self(metalPixelFormat: texture.pixelFormat) 91 | } 92 | 93 | init(cgImage: CGImage) throws { 94 | var bits: Self! 95 | switch cgImage.bitsPerComponent { 96 | case 8: 97 | bits = ._8 98 | case 16: 99 | bits = ._16 100 | default: 101 | throw TMBitsError.bitsPerComponentNotSupported(cgImage.bitsPerComponent) 102 | } 103 | self = bits 104 | } 105 | 106 | init(image: TMImage) throws { 107 | #if os(macOS) 108 | guard let cgImage: CGImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 109 | throw TMBitsError.cgImageNotFound 110 | } 111 | #else 112 | guard let cgImage: CGImage = image.cgImage else { 113 | throw TMBitsError.cgImageNotFound 114 | } 115 | #endif 116 | self = try Self(cgImage: cgImage) 117 | } 118 | 119 | init(ciImage: CIImage) throws { 120 | guard let cgImage: CGImage = ciImage.cgImage else { 121 | throw TMBitsError.cgImageNotFound 122 | } 123 | self = try Self(cgImage: cgImage) 124 | } 125 | 126 | #if os(macOS) 127 | init(bitmap: NSBitmapImageRep) throws { 128 | guard let cgImage: CGImage = bitmap.cgImage else { 129 | throw TMBitsError.cgImageNotFound 130 | } 131 | self = try Self(cgImage: cgImage) 132 | } 133 | #endif 134 | 135 | } 136 | 137 | // MARK: - Image 138 | 139 | public extension TMImage { 140 | 141 | var bits: TMBits { 142 | get throws { 143 | try TMBits(image: self) 144 | } 145 | } 146 | } 147 | 148 | // MARK: - Error 149 | 150 | public extension TMBits { 151 | 152 | enum TMBitsError: LocalizedError { 153 | 154 | case metalPixelFormatNotSupported(MTLPixelFormat) 155 | case cgImageNotFound 156 | case bitsPerComponentNotSupported(Int) 157 | 158 | public var errorDescription: String? { 159 | switch self { 160 | case .metalPixelFormatNotSupported(let metalPixelFormat): 161 | return "Texture Map - Bits - Metal Pixel Format (\(metalPixelFormat.rawValue)) - Not Supported" 162 | case .cgImageNotFound: 163 | return "Texture Map - Bits - Core Graphics Image - Not Found" 164 | case .bitsPerComponentNotSupported(let bitsPerComponent): 165 | return "Texture Map - Bits - Bits Per Component (\(bitsPerComponent)) - Not Supported" 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-08-26. 3 | // 4 | 5 | #if os(macOS) 6 | import AppKit 7 | #else 8 | import UIKit 9 | #endif 10 | 11 | #if os(macOS) 12 | public typealias TMColor = NSColor 13 | #else 14 | public typealias TMColor = UIColor 15 | #endif 16 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMColorSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Anton Heestand on 2022-04-02. 3 | // 4 | 5 | import Foundation 6 | import CoreGraphics 7 | import CoreImage 8 | #if os(macOS) 9 | import AppKit 10 | #endif 11 | 12 | public enum TMColorSpace: Equatable, Sendable { 13 | case linearSRGB 14 | case nonLinearSRGB 15 | case linearDisplayP3 16 | case nonLinearDisplayP3 17 | case xdr 18 | case custom(CGColorSpace) 19 | } 20 | 21 | // MARK: - Defaults 22 | 23 | extension TMColorSpace { 24 | public static let sRGB: TMColorSpace = .nonLinearSRGB 25 | public static let displayP3: TMColorSpace = .nonLinearDisplayP3 26 | } 27 | 28 | // MARK: - Is 29 | 30 | extension TMColorSpace { 31 | var isSRGB: Bool { 32 | [.linearSRGB, .nonLinearSRGB].contains(self) 33 | } 34 | 35 | var isDisplayP3: Bool { 36 | [.linearDisplayP3, .nonLinearDisplayP3].contains(self) 37 | } 38 | } 39 | 40 | // MARK: - Cases 41 | 42 | extension TMColorSpace { 43 | private static var nonCustomCases: [TMColorSpace] { 44 | [ 45 | .linearSRGB, 46 | .nonLinearSRGB, 47 | .linearDisplayP3, 48 | .nonLinearDisplayP3, 49 | .xdr 50 | ] 51 | } 52 | } 53 | 54 | // MARK: - Description 55 | 56 | extension TMColorSpace: CustomStringConvertible { 57 | public var description: String { 58 | switch self { 59 | case .linearSRGB: 60 | return "Linear sRGB" 61 | case .nonLinearSRGB: 62 | return "Non Linear sRGB" 63 | case .linearDisplayP3: 64 | return "Linear Display P3" 65 | case .nonLinearDisplayP3: 66 | return "Non Linear Display P3" 67 | case .xdr: 68 | return "XDR" 69 | case .custom(let cgColorSpace): 70 | return "Custom: \(cgColorSpace)" 71 | } 72 | } 73 | } 74 | 75 | // MARK: - CG Color Space 76 | 77 | extension TMColorSpace { 78 | 79 | public var cgColorSpace: CGColorSpace { 80 | switch self { 81 | case .linearSRGB: 82 | return CGColorSpace(name: CGColorSpace.linearSRGB)! 83 | case .nonLinearSRGB: 84 | return CGColorSpace(name: CGColorSpace.sRGB)! 85 | case .linearDisplayP3: 86 | return CGColorSpace(name: CGColorSpace.linearDisplayP3)! 87 | case .nonLinearDisplayP3: 88 | return CGColorSpace(name: CGColorSpace.displayP3)! 89 | case .xdr: 90 | return CGColorSpace(name: CGColorSpace.itur_2100_PQ)! // HLG 91 | case .custom(let cgColorSpace): 92 | return cgColorSpace 93 | } 94 | } 95 | 96 | var coloredCGColorSpace: CGColorSpace { 97 | if isMonochrome { 98 | return CGColorSpace(name: CGColorSpace.sRGB)! 99 | } else { 100 | return cgColorSpace 101 | } 102 | } 103 | } 104 | 105 | // MARK: - Monochrome 106 | 107 | public extension TMColorSpace { 108 | 109 | var isMonochrome: Bool { 110 | switch self { 111 | case .linearSRGB, .nonLinearSRGB, .linearDisplayP3, .nonLinearDisplayP3, .xdr: 112 | return false 113 | case .custom(let cgColorSpace): 114 | return cgColorSpace.model == .monochrome 115 | } 116 | } 117 | } 118 | 119 | // MARK: - Life Cycle 120 | 121 | public extension TMColorSpace { 122 | 123 | init(cgColorSpace: CGColorSpace) throws { 124 | 125 | for nonCustomCase in Self.nonCustomCases { 126 | if cgColorSpace == nonCustomCase.cgColorSpace { 127 | self = nonCustomCase 128 | return 129 | } 130 | } 131 | 132 | if cgColorSpace.name == CGColorSpace.extendedSRGB { 133 | self = .nonLinearDisplayP3 134 | return 135 | } else if cgColorSpace == CGColorSpace(name: CGColorSpace.itur_2100_PQ)! || 136 | cgColorSpace == CGColorSpace(name: CGColorSpace.itur_2100_HLG)! { 137 | self = .xdr 138 | return 139 | } else { 140 | self = .custom(cgColorSpace) 141 | } 142 | } 143 | 144 | init(cgImage: CGImage) throws { 145 | 146 | guard let colorSpace: CGColorSpace = cgImage.colorSpace else { 147 | throw TMColorSpaceError.notFound 148 | } 149 | 150 | try self.init(cgColorSpace: colorSpace) 151 | } 152 | 153 | init(image: TMImage) throws { 154 | 155 | #if os(macOS) 156 | 157 | if image.representations.isEmpty { 158 | throw TMColorSpaceError.noRepresentationsFound 159 | } 160 | 161 | let colorSpaces: [TMColorSpace] = try image.representations.map { representation in 162 | guard let cgImage: CGImage = representation.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 163 | throw TMColorSpaceError.cgImageNotFound 164 | } 165 | return try TMColorSpace(cgImage: cgImage) 166 | } 167 | 168 | if colorSpaces.contains(.linearDisplayP3) { 169 | 170 | self = .linearDisplayP3 171 | 172 | } else if colorSpaces.contains(.nonLinearDisplayP3) { 173 | 174 | self = .nonLinearDisplayP3 175 | 176 | } else if colorSpaces.contains(.linearSRGB) { 177 | 178 | self = .linearSRGB 179 | 180 | } else if colorSpaces.contains(.nonLinearSRGB) { 181 | 182 | self = .nonLinearSRGB 183 | 184 | } else { 185 | 186 | guard let cgImage: CGImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { 187 | throw TMColorSpaceError.cgImageNotFound 188 | } 189 | 190 | try self.init(cgImage: cgImage) 191 | } 192 | 193 | #else 194 | 195 | guard let cgImage: CGImage = image.cgImage else { 196 | throw TMColorSpaceError.cgImageNotFound 197 | } 198 | 199 | try self.init(cgImage: cgImage) 200 | 201 | #endif 202 | 203 | } 204 | 205 | init(ciImage: CIImage) throws { 206 | 207 | guard let colorSpace: CGColorSpace = ciImage.colorSpace else { 208 | throw TMColorSpaceError.notFound 209 | } 210 | 211 | try self.init(cgColorSpace: colorSpace) 212 | } 213 | 214 | #if os(macOS) 215 | init(bitmap: NSBitmapImageRep) throws { 216 | 217 | guard let cgImage: CGImage = bitmap.cgImage else { 218 | throw TMColorSpaceError.cgImageNotFound 219 | } 220 | 221 | try self.init(cgImage: cgImage) 222 | } 223 | #endif 224 | 225 | } 226 | 227 | // MARK: - Image 228 | 229 | public extension TMImage { 230 | 231 | var colorSpace: TMColorSpace { 232 | get throws { 233 | try TMColorSpace(image: self) 234 | } 235 | } 236 | } 237 | 238 | // MARK: - Error 239 | 240 | public extension TMColorSpace { 241 | 242 | enum TMColorSpaceError: LocalizedError { 243 | 244 | case notFound 245 | case cgImageNotFound 246 | case notSupported(CGColorSpace) 247 | case noRepresentationsFound 248 | 249 | public var errorDescription: String? { 250 | switch self { 251 | case .notFound: 252 | return "Texture Map - Color Space - Not Found" 253 | case .cgImageNotFound: 254 | return "Texture Map - Color Space - Core Graphics Image Not Found" 255 | case .notSupported(let colorSpace): 256 | return "Texture Map - Color Space - Not Supported [\(colorSpace)]" 257 | case .noRepresentationsFound: 258 | return "Texture Map - Color Space - No Representations Found" 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Anton Heestand on 2022-05-13. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TMError: LocalizedError { 11 | 12 | case cgImageNotFound 13 | case createCGImageFailed 14 | case createCIImageFailed 15 | case ciImageColorSpaceNotFound 16 | case tiffRepresentationNotFound 17 | case resolutionZero 18 | case resolutionTooHigh(maximum: Int) 19 | case makeTextureFailed 20 | case bitmapDataNotFound 21 | case makeCommandQueueFailed 22 | case makeCommandBufferFailed 23 | case makeBlitCommandEncoderFailed 24 | 25 | public var errorDescription: String? { 26 | switch self { 27 | case .cgImageNotFound: 28 | return "Texture Map - CGImage Not Found" 29 | case .createCGImageFailed: 30 | return "Texture Map - Create CGImage Failed" 31 | case .createCIImageFailed: 32 | return "Texture Map - Create CIImage Failed" 33 | case .ciImageColorSpaceNotFound: 34 | return "Texture Map - CIImage Color Space Not Found" 35 | case .tiffRepresentationNotFound: 36 | return "Texture Map - TIFF Representation Not Found" 37 | case .resolutionZero: 38 | return "Texture Map - Resolution Zero" 39 | case .resolutionTooHigh(let maximum): 40 | return "Texture Map - Resolution too High (Maximum: \(maximum))" 41 | case .makeTextureFailed: 42 | return "Texture Map - Make Texture Failed" 43 | case .bitmapDataNotFound: 44 | return "Texture Map - Bitmap Data Not Found" 45 | case .makeCommandQueueFailed: 46 | return "Texture Map - Texture Array - Make Command Queue Failed" 47 | case .makeCommandBufferFailed: 48 | return "Texture Map - Texture Array - Make Command Buffer Failed" 49 | case .makeBlitCommandEncoderFailed: 50 | return "Texture Map - Texture Array - Make Blit Command Encoder Failed" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // Created by Anton Heestand on 2021-02-23. 4 | // 5 | 6 | import Foundation 7 | import SwiftUI 8 | #if !os(macOS) 9 | import MobileCoreServices 10 | #endif 11 | import UniformTypeIdentifiers 12 | 13 | #if os(macOS) 14 | public typealias TMImage = NSImage 15 | public typealias TMImageView = NSImageView 16 | public extension Image { 17 | init(tmImage: NSImage) { 18 | self.init(nsImage: tmImage) 19 | } 20 | } 21 | #else 22 | public typealias TMImage = UIImage 23 | public typealias TMImageView = UIImageView 24 | public extension Image { 25 | init(tmImage: UIImage) { 26 | self.init(uiImage: tmImage) 27 | } 28 | } 29 | #endif 30 | 31 | public extension Bundle { 32 | 33 | enum TMImageBundleError: LocalizedError { 34 | 35 | case imageNotFound(bundle: Bundle, imageName: String) 36 | 37 | public var errorDescription: String? { 38 | 39 | switch self { 40 | case let .imageNotFound(bundle, imageName): 41 | return "Texture Map - Bundle (\(bundle.bundleIdentifier ?? "unknown")) - Image - Not Found - Name: \"\(imageName)\"" 42 | } 43 | } 44 | } 45 | 46 | func image(named name: String) throws -> TMImage { 47 | 48 | #if os(macOS) 49 | 50 | guard let image: NSImage = image(forResource: name) else { 51 | throw TMImageBundleError.imageNotFound(bundle: self, imageName: name) 52 | } 53 | 54 | return image 55 | 56 | #else 57 | 58 | guard let image: UIImage = UIImage(named: name, in: self, with: nil) else { 59 | throw TMImageBundleError.imageNotFound(bundle: self, imageName: name) 60 | } 61 | 62 | return image 63 | 64 | #endif 65 | } 66 | } 67 | 68 | public extension TMImage { 69 | 70 | var texture: MTLTexture { 71 | get throws { 72 | try TextureMap.texture(image: self) 73 | } 74 | } 75 | } 76 | 77 | #if os(macOS) 78 | public extension NSImage { 79 | func pngData() -> Data? { 80 | guard let representation = tiffRepresentation else { return nil } 81 | guard let bitmap = NSBitmapImageRep(data: representation) else { return nil } 82 | return bitmap.representation(using: .png, properties: [:]) 83 | } 84 | func jpegData(compressionQuality: CGFloat) -> Data? { 85 | guard let representation = tiffRepresentation else { return nil } 86 | guard let bitmap = NSBitmapImageRep(data: representation) else { return nil } 87 | return bitmap.representation(using: .jpeg, properties: [.compressionFactor: compressionQuality]) 88 | } 89 | func tiffData() -> Data? { 90 | tiffRepresentation 91 | } 92 | } 93 | #else 94 | public extension UIImage { 95 | func tiffData() -> Data? { 96 | guard let cgImage 97 | else { return nil } 98 | let options: NSDictionary = [ 99 | kCGImagePropertyOrientation: imageOrientation, 100 | kCGImagePropertyHasAlpha: true 101 | ] 102 | let data = NSMutableData() 103 | guard let imageDestination = CGImageDestinationCreateWithData(data as CFMutableData, UTType.tiff.identifier as CFString, 1, nil) 104 | else { return nil } 105 | CGImageDestinationAddImage(imageDestination, cgImage, options) 106 | CGImageDestinationFinalize(imageDestination) 107 | return data as Data 108 | } 109 | } 110 | #endif 111 | 112 | #if os(macOS) 113 | public extension NSImage { 114 | var scale: CGFloat { 115 | guard let pixelsWide: Int = representations.first?.pixelsWide else { return 1.0 } 116 | let scale: CGFloat = CGFloat(pixelsWide) / size.width 117 | return scale 118 | } 119 | } 120 | #endif 121 | 122 | #if os(macOS) 123 | public extension NSImage { 124 | var cgImage: CGImage? { 125 | var frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) 126 | return cgImage(forProposedRect: &frame, context: nil, hints: nil) 127 | } 128 | } 129 | #else 130 | public extension UIImage { 131 | convenience init(cgImage: CGImage, size: CGSize) { 132 | self.init(cgImage: cgImage) 133 | } 134 | } 135 | #endif 136 | 137 | #if !os(macOS) 138 | public extension UIImage { 139 | convenience init?(contentsOf url: URL) { 140 | self.init(contentsOfFile: url.path(percentEncoded: false)) 141 | } 142 | } 143 | #endif 144 | -------------------------------------------------------------------------------- /Sources/TextureMap/Types/TMSendableImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TMSendableImage.swift 3 | // TextureMap 4 | // 5 | // Created by Anton on 2024-12-24. 6 | // 7 | 8 | public struct TMSendableImage: @unchecked Sendable { 9 | 10 | private let image: TMImage 11 | 12 | fileprivate init(image: TMImage) { 13 | self.image = image 14 | } 15 | } 16 | 17 | extension TMSendableImage { 18 | public func receive() -> TMImage { 19 | image 20 | } 21 | } 22 | 23 | extension TMImage { 24 | public func send() -> TMSendableImage { 25 | TMSendableImage(image: self) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/TextureMapTests/TextureMapTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TextureMap 3 | 4 | final class TextureMapTests: XCTestCase { 5 | 6 | func testEmptyTexture() async throws { 7 | 8 | let size = CGSize(width: 200, height: 100) 9 | 10 | let emptyTexture: MTLTexture = try .empty(resolution: size, bits: ._8) 11 | 12 | XCTAssertEqual(emptyTexture.width, Int(size.width)) 13 | XCTAssertEqual(emptyTexture.height, Int(size.height)) 14 | } 15 | } 16 | --------------------------------------------------------------------------------