ImageProcessor.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. //
  2. // ImageProcessor.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 2016/08/26.
  6. //
  7. // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import Foundation
  27. import CoreGraphics
  28. #if canImport(AppKit) && !targetEnvironment(macCatalyst)
  29. import AppKit
  30. #else
  31. import UIKit
  32. #endif
  33. /// Represents an item which could be processed by an `ImageProcessor`.
  34. public enum ImageProcessItem: Sendable {
  35. /// Input image. The processor should provide a method to apply
  36. /// processing to this `image` and return the resulting image.
  37. case image(KFCrossPlatformImage)
  38. /// Input data. The processor should provide a method to apply
  39. /// processing to this `data` and return the resulting image.
  40. case data(Data)
  41. }
  42. /// An `ImageProcessor` is used to convert downloaded data into an image.
  43. public protocol ImageProcessor: Sendable {
  44. /// Identifier for the processor.
  45. ///
  46. /// This identifier is used to distinguish the processor when caching and retrieving an image. Ensure that
  47. /// processors with the same properties or functionality share the same identifier so that processed images can be
  48. /// retrieved with the correct key.
  49. ///
  50. /// > Important: Avoid using an empty string for a custom processor, as it is already reserved by the
  51. /// > `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string for your identifier.
  52. var identifier: String { get }
  53. /// Process the input `ImageProcessItem` using this processor.
  54. ///
  55. /// - Parameters:
  56. /// - item: The input item to be processed by `self`.
  57. /// - options: The parsed options for processing the item.
  58. /// - Returns: The processed image.
  59. ///
  60. /// You should return `nil` if processing fails when converting an input item to an image. If the processing
  61. /// caller receives `nil`, an error will be reported, and the processing flow will stop. If processing flow is not
  62. /// critical for your use case, and the input item is already an image (`.image` case), you can also choose to
  63. /// return the input image itself to continue the processing pipeline.
  64. ///
  65. /// > Important: Most processors only support CG-based images. The watchOS is not supported for processors
  66. /// > containing a filter, and the input image will be returned directly on watchOS.
  67. func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
  68. }
  69. extension ImageProcessor {
  70. /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` will
  71. /// be `"\(self.identifier)|>\(another.identifier)"`.
  72. ///
  73. /// - Parameter another: An `ImageProcessor` to be appended to `self`.
  74. /// - Returns: The new `ImageProcessor` that will process the image in the order of the two processors concatenated.
  75. public func append(another: any ImageProcessor) -> any ImageProcessor {
  76. let newIdentifier = identifier.appending("|>\(another.identifier)")
  77. return GeneralProcessor(identifier: newIdentifier) {
  78. item, options in
  79. if let image = self.process(item: item, options: options) {
  80. return another.process(item: .image(image), options: options)
  81. } else {
  82. return nil
  83. }
  84. }
  85. }
  86. }
  87. func ==(left: any ImageProcessor, right: any ImageProcessor) -> Bool {
  88. return left.identifier == right.identifier
  89. }
  90. func !=(left: any ImageProcessor, right: any ImageProcessor) -> Bool {
  91. return !(left == right)
  92. }
  93. typealias ProcessorImp = (@Sendable (ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?)
  94. struct GeneralProcessor: ImageProcessor {
  95. let identifier: String
  96. let p: ProcessorImp
  97. func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  98. return p(item, options)
  99. }
  100. }
  101. /// The default processor. It converts the input data into a valid image.
  102. ///
  103. /// Supported image formats include .PNG, .JPEG, and .GIF. If an image item is provided as the
  104. /// ``ImageProcessItem/image(_:)`` case, ``DefaultImageProcessor`` will leave it unchanged and return the associated
  105. /// image.
  106. public struct DefaultImageProcessor: ImageProcessor {
  107. /// A default instance of ``DefaultImageProcessor`` can be used across the framework.
  108. public static let `default` = DefaultImageProcessor()
  109. public let identifier = ""
  110. /// Create a ``DefaultImageProcessor``.
  111. ///
  112. /// Use ``DefaultImageProcessor/default`` to obtain an instance if you have no specific reason to create your own
  113. /// ``DefaultImageProcessor``.
  114. public init() {}
  115. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  116. switch item {
  117. case .image(let image):
  118. return image.kf.scaled(to: options.scaleFactor)
  119. case .data(let data):
  120. return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
  121. }
  122. }
  123. }
  124. /// Represents the rect corner setting when processing a round corner image.
  125. public struct RectCorner: OptionSet, Sendable {
  126. /// Raw value for the corner radius.
  127. public let rawValue: Int
  128. /// Represents the top left corner.
  129. public static let topLeft = RectCorner(rawValue: 1 << 0)
  130. /// Represents the top right corner.
  131. public static let topRight = RectCorner(rawValue: 1 << 1)
  132. /// Represents the bottom left corner.
  133. public static let bottomLeft = RectCorner(rawValue: 1 << 2)
  134. /// Represents the bottom right corner.
  135. public static let bottomRight = RectCorner(rawValue: 1 << 3)
  136. /// Represents all corners.
  137. public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight]
  138. /// Create a `RectCorner` option set with a specified value.
  139. ///
  140. /// - Parameter rawValue: The value representing a specific corner option.
  141. public init(rawValue: Int) {
  142. self.rawValue = rawValue
  143. }
  144. var cornerIdentifier: String {
  145. if self == .all {
  146. return ""
  147. }
  148. return "_corner(\(rawValue))"
  149. }
  150. }
  151. #if !os(macOS)
  152. /// Processor for applying a blend mode to images.
  153. ///
  154. /// Supported for CG-based images only.
  155. public struct BlendImageProcessor: ImageProcessor {
  156. public let identifier: String
  157. /// The blend mode used to blend the input image.
  158. public let blendMode: CGBlendMode
  159. /// The alpha value used when blending the image.
  160. public let alpha: CGFloat
  161. /// The background color of the output image.
  162. ///
  163. /// If `nil`, the background will remain transparent.
  164. public let backgroundColor: KFCrossPlatformColor?
  165. /// Create a `BlendImageProcessor`.
  166. ///
  167. /// - Parameters:
  168. /// - blendMode: The blend mode to be used for blending the input image.
  169. /// - alpha: The alpha value to be used when blending the image, ranging from 0.0 (completely transparent) to
  170. /// 1.0 (completely solid). Default is 1.0.
  171. /// - backgroundColor: The background color to apply to the output image. Default is `nil`.
  172. public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) {
  173. self.blendMode = blendMode
  174. self.alpha = alpha
  175. self.backgroundColor = backgroundColor
  176. var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))"
  177. if let color = backgroundColor {
  178. identifier.append("_\(color.rgbaDescription)")
  179. }
  180. self.identifier = identifier
  181. }
  182. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  183. switch item {
  184. case .image(let image):
  185. return image.kf.scaled(to: options.scaleFactor)
  186. .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor)
  187. case .data:
  188. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  189. }
  190. }
  191. }
  192. #endif
  193. #if os(macOS)
  194. /// Processor for applying a compositing operation to images.
  195. ///
  196. /// Supported for CG-based images on macOS.
  197. public struct CompositingImageProcessor: ImageProcessor {
  198. public let identifier: String
  199. /// The compositing operation applied to the input image.
  200. public let compositingOperation: NSCompositingOperation
  201. /// The alpha value used when compositing the image.
  202. public let alpha: CGFloat
  203. /// The background color of the output image. If `nil`, the background will remain transparent.
  204. public let backgroundColor: KFCrossPlatformColor?
  205. /// Create a `CompositingImageProcessor`.
  206. ///
  207. /// - Parameters:
  208. /// - compositingOperation: The compositing operation to be applied to the input image.
  209. /// - alpha: The alpha value to be used when compositing the image, ranging from 0.0 (completely transparent) to
  210. /// 1.0 (completely solid). Default is 1.0.
  211. /// - backgroundColor: The background color to apply to the output image. Default is `nil`.
  212. public init(compositingOperation: NSCompositingOperation,
  213. alpha: CGFloat = 1.0,
  214. backgroundColor: KFCrossPlatformColor? = nil)
  215. {
  216. self.compositingOperation = compositingOperation
  217. self.alpha = alpha
  218. self.backgroundColor = backgroundColor
  219. var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))"
  220. if let color = backgroundColor {
  221. identifier.append("_\(color.rgbaDescription)")
  222. }
  223. self.identifier = identifier
  224. }
  225. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  226. switch item {
  227. case .image(let image):
  228. return image.kf.scaled(to: options.scaleFactor)
  229. .kf.image(
  230. withCompositingOperation: compositingOperation,
  231. alpha: alpha,
  232. backgroundColor: backgroundColor)
  233. case .data:
  234. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  235. }
  236. }
  237. }
  238. #endif
  239. /// Represents a radius specified in a ``RoundCornerImageProcessor``.
  240. public enum Radius: Sendable {
  241. /// The radius should be calculated as a fraction of the image width. Typically, the associated value should be
  242. /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image width.
  243. case widthFraction(CGFloat)
  244. /// The radius should be calculated as a fraction of the image height. Typically, the associated value should be
  245. /// between 0 and 0.5, where 0 represents no radius, and 0.5 represents using half of the image height.
  246. case heightFraction(CGFloat)
  247. /// Use a fixed point value as the round corner radius.
  248. case point(CGFloat)
  249. var radiusIdentifier: String {
  250. switch self {
  251. case .widthFraction(let f):
  252. return "w_frac_\(f)"
  253. case .heightFraction(let f):
  254. return "h_frac_\(f)"
  255. case .point(let p):
  256. return p.description
  257. }
  258. }
  259. public func compute(with size: CGSize) -> CGFloat {
  260. let cornerRadius: CGFloat
  261. switch self {
  262. case .point(let point):
  263. cornerRadius = point
  264. case .widthFraction(let widthFraction):
  265. cornerRadius = size.width * widthFraction
  266. case .heightFraction(let heightFraction):
  267. cornerRadius = size.height * heightFraction
  268. }
  269. return cornerRadius
  270. }
  271. }
  272. /// Processor for creating round corner images.
  273. ///
  274. /// Supported for CG-based images on macOS. If a non-CG image is passed in, the processor will have no effect.
  275. ///
  276. /// > Tip: The input image will be rendered with round corner pixels removed. If the image itself does not contain an
  277. /// > alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory for
  278. /// > correct rendering. However, when cached to disk, Kingfisher defaults to preserving the original image format.
  279. /// > This means the alpha channel will be removed for these images. If you load the processed image from the cache
  280. /// > again, you will lose the transparent corners.
  281. /// >
  282. /// > You can use ``FormatIndicatedCacheSerializer/png`` to force Kingfisher to serialize the image to PNG format in this
  283. /// > case.
  284. public struct RoundCornerImageProcessor: ImageProcessor {
  285. public let identifier: String
  286. /// The radius to be applied during processing.
  287. ///
  288. /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with
  289. /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with
  290. /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image.
  291. public let radius: Radius
  292. /// The target corners to round.
  293. public let roundingCorners: RectCorner
  294. /// The target size for the output image. If `nil`, the image will retain its original size after processing.
  295. public let targetSize: CGSize?
  296. /// The background color for the output image. If `nil`, it will use a transparent background.
  297. public let backgroundColor: KFCrossPlatformColor?
  298. /// Create a ``RoundCornerImageProcessor`` with given parameters.
  299. ///
  300. /// - Parameters:
  301. /// - cornerRadius: The corner radius in points to be applied during processing.
  302. /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after
  303. /// processing. Default is `nil`.
  304. /// - corners: The target corners to round. Default is ``RectCorner/all``.
  305. /// - backgroundColor: The background color to apply to the output image. Default is `nil`.
  306. ///
  307. /// This initializer accepts a specific point value for `cornerRadius`. If you don't know the image size but still
  308. /// want to apply a full round corner (making the final image round), or specify the corner radius as a fraction of
  309. /// one dimension of the target image, use the ``init(radius:targetSize:roundingCorners:backgroundColor:)``
  310. /// instead.
  311. public init(
  312. cornerRadius: CGFloat,
  313. targetSize: CGSize? = nil,
  314. roundingCorners corners: RectCorner = .all,
  315. backgroundColor: KFCrossPlatformColor? = nil
  316. )
  317. {
  318. let radius = Radius.point(cornerRadius)
  319. self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor)
  320. }
  321. /// Create a `RoundCornerImageProcessor`.
  322. ///
  323. /// - Parameters:
  324. /// - radius: The radius to be applied during processing.
  325. /// - targetSize: The target size for the output image. If `nil`, the image will retain its original size after
  326. /// processing. Default is `nil`.
  327. /// - corners: The target corners to round. Default is ``RectCorner/all``.
  328. /// - backgroundColor: The background color to apply to the output image. Default is `nil`.
  329. public init(
  330. radius: Radius,
  331. targetSize: CGSize? = nil,
  332. roundingCorners corners: RectCorner = .all,
  333. backgroundColor: KFCrossPlatformColor? = nil
  334. )
  335. {
  336. self.radius = radius
  337. self.targetSize = targetSize
  338. self.roundingCorners = corners
  339. self.backgroundColor = backgroundColor
  340. self.identifier = {
  341. var identifier = ""
  342. if let size = targetSize {
  343. identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" +
  344. "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))"
  345. } else {
  346. identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" +
  347. "(\(radius.radiusIdentifier)\(corners.cornerIdentifier))"
  348. }
  349. if let backgroundColor = backgroundColor {
  350. identifier += "_\(backgroundColor)"
  351. }
  352. return identifier
  353. }()
  354. }
  355. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  356. switch item {
  357. case .image(let image):
  358. let size = targetSize ?? image.kf.size
  359. return image.kf.scaled(to: options.scaleFactor)
  360. .kf.image(
  361. withRadius: radius,
  362. fit: size,
  363. roundingCorners: roundingCorners,
  364. backgroundColor: backgroundColor)
  365. case .data:
  366. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  367. }
  368. }
  369. }
  370. /// Represents a border to be added to the image.
  371. ///
  372. /// Typically used with ``BorderImageProcessor``, which adds the border to the image.
  373. public struct Border: Sendable {
  374. /// The color of the border to create.
  375. public var color: KFCrossPlatformColor
  376. /// The line width of the border to create.
  377. public var lineWidth: CGFloat
  378. /// The radius to be applied during processing.
  379. ///
  380. /// Specify a specific point value with ``Radius/point(_:)``, or a fraction of the target image with
  381. /// ``Radius/widthFraction(_:)`` or ``Radius/heightFraction(_:)``. For example, if you have a square image with
  382. /// equal width and height, `.widthFraction(0.5)` means using half of the width of the size to create a round image.
  383. public var radius: Radius
  384. /// The target corners which will be applied rounding.
  385. public var roundingCorners: RectCorner
  386. /// Creates a border.
  387. /// - Parameters:
  388. /// - color: The color will be used to render the border.
  389. /// - lineWidth: The line width of the border.
  390. /// - radius: The radius of the border corner.
  391. /// - roundingCorners: The target corners type.
  392. public init(
  393. color: KFCrossPlatformColor = .black,
  394. lineWidth: CGFloat = 4,
  395. radius: Radius = .point(0),
  396. roundingCorners: RectCorner = .all
  397. ) {
  398. self.color = color
  399. self.lineWidth = lineWidth
  400. self.radius = radius
  401. self.roundingCorners = roundingCorners
  402. }
  403. var identifier: String {
  404. "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)"
  405. }
  406. }
  407. /// Processor for creating bordered images.
  408. public struct BorderImageProcessor: ImageProcessor {
  409. public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" }
  410. /// The border to be added to the image.
  411. public let border: Border
  412. /// Create a `BorderImageProcessor` with a given `Border`.
  413. ///
  414. /// - Parameter border: The border to be added to the image.
  415. public init(border: Border) {
  416. self.border = border
  417. }
  418. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  419. switch item {
  420. case .image(let image):
  421. return image.kf.addingBorder(border)
  422. case .data:
  423. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  424. }
  425. }
  426. }
  427. /// Represents how a size of content adjusts itself to fit a target size.
  428. public enum ContentMode: Sendable {
  429. /// Does not scale the content.
  430. case none
  431. /// Scales the content to fit the size of the view while maintaining the aspect ratio.
  432. case aspectFit
  433. /// Scales the content to fill the size of the view.
  434. case aspectFill
  435. }
  436. /// Processor for resizing images.
  437. ///
  438. /// If you need to resize an image represented by data to a smaller size, use ``DownsamplingImageProcessor`` instead,
  439. /// which is more efficient and uses less memory.
  440. public struct ResizingImageProcessor: ImageProcessor {
  441. public let identifier: String
  442. /// The reference size for the resizing operation in points.
  443. public let referenceSize: CGSize
  444. /// The target content mode of the output image.
  445. public let targetContentMode: ContentMode
  446. /// Create a ``ResizingImageProcessor``.
  447. ///
  448. /// - Parameters:
  449. /// - referenceSize: The reference size for the resizing operation in points.
  450. /// - mode: The target content mode of the output image.
  451. ///
  452. /// The instance of ``ResizingImageProcessor`` will follow the `mode` argument and attempt to resize the input
  453. /// images to fit or fill the `referenceSize`. This means if you are using a `mode` besides `.none`, you may get an
  454. /// image with a size that is not the same as the `referenceSize`.
  455. ///
  456. /// For example, with an input image size of {100, 200}, `referenceSize` of {100, 100}, and `mode` of `.aspectFit`,
  457. /// you will get an output image with a size of {50, 100} that "fits" the `referenceSize`.
  458. ///
  459. /// > If you need an output image to be exactly a specified size, append or use a ``CroppingImageProcessor``.
  460. public init(referenceSize: CGSize, mode: ContentMode = .none) {
  461. self.referenceSize = referenceSize
  462. self.targetContentMode = mode
  463. if mode == .none {
  464. self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))"
  465. } else {
  466. self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))"
  467. }
  468. }
  469. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  470. switch item {
  471. case .image(let image):
  472. return image.kf.scaled(to: options.scaleFactor)
  473. .kf.resize(to: referenceSize, for: targetContentMode)
  474. case .data:
  475. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  476. }
  477. }
  478. }
  479. /// Processor for adding a blur effect to images.
  480. ///
  481. /// Uses `Accelerate.framework` under the hood for better performance. Applies a simulated Gaussian blur with the
  482. /// specified blur radius.
  483. public struct BlurImageProcessor: ImageProcessor {
  484. public let identifier: String
  485. /// The blur radius for the simulated Gaussian blur.
  486. public let blurRadius: CGFloat
  487. /// Create a `BlurImageProcessor`.
  488. ///
  489. /// - Parameter blurRadius: The blur radius for the simulated Gaussian blur.
  490. public init(blurRadius: CGFloat) {
  491. self.blurRadius = blurRadius
  492. self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))"
  493. }
  494. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  495. switch item {
  496. case .image(let image):
  497. let radius = blurRadius * options.scaleFactor
  498. return image.kf.scaled(to: options.scaleFactor)
  499. .kf.blurred(withRadius: radius)
  500. case .data:
  501. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  502. }
  503. }
  504. }
  505. /// Processor for adding an overlay to images.
  506. ///
  507. /// > Only CG-based images are supported.
  508. public struct OverlayImageProcessor: ImageProcessor {
  509. public let identifier: String
  510. /// The overlay color used to overlay the input image.
  511. public let overlay: KFCrossPlatformColor
  512. /// The fraction used when overlaying the color to the image.
  513. public let fraction: CGFloat
  514. /// Create an ``OverlayImageProcessor``.
  515. ///
  516. /// - Parameters:
  517. /// - overlay: The overlay color used to overlay the input image.
  518. /// - fraction: The fraction used when overlaying the color to the image.
  519. /// Ranges from 0.0 to 1.0. 0.0 means a solid color, and 1.0 means a transparent overlay.
  520. public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) {
  521. self.overlay = overlay
  522. self.fraction = fraction
  523. self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))"
  524. }
  525. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  526. switch item {
  527. case .image(let image):
  528. return image.kf.scaled(to: options.scaleFactor)
  529. .kf.overlaying(with: overlay, fraction: fraction)
  530. case .data:
  531. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  532. }
  533. }
  534. }
  535. /// Processor for tinting images with color.
  536. ///
  537. /// > Only CG-based images are supported.
  538. ///
  539. /// > Important: On watchOS, there is no tint support and the original image will be returned.
  540. public struct TintImageProcessor: ImageProcessor {
  541. public let identifier: String
  542. /// The tint color used to tint the input image.
  543. public let tint: KFCrossPlatformColor
  544. /// Create a ``TintImageProcessor``.
  545. ///
  546. /// - Parameter tint: The tint color used to tint the input image.
  547. public init(tint: KFCrossPlatformColor) {
  548. self.tint = tint
  549. self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))"
  550. }
  551. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  552. switch item {
  553. case .image(let image):
  554. return image.kf.scaled(to: options.scaleFactor)
  555. .kf.tinted(with: tint)
  556. case .data:
  557. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  558. }
  559. }
  560. }
  561. /// Processor for applying color control to images.
  562. ///
  563. /// > Only CG-based images are supported.
  564. ///
  565. /// > Important: On watchOS, there is no color control support and the original image will be returned.
  566. public struct ColorControlsProcessor: ImageProcessor {
  567. public let identifier: String
  568. /// The brightness change applied to the image.
  569. public let brightness: CGFloat
  570. /// The contrast change applied to the image.
  571. public let contrast: CGFloat
  572. /// The saturation change applied to the image.
  573. public let saturation: CGFloat
  574. /// The EV (F-stops brighter or darker) change applied to the image.
  575. public let inputEV: CGFloat
  576. /// Create a ``ColorControlsProcessor``.
  577. ///
  578. /// - Parameters:
  579. /// - brightness: The brightness change applied to the image.
  580. /// - contrast: The contrast change applied to the image.
  581. /// - saturation: The saturation change applied to the image.
  582. /// - inputEV: The EV (F-stops brighter or darker) change applied to the image.
  583. public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) {
  584. self.brightness = brightness
  585. self.contrast = contrast
  586. self.saturation = saturation
  587. self.inputEV = inputEV
  588. self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))"
  589. }
  590. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  591. switch item {
  592. case .image(let image):
  593. return image.kf.scaled(to: options.scaleFactor)
  594. .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV)
  595. case .data:
  596. return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  597. }
  598. }
  599. }
  600. /// Processor for applying black and white effect to images. Only CG-based images are supported.
  601. ///
  602. /// > Only CG-based images are supported.
  603. ///
  604. /// > Important: On watchOS, there is no color control support and the original image will be returned.
  605. public struct BlackWhiteProcessor: ImageProcessor {
  606. public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor"
  607. /// Creates a ``BlackWhiteProcessor``
  608. public init() {}
  609. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  610. return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7)
  611. .process(item: item, options: options)
  612. }
  613. }
  614. /// Processor for cropping an image.
  615. public struct CroppingImageProcessor: ImageProcessor {
  616. public let identifier: String
  617. /// The target size of the output image.
  618. public let size: CGSize
  619. /// Anchor point from which the output size should be calculate.
  620. ///
  621. /// The anchor point is consisted by two values between 0.0 and 1.0.
  622. /// It indicates a related point in current image.
  623. ///
  624. /// See ``CroppingImageProcessor/init(size:anchor:)`` for more.
  625. public let anchor: CGPoint
  626. /// Create a ``CroppingImageProcessor``.
  627. ///
  628. /// - Parameters:
  629. /// - size: The target size of the output image.
  630. /// - anchor: The anchor point from which the size should be calculated. Default is `CGPoint(x: 0.5, y: 0.5)`,
  631. /// which represents the center of the input image.
  632. ///
  633. /// The anchor point is composed of two values between 0.0 and 1.0. It indicates a relative point in the current
  634. /// image, e.g:
  635. /// - (0.0, 0.0) for the top-left corner
  636. /// - (0.5, 0.5) for the center
  637. /// - (1.0, 1.0) for the bottom-right corner
  638. ///
  639. /// The ``CroppingImageProcessor/size`` property will be used along with ``CroppingImageProcessor/anchor`` to
  640. /// calculate a target rectangle in the image size.
  641. ///
  642. /// > The target size will be automatically calculated with a reasonable behavior. For example, when you have an
  643. /// > image size of `CGSize(width: 100, height: 100)` and a target size of `CGSize(width: 20, height: 20)`:
  644. /// >
  645. /// > - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`;
  646. /// > - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}`;
  647. /// > - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`.
  648. public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) {
  649. self.size = size
  650. self.anchor = anchor
  651. self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))"
  652. }
  653. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  654. switch item {
  655. case .image(let image):
  656. return image.kf.scaled(to: options.scaleFactor)
  657. .kf.crop(to: size, anchorOn: anchor)
  658. case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options)
  659. }
  660. }
  661. }
  662. /// Processor for downsampling an image.
  663. ///
  664. /// Compared to ``ResizingImageProcessor``, this processor does not render the images to resize. Instead, it
  665. /// downsamples the input data directly to an image. It is more efficient than ``ResizingImageProcessor``.
  666. ///
  667. /// > Tip: It is preferable to use ``DownsamplingImageProcessor`` whenever possible rather than the
  668. /// > ``ResizingImageProcessor``.
  669. ///
  670. /// > Important: Only CG-based images are supported. Animated images (such as GIFs) are not supported.
  671. public struct DownsamplingImageProcessor: ImageProcessor {
  672. /// The target size of the output image.
  673. ///
  674. /// It should be smaller than the size of the input image. If it is larger, the resulting image will be the same
  675. /// size as the input data without downsampling.
  676. public let size: CGSize
  677. public let identifier: String
  678. /// Creates a `DownsamplingImageProcessor`.
  679. ///
  680. /// - Parameters:
  681. /// - size: The target size of the downsampling operation.
  682. ///
  683. /// > Important: The size should be smaller than the size of the input image. If it is larger, the resulting image
  684. /// will be the same size as the input data without downsampling.
  685. public init(size: CGSize) {
  686. self.size = size
  687. self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))"
  688. }
  689. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  690. switch item {
  691. case .image(let image):
  692. guard let data = image.kf.data(format: .unknown) else {
  693. return nil
  694. }
  695. return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)
  696. case .data(let data):
  697. return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)
  698. }
  699. }
  700. }
  701. // This is an internal processor to provide the same interface for Live Photos.
  702. // It is not intended to be open and used from external.
  703. struct LivePhotoImageProcessor: ImageProcessor {
  704. public static let `default` = LivePhotoImageProcessor()
  705. private init() { }
  706. public let identifier = "com.onevcat.Kingfisher.LivePhotoImageProcessor"
  707. public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
  708. switch item {
  709. case .image(let image):
  710. return image
  711. case .data:
  712. return KFCrossPlatformImage()
  713. }
  714. }
  715. }
  716. infix operator |>: AdditionPrecedence
  717. /// Concatenates two `ImageProcessor`s to create a new one, in which the `left` and `right` are combined in order to
  718. /// process the image.
  719. ///
  720. /// - Parameters:
  721. /// - left: The first processor.
  722. /// - right: The second processor that follows the `left`.
  723. /// - Returns: The new processor that processes the image or the image data in left-to-right order.
  724. public func |>(left: any ImageProcessor, right: any ImageProcessor) -> any ImageProcessor {
  725. return left.append(another: right)
  726. }
  727. extension KFCrossPlatformColor {
  728. var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
  729. var r: CGFloat = 0
  730. var g: CGFloat = 0
  731. var b: CGFloat = 0
  732. var a: CGFloat = 0
  733. #if os(macOS)
  734. (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a)
  735. #else
  736. getRed(&r, green: &g, blue: &b, alpha: &a)
  737. #endif
  738. return (r, g, b, a)
  739. }
  740. var rgbaDescription: String {
  741. let components = self.rgba
  742. return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a)
  743. }
  744. }