From f071ca7351feca49c1b70dd626a72413e9a3ded1 Mon Sep 17 00:00:00 2001 From: Darren Ford Date: Thu, 11 Jul 2024 11:34:02 +1000 Subject: [PATCH] Minor restructure --- Sources/Bitmap/Bitmap.swift | 1 + .../drawing/Bitmap+AdjustColorControls.swift | 78 +++++ Sources/Bitmap/drawing/Bitmap+Colors.swift | 278 ------------------ Sources/Bitmap/drawing/Bitmap+Gamma.swift | 66 +++++ Sources/Bitmap/drawing/Bitmap+Grayscale.swift | 95 ++++++ Sources/Bitmap/drawing/Bitmap+Tint.swift | 129 ++++++++ Tests/BitmapTests/BitmapTests.swift | 46 +++ 7 files changed, 415 insertions(+), 278 deletions(-) create mode 100644 Sources/Bitmap/drawing/Bitmap+AdjustColorControls.swift delete mode 100644 Sources/Bitmap/drawing/Bitmap+Colors.swift create mode 100644 Sources/Bitmap/drawing/Bitmap+Gamma.swift create mode 100644 Sources/Bitmap/drawing/Bitmap+Grayscale.swift create mode 100644 Sources/Bitmap/drawing/Bitmap+Tint.swift diff --git a/Sources/Bitmap/Bitmap.swift b/Sources/Bitmap/Bitmap.swift index f3a0a52..32ec6e9 100644 --- a/Sources/Bitmap/Bitmap.swift +++ b/Sources/Bitmap/Bitmap.swift @@ -41,6 +41,7 @@ public class Bitmap { case cannotFilter case cannotConvertColorSpace case cannotConvert + case notImplemented } /// Raw bitmap information diff --git a/Sources/Bitmap/drawing/Bitmap+AdjustColorControls.swift b/Sources/Bitmap/drawing/Bitmap+AdjustColorControls.swift new file mode 100644 index 0000000..fa4b490 --- /dev/null +++ b/Sources/Bitmap/drawing/Bitmap+AdjustColorControls.swift @@ -0,0 +1,78 @@ +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT license +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import CoreGraphics + +#if canImport(CoreImage) + +import CoreImage + +// MARK: - CoreImage routines + +public extension Bitmap { + /// Adjust saturation, brightness, and contrast values. + /// - Parameters: + /// - saturation: The amount of saturation to apply. The larger the value, the more saturated the result. + /// - brightness: The amount of brightness to apply. The larger the value, the brighter the result. + /// - contrast: The amount of contrast to apply. The larger the value, the more contrast in the resulting image. + @inlinable func adjustColorControls( + saturation: Double = 1.0, + brightness: Double = 0.0, + contrast: Double = 1.0 + ) throws { + try self.assign( + try self.adjustingColorControls( + saturation: saturation, + brightness: brightness, + contrast: contrast + ) + ) + } + + /// Adjust saturation, brightness, and contrast values. + /// - Parameters: + /// - saturation: The amount of saturation to apply. The larger the value, the more saturated the result. + /// - brightness: The amount of brightness to apply. The larger the value, the brighter the result. + /// - contrast: The amount of contrast to apply. The larger the value, the more contrast in the resulting image. + /// - Returns: A new bitmap + func adjustingColorControls( + saturation: Double = 1.0, + brightness: Double = 0.0, + contrast: Double = 1.0 + ) throws -> Bitmap { + guard + let filter = CIFilter( + name: "CIColorControls", + parameters: [ + "inputImage": self.ciImage as Any, + "inputSaturation": 1.0, + "inputBrightness": 0.0, + "inputContrast": 1.0, + ] + ), + let output = filter.outputImage + else { + throw BitmapError.cannotCreateCGImage + } + return try Bitmap(output) + } +} + +#endif // canImport(CoreImage) diff --git a/Sources/Bitmap/drawing/Bitmap+Colors.swift b/Sources/Bitmap/drawing/Bitmap+Colors.swift deleted file mode 100644 index d5423c1..0000000 --- a/Sources/Bitmap/drawing/Bitmap+Colors.swift +++ /dev/null @@ -1,278 +0,0 @@ -// -// Copyright © 2024 Darren Ford. All rights reserved. -// -// MIT license -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial -// portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -import Foundation -import CoreGraphics - -#if canImport(CoreImage) - -import CoreImage - -// MARK: - CoreImage routines - -public extension Bitmap { - /// Tint this bitmap with a color - /// - Parameters: - /// - color: The tinting color - /// - intensity: The tinting intensity (0 -> 1) - @inlinable func tint(with color: CGColor, intensity: CGFloat = 1.0) throws { - try self.assign(try self.tinting(with: color, intensity: intensity)) - } - - /// Returns a new image tinted with a color - /// - Parameters: - /// - color: The tint color - /// - intensity: The tinting intensity (0 -> 1) - /// - Returns: A new bitmap tinted with the specified color - func tinting(with color: CGColor, intensity: CGFloat = 1.0) throws -> Bitmap { - guard - let filter = CIFilter( - name: "CIColorMonochrome", - parameters: [ - "inputImage": self.ciImage as Any, - "inputColor": CIColor(cgColor: color), - "inputIntensity": 1.0, - ] - ), - let output = filter.outputImage - else { - throw BitmapError.cannotCreateCGImage - } - - return try Bitmap(output) - } -} - -public extension Bitmap { - /// Apply a grayscale filter to this bitmap - @inlinable func grayscale() throws { - try self.assign(try self.grayscaling()) - } - - /// Return a grayscale representation of this bitmap - /// - Returns: A grayscale bitmap - func grayscaling() throws -> Bitmap { - guard - let filter = CIFilter( - name: "CIPhotoEffectMono", - parameters: [ - "inputImage": self.ciImage as Any, - ] - ), - let output = filter.outputImage - else { - throw BitmapError.cannotCreateCGImage - } - return try Bitmap(output) - } -} - -public extension Bitmap { - /// Adjusts midtone brightness - /// - Parameter power: The input power. The larger the value, the darker the result - @inlinable func adjustGamma(power: Double) throws { - try self.assign(try self.adjustingGamma(power: power)) - } - - /// Adjusts midtone brightness - /// - Parameter power: The input power. The larger the value, the darker the result - /// - Returns: A new bitmap - func adjustingGamma(power: Double) throws -> Bitmap { - guard - let filter = CIFilter( - name: "CIGammaAdjust", - parameters: [ - "inputImage": self.ciImage as Any, - "inputPower": power, - ] - ), - let output = filter.outputImage - else { - throw BitmapError.cannotCreateCGImage - } - return try Bitmap(output) - } -} - -public extension Bitmap { - /// Adjust saturation, brightness, and contrast values. - /// - Parameters: - /// - saturation: The amount of saturation to apply. The larger the value, the more saturated the result. - /// - brightness: The amount of brightness to apply. The larger the value, the brighter the result. - /// - contrast: The amount of contrast to apply. The larger the value, the more contrast in the resulting image. - @inlinable func adjustColorControls( - saturation: Double = 1.0, - brightness: Double = 0.0, - contrast: Double = 1.0 - ) throws { - try self.assign( - try self.adjustingColorControls( - saturation: saturation, - brightness: brightness, - contrast: contrast - ) - ) - } - - /// Adjust saturation, brightness, and contrast values. - /// - Parameters: - /// - saturation: The amount of saturation to apply. The larger the value, the more saturated the result. - /// - brightness: The amount of brightness to apply. The larger the value, the brighter the result. - /// - contrast: The amount of contrast to apply. The larger the value, the more contrast in the resulting image. - /// - Returns: A new bitmap - func adjustingColorControls( - saturation: Double = 1.0, - brightness: Double = 0.0, - contrast: Double = 1.0 - ) throws -> Bitmap { - guard - let filter = CIFilter( - name: "CIColorControls", - parameters: [ - "inputImage": self.ciImage as Any, - "inputSaturation": 1.0, - "inputBrightness": 0.0, - "inputContrast": 1.0, - ] - ), - let output = filter.outputImage - else { - throw BitmapError.cannotCreateCGImage - } - return try Bitmap(output) - } -} - -#else - -// MARK: - Non CoreImage routines - -public extension Bitmap { - /// Apply a grayscale filter to this bitmap - @inlinable func grayscale() throws { - try self.assign(try self.grayscaling()) - } - - /// Return a grayscale representation of this bitmap - /// - Returns: A grayscale bitmap - func grayscaling() throws -> Bitmap { - guard let cgImage = self.cgImage else { throw BitmapError.cannotCreateCGImage } - - // Create a grayscale context - guard let ctx = CGContext( - data: nil, - width: width, - height: height, - bitsPerComponent: 8, - bytesPerRow: 0, - space: CGColorSpaceCreateDeviceGray(), - bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue - ) - else { - throw BitmapError.invalidContext - } - - /// Draw the image into the new context - let imageRect = CGRect(origin: .zero, size: size) - - /// Draw the image - ctx.draw(cgImage, in: imageRect) - - ctx.setBlendMode(.destinationIn) - ctx.clip(to: imageRect, mask: cgImage) - - guard let image = ctx.makeImage() else { - throw BitmapError.cannotCreateCGImage - } - return try Bitmap(image) - } -} - -public extension Bitmap { - /// Tint this bitmap with a color - /// - Parameters: - /// - color: The tinting color - /// - intensity: The tinting intensity (0 -> 1) - @inlinable func tint(with color: CGColor, intensity: CGFloat = 1.0) throws { - try self.assign(try self.tinting(with: color, intensity: intensity)) - } - - /// Returns a new image tinted with a color - /// - Parameters: - /// - color: The tint color - /// - intensity: The tinting intensity (0 -> 1) - /// - Returns: A new bitmap tinted with the specified color - func tinting(with color: CGColor, intensity: CGFloat = 1.0) throws -> Bitmap { - guard let cgImage = self.cgImage else { throw BitmapError.cannotCreateCGImage } - let rect = CGRect(origin: .zero, size: size) - return try Bitmap(size: size) { ctx in - // draw black background to preserve color of transparent pixels - ctx.setBlendMode(.normal) - ctx.setFillColor(.black) - ctx.fill([rect]) - - // Draw the image - ctx.setBlendMode(.normal) - ctx.draw(cgImage, in: rect) - - // tint image (losing alpha) - the luminosity of the original image is preserved - ctx.setBlendMode(.color) - ctx.setFillColor(color) - ctx.fill([rect]) - - // mask by alpha values of original image - ctx.setBlendMode(.destinationIn) - ctx.draw(cgImage, in: rect) - } - } -} - -#endif // canImport(CoreImage) - -// MARK: - Common - -public extension Bitmap { - /// Tint an area within a bitmap with a specific color - /// - Parameters: - /// - color: The tint color - /// - rect: The rect within the bitmap to tint - /// - intensity: The color intensity (0.0 -> 1.0) - func tinting(with color: CGColor, in rect: CGRect, intensity: CGFloat = 1.0) throws -> Bitmap { - let copy = try self.copy() - try copy.tint(with: color, in: rect, intensity: intensity) - return copy - } - - /// Tint an area within a bitmap with a specific color - /// - Parameters: - /// - color: The tint color - /// - rect: The rect within the bitmap to tint - /// - intensity: The color intensity (0.0 -> 1.0) - func tint(with color: CGColor, in rect: CGRect, intensity: CGFloat = 1.0) throws { - // Crop out the part to be tinted - let component = try self.cropping(to: rect) - - // Tint this component - let tinted = try component.tinting(with: color, intensity: intensity) - guard let ti = tinted.cgImage else { throw BitmapError.cannotCreateCGImage } - - // And draw the tinted part back into the original image - self.drawImage(ti, in: rect) - } -} diff --git a/Sources/Bitmap/drawing/Bitmap+Gamma.swift b/Sources/Bitmap/drawing/Bitmap+Gamma.swift new file mode 100644 index 0000000..080cf47 --- /dev/null +++ b/Sources/Bitmap/drawing/Bitmap+Gamma.swift @@ -0,0 +1,66 @@ +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT license +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import CoreGraphics + +#if canImport(CoreImage) +import CoreImage +#endif + +public extension Bitmap { + /// Adjusts midtone brightness + /// - Parameter power: The input power. The larger the value, the darker the result + @inlinable func adjustGamma(power: Double) throws { + try self.assign(try self.adjustingGamma(power: power)) + } +} + +#if canImport(CoreImage) + +public extension Bitmap { + /// Adjusts midtone brightness + /// - Parameter power: The input power. The larger the value, the darker the result + /// - Returns: A new bitmap + func adjustingGamma(power: Double) throws -> Bitmap { + guard + let filter = CIFilter( + name: "CIGammaAdjust", + parameters: [ + "inputImage": self.ciImage as Any, + "inputPower": power, + ] + ), + let output = filter.outputImage + else { + throw BitmapError.cannotCreateCGImage + } + return try Bitmap(output) + } +} + +#else + +public extension Bitmap { + func adjustingGamma(power: Double) throws -> Bitmap { + throw BitmapError.notImplemented + } +} + +#endif diff --git a/Sources/Bitmap/drawing/Bitmap+Grayscale.swift b/Sources/Bitmap/drawing/Bitmap+Grayscale.swift new file mode 100644 index 0000000..6e7c650 --- /dev/null +++ b/Sources/Bitmap/drawing/Bitmap+Grayscale.swift @@ -0,0 +1,95 @@ +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT license +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import CoreGraphics + +#if canImport(CoreImage) +import CoreImage +#endif + +public extension Bitmap { + /// Apply a grayscale filter to this bitmap + @inlinable func grayscale() throws { + try self.assign(try self.grayscaling()) + } +} + +#if canImport(CoreImage) + +public extension Bitmap { + /// Return a grayscale representation of this bitmap + /// - Returns: A grayscale bitmap + func grayscaling() throws -> Bitmap { + guard + let filter = CIFilter( + name: "CIPhotoEffectMono", + parameters: [ + "inputImage": self.ciImage as Any, + ] + ), + let output = filter.outputImage + else { + throw BitmapError.cannotCreateCGImage + } + return try Bitmap(output) + } +} + +#else + +// MARK: - Non CoreImage routines + +public extension Bitmap { + /// Return a grayscale representation of this bitmap + /// - Returns: A grayscale bitmap + func grayscaling() throws -> Bitmap { + guard let cgImage = self.cgImage else { throw BitmapError.cannotCreateCGImage } + + // Create a grayscale context + guard let ctx = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: 0, + space: CGColorSpaceCreateDeviceGray(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + else { + throw BitmapError.invalidContext + } + + /// Draw the image into the new context + let imageRect = CGRect(origin: .zero, size: size) + + /// Draw the image + ctx.draw(cgImage, in: imageRect) + + ctx.setBlendMode(.destinationIn) + ctx.clip(to: imageRect, mask: cgImage) + + guard let image = ctx.makeImage() else { + throw BitmapError.cannotCreateCGImage + } + return try Bitmap(image) + } +} + +#endif diff --git a/Sources/Bitmap/drawing/Bitmap+Tint.swift b/Sources/Bitmap/drawing/Bitmap+Tint.swift new file mode 100644 index 0000000..2b4a9d7 --- /dev/null +++ b/Sources/Bitmap/drawing/Bitmap+Tint.swift @@ -0,0 +1,129 @@ +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT license +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import CoreGraphics + +#if canImport(CoreImage) +import CoreImage +#endif + +public extension Bitmap { + /// Tint this bitmap with a color + /// - Parameters: + /// - color: The tinting color + /// - intensity: The tinting intensity (0 -> 1) + @inlinable func tint(with color: CGColor, intensity: CGFloat = 1.0) throws { + try self.assign(try self.tinting(with: color, intensity: intensity)) + } +} + +public extension Bitmap { + /// Tint an area within a bitmap with a specific color + /// - Parameters: + /// - color: The tint color + /// - rect: The rect within the bitmap to tint + /// - intensity: The color intensity (0.0 -> 1.0) + func tint(with color: CGColor, in rect: CGRect, intensity: CGFloat = 1.0) throws { + // Crop out the part to be tinted + let component = try self.cropping(to: rect) + + // Tint this component + let tinted = try component.tinting(with: color, intensity: intensity) + guard let ti = tinted.cgImage else { throw BitmapError.cannotCreateCGImage } + + // And draw the tinted part back into the original image + self.drawImage(ti, in: rect) + } + + /// Tint an area within a bitmap with a specific color + /// - Parameters: + /// - color: The tint color + /// - rect: The rect within the bitmap to tint + /// - intensity: The color intensity (0.0 -> 1.0) + /// - Returns: A new bitmap + func tinting(with color: CGColor, in rect: CGRect, intensity: CGFloat = 1.0) throws -> Bitmap { + let copy = try self.copy() + try copy.tint(with: color, in: rect, intensity: intensity) + return copy + } +} + +#if canImport(CoreImage) + +public extension Bitmap { + /// Returns a new image tinted with a color + /// - Parameters: + /// - color: The tint color + /// - intensity: The tinting intensity (0 -> 1) + /// - Returns: A new bitmap tinted with the specified color + func tinting(with color: CGColor, intensity: CGFloat = 1.0) throws -> Bitmap { + guard + let filter = CIFilter( + name: "CIColorMonochrome", + parameters: [ + "inputImage": self.ciImage as Any, + "inputColor": CIColor(cgColor: color), + "inputIntensity": 1.0, + ] + ), + let output = filter.outputImage + else { + throw BitmapError.cannotCreateCGImage + } + + return try Bitmap(output) + } +} + +#else + +public extension Bitmap { + + /// Returns a new image tinted with a color + /// - Parameters: + /// - color: The tint color + /// - intensity: The tinting intensity (0 -> 1) + /// - Returns: A new bitmap tinted with the specified color + func tinting(with color: CGColor, intensity: CGFloat = 1.0) throws -> Bitmap { + guard let cgImage = self.cgImage else { throw BitmapError.cannotCreateCGImage } + let rect = CGRect(origin: .zero, size: size) + return try Bitmap(size: size) { ctx in + // draw black background to preserve color of transparent pixels + ctx.setBlendMode(.normal) + ctx.setFillColor(.black) + ctx.fill([rect]) + + // Draw the image + ctx.setBlendMode(.normal) + ctx.draw(cgImage, in: rect) + + // tint image (losing alpha) - the luminosity of the original image is preserved + ctx.setBlendMode(.color) + ctx.setFillColor(color) + ctx.fill([rect]) + + // mask by alpha values of original image + ctx.setBlendMode(.destinationIn) + ctx.draw(cgImage, in: rect) + } + } +} + +#endif diff --git a/Tests/BitmapTests/BitmapTests.swift b/Tests/BitmapTests/BitmapTests.swift index 1890a79..9b07cb9 100644 --- a/Tests/BitmapTests/BitmapTests.swift +++ b/Tests/BitmapTests/BitmapTests.swift @@ -1672,6 +1672,52 @@ final class BitmapTests: XCTestCase { } } +#if canImport(CoreImage) + func testGamma() throws { + + markdown.h2("Gamma") + + let imgs = [ + imageResource(name: "gps-image", extension: "jpg"), + imageResource(name: "apple-logo-dark", extension: "png") + ] + + try imgs.forEach { cgi in + markdown.raw("| original | gamma (2.2) | gamma (1/2.2) | gamma (5) |\n") + markdown.raw("|--------|--------|--------|--------|\n") + + let bmi = try Bitmap(cgi) + markdown.raw("|") + try markdown.image(cgi, linked: true) + + do { + let bm1 = try bmi.adjustingGamma(power: 2.2) + let cg1 = try XCTUnwrap(bm1.cgImage) + markdown.raw("|") + try markdown.image(cg1, linked: true) + } + + do { + let bm1 = try bmi.adjustingGamma(power: 1/2.2) + let cg1 = try XCTUnwrap(bm1.cgImage) + markdown.raw("|") + try markdown.image(cg1, linked: true) + } + + do { + let bm2 = try bmi.copy() + try bm2.adjustGamma(power: 5) + let cg2 = try XCTUnwrap(bm2.cgImage) + markdown.raw("|") + try markdown.image(cg2, linked: true) + } + + markdown.raw("|") + markdown.br() + } + } +#endif + #if canImport(CoreImage) func testDiagonalLines() throws {