From ec5ab777267278231cbc46acb7b3e6b5b9059073 Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Wed, 3 Jan 2024 13:59:34 -0800 Subject: [PATCH] Add image adapters and tests --- Sources/GoogleAI/PartsRepresentable.swift | 40 +++++++++++++ .../PartsRepresentableTests.swift | 53 ++++++++++++++++++ Tests/GoogleAITests/logo.png | Bin 0 -> 16690 bytes 3 files changed, 93 insertions(+) create mode 100644 Tests/GoogleAITests/PartsRepresentableTests.swift create mode 100644 Tests/GoogleAITests/logo.png diff --git a/Sources/GoogleAI/PartsRepresentable.swift b/Sources/GoogleAI/PartsRepresentable.swift index e7070e1..ea93a55 100644 --- a/Sources/GoogleAI/PartsRepresentable.swift +++ b/Sources/GoogleAI/PartsRepresentable.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +import UniformTypeIdentifiers #if canImport(UIKit) import UIKit // For UIImage extensions. #elseif canImport(AppKit) @@ -77,3 +78,42 @@ extension [any PartsRepresentable]: PartsRepresentable { } } #endif + +extension CGImage: PartsRepresentable { + public var partsValue: [ModelContent.Part] { + let output = NSMutableData() + guard let imageDestination = CGImageDestinationCreateWithData( + output, UTType.jpeg.identifier as CFString, 1, nil + ) else { + Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CGImage.") + return [] + } + CGImageDestinationAddImage(imageDestination, self, nil) + CGImageDestinationSetProperties(imageDestination, [ + kCGImageDestinationLossyCompressionQuality: 0.8, + ] as CFDictionary) + if CGImageDestinationFinalize(imageDestination) { + return [.data(mimetype: "image/jpeg", output as Data)] + } + Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CGImage.") + return [] + } +} + +extension CIImage: PartsRepresentable { + public var partsValue: [ModelContent.Part] { + let context = CIContext() + let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)) + .flatMap { + // The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but + // Swift's type system does not allow this. + // [kCGImageDestinationLossyCompressionQuality: 0.8] + context.jpegRepresentation(of: self, colorSpace: $0, options: [:]) + } + if let jpegData = jpegData { + return [.data(mimetype: "image/jpeg", jpegData)] + } + Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CIImage.") + return [] + } +} diff --git a/Tests/GoogleAITests/PartsRepresentableTests.swift b/Tests/GoogleAITests/PartsRepresentableTests.swift new file mode 100644 index 0000000..1911bb0 --- /dev/null +++ b/Tests/GoogleAITests/PartsRepresentableTests.swift @@ -0,0 +1,53 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +final class PartsRepresentableTests: XCTestCase { + let imageURL = Bundle.main.url(forResource: "logo", withExtension: "png")! + + func testModelContentFromCGImageIsNotEmpty() throws { + guard let dataProvider = CGDataProvider(url: imageURL as CFURL) else { + XCTFail("Could not find data for url: \(imageURL)") + return + } + guard let image = CGImage(pngDataProviderSource: dataProvider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent) else { + XCTFail("Could not construct CGImage from URL: \(imageURL)") + return + } + let modelContent = image.partsValue + XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)") + } + + func testModelContentFromCIImageIsNotEmpty() throws { + guard let image = CIImage(contentsOf: imageURL) else { + XCTFail("Could not construct CIImage from URL: \(imageURL)") + return + } + let modelContent = image.partsValue + XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)") + } + + func testModelContentFromUIImageIsNotEmpty() throws { + guard let image = UIImage(named: "logo.png") else { + XCTFail("Could not find image named logo.png.") + return + } + let modelContent = image.partsValue + XCTAssert(modelContent.count > 0, "Expected non-empty model content for UIImage: \(image)") + } +} diff --git a/Tests/GoogleAITests/logo.png b/Tests/GoogleAITests/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c7cd4ca111a53f1085db2a6070c718ad3e17cdd6 GIT binary patch literal 16690 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 IRP3Mp5020|NdN!< literal 0 HcmV?d00001