From ac65efd07cbcca2010ef1942cc4d01728114dd59 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sat, 30 Jul 2022 21:12:09 +0200 Subject: [PATCH 01/30] Added some useful extensions for applying common transformations to an image. --- Sources/CIImage+Transformation.swift | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Sources/CIImage+Transformation.swift diff --git a/Sources/CIImage+Transformation.swift b/Sources/CIImage+Transformation.swift new file mode 100644 index 0000000..aeb3d63 --- /dev/null +++ b/Sources/CIImage+Transformation.swift @@ -0,0 +1,50 @@ +import CoreImage + + +/// Some useful extensions for performing common transformations on an image. +public extension CIImage { + + /// Returns a new image that represents the original image after scaling it by the given factors in x- and y-direction. + /// + /// The interpolation used for the sampling technique used by the image. + /// This can be changed by calling `samplingLinear()` or `samplingNearest()` on the image + /// before calling this method. + /// Defaults to (bi)linear scaling. + /// + /// - Parameters: + /// - x: The scale factor in x-direction. + /// - y: The scale factor in y-direction. + /// - Returns: A scaled image. + func scaledBy(x: CGFloat, y: CGFloat) -> CIImage { + return self.transformed(by: CGAffineTransform(scaleX: x, y: y)) + } + + /// Returns a new image that represents the original image after translating within the working space + /// by the given amount in x- and y-direction. + /// - Parameters: + /// - dx: The amount to move the image in x-direction. + /// - dy: The amount to move the image in y-direction. + /// - Returns: A moved/translated image. + func translatedBy(dx: CGFloat, dy: CGFloat) -> CIImage { + return self.transformed(by: CGAffineTransform(translationX: dx, y: dy)) + } + + /// Returns a new image that represents the original image after moving its origin within the working space to the given point. + /// - Parameter origin: The new origin point of the image. + /// - Returns: A moved image with the new origin. + func moved(to origin: CGPoint) -> CIImage { + return self.translatedBy(dx: origin.x - self.extent.origin.x, dy: origin.y - self.extent.origin.y) + } + + /// Returns a new image that represents the original image after adding a padding of clear pixels around it, + /// effectively increasing its virtual extent. + /// - Parameters: + /// - dx: The amount of padding to add to the left and right. + /// - dy: The amount of padding to add at the top and bottom. + /// - Returns: A padded image. + func paddedBy(dx: CGFloat, dy: CGFloat) -> CIImage { + let background = CIImage(color: .clear).cropped(to: self.extent.insetBy(dx: -dx, dy: -dy)) + return self.composited(over: background) + } + +} From 79a9336cb1546ce5115e6341f45d96b34b495049 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sat, 30 Jul 2022 21:44:40 +0200 Subject: [PATCH 02/30] Added some useful extensions for convenience access to common color spaces. --- Sources/CGColorSpace+Convenience.swift | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Sources/CGColorSpace+Convenience.swift diff --git a/Sources/CGColorSpace+Convenience.swift b/Sources/CGColorSpace+Convenience.swift new file mode 100644 index 0000000..dba95c4 --- /dev/null +++ b/Sources/CGColorSpace+Convenience.swift @@ -0,0 +1,46 @@ +import CoreGraphics + + +/// Some useful extensions for convenience access to the most common color spaces needed when working with Core Image. +public extension CGColorSpace { + + /// The standard Red Green Blue (sRGB) color space. + static var sRGBColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.sRGB) } + /// The sRGB color space with a linear transfer function and extended-range values. + static var extendedLinearSRGBColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.extendedLinearSRGB) } + + /// The Display P3 color space. + static var displayP3ColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.displayP3) } + /// The Display P3 color space with a linear transfer function and extended-range values. + @available(iOS 12.3, macCatalyst 13.1, macOS 10.14.3, tvOS 12.0, watchOS 5.0, *) + static var extendedLinearDisplayP3ColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3) } + + /// The recommendation of the International Telecommunication Union (ITU) Radiocommunication sector for the BT.2020 color space. + static var itur2020ColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.itur_2020) } + /// The recommendation of the International Telecommunication Union (ITU) Radiocommunication sector for the BT.2020 color space, + /// with a linear transfer function and extended range values. + @available(iOS 12.3, macCatalyst 13.1, macOS 10.14.3, tvOS 12.0, watchOS 5.0, *) + static var extendedLinearITUR2020ColorSpace: CGColorSpace? { CGColorSpace(name: CGColorSpace.extendedLinearITUR_2020) } + + /// The recommendation of the International Telecommunication Union (ITU) Radiocommunication sector for the BT.2100 color space, + /// with the HLG transfer function. + @available(iOS 12.6, macCatalyst 13.1, macOS 10.15.6, tvOS 12.0, watchOS 5.0, *) + static var itur2100HLGColorSpace: CGColorSpace? { + if #available(iOS 14.0, macCatalyst 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { + return CGColorSpace(name: CGColorSpace.itur_2100_HLG) + } else { + return CGColorSpace(name: CGColorSpace.itur_2020_HLG) + } + } + /// The recommendation of the International Telecommunication Union (ITU) Radiocommunication sector for the BT.2100 color space, + /// with the PQ transfer function. + @available(iOS 12.6, macCatalyst 13.1, macOS 10.14.6, tvOS 12.0, watchOS 5.0, *) + static var itur2100PQColorSpace: CGColorSpace? { + if #available(iOS 14.0, macCatalyst 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) { + return CGColorSpace(name: CGColorSpace.itur_2100_PQ) + } else { + return CGColorSpace(name: CGColorSpace.itur_2020_PQ_EOTF) + } + } + +} From 82d5817e6b176ef5406b2fd436f6bad96d6b1fab Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 31 Jul 2022 15:15:33 +0200 Subject: [PATCH 03/30] Added some convenience methods for compositing/blending and colorizing images. --- Sources/CIImage+Blending.swift | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Sources/CIImage+Blending.swift diff --git a/Sources/CIImage+Blending.swift b/Sources/CIImage+Blending.swift new file mode 100644 index 0000000..e7afcdf --- /dev/null +++ b/Sources/CIImage+Blending.swift @@ -0,0 +1,49 @@ +import CoreImage + + +/// Some convenience methods for compositing/blending and colorizing images. +extension CIImage { + + /// Returns a new image created by compositing the original image over the specified background image + /// using the given blend kernel. + /// + /// The `extent` of the result image will be determined by `extent` of the receiver, + /// the `extent` of the `background` images and the `blendKernel` used. For most of the + /// builtin blend kernels (as well as custom blend kernels) the result image + /// `extent` will be the union of the receiver and background image extents. + /// + /// - Parameters: + /// - background: An image to serve as the background of the compositing operation. + /// - blendKernel: The `CIBlendKernel` to use for blending the image with the `background`. + /// - Returns: An image object representing the result of the compositing operation. + public func composited(over background: CIImage, using blendKernel: CIBlendKernel) -> CIImage? { + return blendKernel.apply(foreground: self, background: background) + } + + /// Returns a new image created by compositing the original image over the specified background image + /// using the given blend kernel in the specified colorspace. + /// + /// The `extent` of the result image will be determined by `extent` of the receiver, + /// the `extent` of the `background` images and the `blendKernel` used. For most of the + /// builtin blend kernels (as well as custom blend kernels) the result image + /// `extent` will be the union of the receiver and background image extents. + /// + /// - Parameters: + /// - background: An image to serve as the background of the compositing operation. + /// - blendKernel: The `CIBlendKernel` to use for blending the image with the `background`. + /// - colorSpace: The `CGColorSpace` to perform the blend operation in. + /// - Returns: An image object representing the result of the compositing operation. + @available(iOS 13.0, macCatalyst 13.1, macOS 10.15, tvOS 13.0, *) + public func composited(over background: CIImage, using blendKernel: CIBlendKernel, colorSpace: CGColorSpace) -> CIImage? { + return blendKernel.apply(foreground: self, background: background, colorSpace: colorSpace) + } + + /// Colorizes the image in the given color, i.e., all non-transparent pixels in the receiver will be set to `color`. + /// + /// - Parameter color: The color to override visible pixels of the receiver with. + /// - Returns: The colorized image. + public func colorized(with color: CIColor) -> CIImage? { + return CIBlendKernel.sourceAtop.apply(foreground: CIImage(color: color).cropped(to: self.extent), background: self) + } + +} From a95067d4e6bd795ea91c005b3dcbf5c7be40f832 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 31 Jul 2022 15:32:44 +0200 Subject: [PATCH 04/30] Added some convenience methods for rendering text into a `CIImage`. --- Sources/CIImage+Text.swift | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Sources/CIImage+Text.swift diff --git a/Sources/CIImage+Text.swift b/Sources/CIImage+Text.swift new file mode 100644 index 0000000..7614a06 --- /dev/null +++ b/Sources/CIImage+Text.swift @@ -0,0 +1,90 @@ +import CoreImage +#if canImport(AppKit) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + + +/// Some convenience methods for rendering text into a `CIImage`. +extension CIImage { + + /// Generates an image that contains the given text. + /// - Parameters: + /// - text: The string of text to render. + /// - fontName: The name of the font that should be used for rendering the text. + /// - fontSize: The size of the font that should be used for rendering the text. + /// - color: The color of the text. The background of the text will be transparent. + /// - padding: A padding to add around the text, effectively increasing the text's virtual `extent`. + /// - Returns: An image containing the rendered text. + @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) + public static func text(_ text: String, fontName: String = "HelveticaNeue", fontSize: CGFloat = 12.0, color: CIColor = .black, padding: CGFloat = 0.0) -> CIImage? { + guard let textGenerator = CIFilter(name: "CITextImageGenerator") else { return nil } + + textGenerator.setValue(fontName, forKey: "inputFontName") + textGenerator.setValue(fontSize, forKey: "inputFontSize") + textGenerator.setValue(text, forKey: "inputText") + if #available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, *) { + // Starting from iOS 16 / macOS 13 we can use the built-in padding property... + textGenerator.setValue(padding, forKey: "inputPadding") + return textGenerator.outputImage?.colorized(with: color) + } else { + // ... otherwise we will do the padding manually. + return textGenerator.outputImage?.colorized(with: color)?.paddedBy(dx: padding, dy: padding).moved(to: .zero) + } + } + +#if canImport(AppKit) + + /// Generates an image that contains the given text. + /// - Parameters: + /// - text: The string of text to render. + /// - font: The `NSFont` that should be used for rendering the text. + /// - color: The color of the text. The background of the text will be transparent. + /// - padding: A padding to add around the text, effectively increasing the text's virtual `extent`. + /// - Returns: An image containing the rendered text. + @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) + public static func text(_ text: String, font: NSFont, color: CIColor = .black, padding: CGFloat = 0.0) -> CIImage? { + return self.text(text, fontName: font.fontName, fontSize: font.pointSize, color: color, padding: padding) + } + +#endif + +#if canImport(UIKit) + + /// Generates an image that contains the given text. + /// - Parameters: + /// - text: The string of text to render. + /// - font: The `UIFont` that should be used for rendering the text. + /// - color: The color of the text. The background of the text will be transparent. + /// - padding: A padding to add around the text, effectively increasing the text's virtual `extent`. + /// - Returns: An image containing the rendered text. + @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) + public static func text(_ text: String, font: UIFont, color: CIColor = .black, padding: CGFloat = 0.0) -> CIImage? { + return self.text(text, fontName: font.fontName, fontSize: font.pointSize, color: color, padding: padding) + } + +#endif + + /// Generates an image that contains the given attributed text. + /// - Parameters: + /// - attributedText: The `NSAttributedString` to render. + /// - padding: A padding to add around the text, effectively increasing the text's virtual `extent`. + /// - Returns: An image containing the rendered attributed text + @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) + public static func attributedText(_ attributedText: NSAttributedString, padding: CGFloat = 0.0) -> CIImage? { + guard let textGenerator = CIFilter(name: "CIAttributedTextImageGenerator") else { return nil } + + textGenerator.setValue(attributedText, forKey: "inputText") + if #available(iOS 16.0, macCatalyst 16.0, macOS 13.0, tvOS 16.0, *) { + // Starting from iOS 16 / macOS 13 we can use the built-in padding property... + textGenerator.setValue(padding, forKey: "inputPadding") + return textGenerator.outputImage + } else { + // ... otherwise we will do the padding manually. + return textGenerator.outputImage?.paddedBy(dx: padding, dy: padding).moved(to: .zero) + } + } + +} From a8fd07a7f637f35995f5eeb212a199a9ff0b4783 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 31 Jul 2022 15:59:51 +0200 Subject: [PATCH 05/30] Added some useful extensions to `CIColor`. --- Sources/CIColor+Extensions.swift | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 Sources/CIColor+Extensions.swift diff --git a/Sources/CIColor+Extensions.swift b/Sources/CIColor+Extensions.swift new file mode 100644 index 0000000..798b106 --- /dev/null +++ b/Sources/CIColor+Extensions.swift @@ -0,0 +1,61 @@ +import CoreImage + + +public extension CIColor { + + /// Initializes a `CIColor` object with the specified `white` component value in all three (RGB) channels. + /// - Parameters: + /// - white: The unpremultiplied component value that should be use for all three color channels. + /// - alpha: The alpha (opacity) component of the color. + /// - colorSpace: The color space in which to create the new color. This color space must conform to the `CGColorSpaceModel.rgb` color space model. + convenience init?(white: CGFloat, alpha: CGFloat = 1.0, colorSpace: CGColorSpace? = nil) { + if let colorSpace = colorSpace { + self.init(red: white, green: white, blue: white, alpha: alpha, colorSpace: colorSpace) + } else { + self.init(red: white, green: white, blue: white, alpha: alpha) + } + } + + /// Initializes a `CIColor` object with the specified extended white component value in all three (RGB) channels. + /// + /// The color will use the extended linear sRGB color space, which allows EDR values outside of the `[0...1]` SDR range. + /// + /// - Parameters: + /// - white: The unpremultiplied component value that should be use for all three color channels. + /// This value can be of the `[0...1]` SDR range to create an EDR color. + /// - alpha: The alpha (opacity) component of the color. + convenience init?(extendedWhite white: CGFloat, alpha: CGFloat = 1.0) { + guard let colorSpace = CGColorSpace.extendedLinearSRGBColorSpace else { return nil } + self.init(white: white, alpha: alpha, colorSpace: colorSpace) + } + + /// Initializes a `CIColor` object with the specified extended component values. + /// + /// The color will use the extended linear sRGB color space, which allows EDR values outside of the `[0...1]` SDR range. + /// + /// - Parameters: + /// - r: The unpremultiplied red component value. + /// This value can be of the `[0...1]` SDR range to create an EDR color. + /// - g: The unpremultiplied green component value. + /// This value can be of the `[0...1]` SDR range to create an EDR color. + /// - b: The unpremultiplied blue component value. + /// This value can be of the `[0...1]` SDR range to create an EDR color. + /// - a: The alpha (opacity) component of the color. + convenience init?(extendedRed r: CGFloat, green g: CGFloat, blue b: CGFloat, alpha a: CGFloat = 1.0) { + guard let colorSpace = CGColorSpace.extendedLinearSRGBColorSpace else { return nil } + self.init(red: r, green: g, blue: b, alpha: a, colorSpace: colorSpace) + } + + /// Returns a color that provide a high contrast to the receiver. + /// + /// The returned color is either black or white, depending on which has the better visibility + /// when put over the receiver color. + var contrastColor: CIColor { + let lightColor = CIColor.white + let darkColor = CIColor.black + + let luminance = (self.red * 0.299) + (self.green * 0.587) + (self.blue * 0.114) + return (luminance > 0.5) ? darkColor : lightColor + } + +} From 860c30b82837f06d3e33e09e6cab86e54d4b8fbc Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 11:57:02 +0200 Subject: [PATCH 06/30] Added helper for creating images with rounded corners. --- Sources/CIImage+Transformation.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sources/CIImage+Transformation.swift b/Sources/CIImage+Transformation.swift index aeb3d63..be4fc87 100644 --- a/Sources/CIImage+Transformation.swift +++ b/Sources/CIImage+Transformation.swift @@ -47,4 +47,24 @@ public extension CIImage { return self.composited(over: background) } + /// Returns the same image with rounded corners. The clipped parts of the corner will be transparent. + /// - Parameter radius: The corner radius. + /// - Returns: The same image with rounded corners. + func withRoundedCorners(radius: Double) -> CIImage? { + // We can't apply rounded corners to infinite images. + guard !self.extent.isInfinite else { return self } + + // Generate a white background image with the same extent and rounded corners. + let generator = CIFilter(name: "CIRoundedRectangleGenerator", parameters: [ + kCIInputRadiusKey: radius, + kCIInputExtentKey: CIVector(cgRect: self.extent), + kCIInputColorKey: CIColor.white + ]) + guard let roundedRect = generator?.outputImage else { return nil } + + // Multiply with the image: where the background is white, the resulting color will be that of the image; + // where the background is transparent (the corners), the result will be transparent. + return self.applyingFilter("CISourceAtopCompositing", parameters: [kCIInputBackgroundImageKey: roundedRect]) + } + } From 6dfe85100f37f14161256fa54c7b643a0900bb83 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 11:57:16 +0200 Subject: [PATCH 07/30] Fixed availability checks. --- Sources/CIImage+Blending.swift | 7 ++++++- Sources/CIImage+Text.swift | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Sources/CIImage+Blending.swift b/Sources/CIImage+Blending.swift index e7afcdf..76f23d8 100644 --- a/Sources/CIImage+Blending.swift +++ b/Sources/CIImage+Blending.swift @@ -16,6 +16,7 @@ extension CIImage { /// - background: An image to serve as the background of the compositing operation. /// - blendKernel: The `CIBlendKernel` to use for blending the image with the `background`. /// - Returns: An image object representing the result of the compositing operation. + @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) public func composited(over background: CIImage, using blendKernel: CIBlendKernel) -> CIImage? { return blendKernel.apply(foreground: self, background: background) } @@ -43,7 +44,11 @@ extension CIImage { /// - Parameter color: The color to override visible pixels of the receiver with. /// - Returns: The colorized image. public func colorized(with color: CIColor) -> CIImage? { - return CIBlendKernel.sourceAtop.apply(foreground: CIImage(color: color).cropped(to: self.extent), background: self) + if #available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) { + return CIBlendKernel.sourceAtop.apply(foreground: CIImage(color: color).cropped(to: self.extent), background: self) + } else { + return CIImage(color: color).cropped(to: self.extent).applyingFilter("CISourceAtopCompositing", parameters: [kCIInputBackgroundImageKey: self]) + } } } diff --git a/Sources/CIImage+Text.swift b/Sources/CIImage+Text.swift index 7614a06..4f1b9ea 100644 --- a/Sources/CIImage+Text.swift +++ b/Sources/CIImage+Text.swift @@ -44,7 +44,10 @@ extension CIImage { /// - color: The color of the text. The background of the text will be transparent. /// - padding: A padding to add around the text, effectively increasing the text's virtual `extent`. /// - Returns: An image containing the rendered text. - @available(iOS 11.0, macCatalyst 13.1, macOS 10.13, tvOS 11.0, *) + @available(macOS 10.13, *) + @available(iOS, unavailable) + @available(macCatalyst, unavailable) + @available(tvOS, unavailable) public static func text(_ text: String, font: NSFont, color: CIColor = .black, padding: CGFloat = 0.0) -> CIImage? { return self.text(text, fontName: font.fontName, fontSize: font.pointSize, color: color, padding: padding) } From aee1af574ee1b8d4aba7942ca848b783607c8c11 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 14:41:34 +0200 Subject: [PATCH 08/30] Created a first version of a EDR and wide gamut test pattern. --- Sources/CIImage+TestPattern.swift | 166 ++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 Sources/CIImage+TestPattern.swift diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift new file mode 100644 index 0000000..39484c1 --- /dev/null +++ b/Sources/CIImage+TestPattern.swift @@ -0,0 +1,166 @@ +import CoreImage +import CoreImage.CIFilterBuiltins + + +/// Here we create an EDR brightness and wide gamut color test pattern image that can be used, for instance, +/// to verify colors are displayed correctly, filters conserve those color properties correctly, etc. +/// It also showcases how Core Image can be used for image compositing, though in production +/// a composition with this complexity is probably better done with Core Graphits since its too slow in CI. +/// ⚠️ For testing purposes only! This is not meant to be shipped in production code since generating +/// the pattern is slow and potentially error-prone (lots of force-unwraps in here for convenience). +@available(iOS 12.3, macCatalyst 13.1, macOS 10.14.3, tvOS 12.0, *) +extension CIImage { + + /// Create an EDR brightness and wide gamut color test pattern image that can be used, for instance, + /// to verify colors are displayed correctly, filters conserve those color properties correctly, etc. + /// + /// The pattern contains a scale of different brightness values up to a peak EDR brightness of 3.2, + /// which is the maximum brightness an XDR display from Apple can distinctly display. + /// + /// It also contains color swatches displaying different colors in tree different color spaces with + /// increasing color gamut: sRGB, Display P3, and BT.2020 (from HDR video). Most Apple displays support P3 now, + /// whereas only XDR displays and OLED screens can display the difference between P3 and BT.2020. + /// + /// You can also reduce the brightness of your display, which should reveal more levels of brightness and gamut. + /// + /// - Warning: ⚠️ For testing purposes only! This is not meant to be shipped in production code since generating + /// the pattern is slow and potentially error-prone (lots of force-unwraps in here for convenience). + /// + /// - Returns: An EDR brightness and wide gamut color test pattern image. + public static func testPattern() -> CIImage { + var pattern = CIImage.empty() + + for (column, rowColors) in self.swatchColors.enumerated() { + for (row, color) in rowColors.reversed().enumerated() { + var swatch = CIImage.colorSwatch(for: color) + swatch = swatch.moved(to: CGPoint(x: CGFloat(column) * (swatch.extent.width + self.margin), y: CGFloat(row) * (swatch.extent.height + self.margin))) + pattern = swatch.composited(over: pattern) + } + } + + var brightnessScale = CIImage.brightnessScale(levels: self.brightnessScaleLevels) + brightnessScale = brightnessScale.moved(to: CGPoint(x: 0, y: pattern.extent.maxY + self.margin)) + pattern = brightnessScale.composited(over: pattern) + + let background = CIImage(color: .black).cropped(to: pattern.extent.insetBy(dx: -self.margin, dy: -self.margin)) + return pattern.composited(over: background).moved(to: .zero) + } + + + // MARK: - Internal + + /// The size of the single color/brightness tiles. + private static var tileSize: CGSize { CGSize(width: 160, height: 160) } + /// The corner radius that is applied to color swatches and brightness scale. + private static var tileCornerRadius: Double { 10 } + /// The margin between elements in the pattern. + private static var margin: CGFloat { 50 } + /// The font to use for labels in the pattern. + private static var labelFont: String { ".AppleSystemUIFontCompactRounded-Semibold" } + /// Colors for which swatches should be rendered, ordered by columns (outer) and rows (inner arrays). + private static var swatchColors: [[CIColor]] { [[.red, .green, .blue], [.cyan, .magenta, .yellow]] } + /// The different color spaces in which colors should be rendered in the color swatches. + private static var swatchColorSpaces: [(label: String, colorSpace: CGColorSpace)] = [ + ("sRGB", .extendedLinearSRGBColorSpace!), + ("Display P3", .extendedLinearDisplayP3ColorSpace!), + ("BT.2020", .extendedLinearITUR2020ColorSpace!) + ] + /// The different brightness values that should be rendered in the brightness scale. + /// (The 3.2 is added manually because it's the peak brightness of Apple's XDR screens and should be included.) + private static var brightnessScaleLevels: [Double] { Array(stride(from: 0.0, through: 3.0, by: 0.5)) + [3.2] } + + + /// Creates a single tile (rectangle) filled with the given `color` in the given `colorSpace` with the given `size` + /// and the given `label` string displayed on top of it. + private static func colorTile(for color: CIColor, colorSpace: CGColorSpace, size: CGSize, label: String) -> CIImage { + // Match the color from the given color space to the working space + // so that all colors will be in the same base color space in the pattern. + var tile = CIImage(color: color).matchedToWorkingSpace(from: colorSpace)! + tile = tile.cropped(to: CGRect(origin: .zero, size: size)) + + // Put the label in the bottom left corner over the tile. + // Use a contrast color that is always readable, regardless of the tile's color. + let labelImage = CIImage.text(label, fontName: self.labelFont, fontSize: size.height / 8.0, color: color.contrastColor)! + return labelImage.moved(to: CGPoint(x: 5, y: 5)).composited(over: tile) + } + + /// Creates a swatch (line of colored tiles) for the given `color` for comparison. + /// Each tile will display the color in one of the `swatchColorSpaces`. + private static func colorSwatch(for color: CIColor) -> CIImage { + // Generate a color tile for each color space and place them next to each other. + let swatch = self.swatchColorSpaces.enumerated().reduce(CIImage.empty()) { partialResult, entry in + var tile = CIImage.colorTile(for: color, colorSpace: entry.element.colorSpace, size: self.tileSize, label: entry.element.label) + tile = tile.translatedBy(dx: CGFloat(entry.offset) * tile.extent.width, dy: 0) + return tile.composited(over: partialResult) + } + // Also apply some round corners to the whole swatch. + return swatch.withRoundedCorners(radius: self.tileCornerRadius)! + } + + /// Creates a single tile (rectangle) filled with white (or gray) in the given `brightness` value with the given `size`. + /// The tile will be labeled with the `brightness` value and the corresponding nits value + /// (assuming standard brightness `1.0` = 500 nits). + private static func brightnessTile(for brightness: Double, size: CGSize) -> CIImage { + // Create the tile containing the brightness in all color channels. + var tile = CIImage.containing(value: brightness)! + tile = tile.cropped(to: CGRect(origin: .zero, size: size)) + + // Put a label in the bottom left corner over the tile that displays the brightness value (also in nits). + let labelText = String(format: "%.2f (%d nits)", brightness, Int(brightness * 500)) + let labelColor = CIColor(extendedWhite: brightness)!.contrastColor + let label = CIImage.text(labelText, fontName: self.labelFont, fontSize: size.height / 8.0, color: labelColor)! + return label.moved(to: CGPoint(x: 5, y: 5)).composited(over: tile) + } + + /// Creates a "scale" (basically a long swatch) with tiles of different `levels` of brightness. + /// It also adds an annotation below the scale showing which part of the scale is SDR (standard dynamic range) + /// and which part is EDR (extended dynamic range). + private static func brightnessScale(levels: [Double]) -> CIImage { + // Create a long swatch containing the different brightness tiles. + var scale = levels.enumerated().reduce(CIImage.empty()) { partialResult, entry in + var tile = CIImage.brightnessTile(for: entry.element, size: self.tileSize) + tile = tile.translatedBy(dx: CGFloat(entry.offset) * self.tileSize.width, dy: 0) + return tile.composited(over: partialResult) + } + // Add some round corners. + scale = scale.withRoundedCorners(radius: self.tileCornerRadius)! + // Move the scale up so we can add the annotation below. + scale = scale.translatedBy(dx: 0, dy: 80) + + // Annotation properties. + let annotationColor = CIColor.white + let annotationLineWidth: CGFloat = 2.0 + + // Find the brightness that symbolizes "reference white" (i.e. brightness value 1.0). + let referenceWhiteIndex = levels.firstIndex(where: { $0 >= 1.0 }) ?? levels.indices.upperBound + // Find the place where the separator between SDR and EDR needs to go. + let sdrEndSeparatorX = CGFloat(referenceWhiteIndex + 1) * self.tileSize.width - annotationLineWidth / 2.0 + + // For drawing the "lines" we simply use a white image that we crop to narrow rectangles and place it under the scales. + let line = CIImage(color: annotationColor) + // One long line horizontal line below the scales. + scale = line.cropped(to: CGRect(x: 0, y: 30, width: scale.extent.width, height: annotationLineWidth)).composited(over: scale) + // Three small vertical "tick" lines marking the beginning and end of the swatch... + scale = line.cropped(to: CGRect(x: 0, y: 30, width: annotationLineWidth, height: 40)).composited(over: scale) + scale = line.cropped(to: CGRect(x: scale.extent.maxX - annotationLineWidth, y: 30, width: annotationLineWidth, height: 40)).composited(over: scale) + // ... and the border between SDR and EDR. + scale = line.cropped(to: CGRect(x: sdrEndSeparatorX, y: 30, width: annotationLineWidth, height: 40)).composited(over: scale) + + // Add an "SDR" label in the middle of the line below the SDR part of the scale. + var sdrLabel = CIImage.text("SDR", fontName: self.labelFont, fontSize: 35, color: annotationColor, padding: 10)! + sdrLabel = sdrLabel.moved(to: CGPoint(x: (sdrEndSeparatorX - sdrLabel.extent.width) / 2.0, y: 0)) + // Remove the part below the label from the annotation line by multiplying its color with black (zero) before adding the label on top. + scale = CIImage(color: .black).cropped(to: sdrLabel.extent).composited(over: scale, using: .multiply)! + scale = sdrLabel.composited(over: scale) + + // Add an "EDR" label in the middle of the line below the EDR part of the scale. + var hdrLabel = CIImage.text("EDR", fontName: self.labelFont, fontSize: 35, color: annotationColor, padding: 10)! + hdrLabel = hdrLabel.moved(to: CGPoint(x: sdrEndSeparatorX + (scale.extent.width - sdrEndSeparatorX - hdrLabel.extent.width) / 2.0, y: 0)) + // Remove the part below the label from the annotation line by multiplying its color with black (zero) before adding the label on top. + scale = CIImage(color: .black).cropped(to: hdrLabel.extent).composited(over: scale, using: .multiply)! + scale = hdrLabel.composited(over: scale) + + return scale + } + +} From 3d9f704d6c096cc38822f69b5dc11fc174695096 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 17:41:09 +0200 Subject: [PATCH 09/30] Added helper for centering an image at a given point. --- Sources/CIImage+Transformation.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/CIImage+Transformation.swift b/Sources/CIImage+Transformation.swift index be4fc87..6bae8c7 100644 --- a/Sources/CIImage+Transformation.swift +++ b/Sources/CIImage+Transformation.swift @@ -36,6 +36,13 @@ public extension CIImage { return self.translatedBy(dx: origin.x - self.extent.origin.x, dy: origin.y - self.extent.origin.y) } + /// Returns a new image that represents the original image after moving the center of its extent to the given point. + /// - Parameter point: The new center point of the image. + /// - Returns: A moved image with the new center point. + func centered(in point: CGPoint) -> CIImage { + return self.translatedBy(dx: point.x - self.extent.midX, dy: point.y - self.extent.midY) + } + /// Returns a new image that represents the original image after adding a padding of clear pixels around it, /// effectively increasing its virtual extent. /// - Parameters: From 519fe0666ffecaedcbef7df86de76de4f9a1768b Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 17:41:26 +0200 Subject: [PATCH 10/30] Added a fancy title label to the pattern. --- Sources/CIImage+TestPattern.swift | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index 39484c1..aade98d 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -28,9 +28,14 @@ extension CIImage { /// /// - Returns: An EDR brightness and wide gamut color test pattern image. public static func testPattern() -> CIImage { + // Note: Since Core Image's coordinate system has its origin in the lower left corner, we composite the pattern from the bottom up. + + // Start with an empty image and composite the pattern components on top. var pattern = CIImage.empty() + // Add wide gamut color swatches in a grid in the lower part of the pattern. for (column, rowColors) in self.swatchColors.enumerated() { + // Note: We reverse here so the first color specified in the array is displayed on the top. for (row, color) in rowColors.reversed().enumerated() { var swatch = CIImage.colorSwatch(for: color) swatch = swatch.moved(to: CGPoint(x: CGFloat(column) * (swatch.extent.width + self.margin), y: CGFloat(row) * (swatch.extent.height + self.margin))) @@ -38,10 +43,17 @@ extension CIImage { } } + // Add the brightness scale above the color swatches. var brightnessScale = CIImage.brightnessScale(levels: self.brightnessScaleLevels) brightnessScale = brightnessScale.moved(to: CGPoint(x: 0, y: pattern.extent.maxY + self.margin)) pattern = brightnessScale.composited(over: pattern) + // Add a nice title to the pattern. + var title = self.titleLabel() + title = title.moved(to: CGPoint(x: 0, y: pattern.extent.maxY + self.margin)) + pattern = title.composited(over: pattern) + + // Put everything on a black background. let background = CIImage(color: .black).cropped(to: pattern.extent.insetBy(dx: -self.margin, dy: -self.margin)) return pattern.composited(over: background).moved(to: .zero) } @@ -163,4 +175,55 @@ extension CIImage { return scale } + /// Creates a title label for the pattern that reads "EDR & Wide Gamut Test Pattern" with some fancy gradients. + private static func titleLabel() -> CIImage { + // Create a title label image. The 97 point font size will let the label have roughly the same width as the brightness scale. + var label = CIImage.text("EDR & Wide Gamut Test Pattern", fontName: self.labelFont, fontSize: 97, color: .white)! + + // Add a gradient from bright EDR white to normal white over the "EDR" part of the label. + let edrLabelRect = CGRect(x: 0, y: 0, width: 169, height: 116) + let edrGradient = CIFilter(name: "CISmoothLinearGradient", parameters: [ + "inputPoint0": CIVector(x: edrLabelRect.minX, y: edrLabelRect.midY), + "inputPoint1": CIVector(x: edrLabelRect.maxX, y: edrLabelRect.midY), + "inputColor0": CIColor(extendedWhite: 3.2)!, + "inputColor1": CIColor(white: 1.0)! + ])!.outputImage!.cropped(to: edrLabelRect) + label = edrGradient.composited(over: label, using: .sourceAtop)! + + // Add a "rainbow" color gradient over the "Wide Gamut" part of the Label. + let wideGamutRect = CGRect(x: 248, y: 0, width: 523, height: 116) + let wideGamutGradient = self.hueGradient(from: wideGamutRect.minX, to: wideGamutRect.maxX) + .cropped(to: wideGamutRect) + .matchedToWorkingSpace(from: .extendedLinearITUR2020ColorSpace!)! + label = wideGamutGradient.composited(over: label, using: .sourceAtop)! + + return label + } + + /// Generates an image containing a horizontal "rainbow" gradient containing all color hues. + /// The hues are mapped to the x-axis between `startX` and `endX`. + /// + /// - Warning: ⚠️ This uses a simple color kernel written in the very much deprecated + /// Core Image Kernel Language that is compiled at runtime. + /// This is mainly for convenience reasons, but also because custom + /// Metal kernels can't be compiled in a Swift package yet. + /// If you want to use something similar in production, you should implement + /// it using a Metal kernel that is compiled (and validated) together with the + /// rest of the code. + private static func hueGradient(from startX: CGFloat, to endX: CGFloat) -> CIImage { + let kernelCode = """ + kernel vec4 hueGradient(float startX, float endX) { + // Map the x-axis between the points to the hue value in HSV color space. + float hue = (destCoord().x - startX) / (endX - startX); + vec3 hsv = vec3(clamp(hue, 0.0, 1.0), 1.0, 1.0); + // Convert HSV back to RGB (taken from https://stackoverflow.com/a/17897228/541016). + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www); + return vec4(hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y), 1.0); + } + """ + let kernel = CIColorKernel(source: kernelCode)! + return kernel.apply(extent: .infinite, arguments: [startX, endX])! + } + } From 5caec8eeb1f37c9ebaab60e66ba6c8a54bb1d26f Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 17:49:42 +0200 Subject: [PATCH 11/30] Simplified swatch composition. --- Sources/CIImage+TestPattern.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index aade98d..89e8307 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -100,9 +100,9 @@ extension CIImage { /// Each tile will display the color in one of the `swatchColorSpaces`. private static func colorSwatch(for color: CIColor) -> CIImage { // Generate a color tile for each color space and place them next to each other. - let swatch = self.swatchColorSpaces.enumerated().reduce(CIImage.empty()) { partialResult, entry in - var tile = CIImage.colorTile(for: color, colorSpace: entry.element.colorSpace, size: self.tileSize, label: entry.element.label) - tile = tile.translatedBy(dx: CGFloat(entry.offset) * tile.extent.width, dy: 0) + let swatch = self.swatchColorSpaces.reduce(CIImage.empty()) { partialResult, entry in + var tile = CIImage.colorTile(for: color, colorSpace: entry.colorSpace, size: self.tileSize, label: entry.label) + tile = tile.translatedBy(dx: partialResult.extent.maxX.isInfinite ? 0 : partialResult.extent.maxX, dy: 0) return tile.composited(over: partialResult) } // Also apply some round corners to the whole swatch. @@ -129,9 +129,9 @@ extension CIImage { /// and which part is EDR (extended dynamic range). private static func brightnessScale(levels: [Double]) -> CIImage { // Create a long swatch containing the different brightness tiles. - var scale = levels.enumerated().reduce(CIImage.empty()) { partialResult, entry in - var tile = CIImage.brightnessTile(for: entry.element, size: self.tileSize) - tile = tile.translatedBy(dx: CGFloat(entry.offset) * self.tileSize.width, dy: 0) + var scale = levels.reduce(CIImage.empty()) { partialResult, brightness in + var tile = CIImage.brightnessTile(for: brightness, size: self.tileSize) + tile = tile.translatedBy(dx: partialResult.extent.maxX.isInfinite ? 0 : partialResult.extent.maxX, dy: 0) return tile.composited(over: partialResult) } // Add some round corners. From 6be5f67c739e54abf4db2c5b8f38af6f53ed042d Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 18:20:44 +0200 Subject: [PATCH 12/30] Fixed rounded corner compositing. --- Sources/CIImage+Transformation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CIImage+Transformation.swift b/Sources/CIImage+Transformation.swift index 6bae8c7..a58e1ae 100644 --- a/Sources/CIImage+Transformation.swift +++ b/Sources/CIImage+Transformation.swift @@ -71,7 +71,7 @@ public extension CIImage { // Multiply with the image: where the background is white, the resulting color will be that of the image; // where the background is transparent (the corners), the result will be transparent. - return self.applyingFilter("CISourceAtopCompositing", parameters: [kCIInputBackgroundImageKey: roundedRect]) + return self.applyingFilter("CIMultiplyCompositing", parameters: [kCIInputBackgroundImageKey: roundedRect]) } } From e6220cc66c89bb8dedd227065ea5652842a4c548 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 18:20:56 +0200 Subject: [PATCH 13/30] Better luminance threshold for contrast color. --- Sources/CIColor+Extensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CIColor+Extensions.swift b/Sources/CIColor+Extensions.swift index 798b106..adaf47b 100644 --- a/Sources/CIColor+Extensions.swift +++ b/Sources/CIColor+Extensions.swift @@ -55,7 +55,7 @@ public extension CIColor { let darkColor = CIColor.black let luminance = (self.red * 0.299) + (self.green * 0.587) + (self.blue * 0.114) - return (luminance > 0.5) ? darkColor : lightColor + return (luminance > 0.4) ? darkColor : lightColor } } From 7580275aa4498e1a5f0fef5c8d6ffd58b538df1f Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Fri, 5 Aug 2022 18:22:17 +0200 Subject: [PATCH 14/30] Added labels for color values to the swatch. --- Sources/CIImage+TestPattern.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index 89e8307..dfb035a 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -98,15 +98,30 @@ extension CIImage { /// Creates a swatch (line of colored tiles) for the given `color` for comparison. /// Each tile will display the color in one of the `swatchColorSpaces`. + /// A label is added to the left next to the swatch displaying the color values. private static func colorSwatch(for color: CIColor) -> CIImage { + // Generate a label to put next to the swatch. + let labelText = String(format: """ + R: %.2f + G: %.2f + B: %.2f + """, color.red, color.green, color.blue) + var label = CIImage.text(labelText, fontName: ".AppleSystemUIFontMonospaced-Semibold", fontSize: self.tileSize.height / 7.0, color: .white, padding: 18)! + // Generate a color tile for each color space and place them next to each other. - let swatch = self.swatchColorSpaces.reduce(CIImage.empty()) { partialResult, entry in + var swatch = self.swatchColorSpaces.reduce(CIImage.empty()) { partialResult, entry in var tile = CIImage.colorTile(for: color, colorSpace: entry.colorSpace, size: self.tileSize, label: entry.label) tile = tile.translatedBy(dx: partialResult.extent.maxX.isInfinite ? 0 : partialResult.extent.maxX, dy: 0) return tile.composited(over: partialResult) } // Also apply some round corners to the whole swatch. - return swatch.withRoundedCorners(radius: self.tileCornerRadius)! + swatch = swatch.withRoundedCorners(radius: self.tileCornerRadius)! + + // Adjust placement of label and swatch. + label = label.centered(in: CGPoint(x: label.extent.midX, y: swatch.extent.midY)) + swatch = swatch.moved(to: CGPoint(x: label.extent.maxX, y: 0)) + + return label.composited(over: swatch) } /// Creates a single tile (rectangle) filled with white (or gray) in the given `brightness` value with the given `size`. From 7705d59180f7a63f53597ba7ad036600a34efb3f Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sat, 6 Aug 2022 15:14:36 +0200 Subject: [PATCH 15/30] Added an optional label to the pattern. --- Sources/CIImage+TestPattern.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index dfb035a..ebf3c27 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -26,8 +26,10 @@ extension CIImage { /// - Warning: ⚠️ For testing purposes only! This is not meant to be shipped in production code since generating /// the pattern is slow and potentially error-prone (lots of force-unwraps in here for convenience). /// + /// - Parameter label: An optional label text that will be added at the bottom left corner of the pattern. + /// This can be used, for instance, for adding information about file format and color space of the rendering. /// - Returns: An EDR brightness and wide gamut color test pattern image. - public static func testPattern() -> CIImage { + public static func testPattern(label: String? = nil) -> CIImage { // Note: Since Core Image's coordinate system has its origin in the lower left corner, we composite the pattern from the bottom up. // Start with an empty image and composite the pattern components on top. @@ -53,6 +55,13 @@ extension CIImage { title = title.moved(to: CGPoint(x: 0, y: pattern.extent.maxY + self.margin)) pattern = title.composited(over: pattern) + // Add the given text label at the bottom left corner of the image, if any. + if let labelText = label, !labelText.isEmpty { + var label = CIImage.text(labelText, fontName: self.labelFont, fontSize: 30, color: .white)! + label = label.moved(to: CGPoint(x: 0, y: -label.extent.height - self.margin)) + pattern = label.composited(over: pattern) + } + // Put everything on a black background. let background = CIImage(color: .black).cropped(to: pattern.extent.insetBy(dx: -self.margin, dy: -self.margin)) return pattern.composited(over: background).moved(to: .zero) From 586445c20c4e8c4de831b3049ea966ffb581a3a2 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sat, 6 Aug 2022 19:37:50 +0200 Subject: [PATCH 16/30] Added tests for generating test patterns in various file formats and color spaces. --- Tests/TestPatternTests.swift | 126 +++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 Tests/TestPatternTests.swift diff --git a/Tests/TestPatternTests.swift b/Tests/TestPatternTests.swift new file mode 100644 index 0000000..1f5f9f2 --- /dev/null +++ b/Tests/TestPatternTests.swift @@ -0,0 +1,126 @@ +import CoreImage +import CoreImageExtensions +import UniformTypeIdentifiers +import XCTest + + +/// These tests can be used to generate EDR & wide gamut test pattern images in various formats. +/// The generated images are attached to the test runs and can be found when opening the test result +/// in the Reports navigator. +class TestPatternTests: XCTestCase { + + let context = CIContext() + + /// Helper type for assigning some human-readable labels to color spaces. + private typealias ColorSpace = (cgSpace: CGColorSpace, label: String) + + // Some common color spaces that we want to generate patterns in. + private let sRGBColorSpace = ColorSpace(.sRGBColorSpace!, "sRGB") + private let extendedLinearSRGBColorSpace = ColorSpace(.extendedLinearSRGBColorSpace!, "extended linear sRGB") + private let displayP3ColorSpace = ColorSpace(.displayP3ColorSpace!, "Display P3") + private let itur2020ColorSpace = ColorSpace(.itur2020ColorSpace!, "BT.2020") + private let itur2100HLGColorSpace = ColorSpace(.itur2100HLGColorSpace!, "BT.2100 HLG") + private let itur2100PQColorSpace = ColorSpace(.itur2100PQColorSpace!, "BT.2100 PQ") + + /// Color spaces that are suitable for image formats with low bit-depth (i.e. 8-bit). + private lazy var lowBitColorSpaces: [ColorSpace] = [sRGBColorSpace, displayP3ColorSpace] + /// Color spaces that require image formats with higher bit depth to not cause quantization artifacts + /// between consecutive color values. + private lazy var highBitColorSpaces: [ColorSpace] = [itur2020ColorSpace, itur2100HLGColorSpace, itur2100PQColorSpace] + + + /// Generates a test pattern image. The parameters are used to compose the label that is added at the bottom of the pattern image. + private func testPattern(for fileType: UTType, bitDepth: Int, isFloat: Bool, colorSpace: ColorSpace) -> CIImage { + let label = "\(fileType.preferredFilenameExtension!.uppercased()), \(bitDepth)-bit, \(isFloat ? "float, " : "")\(colorSpace.label)" + return CIImage.testPattern(label: label) + } + + /// Attaches the given image `data` of the given `type` to the test case so it can be retrieved from the test report after the run. + /// The other parameters are used for naming the file in accordance with the image properties. + private func attach(_ data: Data, type: UTType, bitDepth: Int, isFloat: Bool, colorSpace: ColorSpace) { + let attachment = XCTAttachment(data: data, uniformTypeIdentifier: type.identifier) + attachment.lifetime = .keepAlways + let colorSpaceFileName = colorSpace.label.replacingOccurrences(of: " ", with: "-") + attachment.name = "TestPattern_\(bitDepth)bit_\(isFloat ? "float_" : "")\(colorSpaceFileName).\(type.preferredFilenameExtension!)" + self.add(attachment) + } + + + /// Generates an EDR & wide gamut test pattern image in EXR file format. + /// Since EDR can store values as-is, we only generate one 16-bit float image in extended linear sRGB color space, + /// which are the reference properties when composing the test pattern. + func testEXRPatternGeneration() throws { + let testPatter = self.testPattern(for: .exr, bitDepth: 16, isFloat: true, colorSpace: extendedLinearSRGBColorSpace) + let data = try self.context.exrRepresentation(of: testPatter, format: .RGBAh, colorSpace: extendedLinearSRGBColorSpace.cgSpace) + self.attach(data, type: .exr, bitDepth: 16, isFloat: true, colorSpace: extendedLinearSRGBColorSpace) + + } + + /// Generates EDR & wide gamut test pattern images in TIFF file format. + /// Since TIFF can store 16-bit floating-point values, we generate an image in extended linear sRGB color space, + /// which are the reference properties when composing the test pattern. + /// We also generate images in the HDR color spaces for reference. + func testTIFFPatternGeneration() { + for colorSpace in highBitColorSpaces + [extendedLinearSRGBColorSpace] { + let testPatter = self.testPattern(for: .tiff, bitDepth: 16, isFloat: true, colorSpace: colorSpace) + let data = self.context.tiffRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace)! + self.attach(data, type: .tiff, bitDepth: 16, isFloat: true, colorSpace: colorSpace) + } + } + + /// Generates EDR & wide gamut test pattern images in TIFF file format. + /// PNG supports 8- and 16-bit color depths, so we generate patterns in the color spaces fitting those bit depths. + /// However, PNG does not support floating-point values, so we don't need to generate an image in extended color space. + func testPNGPatternGeneration() { + for colorSpace in lowBitColorSpaces { + let testPatter = self.testPattern(for: .png, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPatter, format: .RGBA8, colorSpace: colorSpace.cgSpace)! + self.attach(data, type: .png, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + } + + for colorSpace in highBitColorSpaces { + let testPatter = self.testPattern(for: .png, bitDepth: 16, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace)! + self.attach(data, type: .png, bitDepth: 16, isFloat: false, colorSpace: colorSpace) + } + } + + /// Generates EDR & wide gamut test pattern images in JPEG file format. + /// JPEG only supports 8-bit color depth, so we only generate images in color spaces that are fitting for 8-bit. + func testJPEGPatternGeneration() { + for colorSpace in lowBitColorSpaces { + let testPatter = self.testPattern(for: .jpeg, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.jpegRepresentation(of: testPatter, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + self.attach(data, type: .jpeg, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + } + } + + /// Generates EDR & wide gamut test pattern images in HEIC file format. + /// HEIC supports 8- and 10-bit color depths, so we generate patterns in the color spaces fitting those bit depths. + /// However, HEIC does not support floating-point values, so we don't need to generate an image in extended color space. + func testHEICPatternGeneration() throws { + for colorSpace in lowBitColorSpaces { + let testPatter = self.testPattern(for: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPatter, format: .RGBA8, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + self.attach(data, type: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + } + + if #available(macOS 12.0, *) { + for colorSpace in highBitColorSpaces { + let testPatter = self.testPattern(for: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + self.attach(data, type: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) + } + } + } + +} + + +private extension UTType { + static var exr: Self { UTType("com.ilm.openexr-image")! } +} + +private extension CIImageRepresentationOption { + static var quality: Self { CIImageRepresentationOption(rawValue: kCGImageDestinationLossyCompressionQuality as String) } +} From d7f0e02d32322e97e96f0b1e2186349336047e7c Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sat, 6 Aug 2022 19:51:14 +0200 Subject: [PATCH 17/30] Fixed availability checks. --- Tests/TestPatternTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/TestPatternTests.swift b/Tests/TestPatternTests.swift index 1f5f9f2..fb946dd 100644 --- a/Tests/TestPatternTests.swift +++ b/Tests/TestPatternTests.swift @@ -7,6 +7,7 @@ import XCTest /// These tests can be used to generate EDR & wide gamut test pattern images in various formats. /// The generated images are attached to the test runs and can be found when opening the test result /// in the Reports navigator. +@available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, *) class TestPatternTests: XCTestCase { let context = CIContext() @@ -105,7 +106,7 @@ class TestPatternTests: XCTestCase { self.attach(data, type: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) } - if #available(macOS 12.0, *) { + if #available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, *) { for colorSpace in highBitColorSpaces { let testPatter = self.testPattern(for: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) let data = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! @@ -117,6 +118,7 @@ class TestPatternTests: XCTestCase { } +@available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, *) private extension UTType { static var exr: Self { UTType("com.ilm.openexr-image")! } } From e6c2cc820ea2e0ff8761022347fa0e81f2e0e859 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 7 Aug 2022 15:44:58 +0200 Subject: [PATCH 18/30] Added generated test patterns to repo (via LFS) for easy download. --- .gitattributes | 1 + EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic | 3 +++ .../TestPattern_10bit_BT.2100-HLG.heic | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2020.png | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-HLG.png | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png | 3 +++ .../TestPattern_16bit_float_BT.2020.tiff | 3 +++ .../TestPattern_16bit_float_BT.2100-HLG.tiff | 3 +++ .../TestPattern_16bit_float_BT.2100-PQ.tiff | 3 +++ .../TestPattern_16bit_float_extended-linear-sRGB.exr | 3 +++ .../TestPattern_16bit_float_extended-linear-sRGB.tiff | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.jpeg | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.png | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.jpeg | 3 +++ EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.png | 3 +++ 18 files changed, 52 insertions(+) create mode 100644 .gitattributes create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2020.png create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-HLG.png create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2020.tiff create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-HLG.tiff create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-PQ.tiff create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.jpeg create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.png create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.jpeg create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.png diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfb4dd5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +EDR_Wide_Gamut_Test_Patterns/* filter=lfs diff=lfs merge=lfs -text diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic new file mode 100644 index 0000000..f7fe1fa --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48743c06764d77cebceca19387a4d88686f9732f8a74362e7356ac2cc3a53e7c +size 235683 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic new file mode 100644 index 0000000..faff32e --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e64ba758c2e79ff968204e55fb7e016b314c6986c00951b833e20f9b9b88eee8 +size 244888 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic new file mode 100644 index 0000000..77ee0e6 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d82f01bea197ec7ca8e236cefbfa80ea8513b5d33975a02ee24bb20317c55b1 +size 250837 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2020.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2020.png new file mode 100644 index 0000000..7c21a21 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2020.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db32326781f6ed194245f8505c7887dc6cc331ad5ad4a564b7a5c41acdf97942 +size 236072 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-HLG.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-HLG.png new file mode 100644 index 0000000..0cc3b10 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-HLG.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa08b2e9cd571856781404477984f6aa69d1117454aa8a4af8afab951fb07ef0 +size 245955 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png new file mode 100644 index 0000000..44ac227 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb8d6899c56eaa37140666a2e7752727544b6a3f4f8d5b58c0e09edca986437e +size 251702 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2020.tiff b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2020.tiff new file mode 100644 index 0000000..b0546de --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2020.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7dd203ceb00dfbde2e3fc03557c841b50fb87417ea5f8e7851eed3c938e181dd +size 13562298 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-HLG.tiff b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-HLG.tiff new file mode 100644 index 0000000..f9242bc --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-HLG.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d95ddda6e440eac4a7239d3efaabe455f000dd89e86198f0a8763b823928390e +size 13569062 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-PQ.tiff b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-PQ.tiff new file mode 100644 index 0000000..0956eeb --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_BT.2100-PQ.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d0a3c55123236a0fc4dc4e06b6962daae9578555d85d5c9e95189679e5ab9d2 +size 13575206 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr new file mode 100644 index 0000000..ac1c908 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37331e3287a3838d93160484f41db8cdf65eb2c3ad09be6eed90555d164cd3f5 +size 247891 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff new file mode 100644 index 0000000..361cae4 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e673af08e67c29096f6af7a10bf66d0dda7c207852b9dd6c375068a5919cb8f4 +size 13562290 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic new file mode 100644 index 0000000..a3ce872 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6533402a8c0dd6e6115e597f7bb605f9ecd376ff588c77cb901113ec156e52d5 +size 146362 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.jpeg b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.jpeg new file mode 100644 index 0000000..8c4eff1 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca26660893bf24e1358dbbe2d5e8ab51b198f4dd88f5f7c2b6c977e7c4f90841 +size 375001 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.png new file mode 100644 index 0000000..3259d15 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2e9325e2ea64cf3b4476158d169c8a135fb8280724249d5f340fe4fd8b41061 +size 146355 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic new file mode 100644 index 0000000..2e323ef --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74ad3a8ab39907a542a6b176bc3f7d1e1b800f055ae3d04d45fb4ddea50f90e8 +size 140324 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.jpeg b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.jpeg new file mode 100644 index 0000000..4b65d5a --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97de9bd2b83de9e7e85f3779d690dffa34fd322e8c7ab934e462277fce366e79 +size 368584 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.png new file mode 100644 index 0000000..868c56b --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:941968cacd8d0c3a7360c47660993f4b359988a61c70cef818adfd7efa5f5e25 +size 140555 From 72eeb5146c3cf26314e8e9248bff7b6f28660824 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 7 Aug 2022 22:06:36 +0200 Subject: [PATCH 19/30] Added APIs for runtime compilation of Metal CIKernels from source stings. --- Sources/CIKernel+MetalSource.swift | 126 +++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 Sources/CIKernel+MetalSource.swift diff --git a/Sources/CIKernel+MetalSource.swift b/Sources/CIKernel+MetalSource.swift new file mode 100644 index 0000000..a482aba --- /dev/null +++ b/Sources/CIKernel+MetalSource.swift @@ -0,0 +1,126 @@ +import CoreImage + + +public extension CIKernel { + + /// Errors that can be thrown by the Metal kernel runtime compilation APIs. + enum MetalKernelError: Swift.Error { + case functionNotFound(_ message: String) + case noMatchingKernelFound(_ message: String) + case blendKernelsNotSupported(_ message: String) + case ciklKernelCreationFailed(_ message: String) + } + + + /// Compiles a Core Image kernel at runtime from the given Metal `source` string. + /// + /// ⚠️ Important: There are a few limitations to this API: + /// - It only works when the kernels are attributed as `[[ stitchable ]]`. + /// Please refer to [this WWDC talk](https://developer.apple.com/wwdc21/10159) for details. + /// - It only works when the Metal device used by Core Image supports dynamic libraries. + /// You can check ``MTLDevice.supportsDynamicLibraries`` to see if runtime compilation of Metal-based + /// CIKernels is supported. + /// - `CIBlendKernel` can't be compiled this way, unfortunately. The ``CIKernel.kernels(withMetalString:)`` + /// API just identifies them as `CIColorKernel` + /// + /// It is generally a much better practice to compile Metal CIKernels along with the rest of your sources + /// and only use runtime compilation as an exception. This way the compiler can check your sources at + /// build-time, and initializing a CIKernel at runtime from pre-compiled sources is much faster. + /// A notable exception might arise when you need a custom kernel inside a Swift package since CI Metal kernels + /// can't be built with Swift packages (yet). But this should only be used as a last resort. + /// + /// - Parameters: + /// - source: A Metal source code string that contain one or more kernel routines. + /// - kernelName: The name of the kernel function to use for this kernel. Use this if multiple kernels + /// are defined in the source string and you want to load a specific one. Otherwise the + /// first function that matches the kernel type is used. + /// - Returns: The compiled Core Image kernel. + @available(iOS 15.0, macCatalyst 15.0, macOS 12.0, tvOS 15.0, *) + @objc class func kernel(withMetalString source: String, kernelName: String? = nil) throws -> Self { + // Try to compile all kernel routines found in `source`. + let kernels = try CIKernel.kernels(withMetalString: source) + + if let kernelName = kernelName { + // If we were given a specific kernel function name, try to find the kernel with that name that also matches + // the type of the CIKernel (sub-)class (`Self`). + guard let kernel = kernels.first(where: { $0.name == kernelName }), let kernel = kernel as? Self else { + throw MetalKernelError.functionNotFound("No matching kernel function named \"\(kernelName)\" found.") + } + return kernel + } else { + // Otherwise just return the first kernel with a matching kernel type. + guard let kernel = kernels.compactMap({ $0 as? Self }).first else { + throw MetalKernelError.noMatchingKernelFound("No matching kernel of type \(String(reflecting: Self.self)) found.") + } + return kernel + } + } + + /// Compiles a Core Image kernel at runtime from the given Metal `source` string. + /// If this feature is not supported by the OS, the legacy Core Image Kernel Language `ciklSource` are used instead. + /// + /// ⚠️ Important: There are a few limitations to this API: + /// - Run-time compilation of Metal kernels is only supported starting from iOS 15 and macOS 12. + /// If the system doesn't support this feature, the legacy Core Image Kernel Language `ciklSource` are used instead. + /// Note, however, that this API was deprecated with macOS 10.14 and can drop support soon. + /// This API is meant to be used as a temporary solution for when older OSes than iOS 15 and macOS 12 still need to be supported. + /// - It only works when the Metal kernels are attributed as `[[ stitchable ]]`. + /// Please refer to [this WWDC talk](https://developer.apple.com/wwdc21/10159) for details. + /// - It only works when the Metal device used by Core Image supports dynamic libraries. + /// You can check ``MTLDevice.supportsDynamicLibraries`` to see if runtime compilation of Metal-based + /// CIKernels is supported. + /// - `CIBlendKernel` can't be compiled this way, unfortunately. The ``CIKernel.kernels(withMetalString:)`` + /// API just identifies them as `CIColorKernel` + /// + /// It is generally a much better practice to compile Metal CIKernels along with the rest of your sources + /// and only use runtime compilation as an exception. This way the compiler can check your sources at + /// build-time, and initializing a CIKernel at runtime from pre-compiled sources is much faster. + /// A notable exception might arise when you need a custom kernel inside a Swift package since CI Metal kernels + /// can't be built with Swift packages (yet). But this should only be used as a last resort. + /// + /// - Parameters: + /// - source: A Metal source code string that contain one or more kernel routines. + /// - metalKernelName: The name of the kernel function to use for this kernel. Use this if multiple kernels + /// are defined in the source string and you want to load a specific one. Otherwise the + /// first function that matches the kernel type is used. + /// - fallbackCIKLString: The kernel code in the legacy Core Image Kernel Language that is used as a fallback + /// option on older OSes. + /// - Returns: The compiled Core Image kernel. + @objc class func kernel(withMetalString metalSource: String, metalKernelName: String? = nil, fallbackCIKLString ciklSource: String) throws -> Self { + if #available(iOS 15.0, macCatalyst 15.0, macOS 12.0, tvOS 15.0, *) { + return try self.kernel(withMetalString: metalSource, kernelName: metalKernelName) + } else { + guard let fallbackKernel = self.init(source: ciklSource) else { + throw MetalKernelError.ciklKernelCreationFailed("Failed to create fallback kernel from CIKL source string.") + } + return fallbackKernel + } + } + +} + +public extension CIBlendKernel { + + /// ⚠️ `CIBlendKernel` can't be compiled from Metal sources at runtime at the moment. + /// Please see ``CIKernel.kernel(withMetalString:kernelName:)`` for details. + /// You can still compile them using the legacy Core Image Kernel Language and the ``CIBlendKernel.init?(source:)`` API, though. + @available(iOS, unavailable) + @available(macCatalyst, unavailable) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @objc override class func kernel(withMetalString source: String, kernelName: String? = nil) throws -> Self { + throw MetalKernelError.blendKernelsNotSupported("CIBlendKernels can't be initialized with a Metal source string at runtime. Compile them at built-time instead.") + } + + /// ⚠️ `CIBlendKernel` can't be compiled from Metal sources at runtime at the moment. + /// Please see ``CIKernel.kernel(withMetalString:metalKernelName:fallbackCIKLString:)`` for details. + /// You can still compile them using the legacy Core Image Kernel Language and the ``CIBlendKernel.init?(source:)`` API, though. + @available(iOS, unavailable) + @available(macCatalyst, unavailable) + @available(macOS, unavailable) + @available(tvOS, unavailable) + @objc override class func kernel(withMetalString metalSource: String, metalKernelName: String? = nil, fallbackCIKLString ciklSource: String) throws -> Self { + throw MetalKernelError.blendKernelsNotSupported("CIBlendKernels can't be initialized with a Metal source string at runtime. Compile them at built-time instead.") + } + +} From 05084069b9f0c6ebe9a60662f7b815622e965d12 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 7 Aug 2022 22:10:28 +0200 Subject: [PATCH 20/30] Generate pattern hue gradient with a Metal kernel if possible. --- Sources/CIImage+TestPattern.swift | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index ebf3c27..cc460bf 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -227,15 +227,30 @@ extension CIImage { /// Generates an image containing a horizontal "rainbow" gradient containing all color hues. /// The hues are mapped to the x-axis between `startX` and `endX`. /// - /// - Warning: ⚠️ This uses a simple color kernel written in the very much deprecated - /// Core Image Kernel Language that is compiled at runtime. + /// - Warning: ⚠️ This uses a simple color kernel written in Metal and a fallback version + /// for older OSes, written in the very much deprecated + /// Core Image Kernel Language that are compiled at runtime. /// This is mainly for convenience reasons, but also because custom - /// Metal kernels can't be compiled in a Swift package yet. + /// Metal kernels can't be compiled at build-time in a Swift package yet. /// If you want to use something similar in production, you should implement /// it using a Metal kernel that is compiled (and validated) together with the /// rest of the code. private static func hueGradient(from startX: CGFloat, to endX: CGFloat) -> CIImage { - let kernelCode = """ + let metalKernelCode = """ + #include + using namespace metal; + + [[ stitchable ]] half4 hueGradient(half startX, half endX, coreimage::destination dest) { + // Map the x-axis between the points to the hue value in HSV color space. + const half hue = (dest.coord().x - startX) / (endX - startX); + const half3 hsv = half3(clamp(hue, 0.0h, 1.0h), 1.0h, 1.0h); + // Convert HSV back to RGB (taken from https://stackoverflow.com/a/17897228/541016). + const half4 K = half4(1.0h, 2.0h / 3.0h, 1.0h / 3.0h, 3.0h); + const half3 p = abs(fract(hsv.xxx + K.xyz) * 6.0h - K.www); + return half4(hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0h, 1.0h), hsv.y), 1.0h); + } + """ + let ciklKernelCode = """ kernel vec4 hueGradient(float startX, float endX) { // Map the x-axis between the points to the hue value in HSV color space. float hue = (destCoord().x - startX) / (endX - startX); @@ -246,7 +261,7 @@ extension CIImage { return vec4(hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y), 1.0); } """ - let kernel = CIColorKernel(source: kernelCode)! + let kernel = try! CIColorKernel.kernel(withMetalString: metalKernelCode, fallbackCIKLString: ciklKernelCode) return kernel.apply(extent: .infinite, arguments: [startX, endX])! } From 7791745eaacac8c591598adab2394b4ad02447bf Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 08:19:55 +0200 Subject: [PATCH 21/30] Added tests for new color extensions. --- Tests/ColorExtensionsTests.swift | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 Tests/ColorExtensionsTests.swift diff --git a/Tests/ColorExtensionsTests.swift b/Tests/ColorExtensionsTests.swift new file mode 100644 index 0000000..d4911a5 --- /dev/null +++ b/Tests/ColorExtensionsTests.swift @@ -0,0 +1,55 @@ +import CoreImage +import CoreImageExtensions +import XCTest + + +class ColorExtensionsTests: XCTestCase { + + func testWhite() { + let whiteColor = CIColor(white: 0.42, alpha: 0.21, colorSpace: .itur2020ColorSpace) + + XCTAssertEqual(whiteColor?.red, 0.42) + XCTAssertEqual(whiteColor?.green, 0.42) + XCTAssertEqual(whiteColor?.blue, 0.42) + XCTAssertEqual(whiteColor?.alpha, 0.21) + XCTAssertEqual(whiteColor?.colorSpace, .itur2020ColorSpace) + + let clampedWhite = CIColor(white: 1.5) + XCTAssertEqual(clampedWhite?.red, 1.0) + XCTAssertEqual(clampedWhite?.green, 1.0) + XCTAssertEqual(clampedWhite?.blue, 1.0) + XCTAssertEqual(clampedWhite?.alpha, 1.0) + } + + func testExtendedWhite() { + let extendedWhiteColor = CIColor(extendedWhite: 1.42, alpha: 0.34) + + XCTAssertEqual(extendedWhiteColor?.red, 1.42) + XCTAssertEqual(extendedWhiteColor?.green, 1.42) + XCTAssertEqual(extendedWhiteColor?.blue, 1.42) + XCTAssertEqual(extendedWhiteColor?.alpha, 0.34) + XCTAssertEqual(extendedWhiteColor?.colorSpace, .extendedLinearSRGBColorSpace) + } + + func testExtendedColor() { + let extendedColor = CIColor(extendedRed: -0.12, green: 0.43, blue: 1.45, alpha: 0.89) + + XCTAssertEqual(extendedColor?.red, -0.12) + XCTAssertEqual(extendedColor?.green, 0.43) + XCTAssertEqual(extendedColor?.blue, 1.45) + XCTAssertEqual(extendedColor?.alpha, 0.89) + XCTAssertEqual(extendedColor?.colorSpace, .extendedLinearSRGBColorSpace) + } + + func testContrastColor() { + XCTAssertEqual(CIColor.white.contrastColor, .black) + XCTAssertEqual(CIColor.red.contrastColor, .white) + XCTAssertEqual(CIColor.green.contrastColor, .black) + XCTAssertEqual(CIColor.blue.contrastColor, .white) + XCTAssertEqual(CIColor.yellow.contrastColor, .black) + XCTAssertEqual(CIColor.cyan.contrastColor, .black) + XCTAssertEqual(CIColor.magenta.contrastColor, .black) + XCTAssertEqual(CIColor.black.contrastColor, .white) + } + +} From 276f2b9534f27357fabf18c49d169ad2a0d39ca5 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 08:50:44 +0200 Subject: [PATCH 22/30] Added tests for new image transformation extensions. --- Tests/ImageTransformationTests.swift | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Tests/ImageTransformationTests.swift diff --git a/Tests/ImageTransformationTests.swift b/Tests/ImageTransformationTests.swift new file mode 100644 index 0000000..228f2b1 --- /dev/null +++ b/Tests/ImageTransformationTests.swift @@ -0,0 +1,38 @@ +import CoreImage +import CoreImageExtensions +import XCTest + + +class ImageTransformationTests: XCTestCase { + + /// Empty dummy image we can transform around to test effects on image extent. + let testImage = CIImage.clear.cropped(to: CGRect(x: 0, y: 0, width: 200, height: 100)) + + + func testScaling() { + let scaledImage = self.testImage.scaledBy(x: 0.5, y: 2.0) + XCTAssertEqual(scaledImage.extent, CGRect(x: 0, y: 0, width: 100, height: 200)) + } + + func testTranslation() { + let translatedImage = self.testImage.translatedBy(dx: 42.0, dy: -321.0) + XCTAssertEqual(translatedImage.extent, CGRect(origin: CGPoint(x: 42.0, y: -321.0), size: self.testImage.extent.size)) + } + + func testMovingOrigin() { + let newOrigin = CGPoint(x: 21.0, y: -42.0) + let movedImage = self.testImage.moved(to: newOrigin) + XCTAssertEqual(movedImage.extent, CGRect(origin: newOrigin, size: self.testImage.extent.size)) + } + + func testCentering() { + let recenteredImage = self.testImage.centered(in: .zero) + XCTAssertEqual(recenteredImage.extent, CGRect(origin: CGPoint(x: -100.0, y: -50.0), size: self.testImage.extent.size)) + } + + func testPadding() { + let paddedImage = self.testImage.paddedBy(dx: 20, dy: 60) + XCTAssertEqual(paddedImage.extent, CGRect(x: -20, y: -60, width: 240, height: 220)) + } + +} From 7f47dff99a1b269ab35f5bd3968cc9df48490847 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 09:02:18 +0200 Subject: [PATCH 23/30] Better formula for contrast color. --- Sources/CIColor+Extensions.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/CIColor+Extensions.swift b/Sources/CIColor+Extensions.swift index adaf47b..91a1c99 100644 --- a/Sources/CIColor+Extensions.swift +++ b/Sources/CIColor+Extensions.swift @@ -54,8 +54,10 @@ public extension CIColor { let lightColor = CIColor.white let darkColor = CIColor.black - let luminance = (self.red * 0.299) + (self.green * 0.587) + (self.blue * 0.114) - return (luminance > 0.4) ? darkColor : lightColor + // Calculate luminance based on D65 white point (assuming linear color values). + let luminance = (self.red * 0.2126) + (self.green * 0.7152) + (self.blue * 0.0722) + // Compare against the perceptual luminance midpoint (0.5 after gamma correction). + return (luminance > 0.214) ? darkColor : lightColor } } From 19b0fdd2cf5c5cae824745eb05d20a3fcc63f2dd Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 10:16:09 +0200 Subject: [PATCH 24/30] Added tests for Metal kernel runtime compilation. --- Tests/RuntimeMetalKernelTests.swift | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 Tests/RuntimeMetalKernelTests.swift diff --git a/Tests/RuntimeMetalKernelTests.swift b/Tests/RuntimeMetalKernelTests.swift new file mode 100644 index 0000000..817fced --- /dev/null +++ b/Tests/RuntimeMetalKernelTests.swift @@ -0,0 +1,127 @@ +import CoreImage +import CoreImageExtensions +import XCTest + + +class RuntimeMetalKernelTests: XCTestCase { + + let metalKernelCode = """ + #include + using namespace metal; + + [[ stitchable ]] half4 general(coreimage::sampler_h src) { + return src.sample(src.coord()); + } + + [[ stitchable ]] half4 otherGeneral(coreimage::sampler_h src) { + return src.sample(src.coord()); + } + + [[ stitchable ]] half4 color(coreimage::sample_h src) { + return src; + } + + [[ stitchable ]] float2 warp(coreimage::destination dest) { + return dest.coord(); + } + """ + + @available(iOS 15.0, macCatalyst 15.0, macOS 12.0, tvOS 15.0, *) + func testRuntimeGeneralKernelCompilation() { + XCTAssertNoThrow { + let kernel = try CIKernel.kernel(withMetalString: self.metalKernelCode) + XCTAssertEqual(kernel.name, "general") + } + XCTAssertNoThrow { + let kernel = try CIKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "otherGeneral") + XCTAssertEqual(kernel.name, "otherGeneral") + } + XCTAssertThrowsError(try CIKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "notFound")) + } + + func testRuntimeGeneralKernelCompilationWithFallback() { + let ciklKernelCode = """ + kernel vec4 passThrough(sampler src) { + return sample(src, samplerTransform(src, destCoord())); + } + """ + XCTAssertNoThrow { + let kernel = try CIKernel.kernel(withMetalString: self.metalKernelCode, fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "general") + } + XCTAssertNoThrow { + let kernel = try CIKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "otherGeneral", fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "otherGeneral") + } + XCTAssertThrowsError(try CIKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "notFound", fallbackCIKLString: ciklKernelCode)) + } + + @available(iOS 15.0, macCatalyst 15.0, macOS 12.0, tvOS 15.0, *) + func testRuntimeColorKernelCompilation() { + XCTAssertNoThrow { + let kernel = try CIColorKernel.kernel(withMetalString: self.metalKernelCode) + XCTAssertEqual(kernel.name, "color") + } + XCTAssertNoThrow { + let kernel = try CIColorKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "color") + XCTAssertEqual(kernel.name, "color") + } + XCTAssertThrowsError(try CIColorKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "notFound")) + XCTAssertThrowsError(try CIColorKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "general"), + "Should not compile the general kernel as color kernel.") + } + + func testRuntimeColorKernelCompilationWithFallback() { + let ciklKernelCode = """ + kernel vec4 color(__sample src) { + return src.rgba; + } + """ + XCTAssertNoThrow { + let kernel = try CIColorKernel.kernel(withMetalString: self.metalKernelCode, fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "color") + } + XCTAssertNoThrow { + let kernel = try CIColorKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "color", fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "color") + } + XCTAssertThrowsError(try CIColorKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "notFound", fallbackCIKLString: ciklKernelCode)) + XCTAssertThrowsError(try CIColorKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "general", fallbackCIKLString: ciklKernelCode), + "Should not compile the general kernel as color kernel.") + } + + @available(iOS 15.0, macCatalyst 15.0, macOS 12.0, tvOS 15.0, *) + func testRuntimeWarpKernelCompilation() { + XCTAssertNoThrow { + let kernel = try CIWarpKernel.kernel(withMetalString: self.metalKernelCode) + XCTAssertEqual(kernel.name, "warp") + } + XCTAssertNoThrow { + let kernel = try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "warp") + XCTAssertEqual(kernel.name, "warp") + } + XCTAssertThrowsError(try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "notFound")) + XCTAssertThrowsError(try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, kernelName: "general"), + "Should not compile the general kernel as warp kernel.") + } + + func testRuntimeWarpKernelCompilationWithFallback() { + let ciklKernelCode = """ + kernel vec2 warp() { + return destCoord(); + } + """ + XCTAssertNoThrow { + let kernel = try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "warp") + } + XCTAssertNoThrow { + let kernel = try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "warp", fallbackCIKLString: ciklKernelCode) + XCTAssertEqual(kernel.name, "warp") + } + XCTAssertThrowsError(try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "notFound", fallbackCIKLString: ciklKernelCode)) + XCTAssertThrowsError(try CIWarpKernel.kernel(withMetalString: self.metalKernelCode, metalKernelName: "general", fallbackCIKLString: ciklKernelCode), + "Should not compile the general kernel as warp kernel.") + } + +} From a351b3a2f4ebdffd065c7c1eac444bd767f6ebae Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 14:28:39 +0200 Subject: [PATCH 25/30] Updated README with new features. --- README.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/README.md b/README.md index 3d53d9c..2501555 100644 --- a/README.md +++ b/README.md @@ -96,3 +96,163 @@ For reading EXR files into a `CIImage`, the usual initializers like `CIImage(con ### OpenEXR Test Images All EXR test images used in this project have been taken from [here](https://github.com/AcademySoftwareFoundation/openexr-images/). + +## Image Transformations +We added some convenience methods to `CIImage` to do common affine transformations on an image in a one-liner (instead of working with `CGAffineTransform`): +```swift +// Scaling the image by the given factors in x- and y-direction. +let scaledImage = image.scaledBy(x: 0.5, y: 2.0) +// Translating the image within the working space by the given amount in x- and y-direction. +let translatedImage = image.translatedBy(dx: 42, dy: -321) +// Moving the image's origin within the working space to the given point. +let movedImage = image.moved(to: CGPoint(x: 50, y: 100)) +// Moving the center of the image's extent to the given point. +let centeredImage = image.centered(in: .zero) +// Adding a padding of clear pixels around the image, effectively increasing its virtual extent. +let paddedImage = image.paddedBy(dx: 20, dy: 60) +``` + +You can also add rounded (transparent) corners to an image like this: +```swift +let imageWithRoundedCorners = image.withRoundedCorners(radius: 5) +``` + +## Image Composition +We added convenience APIs for compositing two images using different blend kernels (not just `sourceOver`, as in the built-in `CIImage.composited(over:)` API): +```swift +// Compositing the image over the specified background image using the given blend kernel. +let composition = image.composited(over: otherImage, using: .multiply) +// Compositing the image over the specified background image using the given blend kernel in the given color space. +let composition = image.composited(over: otherImage, using: .softLight, colorSpace: .displayP3ColorSpace) +``` + +You can also easily colorize an image (i.e., turn all visible pixels into the given color) like this: +```swift +// Colorizes visible pixels of the image in the given CIColor. +let colorized = image.colorized(with: .red) +``` + +## Color Extensions +`CIColor`s usually clamp their component values to `[0...1]`, which is impractical when working with wide gamut and/or extended dynamic range (EDR) colors. +One can work around that by initializing the color with an extended color space that allows component values outside of those bounds. +We added some convenient extensions for initializing colors with (extended) white and color values. Extended colors will be defined in linear sRGB, meaning a value of `1.0` will match the maximum component value in sRGB. Everything beyond is considered wide gamut. +```swift +// Convenience initializer for standard linear sRGB 50% gray. +let gray = CIColor(white: 0.5) +// A bright EDR white, way outside of the standard sRGB range. +let brightWhite = CIColor(extendedWhite: 2.0) +// A bright red color, way outside of the standard sRGB range. +// It will likely be clipped to the maximum value of the target color space when rendering. +let brightRed = CIColor(extendedRed: 2.0, green: 0.0, blue: 0.0) +``` + +We also added a convenience property to get a contrast color (either black or white) that is clearly visible when overlayed over the current color. +This can be used, for instance, to colorize text label overlays. +```swift +// A color that provide a high contrast to `backgroundColor`. +let labelColor = backgroundColor.contrastColor +``` + +## Color Space Convenience +A `CGColorSpace` is usually initialized by its name like this `CGColorSpace(name: CGColorSpace.extendedLinearSRGB). +This this is rather long, we added some static accessors for the most common color spaces used when working with Core Image for convenience: +```swift +CGColorSpace.sRGBColorSpace +CGColorSpace.extendedLinearSRGBColorSpace +CGColorSpace.displayP3ColorSpace +CGColorSpace.extendedLinearDisplayP3ColorSpace +CGColorSpace.itur2020ColorSpace +CGColorSpace.extendedLinearITUR2020ColorSpace +CGColorSpace.itur2100HLGColorSpace +CGColorSpace.itur2100PQColorSpace +``` + +These can be nicely used inline like this: +```swift +let color = CIColor(red: 1.0, green: 0.5, blue: 0.0, colorSpace: .displayP3ColorSpace) +``` + +## Text Generation +Core Image can generate images that contain text using `CITextImageGenerator` and `CIAttributedTextImageGenerator`. +We added extensions to make them much more convenient to use: +```swift +// Generating a text image with default settings. +let textImage = CIImage.text("This is text") +// Generating a text image with adjust text settings. +let textImage = CIImage.text("This is text", fontName: "Arial", fontSize: 24, color: .white, padding: 10) +// Generating a text image with a `UIFont` or `NSFont`. +let textImage = CIImage.text("This is text", font: someFont, color: .red, padding: 42) +// Generating a text image with an attributed string. +let attributedTextImage = CIImage.attributedText(someAttributedString, padding: 10) +``` + +## Runtime Kernel Compilation from Metal Sources +With the legacy Core Image Kernel Language it was possible (and even required) to compile custom kernel routines at runtime from CIKL source strings. +For custom kernels written in Metal the sources needed to be compiled (with specific flags) together with the rest of the sources at build-time. +While this has the huge benefits of compile-time source checking and huge performance improvements at runtime, it also looses some flexibility. +Most notably when it comes to prototyping, since setting up the Core Image Metal build toolchain is rather complicated and loading pre-compiled kernels require some boilerplate code. + +New in iOS 15 and macOS 12, however, is the ability to also compile Metal-based kernels at runtime using the `CIKernel.kernels(withMetalString:)` API. +However, this API requires some type checking and boilerplate code to retrieve an actual `CIKernel` instance of an appropriate type. +So we added the following convenience API to ease the process: +```swift +let metalKernelCode = """ + #include + using namespace metal; + + [[ stitchable ]] half4 general(coreimage::sampler_h src) { + return src.sample(src.coord()); + } + [[ stitchable ]] half4 otherGeneral(coreimage::sampler_h src) { + return src.sample(src.coord()); + } + [[ stitchable ]] half4 color(coreimage::sample_h src) { + return src; + } + [[ stitchable ]] float2 warp(coreimage::destination dest) { + return dest.coord(); + } +""" +// Load the first kernel that matches the type (CIKernel) from the metal sources. +let generalKernel = try CIKernel.kernel(withMetalString: metalKernelCode) // loads "general" kernel function +// Load the kernel with a specific function name. +let otherGeneralKernel = try CIKernel.kernel(withMetalString: metalKernelCode, kernelName: "otherGeneral") +// Load the first color kernel from the metal sources. +let colorKernel = try CIColorKernel.kernel(withMetalString: metalKernelCode) +// Load the first warp kernel from the metal sources. +let colorKernel = try CIWarp.kernel(withMetalString: metalKernelCode) +``` + +> **⚠️ _Important:_** +> There are a few limitations to this API: +> - Run-time compilation of Metal kernels is only supported starting from iOS 15 and macOS 12. +> - It only works when the Metal kernels are attributed as `[[ stitchable ]]`. +> Please refer to [this WWDC talk](https://developer.apple.com/wwdc21/10159) for details. +> - It only works when the Metal device used by Core Image supports dynamic libraries. +> You can check `MTLDevice.supportsDynamicLibraries` to see if runtime compilation of Metal-based CIKernels is supported. +> - `CIBlendKernel` can't be compiled this way, unfortunately. The `CIKernel.kernels(withMetalString:)` API just identifies them as `CIColorKernel` + +If your minimum deployment target doesn't yet support runtime compilation of Metal kernels, you can use the following API instead. +It allows to provide a backup kernel implementation in CIKL what is used on older system where Metal runtime compilation is not supported: +```swift +let metalKernelCode = """ + #include + using namespace metal; + + [[ stitchable ]] half4 general(coreimage::sampler_h src) { + return src.sample(src.coord()); + } +""" +let ciklKernelCode = """ + kernel vec4 general(sampler src) { + return sample(src, samplerTransform(src, destCoord())); + } +""" +let kernel = try CIKernel.kernel(withMetalString: metalKernelCode, fallbackCIKLString: ciklKernelCode) +``` + +> **_Note:_** +> It is generally a much better practice to compile Metal CIKernels along with the rest of your and only use runtime compilation as an exception. +> This way the compiler can check your sources at build-time, and initializing a CIKernel at runtime from pre-compiled sources is much faster. +> A notable exception might arise when you need a custom kernel inside a Swift package since CI Metal kernels can't be built with Swift packages (yet). +> But this should only be used as a last resort. From c7cdcc895c77c8fdca02a8001eb36969ddcb11f9 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 14:34:28 +0200 Subject: [PATCH 26/30] README formatting fixes. --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2501555..4e1a33b 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ let labelColor = backgroundColor.contrastColor ``` ## Color Space Convenience -A `CGColorSpace` is usually initialized by its name like this `CGColorSpace(name: CGColorSpace.extendedLinearSRGB). +A `CGColorSpace` is usually initialized by its name like this `CGColorSpace(name: CGColorSpace.extendedLinearSRGB)`. This this is rather long, we added some static accessors for the most common color spaces used when working with Core Image for convenience: ```swift CGColorSpace.sRGBColorSpace @@ -218,19 +218,19 @@ let generalKernel = try CIKernel.kernel(withMetalString: metalKernelCode) // loa // Load the kernel with a specific function name. let otherGeneralKernel = try CIKernel.kernel(withMetalString: metalKernelCode, kernelName: "otherGeneral") // Load the first color kernel from the metal sources. -let colorKernel = try CIColorKernel.kernel(withMetalString: metalKernelCode) +let colorKernel = try CIColorKernel.kernel(withMetalString: metalKernelCode) // loads "color" kernel function // Load the first warp kernel from the metal sources. -let colorKernel = try CIWarp.kernel(withMetalString: metalKernelCode) +let colorKernel = try CIWarp.kernel(withMetalString: metalKernelCode) // loads "warp" kernel function ``` -> **⚠️ _Important:_** -> There are a few limitations to this API: -> - Run-time compilation of Metal kernels is only supported starting from iOS 15 and macOS 12. -> - It only works when the Metal kernels are attributed as `[[ stitchable ]]`. -> Please refer to [this WWDC talk](https://developer.apple.com/wwdc21/10159) for details. -> - It only works when the Metal device used by Core Image supports dynamic libraries. -> You can check `MTLDevice.supportsDynamicLibraries` to see if runtime compilation of Metal-based CIKernels is supported. -> - `CIBlendKernel` can't be compiled this way, unfortunately. The `CIKernel.kernels(withMetalString:)` API just identifies them as `CIColorKernel` +**⚠️ _Important:_** +There are a few limitations to this API: +- Run-time compilation of Metal kernels is only supported starting from iOS 15 and macOS 12. +- It only works when the Metal kernels are attributed as `[[ stitchable ]]`. + Please refer to [this WWDC talk](https://developer.apple.com/wwdc21/10159) for details. +- It only works when the Metal device used by Core Image supports dynamic libraries. + You can check `MTLDevice.supportsDynamicLibraries` to see if runtime compilation of Metal-based CIKernels is supported. +- `CIBlendKernel` can't be compiled this way, unfortunately. The `CIKernel.kernels(withMetalString:)` API just identifies them as `CIColorKernel` If your minimum deployment target doesn't yet support runtime compilation of Metal kernels, you can use the following API instead. It allows to provide a backup kernel implementation in CIKL what is used on older system where Metal runtime compilation is not supported: From cb9b093681c8f0dfd611995b62308bc7a9c40835 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 16:43:03 +0200 Subject: [PATCH 27/30] Added information about the test pattern images to README. --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 4e1a33b..16cb1dd 100644 --- a/README.md +++ b/README.md @@ -256,3 +256,35 @@ let kernel = try CIKernel.kernel(withMetalString: metalKernelCode, fallbackCIKLS > This way the compiler can check your sources at build-time, and initializing a CIKernel at runtime from pre-compiled sources is much faster. > A notable exception might arise when you need a custom kernel inside a Swift package since CI Metal kernels can't be built with Swift packages (yet). > But this should only be used as a last resort. + +## EDR & Wide Gamut Test Pattern +Most Apple devices can capture and display images with colors that are outside of the standard sRGB color gamut and range (_Standard Dynamic Range_, _SDR_). +The Display P3 color space is the de facto standard on Apple platforms now. +Some high-end devices and monitors also have XDR screens that can go even beyond Display P3, and new iPhones can record movies in HDR now. +All those systems (wide color gamut, high display brightness, extended color spaces) are subsumed by Apple under the term _EDR_ (_Extended Dynamic Range_). + +To ensure that all parts of our apps can properly process and display EDR media, we designed a test pattern image that +- displays a stripe of tiles with increasing pixel brightness value (up to the XDRs peak brightness of 1600 nits) +- and swatches of various colors in three common color gamuts (sRGB, Display P3, and BT.2020). + +![EDR & Wide Gamut Test Pattern](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png)] + +> **_Note:_** +> Your browser and operating system will likely tone-map the image above to the maximum range of your screen, which is actually a feature and not a bug. +> If you want to see the colors in their correct form (and see where you screen has to clip colors), check out the extended range [EXR](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr?raw=true) or [TIFF](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff?raw=true) version of the image. +> The tone-mapped PNG version above was chosen so you can better see the intent of the pattern. + +The pattern image itself is generated with Core Image compositing techniques. +You can generate it as `CIImage` just like this: +```swift +let testPattern = CIImage.testPattern() +``` + +> **_Note:_** +> You should use this for testing purposes only! +> This is not meant to be shipped in production code since generating the pattern is slow and potentially error-prone (lots of force-unwraps in the code for convenience). +> If you need a fast-loading version of it, best use the pre-generated [EXR version](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr?raw=true). + +You can find many pre-generated test pattern images in various file formats, bit depths, and color spaces in the [EDR_Wide_Gamut_Test_Patterns](EDR_Wide_Gamut_Test_Patterns/) folder for download. +Those images can also be generated using the `TestPatternTests`. +The generated images are attached to the test runs and can be found when opening the test result in the Reports navigator. From e4f7a686f6d3cc4faa8a1ef59d91d6af8b280582 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Mon, 8 Aug 2022 17:46:53 +0200 Subject: [PATCH 28/30] Provided a tone-mapped version of the pattern for illustration purposes. --- .../TestPattern_tone-mapped.png | 3 +++ README.md | 4 ++-- Tests/TestPatternTests.swift | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 EDR_Wide_Gamut_Test_Patterns/TestPattern_tone-mapped.png diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_tone-mapped.png b/EDR_Wide_Gamut_Test_Patterns/TestPattern_tone-mapped.png new file mode 100644 index 0000000..7991961 --- /dev/null +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_tone-mapped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ace0cc08116b9bbd5f9a7617aa6f57eed1f8e40bb223c93dedb5ee63104c0ff +size 154527 diff --git a/README.md b/README.md index 16cb1dd..31f10df 100644 --- a/README.md +++ b/README.md @@ -267,10 +267,10 @@ To ensure that all parts of our apps can properly process and display EDR media, - displays a stripe of tiles with increasing pixel brightness value (up to the XDRs peak brightness of 1600 nits) - and swatches of various colors in three common color gamuts (sRGB, Display P3, and BT.2020). -![EDR & Wide Gamut Test Pattern](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_BT.2100-PQ.png)] +![EDR & Wide Gamut Test Pattern](EDR_Wide_Gamut_Test_Patterns/TestPattern_tone-mapped.png) > **_Note:_** -> Your browser and operating system will likely tone-map the image above to the maximum range of your screen, which is actually a feature and not a bug. +> The image above is tone-mapped from HDR to SDR to demonstrate what it will roughly look like on a HDR-capable screen (just much dimmer). > If you want to see the colors in their correct form (and see where you screen has to clip colors), check out the extended range [EXR](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.exr?raw=true) or [TIFF](EDR_Wide_Gamut_Test_Patterns/TestPattern_16bit_float_extended-linear-sRGB.tiff?raw=true) version of the image. > The tone-mapped PNG version above was chosen so you can better see the intent of the pattern. diff --git a/Tests/TestPatternTests.swift b/Tests/TestPatternTests.swift index fb946dd..603b11c 100644 --- a/Tests/TestPatternTests.swift +++ b/Tests/TestPatternTests.swift @@ -69,7 +69,7 @@ class TestPatternTests: XCTestCase { } } - /// Generates EDR & wide gamut test pattern images in TIFF file format. + /// Generates EDR & wide gamut test pattern images in PNG file format. /// PNG supports 8- and 16-bit color depths, so we generate patterns in the color spaces fitting those bit depths. /// However, PNG does not support floating-point values, so we don't need to generate an image in extended color space. func testPNGPatternGeneration() { @@ -115,6 +115,22 @@ class TestPatternTests: XCTestCase { } } + /// Generates EDR & wide gamut test pattern images in PNG file format that is tone-mapped from BT.2100 PQ (HDR) to sRGB + /// to demonstrate what it might roughly like on EDR-capable screens (just much dimmer). + func testToneMappedPatternGeneration() { + let testPatter = CIImage.testPattern(label: "BT.2100 PQ (HDR) tone-mapped to sRGB") + // Create a pattern image that contains HDR data. + let hdrData = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: .itur2100PQColorSpace!)! + // Load that data again into a `CIImage` and let CI perform tone-mapping to SDR. + let toneMappedImage = CIImage(data: hdrData, options: [CIImageOption.toneMapHDRtoSDR: true])! + // Render the tone-mapped SDR image in sRGB and save as attachment. + let data = self.context.pngRepresentation(of: toneMappedImage, format: .RGBA8, colorSpace: .sRGBColorSpace!)! + let attachment = XCTAttachment(data: data, uniformTypeIdentifier: UTType.png.identifier) + attachment.lifetime = .keepAlways + attachment.name = "TestPattern_tone-mapped.png" + self.add(attachment) + } + } From d9096149596eec3f2c39629a1892e03be6450a20 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Tue, 16 Aug 2022 12:35:11 +0200 Subject: [PATCH 29/30] Fixed typos. --- Sources/CIImage+TestPattern.swift | 2 +- Tests/TestPatternTests.swift | 32 +++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/CIImage+TestPattern.swift b/Sources/CIImage+TestPattern.swift index 1790613..4363132 100644 --- a/Sources/CIImage+TestPattern.swift +++ b/Sources/CIImage+TestPattern.swift @@ -5,7 +5,7 @@ import CoreImage.CIFilterBuiltins /// Here we create an EDR brightness and wide gamut color test pattern image that can be used, for instance, /// to verify colors are displayed correctly, filters conserve those color properties correctly, etc. /// It also showcases how Core Image can be used for image compositing, though in production -/// a composition with this complexity is probably better done with Core Graphits since its too slow in CI. +/// a composition with this complexity is probably better done with Core Graphics since its too slow in CI. /// ⚠️ For testing purposes only! This is not meant to be shipped in production code since generating /// the pattern is slow and potentially error-prone (lots of force-unwraps in here for convenience). @available(iOS 12.3, macCatalyst 13.1, macOS 10.14.3, tvOS 12.0, *) diff --git a/Tests/TestPatternTests.swift b/Tests/TestPatternTests.swift index 603b11c..2a532a6 100644 --- a/Tests/TestPatternTests.swift +++ b/Tests/TestPatternTests.swift @@ -51,8 +51,8 @@ class TestPatternTests: XCTestCase { /// Since EDR can store values as-is, we only generate one 16-bit float image in extended linear sRGB color space, /// which are the reference properties when composing the test pattern. func testEXRPatternGeneration() throws { - let testPatter = self.testPattern(for: .exr, bitDepth: 16, isFloat: true, colorSpace: extendedLinearSRGBColorSpace) - let data = try self.context.exrRepresentation(of: testPatter, format: .RGBAh, colorSpace: extendedLinearSRGBColorSpace.cgSpace) + let testPattern = self.testPattern(for: .exr, bitDepth: 16, isFloat: true, colorSpace: extendedLinearSRGBColorSpace) + let data = try self.context.exrRepresentation(of: testPattern, format: .RGBAh, colorSpace: extendedLinearSRGBColorSpace.cgSpace) self.attach(data, type: .exr, bitDepth: 16, isFloat: true, colorSpace: extendedLinearSRGBColorSpace) } @@ -63,8 +63,8 @@ class TestPatternTests: XCTestCase { /// We also generate images in the HDR color spaces for reference. func testTIFFPatternGeneration() { for colorSpace in highBitColorSpaces + [extendedLinearSRGBColorSpace] { - let testPatter = self.testPattern(for: .tiff, bitDepth: 16, isFloat: true, colorSpace: colorSpace) - let data = self.context.tiffRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace)! + let testPattern = self.testPattern(for: .tiff, bitDepth: 16, isFloat: true, colorSpace: colorSpace) + let data = self.context.tiffRepresentation(of: testPattern, format: .RGBAh, colorSpace: colorSpace.cgSpace)! self.attach(data, type: .tiff, bitDepth: 16, isFloat: true, colorSpace: colorSpace) } } @@ -74,14 +74,14 @@ class TestPatternTests: XCTestCase { /// However, PNG does not support floating-point values, so we don't need to generate an image in extended color space. func testPNGPatternGeneration() { for colorSpace in lowBitColorSpaces { - let testPatter = self.testPattern(for: .png, bitDepth: 8, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPatter, format: .RGBA8, colorSpace: colorSpace.cgSpace)! + let testPattern = self.testPattern(for: .png, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPattern, format: .RGBA8, colorSpace: colorSpace.cgSpace)! self.attach(data, type: .png, bitDepth: 8, isFloat: false, colorSpace: colorSpace) } for colorSpace in highBitColorSpaces { - let testPatter = self.testPattern(for: .png, bitDepth: 16, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace)! + let testPattern = self.testPattern(for: .png, bitDepth: 16, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPattern, format: .RGBAh, colorSpace: colorSpace.cgSpace)! self.attach(data, type: .png, bitDepth: 16, isFloat: false, colorSpace: colorSpace) } } @@ -90,8 +90,8 @@ class TestPatternTests: XCTestCase { /// JPEG only supports 8-bit color depth, so we only generate images in color spaces that are fitting for 8-bit. func testJPEGPatternGeneration() { for colorSpace in lowBitColorSpaces { - let testPatter = self.testPattern(for: .jpeg, bitDepth: 8, isFloat: false, colorSpace: colorSpace) - let data = self.context.jpegRepresentation(of: testPatter, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + let testPattern = self.testPattern(for: .jpeg, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.jpegRepresentation(of: testPattern, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! self.attach(data, type: .jpeg, bitDepth: 8, isFloat: false, colorSpace: colorSpace) } } @@ -101,15 +101,15 @@ class TestPatternTests: XCTestCase { /// However, HEIC does not support floating-point values, so we don't need to generate an image in extended color space. func testHEICPatternGeneration() throws { for colorSpace in lowBitColorSpaces { - let testPatter = self.testPattern(for: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPatter, format: .RGBA8, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + let testPattern = self.testPattern(for: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPattern, format: .RGBA8, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! self.attach(data, type: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) } if #available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, *) { for colorSpace in highBitColorSpaces { - let testPatter = self.testPattern(for: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + let testPattern = self.testPattern(for: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) + let data = self.context.pngRepresentation(of: testPattern, format: .RGBAh, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! self.attach(data, type: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) } } @@ -118,9 +118,9 @@ class TestPatternTests: XCTestCase { /// Generates EDR & wide gamut test pattern images in PNG file format that is tone-mapped from BT.2100 PQ (HDR) to sRGB /// to demonstrate what it might roughly like on EDR-capable screens (just much dimmer). func testToneMappedPatternGeneration() { - let testPatter = CIImage.testPattern(label: "BT.2100 PQ (HDR) tone-mapped to sRGB") + let testPattern = CIImage.testPattern(label: "BT.2100 PQ (HDR) tone-mapped to sRGB") // Create a pattern image that contains HDR data. - let hdrData = self.context.pngRepresentation(of: testPatter, format: .RGBAh, colorSpace: .itur2100PQColorSpace!)! + let hdrData = self.context.pngRepresentation(of: testPattern, format: .RGBAh, colorSpace: .itur2100PQColorSpace!)! // Load that data again into a `CIImage` and let CI perform tone-mapping to SDR. let toneMappedImage = CIImage(data: hdrData, options: [CIImageOption.toneMapHDRtoSDR: true])! // Render the tone-mapped SDR image in sRGB and save as attachment. From 68aaac3085ce314e913840240a43f39bce045291 Mon Sep 17 00:00:00 2001 From: Frank Rupprecht Date: Sun, 11 Sep 2022 18:54:40 +0200 Subject: [PATCH 30/30] Fixed copy-paste mistake for generating HEIC images. --- EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic | 4 ++-- .../TestPattern_10bit_BT.2100-HLG.heic | 4 ++-- .../TestPattern_10bit_BT.2100-PQ.heic | 4 ++-- EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic | 4 ++-- EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic | 4 ++-- Tests/TestPatternTests.swift | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic index f7fe1fa..bfceab9 100644 --- a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2020.heic @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48743c06764d77cebceca19387a4d88686f9732f8a74362e7356ac2cc3a53e7c -size 235683 +oid sha256:ffcc0ec78e7c78927ef41dfe2bcfad68af1593d53c28fbafb1ee34355d9ad1b1 +size 131493 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic index faff32e..f8b7f46 100644 --- a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-HLG.heic @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e64ba758c2e79ff968204e55fb7e016b314c6986c00951b833e20f9b9b88eee8 -size 244888 +oid sha256:4d3a1292b92fa1c80f14678daed3ddea197c3b162fcce36dc62559a2f4c1c3f2 +size 129158 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic index 77ee0e6..356d344 100644 --- a/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_10bit_BT.2100-PQ.heic @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d82f01bea197ec7ca8e236cefbfa80ea8513b5d33975a02ee24bb20317c55b1 -size 250837 +oid sha256:07edd88bb72db7e9b227183c2ed05f565c7a40f3094dc402803fa0490c40d75f +size 116308 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic index a3ce872..40f261b 100644 --- a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_Display-P3.heic @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6533402a8c0dd6e6115e597f7bb605f9ecd376ff588c77cb901113ec156e52d5 -size 146362 +oid sha256:722a4d80e1c7febcb0cd9baad13f0f550f3705663c581a2419a4b4d661dff386 +size 97933 diff --git a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic index 2e323ef..144cb88 100644 --- a/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic +++ b/EDR_Wide_Gamut_Test_Patterns/TestPattern_8bit_sRGB.heic @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74ad3a8ab39907a542a6b176bc3f7d1e1b800f055ae3d04d45fb4ddea50f90e8 -size 140324 +oid sha256:e623635cd1a83a882d902ae0bc511937d950968cd9dc08c5e1b3f45a8ee5edd3 +size 95670 diff --git a/Tests/TestPatternTests.swift b/Tests/TestPatternTests.swift index 2a532a6..fdc2cdf 100644 --- a/Tests/TestPatternTests.swift +++ b/Tests/TestPatternTests.swift @@ -102,14 +102,14 @@ class TestPatternTests: XCTestCase { func testHEICPatternGeneration() throws { for colorSpace in lowBitColorSpaces { let testPattern = self.testPattern(for: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPattern, format: .RGBA8, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + let data = self.context.heifRepresentation(of: testPattern, format: .RGBA8, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! self.attach(data, type: .heic, bitDepth: 8, isFloat: false, colorSpace: colorSpace) } if #available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, *) { for colorSpace in highBitColorSpaces { let testPattern = self.testPattern(for: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) - let data = self.context.pngRepresentation(of: testPattern, format: .RGBAh, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0])! + let data = try! self.context.heif10Representation(of: testPattern, colorSpace: colorSpace.cgSpace, options: [.quality: 1.0]) self.attach(data, type: .heic, bitDepth: 10, isFloat: false, colorSpace: colorSpace) } }