Skip to content

Commit

Permalink
Add ImageTiler to tile images when blurring image variations (i.e. in…
Browse files Browse the repository at this point in the history
…terface style)
  • Loading branch information
Eskils committed Dec 7, 2023
1 parent c484a85 commit 6ba1ca5
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 93 deletions.
139 changes: 139 additions & 0 deletions Sources/VariableBlurImageView/ImageTiler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// ImageTiler.swift
//
//
// Created by Eskil Gjerde Sviggum on 07/12/2023.
//

import Foundation
import CoreGraphics

struct ImageTiler {

private let tileMode: ImageTileMode

init(tileMode: ImageTileMode) {
self.tileMode = tileMode
}

func tile(images: [CGImage]) -> CGImage? {
if images.isEmpty {
return nil
}

if images.count == 1 {
return images[0]
}

switch tileMode {
case .vertical:
return tileVertically(images: images)
case .horizontal:
return tileHorizontally(images: images)
}
}

func getComponentImages(image: CGImage, desiredSize size: CGSize) -> [CGImage] {
let numberOfImages = Int(CGFloat(tileMode.tilingDimensionSize(inImage: image)) / size[keyPath: tileMode.tilingDimensionCGSizeKeypath])

var accumulatedOffset = CGPoint.zero

return (0..<numberOfImages).compactMap { i in
let croppedImage = image.cropping(to: CGRect(origin: accumulatedOffset, size: size))
accumulatedOffset[keyPath: tileMode.tilingDimensionCGPointKeypath] += size[keyPath: tileMode.tilingDimensionCGSizeKeypath]
return croppedImage
}
}

private func tileHorizontally(images: [CGImage]) -> CGImage? {
let width = images.reduce(0) { $0 + $1.width }
let height = images.map { $0.height }.max() ?? 0

guard let cgContext = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}

var accumulatedWidth: CGFloat = 0
for image in images {
let width = CGFloat(image.width)
let rect = CGRect(x: accumulatedWidth, y: 0, width: width, height: CGFloat(image.height))
cgContext.draw(image, in: rect, byTiling: false)
accumulatedWidth += width
}

return cgContext.makeImage()
}

private func tileVertically(images: [CGImage]) -> CGImage? {
let width = images.map { $0.width }.max() ?? 0
let height = images.reduce(0) { $0 + $1.height }

guard let cgContext = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}

var accumulatedHeight: CGFloat = 0
for image in images.reversed() {
let height = CGFloat(image.height)
let rect = CGRect(x: 0, y: accumulatedHeight, width: CGFloat(image.width), height: height)
cgContext.draw(image, in: rect, byTiling: false)
accumulatedHeight += height
}

return cgContext.makeImage()
}
}

extension ImageTiler {
enum ImageTileMode {
case vertical
case horizontal

fileprivate func tilingDimensionSize(inImage image: CGImage) -> Int {
image[keyPath: tilingDimensionCGImageKeypath]
}

fileprivate var tilingDimensionCGSizeKeypath: WritableKeyPath<CGSize, CGFloat> {
switch self {
case .vertical:
return \.height
case .horizontal:
return \.width
}
}

fileprivate var tilingDimensionCGImageKeypath: KeyPath<CGImage, Int> {
switch self {
case .vertical:
return \.height
case .horizontal:
return \.width
}
}

fileprivate var tilingDimensionCGPointKeypath: WritableKeyPath<CGPoint, CGFloat> {
switch self {
case .vertical:
return \.y
case .horizontal:
return \.x
}
}
}
}
119 changes: 71 additions & 48 deletions Sources/VariableBlurImageView/VariableBlurImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ open class VariableBlurImageView: UIImageView {
private let variableBlurEngine = VariableBlurEngine()

public func verticalVariableBlur(image: UIImage, startPoint: CGFloat, endPoint: CGFloat, startRadius: CGFloat, endRadius: CGFloat) {
transformAllVariations(ofImage: image) { cgImage in
transformAllVariations(ofImage: image, variationTransformMode: .sequential) { cgImage in
try self.variableBlurEngine.applyVerticalVariableBlur(
toImage: cgImage,
startPoint: startPoint,
Expand All @@ -24,7 +24,7 @@ open class VariableBlurImageView: UIImageView {
}

public func horizontalVariableBlur(image: UIImage, startPoint: CGFloat, endPoint: CGFloat, startRadius: CGFloat, endRadius: CGFloat) {
transformAllVariations(ofImage: image) { cgImage in
transformAllVariations(ofImage: image, variationTransformMode: .sequential) { cgImage in
try self.variableBlurEngine.applyHorizontalVariableBlur(
toImage: cgImage,
startPoint: startPoint,
Expand All @@ -36,7 +36,7 @@ open class VariableBlurImageView: UIImageView {
}

public func variableBlur(image: UIImage, startPoint: CGPoint, endPoint: CGPoint, startRadius: CGFloat, endRadius: CGFloat) {
transformAllVariations(ofImage: image) { cgImage in
transformAllVariations(ofImage: image, variationTransformMode: .sequential) { cgImage in
try self.variableBlurEngine.applyVariableBlur(
toImage: cgImage,
startPoint: startPoint,
Expand All @@ -47,21 +47,49 @@ open class VariableBlurImageView: UIImageView {
}
}

private func transformAllVariations(ofImage image: UIImage, applyingTransform block: @escaping (CGImage) throws -> CGImage) {
private func transformAllVariations(ofImage image: UIImage, variationTransformMode: VariationTansformMode, applyingTransform block: @escaping (CGImage) throws -> CGImage) {
self.image = image

let currentStyle = traitCollection.userInterfaceStyle

let imageVariations = self.getImageVariations(image: image, currentStyleFirst: variationTransformMode.currentStyleFirst)

DispatchQueue.global().async {
do {
let imageSize = image.size
let imageVariations = self.getImageVariations(image: image)
let horizontalVariations = self.composeDoubleImageHorizontally(images: imageVariations)

guard let cgImage = self.getCGImage(fromUIImage: horizontalVariations) else {
let cgImagesOfVariations = imageVariations.compactMap { self.getCGImage(fromUIImage: $0) }

guard cgImagesOfVariations.count == imageVariations.count else {
throw VariableBlurImageViewError.cannotExtractCGImageFromProvidedImage
}

let blurredImage = try block(cgImage)
let blurredImageVariations: [CGImage]
switch variationTransformMode {
case .tile(let tileMode):
let imageTiler = ImageTiler(tileMode: tileMode)
guard let tiledImage = imageTiler.tile(images: cgImagesOfVariations) else {
throw VariableBlurImageViewError.cannotTileImage
}
let blurredImage = try block(tiledImage)
blurredImageVariations = imageTiler.getComponentImages(image: blurredImage, desiredSize: imageSize)
case .sequential:
var variations = [CGImage]()
for (i, cgImage) in cgImagesOfVariations.enumerated() {
let blurredImage = try block(cgImage)
// Set image when first result is ready
if i == 0 {
DispatchQueue.main.async {
self.image = UIImage(cgImage: blurredImage)
}
}

variations.append(blurredImage)
}
blurredImageVariations = variations
}

let imageWithVariations = self.doubleImageToUserInterfaceStyleVariations(cgImage: blurredImage, size: imageSize) ?? UIImage(cgImage: blurredImage)
let imageWithVariations = self.makeSingleImageWithStyleVariations(fromImages: blurredImageVariations, currentStyleFirst: variationTransformMode.currentStyleFirst, currentStyle: currentStyle)

DispatchQueue.main.async {
self.image = imageWithVariations
Expand All @@ -70,12 +98,14 @@ open class VariableBlurImageView: UIImageView {
#if DEBUG
print("Could not apply variable blur to image: \(error)")
#endif
self.image = image
DispatchQueue.main.async {
self.image = image
}
}
}
}

private func getImageVariations(image: UIImage) -> [UIImage] {
private func getImageVariations(image: UIImage, currentStyleFirst: Bool) -> [UIImage] {
guard let imageAsset = image.imageAsset else {
return [image]
}
Expand All @@ -90,51 +120,26 @@ open class VariableBlurImageView: UIImageView {
return [image]
}

return [lightImage, darkImage]
}

private func composeDoubleImageHorizontally(images: [UIImage]) -> UIImage {
if images.isEmpty {
return UIImage()
}
// Return the current user interface style first.
let currentStyle = traitCollection.userInterfaceStyle

if images.count == 1 {
return images.first!
if currentStyleFirst && currentStyle == .dark {
return [darkImage, lightImage,]
}

let singleImageSize = images.first!.size

let cgContext = CGContext(data: nil, width: images.count * Int(singleImageSize.width), height: Int(singleImageSize.height), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)

guard let cgContext else {
return images.first!
}

for (i, image) in images.enumerated() {
if let cgImage = image.cgImage {
let rect = CGRect(x: CGFloat(i) * singleImageSize.width, y: 0, width: singleImageSize.width, height: singleImageSize.height)
cgContext.draw(cgImage, in: rect, byTiling: false)
}
}

guard let image = cgContext.makeImage() else {
return images.first!
}

return UIImage(cgImage: image)
return [lightImage, darkImage]
}

private func doubleImageToUserInterfaceStyleVariations(cgImage: CGImage, size: CGSize) -> UIImage? {
let context = CIContext()
let ciImage = CIImage(cgImage: cgImage)

guard
let lightImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: size)),
let darkImage = context.createCGImage(ciImage, from: CGRect(origin: CGPoint(x: size.width, y: 0), size: size))
else {
private func makeSingleImageWithStyleVariations(fromImages images: [CGImage], currentStyleFirst: Bool, currentStyle: UIUserInterfaceStyle) -> UIImage? {
guard images.count >= 2 else {
return nil
}

let darkFirst = currentStyleFirst && currentStyle == .dark

let lightImage = darkFirst ? images[1] : images[0]
let darkImage = darkFirst ? images[0] : images[1]

let imageAsset = UIImageAsset()

let lightMode = UITraitCollection(traitsFrom: [.init(userInterfaceStyle: .light)])
Expand All @@ -158,10 +163,28 @@ open class VariableBlurImageView: UIImageView {
return nil
}

enum VariationTansformMode {
/// Tiles the image variations into one image and performs one transform
case tile(tileMode: ImageTiler.ImageTileMode)

/// Performs the transform on each image variation sequentially
case sequential

var currentStyleFirst: Bool {
switch self {
case .tile(_):
return false
case .sequential:
return true
}
}
}

}

extension VariableBlurImageView {
enum VariableBlurImageViewError: String, Error {
case cannotExtractCGImageFromProvidedImage
case cannotTileImage
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6ba1ca5

Please sign in to comment.