PartsRepresentable+Image.swift 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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. import UniformTypeIdentifiers
  15. #if canImport(UIKit)
  16. import UIKit // For UIImage extensions.
  17. #elseif canImport(AppKit)
  18. import AppKit // For NSImage extensions.
  19. #endif
  20. private let imageCompressionQuality: CGFloat = 0.8
  21. /// An enum describing failures that can occur when converting image types to model content data.
  22. /// For some image types like `CIImage`, creating valid model content requires creating a JPEG
  23. /// representation of the image that may not yet exist, which may be computationally expensive.
  24. public enum ImageConversionError: Error {
  25. /// The image (the receiver of the call `toModelContentParts()`) was invalid.
  26. case invalidUnderlyingImage
  27. /// A valid image destination could not be allocated.
  28. case couldNotAllocateDestination
  29. /// JPEG image data conversion failed, accompanied by the original image, which may be an
  30. /// instance of `NSImageRep`, `UIImage`, `CGImage`, or `CIImage`.
  31. case couldNotConvertToJPEG(Any)
  32. }
  33. #if canImport(UIKit)
  34. /// Enables images to be representable as ``ThrowingPartsRepresentable``.
  35. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  36. extension UIImage: ThrowingPartsRepresentable {
  37. public func tryPartsValue() throws -> [ModelContent.Part] {
  38. guard let data = jpegData(compressionQuality: imageCompressionQuality) else {
  39. throw ImageConversionError.couldNotConvertToJPEG(self)
  40. }
  41. return [ModelContent.Part.data(mimetype: "image/jpeg", data)]
  42. }
  43. }
  44. #elseif canImport(AppKit)
  45. /// Enables images to be representable as ``ThrowingPartsRepresentable``.
  46. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  47. extension NSImage: ThrowingPartsRepresentable {
  48. public func tryPartsValue() throws -> [ModelContent.Part] {
  49. guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
  50. throw ImageConversionError.invalidUnderlyingImage
  51. }
  52. let bmp = NSBitmapImageRep(cgImage: cgImage)
  53. guard let data = bmp.representation(using: .jpeg, properties: [.compressionFactor: 0.8])
  54. else {
  55. throw ImageConversionError.couldNotConvertToJPEG(bmp)
  56. }
  57. return [ModelContent.Part.data(mimetype: "image/jpeg", data)]
  58. }
  59. }
  60. #endif
  61. /// Enables `CGImages` to be representable as model content.
  62. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  63. extension CGImage: ThrowingPartsRepresentable {
  64. public func tryPartsValue() throws -> [ModelContent.Part] {
  65. let output = NSMutableData()
  66. guard let imageDestination = CGImageDestinationCreateWithData(
  67. output, UTType.jpeg.identifier as CFString, 1, nil
  68. ) else {
  69. throw ImageConversionError.couldNotAllocateDestination
  70. }
  71. CGImageDestinationAddImage(imageDestination, self, nil)
  72. CGImageDestinationSetProperties(imageDestination, [
  73. kCGImageDestinationLossyCompressionQuality: imageCompressionQuality,
  74. ] as CFDictionary)
  75. if CGImageDestinationFinalize(imageDestination) {
  76. return [.data(mimetype: "image/jpeg", output as Data)]
  77. }
  78. throw ImageConversionError.couldNotConvertToJPEG(self)
  79. }
  80. }
  81. /// Enables `CIImages` to be representable as model content.
  82. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  83. extension CIImage: ThrowingPartsRepresentable {
  84. public func tryPartsValue() throws -> [ModelContent.Part] {
  85. let context = CIContext()
  86. let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB))
  87. .flatMap {
  88. // The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but
  89. // Swift's type system does not allow this.
  90. // [kCGImageDestinationLossyCompressionQuality: imageCompressionQuality]
  91. context.jpegRepresentation(of: self, colorSpace: $0, options: [:])
  92. }
  93. if let jpegData = jpegData {
  94. return [.data(mimetype: "image/jpeg", jpegData)]
  95. }
  96. throw ImageConversionError.couldNotConvertToJPEG(self)
  97. }
  98. }