Skip to content

Commit

Permalink
Add image converter example
Browse files Browse the repository at this point in the history
  • Loading branch information
gh123man committed Apr 7, 2024
1 parent 2a21c56 commit 50ad92d
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 3 deletions.
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

Examples
.netrc
8 changes: 8 additions & 0 deletions Examples/ImageConverter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
26 changes: 26 additions & 0 deletions Examples/ImageConverter/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ImageConverter",
platforms: [
.macOS(.v10_15)
],
products: [
.executable(
name: "ImageConverter",
targets: ["ImageConverter"]),
],
dependencies: [
.package(name: "AsyncChannels", path: "../../")
],
targets: [
.target(
name: "ImageConverter",
dependencies: [
.product(name: "AsyncChannels", package: "AsyncChannels")
]),
]
)
97 changes: 97 additions & 0 deletions Examples/ImageConverter/Sources/ImageConverter/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
///
/// A simple backpressure managed HEIC -> JPG converter using Async Channels.
/// A worker is spawned for each avalible CPU. A stream of image paths will
/// fan-out to each worker to perform the conversion, and fan-in the converted
/// images to be written to disk. The CPU should be saturated and images will
/// only be processed as fast as they can be converted.
///
/// /-> worker -\
/// files --> worker --> disk
/// \-> worker -/


import Foundation
import AsyncChannels
import AppKit

// MARK: Helper functions

func convertHEICToJPG(heicImage: NSImage, compressionFactor: CGFloat = 1.0) -> Data? {
guard let tiffRepresentation = heicImage.tiffRepresentation, let bitmapImage = NSBitmapImageRep(data: tiffRepresentation) else { return nil }
return bitmapImage.representation(using: .jpeg, properties: [.compressionFactor: compressionFactor])
}

func getFiles(matchingExtension fileExtension: String, inDirectory directoryPath: String) -> [String] {
return try! FileManager.default.contentsOfDirectory(atPath: directoryPath)
.filter { $0.hasSuffix(".\(fileExtension)") }
.map { "\(directoryPath)/\($0)" }
}

// Constants

let directoryPath = "<PATH TO IMAGES>" // Change me!
let files = getFiles(matchingExtension: "HEIC", inDirectory: directoryPath)

// A queue of file paths
let input = Channel<String>(capacity: 100)

// A queue of converted images and their names
let output = Channel<(Data, String)>(capacity: 100)

// keep track of how many workers are active
let waitGroup = WaitGroup()

// Keep track of when we are done writing images to disk
let done = Channel<Bool>()

// Create a worker for each avalible CPU
for i in 0..<ProcessInfo.processInfo.activeProcessorCount {
await waitGroup.add(1)
Task.detached {
for await path in input {
print("Process \(path) on task \(i)")
guard let heicImage = NSImage(contentsOfFile: path) else {
print("failed to open image \(path)")
continue
}

guard let jpgImage = convertHEICToJPG(heicImage: heicImage) else {
print("Failed to convert image \(path)")
continue
}

let name = String(path.split(separator: "/").last!.split(separator: ".").first!)
print("Converted \(name) to jpg")

await output <- (jpgImage, name)
}
await waitGroup.done()
}
}

// Recieve the converted images on one task and write to disk
Task {
try! FileManager.default.createDirectory(at: URL(fileURLWithPath: "\(directoryPath)/converted/"), withIntermediateDirectories: true, attributes: nil)

for await (image, name) in output {
print("Write \(name).jpg")
try! image.write(to: URL(fileURLWithPath: "\(directoryPath)/converted/\(name).jpg"))
}
await done <- true
}

for file in files {
await input <- file
}

// Close the input when done writing paths
input.close()

// Wait for all workers to finish
await waitGroup.wait()

// Close the output
output.close()

// Wait for all files to be flushed to disk
await <-done
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ for _ in (0..<20) {
}
```

## Code Samples

See the [Examples](/Examples/) folder for real world usage.

- [Parallel image converter](/Examples/ImageConverter/) Saturate the CPU to convert images applying back pressrue to the input.

## Notes

If you are looking for a blocking variant of this library for traditional swift concurrency, check out my previous project [Swigo](https://github.com/gh123man/Swigo) which this library is based off of.
Expand Down

0 comments on commit 50ad92d

Please sign in to comment.