Show Menu

Looking to hire an app developer?

Submit your 30 day Job Listing for FREE

In this functional Swift programming tutorial I will show you how to generate ASCII Art by converting images in your iOS 9 + applications.

So, what exactly in ASCII art. I will show you with this example. Take this lovely picture of Lenna

non ascii swift

If we were to convert this to an ASCII image, the result would be:

swift ascii converted image

We can also take a closer look at her face so that you can see the structure of the generated ASCII output.

ascii close up

Looks pretty cool right? let’s take a look at how Swift Creates ASCII images.

Generate ASCII

Ever looked really close at a TV screen? Maybe you had an image sent to you that was stretched too large and you could see those tiny little squares? Well, Those are called Pixels and each pixel in an image file is mapped to an ASCII value, referred to here as a symbol.

swift pixel to ascii

Then an image’s pixels are first transformed to an intermediary value by converting the grayscale color value. The grayscale color’s intensity (i.e. brightness) is normalized to a value between 0 and 1, where black is 0 and white is 1.

pixel to intensities to symbol ascii

Once that has been done, then each color intensity value is translated to an ASCII symbol. Later we will visit the AsciiPalette class, which supports that.

And finally, all the rows of ASCII symbols are joined to form a giant, multi-line string which is the resulted of the creates ASCII image (art)

To achieve this in swift, we can use three simple lines in our class (Swift Class: AsciiArtist())


intensities  = intensityMatrixFromPixelPointer(pixelPointer),
symbolMatrix = symbolMatrixFromIntensityMatrix(intensities)
return symbolMatrix.joinWithSeparator("\n")

Note: An AsciiArtist object relies on Pixel and AsciiPalette, which we’ll look at next.

Transforming pixels to intensities in Swift

A Pixel is a representation of the color at a specific coordinate of the image. A color consists of four bytes; one byte per channel RGBA (red, green, blue, and alpha). AsciiArtist asks a Pixel to determine its color intensity, which, as previously mentioned, is calculated by normalizing the pixel’s grayscale value to a percentage. This is how that works:


/** Represents the memory address of a pixel. */
typealias PixelPointer = UnsafePointer

/** A point in an image converted to an ASCII character. */
struct Pixel
{
    /** The number of bytes a pixel occupies. 1 byte per channel (RGBA). */
    static let bytesPerPixel = 4
    
    private let offset: Int
    private init(_ offset: Int) { self.offset = offset }
    
    static func createPixelMatrix(width: Int, _ height: Int) -> [[Pixel]]
    {
        return (0.. Double
    {
        let
        red   = pointer[offset + 0],
        green = pointer[offset + 1],
        blue  = pointer[offset + 2]
        return Pixel.calculateIntensity(red, green, blue)
    }
    
    private static func calculateIntensity(r: UInt8, _ g: UInt8, _ b: UInt8) -> Double
    {
        // Normalize the pixel's grayscale value to between 0 and 1.
        // Weights from http://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems
        let
        redWeight   = 0.229,
        greenWeight = 0.587,
        blueWeight  = 0.114,
        weightedMax = 255.0 * redWeight   +
                      255.0 * greenWeight +
                      255.0 * blueWeight,
        weightedSum = Double(r) * redWeight   +
                      Double(g) * greenWeight +
                      Double(b) * blueWeight
        return weightedSum / weightedMax
    }
}

If you’re wondering what those weight values are on lines 47 to 49, they are just industry-standard numbers used for grayscale conversions, as explained in this article: Luma coding in video systems

Transforming intensities to symbols

The symbolFromIntensity function in the AsciiArtist class will convert the color intensity to an ASCII symbol, by converting a normalized intensity value to an array index. That array is provided by AsciiPalette.


private func symbolFromIntensity(intensity: Double) -> String
    {
        assert(0.0 <= intensity && intensity <= 1.0)
        
        let
        factor = palette.symbols.count - 1,
        value  = round(intensity * Double(factor)),
        index  = Int(value)
        return palette.symbols[index]
    }

The first symbol in the array is used for very dark pixels and the last symbol is for very bright pixels. The question is: which ASCII symbols should be in the array, and in what order? A poorly designed symbol palette yields improperly shaded ASCII art.

The best way to get the best results when using Swift to create ASCII images is to let the compiler figure out what is best to use. After all its computer generated. The AsciiPalette class will render each symbol to a separate image, with black text on a white background.

It then sorts the symbols by the number of white pixels in their images. The more white pixels in the image, the higher the symbol’s intensity. The blank space character (' ') has the highest intensity value since it contains only white pixels, and would therefore be the last symbol in the array.


class AsciiPalette
{
    private let font: UIFont
    
    init(font: UIFont) { self.font = font }
    
    lazy var symbols: [String] = self.loadSymbols()
    
    private func loadSymbols() -> [String]
    {
        return symbolsSortedByIntensityForAsciiCodes(32...126) // from ' ' to '~'
    }
    
    private func symbolsSortedByIntensityForAsciiCodes(codes: Range) -> [String]
    {
        let
        symbols          = codes.map { self.symbolFromAsciiCode($0) },
        symbolImages     = symbols.map { UIImage.imageOfSymbol($0, self.font) },
        whitePixelCounts = symbolImages.map { self.countWhitePixelsInImage($0) },
        sortedSymbols    = sortByIntensity(symbols, whitePixelCounts)
        return sortedSymbols
    }
    
    private func symbolFromAsciiCode(code: Int) -> String
    {
        return String(Character(UnicodeScalar(code)))
    }
    
    private func countWhitePixelsInImage(image: UIImage) -> Int
    {
        let
        dataProvider = CGImageGetDataProvider(image.CGImage),
        pixelData    = CGDataProviderCopyData(dataProvider),
        pixelPointer = CFDataGetBytePtr(pixelData),
        byteCount    = CFDataGetLength(pixelData),
        pixelOffsets = 0.stride(to: byteCount, by: Pixel.bytesPerPixel)
        return pixelOffsets.reduce(0) { (count, offset) -> Int in
            let
            r = pixelPointer[offset + 0],
            g = pixelPointer[offset + 1],
            b = pixelPointer[offset + 2],
            isWhite = (r == 255) && (g == 255) && (b == 255)
            return isWhite ? count + 1 : count
        }
    }
    
    private func sortByIntensity(symbols: [String], _ whitePixelCounts: [Int]) -> [String]
    {
        let
        mappings      = NSDictionary(objects: symbols, forKeys: whitePixelCounts),
        uniqueCounts  = Set(whitePixelCounts),
        sortedCounts  = uniqueCounts.sort(),
        sortedSymbols = sortedCounts.map { mappings[$0] as! String }
        return sortedSymbols
    }
}

The AsciiPalette designated initializer requires the UIFont argument because the choice of font affects how a character is rendered, impacting the number of white pixels around it.

There you go, be sure to check out the download below for the full code. Have fun creatiing ASCII images in swift and feel free to post any questions and comments below.


Swift Class: AsciiArtist()


/** Transforms an image to ASCII art. */
class AsciiArtist
{
    private let
    image:   UIImage,
    palette: AsciiPalette
    
    init(_ image: UIImage, _ palette: AsciiPalette)
    {
        self.image   = image
        self.palette = palette
    }
    
    func createAsciiArt() -> String
    {
        let
        dataProvider = CGImageGetDataProvider(image.CGImage),
        pixelData    = CGDataProviderCopyData(dataProvider),
        pixelPointer = CFDataGetBytePtr(pixelData),
        intensities  = intensityMatrixFromPixelPointer(pixelPointer),
        symbolMatrix = symbolMatrixFromIntensityMatrix(intensities)
        return symbolMatrix.joinWithSeparator("\n")
    }
    
    private func intensityMatrixFromPixelPointer(pointer: PixelPointer) -> [[Double]]
    {
        let
        width  = Int(image.size.width),
        height = Int(image.size.height),
        matrix = Pixel.createPixelMatrix(width, height)
        return matrix.map { pixelRow in
            pixelRow.map { pixel in
                pixel.intensityFromPixelPointer(pointer)
            }
        }
    }
    
    private func symbolMatrixFromIntensityMatrix(matrix: [[Double]]) -> [String]
    {
        return matrix.map { intensityRow in
            intensityRow.reduce("") {
                $0 + self.symbolFromIntensity($1)
            }
        }
    }
    
    private func symbolFromIntensity(intensity: Double) -> String
    {
        assert(0.0 <= intensity && intensity <= 1.0)
        
        let
        factor = palette.symbols.count - 1,
        value  = round(intensity * Double(factor)),
        index  = Int(value)
        return palette.symbols[index]
    }
}

Download Files

Download the Create ASCII images from Swift source files by helping out this blog with a share or like.

having issues?

We have a Questions and Answer section where you can ask your iOS Development questions to thousands of iOS Developers.

Ask Question

FREE Download!

Get your FREE Swift 2 Cheat Sheet and quick reference guide PDF download when you sign up to SwiftMonthly


Sharing is caring

If you enjoyed this tutorial, please help us and others by sharing using one of the social media buttons below.


Written by:

I like to write about the process of learning how to write software that runs on Apple devices. Check out my book : iOS for .Net Devs

Comments

comments