diff --git a/.github/FUNDING.yml~ b/.github/FUNDING.yml~ deleted file mode 100644 index e69de29..0000000 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md~ b/.github/ISSUE_TEMPLATE/bug_report.md~ deleted file mode 100644 index e69de29..0000000 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md~ b/.github/ISSUE_TEMPLATE/feature_request.md~ deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index bcde836..cda61a2 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -17,6 +17,6 @@ jobs: steps: - uses: actions/checkout@v3 - name: Build - run: swift build -v -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.0-simulator" + run: swift build -v - name: Run tests - run: xcodebuild test -scheme OpenAIKit -sdk iphonesimulator -destination "OS=16.0,name=iPhone 14 Pro Max" + run: swift test -v diff --git a/README.md b/README.md index 062072a..d429416 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,7 @@ OpenAIKit is a community-maintained API for the OpenAI REST endpoint used to get | Platform | Minimum Swift Version | Installation | Status | | --- | --- | --- | --- | -| iOS 13.0+ / tvOS 13.0+ / watchOS 6.0+ | 5.5 | [Swift Package Manager](#swift-package-manager) | Fully Tested | -| macOS 10.15+ | N/A | N/A | Work in Progress | +| iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ | 5.5 | [Swift Package Manager](#swift-package-manager) | Fully Tested | ## Installation diff --git a/Sources/OpenAIKit/Extensions/NSImageExtension.swift b/Sources/OpenAIKit/Extensions/NSImageExtension.swift new file mode 100644 index 0000000..b25ae3e --- /dev/null +++ b/Sources/OpenAIKit/Extensions/NSImageExtension.swift @@ -0,0 +1,82 @@ +// +// NSImageExtension.swift +// OpenAIKit +// +// Copyright (c) 2022 MarcoDotIO +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if os(macOS) +import Cocoa + +extension NSImage { + public func pngData( + size: ImageResolutions, + imageInterpolation: NSImageInterpolation = .high + ) -> Data? { + var cgSize = CGSize() + + switch size { + case .small: + cgSize.width = 256 + cgSize.height = 256 + break + case .medium: + cgSize.width = 512 + cgSize.height = 512 + break + case .large: + cgSize.width = 1024 + cgSize.height = 1024 + break + } + + guard let bitmap = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(cgSize.width), + pixelsHigh: Int(cgSize.height), + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bitmapFormat: [], + bytesPerRow: 0, + bitsPerPixel: 0 + ) else { + return nil + } + + bitmap.size = cgSize + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: bitmap) + NSGraphicsContext.current?.imageInterpolation = imageInterpolation + draw( + in: NSRect(origin: .zero, size: cgSize), + from: .zero, + operation: .copy, + fraction: 1.0 + ) + NSGraphicsContext.restoreGraphicsState() + + return bitmap.representation(using: .png, properties: [:]) + } +} +#endif diff --git a/Sources/OpenAIKit/OpenAI.swift b/Sources/OpenAIKit/OpenAI.swift index 7b9a002..5e121b6 100644 --- a/Sources/OpenAIKit/OpenAI.swift +++ b/Sources/OpenAIKit/OpenAI.swift @@ -25,6 +25,10 @@ import SwiftUI +#if os(iOS) || os(tvOS) +import UIKit +#endif + /// OpenAI provides the needed core functions of OpenAIKit. @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public final class OpenAI { @@ -35,6 +39,7 @@ public final class OpenAI { self.config = config } + #if os(iOS) || os(tvOS) /// Input a `Base64` image binary `String` to receive an `UIImage` object. /// - Parameter b64Data: The `Base64` data itself in `String` form. /// - Returns: A `UIImage` object. @@ -53,6 +58,28 @@ public final class OpenAI { throw OpenAIError.invalidData } } + #endif + + #if os(macOS) + /// Input a `Base64` image binary `String` to receive an `NSImage` object. + /// - Parameter b64Data: The `Base64` data itself in `String` form. + /// - Returns: A `UIImage` object. + public func decodeBase64Image(_ b64Data: String) throws -> NSImage { + do { + guard let data = Data(base64Encoded: b64Data) else { + throw OpenAIError.invalidData + } + + guard let image = NSImage(data: data) else { + throw OpenAIError.invalidData + } + + return image + } catch { + throw OpenAIError.invalidData + } + } + #endif /// Return a `URL` with the OpenAI API endpoint as the `URL` /// - Parameter path: The `String` path. diff --git a/Sources/OpenAIKit/Protocols/OpenAIProtocol.swift b/Sources/OpenAIKit/Protocols/OpenAIProtocol.swift index 59772b4..f146645 100644 --- a/Sources/OpenAIKit/Protocols/OpenAIProtocol.swift +++ b/Sources/OpenAIKit/Protocols/OpenAIProtocol.swift @@ -23,8 +23,6 @@ // THE SOFTWARE. // -import UIKit - public protocol OpenAIProtocol { // MARK: Models Functions /// List and describe the various models available in the API. You can refer to the [Models](https://beta.openai.com/docs/models) diff --git a/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageEditParameters.swift b/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageEditParameters.swift index 52a56bf..2e99f16 100644 --- a/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageEditParameters.swift +++ b/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageEditParameters.swift @@ -61,7 +61,8 @@ public struct ImageEditParameters { /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// [Learn more.](https://beta.openai.com/docs/guides/safety-best-practices/end-user-ids) public var user: String? - + + #if os(iOS) || os(tvOS) || os(watchOS) public init( image: UIImage, mask: UIImage, @@ -86,6 +87,34 @@ public struct ImageEditParameters { throw OpenAIError.invalidData } } + #endif + + #if os(macOS) + public init( + image: NSImage, + mask: NSImage, + prompt: String, + @Clamped(range: 1...10) numberOfImages: Int = 1, + resolution: ImageResolutions = .large, + responseFormat: ResponseFormat = .url, + user: String? = nil + ) throws { + do { + guard let imageData = image.pngData(size: resolution) else { throw OpenAIError.invalidData } + guard let maskData = mask.pngData(size: resolution) else { throw OpenAIError.invalidData } + + self.image = FormData(data: imageData, mimeType: "image/png", fileName: "image.png") + self.mask = FormData(data: maskData, mimeType: "image/png", fileName: "mask.png") + self.prompt = prompt + self.numberOfImages = numberOfImages + self.resolution = resolution + self.responseFormat = responseFormat + self.user = user + } catch { + throw OpenAIError.invalidData + } + } + #endif /// The body of the URL used for OpenAI API requests. public var body: [String: Any] { diff --git a/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageVariationParameters.swift b/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageVariationParameters.swift index c7ac211..e50cafe 100644 --- a/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageVariationParameters.swift +++ b/Sources/OpenAIKit/Types/Structs/Parameters/Images/ImageVariationParameters.swift @@ -51,6 +51,7 @@ public struct ImageVariationParameters { /// [Learn more.](https://beta.openai.com/docs/guides/safety-best-practices/end-user-ids) public var user: String? + #if os(iOS) || os(tvOS) || os(watchOS) public init( image: UIImage, @Clamped(range: 1...10) numberOfImages: Int = 1, @@ -70,7 +71,30 @@ public struct ImageVariationParameters { throw OpenAIError.invalidData } } + #endif + + #if os(macOS) + public init( + image: NSImage, + @Clamped(range: 1...10) numberOfImages: Int = 1, + resolution: ImageResolutions = .large, + responseFormat: ResponseFormat = .url, + user: String? = nil + ) throws { + do { + guard let imageData = image.pngData(size: resolution) else { throw OpenAIError.invalidData } + self.image = FormData(data: imageData, mimeType: "image/png", fileName: "image.png") + self.numberOfImages = numberOfImages + self.resolution = resolution + self.responseFormat = responseFormat + self.user = user + } catch { + throw OpenAIError.invalidData + } + } + #endif + /// The body of the URL used for OpenAI API requests. public var body: [String: Any] { var result: [String: Any] = ["image": self.image,