Implementing Image Filters in Swift

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In this segment, you’ll implement three image processing algorithms in Swift: grayscale conversion, blur effect, and brightness adjustment. You’ll work directly with raw pixel data, learning how to manipulate individual color channels to create visual effects.

Image processing algorithms operate on pixel arrays, transforming color values according to mathematical formulas. Swift’s type safety and array handling make it well-suited for this kind of algorithmic work.

Understanding the ImageProcessor Structure

Your Starter project already has ImageProcessor.swift with TODO stubs for three filter functions. You’ll implement each filter function to process RGBA pixel data.

Implementing the Grayscale Filter

The grayscale filter converts color images to black and white by calculating a weighted average of the RGB channels.

public static func applyGrayscaleFilter(
  imageData: Data,
  width: Int32,
  height: Int32
) -> Data? {
  // 1
  guard width > 0, height > 0 else { return nil }

  // 2
  var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
  imageData.copyBytes(to: &pixels, count: pixels.count)

  // 3
  for i in stride(from: 0, to: pixels.count, by: 4) {
    let r = Float(pixels[i])
    let g = Float(pixels[i + 1])
    let b = Float(pixels[i + 2])

    // 4
    let gray = UInt8(0.299 * r + 0.587 * g + 0.114 * b)

    // 5
    pixels[i] = gray
    pixels[i + 1] = gray
    pixels[i + 2] = gray
    // Alpha (i+3) remains unchanged
  }

  // 6
  return Data(pixels)
}
gray = 0.299 * 255 + 0.587 * 255 + 0.114 * 0
gray = 76.245 + 149.685 + 0
gray = 225.93 → 226 (rounded)
Result: [226, 226, 226, 255]

Implementing the Blur Filter

The blur filter creates a soft-focus effect by averaging each pixel with its surrounding neighbors. This is called a box blur.

public static func applyBlurFilter(
  imageData: Data,
  width: Int32,
  height: Int32,
  radius: Int32 = 5
) -> Data? {
  // 1
  guard width > 0, height > 0, radius > 0 else { return nil }

  // 2
  let safeRadius = min(radius, 10)

  var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
  imageData.copyBytes(to: &pixels, count: pixels.count)

  var output = [UInt8](repeating: 0, count: Int(width * height * 4))

  // 3
  for i in stride(from: 0, to: pixels.count, by: 4) {
    output[i + 3] = pixels[i + 3]
  }

  // 4
  let step = 1

  for y in stride(from: 0, to: Int(height), by: step) {
    for x in stride(from: 0, to: Int(width), by: step) {
      // 5
      var r: Float = 0, g: Float = 0, b: Float = 0
      var count: Float = 0

      // 6
      for ky in -Int(safeRadius)...Int(safeRadius) {
        for kx in -Int(safeRadius)...Int(safeRadius) {
          let px = x + kx
          let py = y + ky

          // 7
          guard px >= 0, px < Int(width), py >= 0, py < Int(height) else { continue }

          let index = (py * Int(width) + px) * 4

          r += Float(pixels[index])
          g += Float(pixels[index + 1])
          b += Float(pixels[index + 2])
          count += 1
        }
      }

      // 8
      let index = (y * Int(width) + x) * 4
      output[index] = UInt8(r / count)
      output[index + 1] = UInt8(g / count)
      output[index + 2] = UInt8(b / count)
    }
  }

  return Data(output)
}
Ejeyafen 7w4 hmed az zuuxyjoxx Melhak bavoq ox (k, q) 95 bilipc cojoj Ujozogac BZY wehuus Mojs duvawt am hid sepug Qaj Jdel Lirjub (domaad = 6)
Zel bweg nayjik liheiconewaoj

Implementing Brightness Adjustment

The brightness filter adds or subtracts a value from each RGB channel, making the image lighter or darker.

public static func adjustBrightness(
  imageData: Data,
  width: Int32,
  height: Int32,
  amount: Float
) -> Data? {
  // 1
  guard width > 0, height > 0 else { return nil }

  var pixels = [UInt8](repeating: 0, count: Int(width * height * 4))
  imageData.copyBytes(to: &pixels, count: pixels.count)

  // 2
  for i in stride(from: 0, to: pixels.count, by: 4) {
    let r = Float(pixels[i]) + amount
    let g = Float(pixels[i + 1]) + amount
    let b = Float(pixels[i + 2]) + amount

    // 3
    pixels[i] = UInt8(max(0, min(255, r)))
    pixels[i + 1] = UInt8(max(0, min(255, g)))
    pixels[i + 2] = UInt8(max(0, min(255, b)))
    // Alpha (i+3) remains unchanged
  }

  return Data(pixels)
}
// Without clamping
let r = 200.0 + 100.0  // = 300.0 (invalid!)
let pixel = UInt8(r)    // Wraps to 44 (incorrect)

// With clamping
let r = max(0, min(255, 200.0 + 100.0))  // = 255.0 (correct)
let pixel = UInt8(r)    // = 255 (white, not wrapped)

Implementing the File Processing Wrapper

Now implement the function that ties everything together by reading files, applying filters, and writing results.

public static func processImageFile(
  inputPath: String,
  outputPath: String,
  filterType: String,
  amount: Float = 0
) -> Bool {
  // 1
  let inputURL = URL(fileURLWithPath: inputPath)
  guard let fileData = try? Data(contentsOf: inputURL), fileData.count >= 8 else {
    return false
  }

  // 2
  let headerBytes = Array(fileData.prefix(8))

  let width = Int32((UInt32(headerBytes[0]) << 24) |
                    (UInt32(headerBytes[1]) << 16) |
                    (UInt32(headerBytes[2]) << 8) |
                    UInt32(headerBytes[3]))
  let height = Int32((UInt32(headerBytes[4]) << 24) |
                     (UInt32(headerBytes[5]) << 16) |
                     (UInt32(headerBytes[6]) << 8) |
                     UInt32(headerBytes[7]))

  guard width > 0, height > 0 else {
    return false
  }

  // 3
  let pixelData = fileData.subdata(in: 8..<fileData.count)

  // 4
  let processedData: Data?
  switch filterType.lowercased() {
  case "grayscale":
    processedData = applyGrayscaleFilter(imageData: pixelData, width: width, height: height)
  case "blur":
    processedData = applyBlurFilter(imageData: pixelData, width: width, height: height)
  case "brighter":
    processedData = adjustBrightness(imageData: pixelData, width: width, height: height, amount: 50)
  case "darker":
    processedData = adjustBrightness(imageData: pixelData, width: width, height: height, amount: -50)
  default:
    processedData = pixelData
  }

  guard let finalPixels = processedData else {
    return false
  }

  // 5
  let outputURL = URL(fileURLWithPath: outputPath)
  do {
    var outputData = Data()

    // 6
    outputData.append(UInt8((width >> 24) & 0xFF))
    outputData.append(UInt8((width >> 16) & 0xFF))
    outputData.append(UInt8((width >> 8) & 0xFF))
    outputData.append(UInt8(width & 0xFF))
    outputData.append(UInt8((height >> 24) & 0xFF))
    outputData.append(UInt8((height >> 16) & 0xFF))
    outputData.append(UInt8((height >> 8) & 0xFF))
    outputData.append(UInt8(height & 0xFF))

    // 7
    outputData.append(finalPixels)

    try outputData.write(to: outputURL)
    return true
  } catch {
    return false
  }
}

Building and Testing

Build your Swift package to verify everything compiles:

cd taskmanager-lib
swift build

Key Takeaways

In this segment, you’ve:

See forum comments
Download course materials from Github
Previous: Binary Data & Image Format Next: UI Integration & Testing