Sample Code

Compressing and Decompressing Files with Swift Stream Compression

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

Download

Overview

This sample code project uses the Compression framework to encode (compress) and decode (decompress) files when you drag them to the app window. The app decompresses files that have extensions matching one of four supported compression algorithms: .lz4, .zlib, lzma, or .lzfse. The app compresses all files, regardless of their extension. The app writes the encoded or decoded result to the temporary directory returned by the NSTemporaryDirectory() function.

The code in this sample is useful in applications that store or transmit files, such as PDF or text, where saving or sending smaller files can improve your app’s performance and reduce storage overhead.

This sample walks you through the steps to implement stream compression, where the app reads chucks of data from a source buffer repeatedly to compress or decompress data, appending to a destination buffer.

  1. Accept dropped files.

  2. Select a compression algorithm.

  3. Distinguish whether the dropped file is compressed or uncompressed.

  4. Define the source and destination file handles.

  5. Create a destination buffer.

  6. Create the output filter.

  7. Compress or decompress the dragged file.

  8. Handle any errors that occurred during compression.

  9. Close the source and destination files.

By moving the encoding or decoding to a background thread, you’re able to keep your app interactive and update the user with progress of the operation (for example, with an NSProgressIndicator). Stream compression also enables tasks such as:

  • Decoding a compressed stream into a buffer, with the ability to grow that buffer and resume decoding if the expanded stream is too large to fit, without repeating any work.

  • Encoding a stream as pieces of it become available, without ever needing to create a buffer large enough to hold all the uncompressed data at one time.

Accept Dropped Files

Register the app’s view, DragDropCompressView, as a destination for file URLs, by adding registerForDraggedTypes(_:) to the viewDidMoveToSuperview() method:

override func viewDidMoveToSuperview() {
    registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL])
}

After the user drops a conforming file onto the app, the view calls the performDragOperation(_:) method. To ensure each item is a URL instance, your app iterates over the dragged items inside performDragOperation, using guard:

sender.enumerateDraggingItems(options: [],
                              for: nil,
                              classes: [ NSURL.self ],
                              searchOptions: [ .urlReadingFileURLsOnly: true ]) { (draggingItem, _, _) in
    guard
        let url = draggingItem.item as? URL else {
            return
    }

Select a Compression Algorithm

If speed and compression ratio are important for your application of the Compression framework, use Algorithm.lzfse:

let encodeAlgorithm = Algorithm.lzfse

If you require interoperability with non-Apple devices, use Algorithm.zlib instead. For more information about other compression algorithms, see compression_algorithm.

Distinguish Between Compressed and Uncompressed Files

Use the path extension to infer whether a file is already compressed, or if the file needs to be compressed. To simplify this process, create a failable initializer in an extension to the Compression framework’s Algorithm enumeration:

init?(name: String) {
    switch name.lowercased() {
    case "lz4":
        self = .lz4
    case "zlib":
        self = .zlib
    case "lzma":
        self = .lzma
    case "lzfse":
        self = .lzfse
    default:
        return nil
    }
}

Use this new initializer to define the algorithm and operation constants:

let algorithm: Algorithm
let operation: FilterOperation
                            
if let decodeAlgorithm = Algorithm(name: url.pathExtension) {
    algorithm = decodeAlgorithm
    operation = .decompress
} else {
    algorithm = self.encodeAlgorithm
    operation = .compress
}

Define the Source and Destination File Handles

The sample uses FileHandle instances to read from the source file and write to the destination file. Use optional binding to define the required file handles:

if
    let sourceFileHandle = try? FileHandle(forReadingFrom: url),
    let sourceLength = FileHelper.fileSize(atURL: url),
    let fileName = url.pathComponents.last,
    let fileNameDeletingPathExtension = url.deletingPathExtension().pathComponents.last,
    let destinationFileHandle = FileHandle.makeFileHandle(forWritingToFileNameInTempDirectory:
        operation == .compress
            ? fileName + self.encodeAlgorithm.pathExtension
            : fileNameDeletingPathExtension)
{

If the optional binding succeeded, the destination file handle points to the source filename by appending the respective compression algorithm extension, or removing the extension in the case of decompression.

For example, the compressed source file MyCompressedFile.PDF.lzfse would have a decompressed destination of MyCompressedFile.PDF; and the uncompressed source file, MyRawFile.PDF, would have a compressed destination of MyRawFile.PDF.lzfse.

Pass the source and destination file handles, with the operation and algorithm values to the helper function streamingCompression:

Compressor.streamingCompression(operation: operation,
                                sourceFileHandle: sourceFileHandle,
                                destinationFileHandle: destinationFileHandle,
                                algorithm: algorithm) {
                                    self.progress.completedUnitCount = $0
}
print("Operation complete. Result written to:", NSTemporaryDirectory())

Create a Destination Buffer

The streamingCompression method iterates over the source data, encoding or decoding data in blocks based on the length defined by bufferSize. The method writes the result into the destination buffer, and writes the destination buffer data to the destination file handle. The following declares the buffer size and allocates the destination buffer:

let bufferSize = 32_768

Create the Output Filter

You create an OutputFilter instance, that specifies the operation and the compression algorithm, by using the parameters passed to streamingCompression For more information about other compression algorithms, see compression_algorithm. The final initializer parameter is a closure the instance calls as it writes each encoded or decoded block of data to the destination file handler.

do {
    let outputFilter = try OutputFilter(operation,
                                        using: algorithm) {(data: Data?) -> Void in
                                            if let data = data {
                                                destinationFileHandle.write(data)
                                            }
    }

Compress or Decompress the Dragged File

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

while true {
    let subdata = sourceFileHandle.readData(ofLength: bufferSize)

    progressUpdateFunction(Int64(sourceFileHandle.offsetInFile))
    
    try outputFilter.write(subdata)
    if subdata.count < bufferSize {
        break
    }
}

Handle any Errors That Occurred During Compression

The output filter’s init(_:using:bufferCapacity:writingTo:) and write(_:) methods can both throw errors. Write code to catch and handle any errors that the output filter may have thrown during the compression stage.

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

Close the Source and Destination Files

After you’re finished with the source and destination file handles, close them with the closeFile() method:

sourceFileHandle.closeFile()
destinationFileHandle.closeFile()

See Also

Objects that Simplify Multiple-Step Compression

Compressing and Decompressing Data with Input and Output Filters

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

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.