PartsRepresentable+Image.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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, watchOS 8.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, watchOS 8.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. #if !os(watchOS) // This code does not build on watchOS.
  62. /// Enables `CGImages` to be representable as model content.
  63. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  64. extension CGImage: ThrowingPartsRepresentable {
  65. public func tryPartsValue() throws -> [ModelContent.Part] {
  66. let output = NSMutableData()
  67. guard let imageDestination = CGImageDestinationCreateWithData(
  68. output, UTType.jpeg.identifier as CFString, 1, nil
  69. ) else {
  70. throw ImageConversionError.couldNotAllocateDestination
  71. }
  72. CGImageDestinationAddImage(imageDestination, self, nil)
  73. CGImageDestinationSetProperties(imageDestination, [
  74. kCGImageDestinationLossyCompressionQuality: imageCompressionQuality,
  75. ] as CFDictionary)
  76. if CGImageDestinationFinalize(imageDestination) {
  77. return [.data(mimetype: "image/jpeg", output as Data)]
  78. }
  79. throw ImageConversionError.couldNotConvertToJPEG(self)
  80. }
  81. }
  82. #endif // !os(watchOS)
  83. #if canImport(CoreImage)
  84. /// Enables `CIImages` to be representable as model content.
  85. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, tvOS 15.0, *)
  86. extension CIImage: ThrowingPartsRepresentable {
  87. public func tryPartsValue() throws -> [ModelContent.Part] {
  88. let context = CIContext()
  89. let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB))
  90. .flatMap {
  91. // The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but
  92. // Swift's type system does not allow this.
  93. // [kCGImageDestinationLossyCompressionQuality: imageCompressionQuality]
  94. context.jpegRepresentation(of: self, colorSpace: $0, options: [:])
  95. }
  96. if let jpegData = jpegData {
  97. return [.data(mimetype: "image/jpeg", jpegData)]
  98. }
  99. throw ImageConversionError.couldNotConvertToJPEG(self)
  100. }
  101. }
  102. #endif // canImport(CoreImage)