Skip to content

Commit 7442ebf

Browse files
authored
Add image adapters and tests (#80)
* Add image adapters and tests * code review feedback
1 parent 3e2302a commit 7442ebf

File tree

4 files changed

+116
-5
lines changed

4 files changed

+116
-5
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ Mint
3636
# CLI Tool
3737
Apps/GoogleAICLI/GoogleAICLI.xcodeproj/xcshareddata/xcschemes/*
3838
GenerativeAI-Info.plist
39+
40+
xcodebuild.log

Package.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@ let package = Package(
2525
.macCatalyst(.v15),
2626
],
2727
products: [
28-
// Products define the executables and libraries a package produces, making them visible to
29-
// other packages.
3028
.library(
3129
name: "GoogleGenerativeAI",
3230
targets: ["GoogleGenerativeAI"]
3331
),
3432
],
3533
targets: [
36-
// Targets are the basic building blocks of a package, defining a module or a test suite.
37-
// Targets can depend on other targets in this package and products from dependencies.
3834
.target(
3935
name: "GoogleGenerativeAI",
4036
path: "Sources"

Sources/GoogleAI/PartsRepresentable.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
// limitations under the License.
1414

1515
import Foundation
16+
import UniformTypeIdentifiers
1617
#if canImport(UIKit)
1718
import UIKit // For UIImage extensions.
1819
#elseif canImport(AppKit)
1920
import AppKit // For NSImage extensions.
2021
#endif
2122

23+
private let imageCompressionQuality: CGFloat = 0.8
24+
2225
/// A protocol describing any data that could be interpreted as model input data.
2326
public protocol PartsRepresentable {
2427
var partsValue: [ModelContent.Part] { get }
@@ -50,7 +53,7 @@ extension [any PartsRepresentable]: PartsRepresentable {
5053
/// Enables images to be representable as ``PartsRepresentable``.
5154
extension UIImage: PartsRepresentable {
5255
public var partsValue: [ModelContent.Part] {
53-
guard let data = jpegData(compressionQuality: 0.8) else {
56+
guard let data = jpegData(compressionQuality: imageCompressionQuality) else {
5457
Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from UIImage.")
5558
return []
5659
}
@@ -77,3 +80,42 @@ extension [any PartsRepresentable]: PartsRepresentable {
7780
}
7881
}
7982
#endif
83+
84+
extension CGImage: PartsRepresentable {
85+
public var partsValue: [ModelContent.Part] {
86+
let output = NSMutableData()
87+
guard let imageDestination = CGImageDestinationCreateWithData(
88+
output, UTType.jpeg.identifier as CFString, 1, nil
89+
) else {
90+
Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CGImage.")
91+
return []
92+
}
93+
CGImageDestinationAddImage(imageDestination, self, nil)
94+
CGImageDestinationSetProperties(imageDestination, [
95+
kCGImageDestinationLossyCompressionQuality: imageCompressionQuality,
96+
] as CFDictionary)
97+
if CGImageDestinationFinalize(imageDestination) {
98+
return [.data(mimetype: "image/jpeg", output as Data)]
99+
}
100+
Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CGImage.")
101+
return []
102+
}
103+
}
104+
105+
extension CIImage: PartsRepresentable {
106+
public var partsValue: [ModelContent.Part] {
107+
let context = CIContext()
108+
let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB))
109+
.flatMap {
110+
// The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but
111+
// Swift's type system does not allow this.
112+
// [kCGImageDestinationLossyCompressionQuality: imageCompressionQuality]
113+
context.jpegRepresentation(of: self, colorSpace: $0, options: [:])
114+
}
115+
if let jpegData = jpegData {
116+
return [.data(mimetype: "image/jpeg", jpegData)]
117+
}
118+
Logging.default.error("[GoogleGenerativeAI] Couldn't create JPEG from CIImage.")
119+
return []
120+
}
121+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import CoreGraphics
16+
import CoreImage
17+
import XCTest
18+
#if canImport(UIKit)
19+
import UIKit
20+
#else
21+
import AppKit
22+
#endif
23+
24+
final class PartsRepresentableTests: XCTestCase {
25+
func testModelContentFromCGImageIsNotEmpty() throws {
26+
// adapted from https://forums.swift.org/t/creating-a-cgimage-from-color-array/18634/2
27+
var srgbArray = [UInt32](repeating: 0xFFFF_FFFF, count: 8 * 8)
28+
let image = srgbArray.withUnsafeMutableBytes { ptr -> CGImage in
29+
let ctx = CGContext(
30+
data: ptr.baseAddress,
31+
width: 8,
32+
height: 8,
33+
bitsPerComponent: 8,
34+
bytesPerRow: 4 * 8,
35+
space: CGColorSpace(name: CGColorSpace.sRGB)!,
36+
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue +
37+
CGImageAlphaInfo.premultipliedFirst.rawValue
38+
)!
39+
return ctx.makeImage()!
40+
}
41+
let modelContent = image.partsValue
42+
XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)")
43+
}
44+
45+
func testModelContentFromCIImageIsNotEmpty() throws {
46+
let image = CIImage(color: CIColor.red)
47+
.cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16)))
48+
let modelContent = image.partsValue
49+
XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)")
50+
}
51+
52+
#if canImport(UIKit)
53+
func testModelContentFromUIImageIsNotEmpty() throws {
54+
let coreImage = CIImage(color: CIColor.red)
55+
.cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16)))
56+
let image = UIImage(ciImage: coreImage)
57+
let modelContent = image.partsValue
58+
XCTAssert(modelContent.count > 0, "Expected non-empty model content for UIImage: \(image)")
59+
}
60+
#else
61+
func testModelContentFromNSImageIsNotEmpty() throws {
62+
let coreImage = CIImage(color: CIColor.red)
63+
.cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16)))
64+
let rep = NSCIImageRep(ciImage: coreImage)
65+
let image = NSImage(size: rep.size)
66+
image.addRepresentation(rep)
67+
let modelContent = image.partsValue
68+
XCTAssert(modelContent.count > 0, "Expected non-empty model content for NSImage: \(image)")
69+
}
70+
#endif
71+
}

0 commit comments

Comments
 (0)