PartsRepresentable+Image.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  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. 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.
  30. case couldNotConvertToJPEG
  31. }
  32. #if canImport(UIKit)
  33. /// Enables images to be representable as ``PartsRepresentable``.
  34. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  35. extension UIImage: PartsRepresentable {
  36. public var partsValue: [any Part] {
  37. guard let data = jpegData(compressionQuality: imageCompressionQuality) else {
  38. return [ErrorPart(ImageConversionError.couldNotConvertToJPEG)]
  39. }
  40. return [InlineDataPart(data: data, mimeType: "image/jpeg")]
  41. }
  42. }
  43. #elseif canImport(AppKit)
  44. /// Enables images to be representable as ``PartsRepresentable``.
  45. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *)
  46. extension NSImage: PartsRepresentable {
  47. public var partsValue: [any Part] {
  48. guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
  49. return [ErrorPart(ImageConversionError.invalidUnderlyingImage)]
  50. }
  51. let bmp = NSBitmapImageRep(cgImage: cgImage)
  52. guard let data = bmp.representation(using: .jpeg, properties: [.compressionFactor: 0.8])
  53. else {
  54. return [ErrorPart(ImageConversionError.couldNotConvertToJPEG)]
  55. }
  56. return [InlineDataPart(data: data, mimeType: "image/jpeg")]
  57. }
  58. }
  59. #endif
  60. #if !os(watchOS) // This code does not build on watchOS.
  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: PartsRepresentable {
  64. public var partsValue: [any Part] {
  65. let output = NSMutableData()
  66. guard let imageDestination = CGImageDestinationCreateWithData(
  67. output, UTType.jpeg.identifier as CFString, 1, nil
  68. ) else {
  69. return [ErrorPart(ImageConversionError.couldNotAllocateDestination)]
  70. }
  71. CGImageDestinationAddImage(imageDestination, self, nil)
  72. CGImageDestinationSetProperties(imageDestination, [
  73. kCGImageDestinationLossyCompressionQuality: imageCompressionQuality,
  74. ] as CFDictionary)
  75. if CGImageDestinationFinalize(imageDestination) {
  76. return [InlineDataPart(data: output as Data, mimeType: "image/jpeg")]
  77. }
  78. return [ErrorPart(ImageConversionError.couldNotConvertToJPEG)]
  79. }
  80. }
  81. #endif // !os(watchOS)
  82. #if canImport(CoreImage)
  83. /// Enables `CIImages` to be representable as model content.
  84. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  85. extension CIImage: PartsRepresentable {
  86. public var partsValue: [any Part] {
  87. let context = CIContext()
  88. let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB))
  89. .flatMap {
  90. // The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but
  91. // Swift's type system does not allow this.
  92. // [kCGImageDestinationLossyCompressionQuality: imageCompressionQuality]
  93. context.jpegRepresentation(of: self, colorSpace: $0, options: [:])
  94. }
  95. if let jpegData = jpegData {
  96. return [InlineDataPart(data: jpegData, mimeType: "image/jpeg")]
  97. }
  98. return [ErrorPart(ImageConversionError.couldNotConvertToJPEG)]
  99. }
  100. }
  101. #endif // canImport(CoreImage)