Article

Compressing and Decompressing Data with Input and Output Filters

Compress and decompress streamed or from-memory data, using input and output filters.

Overview

In this article, you’ll learn how to use InputFilter and OutputFilter to compress and decompress a String instance. The article walks you through the following steps:

  1. Create the sample source data.

  2. Specify the page size, in bytes.

  3. Create the destination buffer.

  4. Create the output filter that will compress the sample source data.

  5. Compress the sample source data and handle any errors.

  6. Create the input filter that will decompress the compressed result from the previous step.

  7. Decompress the compressed data.

  8. Create a string from the decompressed data from the previous step.

Although the article uses a string to provide a simplified example, use this API when working with data that’s streamed to or from memory. For example, when reading from or writing to a file.

If you’re compressing and decompressing data that’s held entirely in memory, consider using compressed(using:) and decompressed(using:). These functions provide a simple API to compress and decompress data in a single step.

Create the Source Data

Typically, your app dynamically generates the source data that it compresses, but for this example, the source data is a hard-coded string:

let sourceData = """
    Lorem ipsum dolor sit amet consectetur adipiscing elit mi
    nibh ornare proin blandit diam ridiculus, faucibus mus
    dui eu vehicula nam donec dictumst sed vivamus bibendum
    aliquet efficitur. Felis imperdiet sodales dictum morbi
    vivamus augue dis duis aliquet velit ullamcorper porttitor,
    lobortis dapibus hac purus aliquam natoque iaculis blandit
    montes nunc pretium.
    """.data(using: .utf8)!

On return, sourceData contains the UTF-8 representation of the source string.

Specify the Page Size

InputFilter and OutputFilter instances compress and decompress pages of data. Specify the number of bytes in each page that are read from or written to a stream. Smaller values allow your app to report progress or perform other tasks at higher frequencies than larger values. However, larger values allow your app to compress or decompress using fewer steps, possibly in less time.

For this example, use a page size of 128 bytes:

let pageSize = 128

Create the Compression Destination Buffer

Create a mutable, empty Data structure to receive the compressed data:

var compressedData = Data()

Create the Output Filter

Create an OutputFilter instance, and specify the operation as FilterOperation.compress and the compression algorithm as Algorithm.lzfse. For more information about other compression algorithms, see compression_algorithm.

The final initializer parameter is a closure that the instance calls as it writes each compressed block of data to compressedData.

do {
    let outputFilter = try OutputFilter(.compress,
                                         using: .lzfse) { (data: Data?) -> Void in
                                            if let data = data {
                                                compressedData.append(data)
                                            }
    }

Compress the Data

Iterate over the source data and call the subdata(in:) method to copy pageSize chunks to subdata. The write(_:) method compresses each chunk and uses the closure specified in the OutputFilter initializer to write the result to compressedData.

    var index = 0
    let bufferSize = sourceData.count
    
    while true {
        let rangeLength = min(pageSize, bufferSize - index)
        
        let subdata = sourceData.subdata(in: index ..< index + rangeLength)
        index += rangeLength

        try outputFilter.write(subdata)

        if (rangeLength == 0) {
            break
        }
    }

On return, compressedData contains a compressed version of the original source data.

Handle any Errors That Occured During Compression.

The output filter’s initializer and write method can both throw errors. Your code should catch and handle any errors the output filter may have thrown during the compression stage.

} catch {
    fatalError("Error occurred during encoding: \(error.localizedDescription).")
}

Create the Decompression Destination Buffer

Create a mutable, empty Data structure to receive the decompressed data:

var decompressedData = Data()

Create the Input Filter

Create an InputFilter instance, and specify the operation as FilterOperation.decompress, and the compression algorithm as Algorithm.lzfse.

The final initializer parameter is a closure the instance calls as it reads each compressed block of data.

do {
    var index = 0
    let bufferSize = compressedData.count
    
    let inputFilter = try InputFilter(.decompress,
                                      using: .lzfse) { (length: Int) -> Data? in
                                        let rangeLength = min(length, bufferSize - index)
                                        let subdata = compressedData.subdata(in: index ..< index + rangeLength)
                                        index += rangeLength
                                        
                                        return subdata
    }

Decompress the Data

You iterate over the compressed data by repeatedly calling readData(ofLength:), until the function returns nil. With each iteration, append the data returned by the input filter to decompressedData:

    while let page = try inputFilter.readData(ofLength: pageSize) {
        decompressedData.append(page)
    }

Handle any Errors That Occurred During Decompression.

The input filter’s initializer and read method can both throw errors. Your code should catch and handle any errors the output filter may have thrown during the decompression stage.

} catch {
    fatalError("Error occurred during decoding: \(error.localizedDescription).")
}

Create a String from the Decompressed Data

Use init(data:encoding:) to recreate a string from the decompressed data:

let decompressedString = String(data: decompressedData,
                                encoding: .utf8)

On return, decompressedString contains the original text shown in Create the Source Data.

Select Input and Output Filters Based on Requirements

You’re not tied to using output filters for compression and input filters for decompression. You should select the appropriate compressor-decompressor based on your app’s requirements.

For example, the following code shows an InputFilter instance as the compressor:

var compressedData = Data()

do {
    var index = 0
    let bufferSize = sourceData.count
    
    let inputFilter = try InputFilter(.compress,
                                      using: .lzfse) { (length: Int) -> Data? in
        let rangeLength = min(length, bufferSize - index)
        let subdata = sourceData.subdata(in: index ..< index + rangeLength)
        index += rangeLength
        
        return subdata
    }
    
    while let page = try inputFilter.readData(ofLength: pageSize) {
        compressedData.append(page)
    }
} catch {
    fatalError("Error occurred during encoding: \(error.localizedDescription).")
}

The following shows an OutputFilter instance as the decompressor:

var decompressedData = Data()

do {
    let outputFilter = try OutputFilter(.decompress,
                                        using: .lzfse) {(data: Data?) -> Void in
                                            if let data = data {
                                                decompressedData.append(data)
                                            }
    }

    var index = 0
    let bufferSize = compressedData.count
    
    while true {
        let rangeLength = min(pageSize, bufferSize - index)
        
        let subdata = compressedData.subdata(in: index ..< index + rangeLength)
        index += rangeLength
        try outputFilter.write(subdata)
        if (rangeLength == 0) {
            break
        }
    }
} catch {
    fatalError("Error occurred during decoding: \(error.localizedDescription).")
}

See Also

Objects that Simplify Multiple-Step Compression

Compressing and Decompressing Files with Swift Stream Compression

Perform compression for all files and decompression for files with supported extension types.

class InputFilter

An encoder-decoder that reads input data from a stream.

class OutputFilter

An encoder-decoder that writes output data to a stream.

enum Algorithm

Algorithms used for compression or decompression.

enum FilterError

Errors that occur during compression.

enum FilterOperation

Operations that define whether input and output filters compress or decompress data.