Note: while it will be helpful to have an understanding of Swift's Codable family of interfaces, Binary Codable is a separate implementation optimized for binary representations and should be understood as familiar, yet distinct.
This introductory guide will explore how one might read and write GIF files by implementing the GIF file format using Binary Codable.
A GIF file starts with a header. Let's start by defining this type:
struct GIFHeader {
}
We'll implement decoding first because it's easier to test our code against an existing gif file.
// New
import BinaryCodable
// Modified
struct GIFHeader: BinaryDecodable {
// New
init(from decoder: BinaryDecoder) throws {
}
}
// Use these three lines of code to debug your implementation as we go along.
let data = try Data(contentsOf: gifUrl)
let decoder = BinaryDataDecoder()
let header = try decoder.decode(GIFHeader.self, from: data)
Aside: the biggest distinction between Swift Codable and Binary Codable is that we do not get encoding and decoding implementations for complex types for free. This is presently by design, though there are opportunities for improving this in the future. That being said, Binary Codable does provide automatic implementations for RawRepresentable types (namely enums with raw values).
Like Swift Codable, we first create a sequential container.
Note: the container variable needs to be a var
because we will mutate it.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
init(from decoder: BinaryDecoder) throws {
// New
// A nil maxLength means we don't know how long this container is.
var container = decoder.container(maxLength: nil)
}
}
We can now use our container to decode values from the external representation. But first, let's take a look at the GIF header specification:
typedef struct _GifHeader
{
// Header
BYTE Signature[3]; /* Header Signature (always "GIF") */
BYTE Version[3]; /* GIF format version("87a" or "89a") */
// Logical Screen Descriptor
WORD ScreenWidth; /* Width of Display Screen in Pixels */
WORD ScreenHeight; /* Height of Display Screen in Pixels */
BYTE Packed; /* Screen and Color Map Information */
BYTE BackgroundColor; /* Background Color Index */
BYTE AspectRatio; /* Pixel Aspect Ratio */
} GIFHEAD;
If we count up the bytes in the header we'll see that there are exactly 13 bytes. Let's make our implementation a bit safer by coding this known size into our container.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
init(from decoder: BinaryDecoder) throws {
// Modified
var container = decoder.container(maxLength: 13)
}
}
Time to start decoding some bytes!
The first three bytes are the GIF's signature, which is always "GIF". Let's decode those three bytes and consider any value other than "GIF" as a decoding error.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
init(from decoder: BinaryDecoder) throws {
var container = decoder.container(maxLength: 13)
// New
let signature = try container.decode(length: 3)
if signature != Data("GIF".utf8) {
throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Missing GIF header."))
}
}
}
The next three bytes are the GIF format version, which will either by 87a or 89a. We could use the same approach as above to decode these next three bytes, but that would require we implement validation for the possible versions. Instead, let's let Swift's enum type do the validation for us.
In the example below we:
- Define a
Version
enum type with the GIF format versions we're aware of. The enum type needs to be a raw representable type. It also needs to conform toBinaryDecodable
. - Define a property of this type for future reference.
- Create a nested container with a maximum length of 3 bytes.
- Decode our version type using the nested container.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
// New
enum Version: String, BinaryDecodable {
case gif87a = "87a"
case gif89a = "89a"
}
let version: Version
init(from decoder: BinaryDecoder) throws {
var container = decoder.container(maxLength: 13)
let signature = try container.decode(length: 3)
if signature != Data("GIF".utf8) {
throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Missing GIF header."))
}
// New
var versionContainer = container.nestedContainer(maxLength: 3)
self.version = try versionContainer.decode(Version.self)
}
}
By using an enum we've accomplished two things:
- Clearly defined the expected values for this field.
- Added error handling for unexpected values: if a GIF format version other than 87a or 89a is encountered, a
BinaryDecodingError.dataCorrupted
exception will be thrown.
Note: we can also apply this pattern to signature
using a single-value String enum. Try cleaning up your implementation accordingly!
Let's move on to the next part of the header: the width and height. These two 16-bit values are relatively easy to decode. We'll also store these values because they'll be useful later.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
enum Version: String, BinaryDecodable {
case gif87a = "87a"
case gif89a = "89a"
}
let version: Version
// New
let screenWidth: UInt16
let screenHeight: UInt16
init(from decoder: BinaryDecoder) throws {
var container = decoder.container(maxLength: 13)
let signature = try container.decode(length: 3)
if signature != Data("GIF".utf8) {
throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Missing GIF header."))
}
var versionContainer = container.nestedContainer(maxLength: 3)
self.version = try versionContainer.decode(Version.self)
// New
self.screenWidth = try container.decode(UInt16.self)
self.screenHeight = try container.decode(UInt16.self)
}
}
The next byte in the header is the packed
parameter which is defined as follows:
Packed contains the following four subfields of data:
Bits 0-2: Size of the Global Color Table Bit 3: Color Table Sort Flag Bits 4-6: Color Resolution Bit 7: Global Color Table Flag
We can again capture this information in a Swift data type, but this time as an OptionSet
. This next part gets a little more complicated due to bit unpacking; there's room for improvement here.
import BinaryCodable
struct GIFHeader: BinaryDecodable {
enum Version: String, BinaryDecodable {
case gif87a = "87a"
case gif89a = "89a"
}
let version: Version
let screenWidth: UInt16
let screenHeight: UInt16
// New
struct Packed: OptionSet, BinaryDecodable {
let rawValue: UInt8
static let globalColorTableSizeMask = Packed(rawValue: 0b00000111)
static let colorTableSort = Packed(rawValue: 0b00001000)
static let colorResolutionMask = Packed(rawValue: 0b01110000)
static let globalColorTable = Packed(rawValue: 0b10000000)
}
let numberOfGlobalColorTableEntries: Int
let colorTableEntriesSortedByImportance: Bool
let colorResolution: UInt8
let hasGlobalColorTable: Bool
init(from decoder: BinaryDecoder) throws {
var container = decoder.container(maxLength: 13)
let signature = try container.decode(length: 3)
if signature != Data("GIF".utf8) {
throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Missing GIF header."))
}
var versionContainer = container.nestedContainer(maxLength: 3)
self.version = try versionContainer.decode(Version.self)
self.screenWidth = try container.decode(type(of: self.screenWidth))
self.screenHeight = try container.decode(type(of: self.screenHeight))
// New
let packed = try container.decode(Packed.self)
let globalColorTableSize = packed.intersection(.globalColorTableSizeMask).rawValue
self.numberOfGlobalColorTableEntries = 1 << (Int(globalColorTableSize) + 1)
self.colorTableEntriesSortedByImportance = packed.intersection(.colorTableSort).rawValue != 0
self.colorResolution = (packed.intersection(.colorResolutionMask).rawValue >> 4) + 1
self.hasGlobalColorTable = packed.intersection(.globalColorTable).rawValue != 0
}
}
Phew. Not the prettiest code, but we now have meaningful properties initialized from the packed bits.
The last two values are relatively straightforward:
import BinaryCodable
struct GIFHeader: BinaryDecodable {
enum Version: String, BinaryDecodable {
case gif87a = "87a"
case gif89a = "89a"
}
let version: Version
let screenWidth: UInt16
let screenHeight: UInt16
struct Packed: OptionSet, BinaryDecodable {
let rawValue: UInt8
static let globalColorTableSizeMask = Packed(rawValue: 0b00000111)
static let colorTableSort = Packed(rawValue: 0b00001000)
static let colorResolutionMask = Packed(rawValue: 0b01110000)
static let globalColorTable = Packed(rawValue: 0b10000000)
}
let numberOfGlobalColorTableEntries: Int
let colorTableEntriesSortedByImportance: Bool
let colorResolution: UInt8
let hasGlobalColorTable: Bool
// New
let backgroundColorIndex: UInt8
let aspectRatio: UInt8
init(from decoder: BinaryDecoder) throws {
var container = decoder.container(maxLength: 13)
let signature = try container.decode(length: 3)
if signature != Data("GIF".utf8) {
throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Missing GIF header."))
}
var versionContainer = container.nestedContainer(maxLength: 3)
self.version = try versionContainer.decode(Version.self)
self.screenWidth = try container.decode(type(of: screenWidth))
self.screenHeight = try container.decode(type(of: screenHeight))
let packed = try container.decode(Packed.self)
let globalColorTableSize = packed.intersection(.globalColorTableSizeMask).rawValue
self.numberOfGlobalColorTableEntries = 1 << (Int(globalColorTableSize) + 1)
self.colorTableEntriesSortedByImportance = packed.intersection(.colorTableSort).rawValue != 0
self.colorResolution = (packed.intersection(.colorResolutionMask).rawValue >> 4) + 1
self.hasGlobalColorTable = packed.intersection(.globalColorTable).rawValue != 0
// New
self.backgroundColorIndex = try container.decode(type(of: backgroundColorIndex))
self.aspectRatio = try container.decode(type(of: aspectRatio))
}
}
Hurrah! We've implemented a GIF header decoder in Swift using Binary Codable. Implementing the remainder of the GIF specification will be left as an exercise for you.
Now let's add encoding support.
Let's expand on the GIFHeader data type we fleshed out in the preceding section. We'll start by making the type fully BinaryCodable.
Note: I'm leaving the init(from decoder: BinaryDecoder) throws
implementation out from here on for readability's sake. We'll also need to implement an initailizer so that we can create customized instances of the GIFHeader type.
import BinaryCodable
struct GIFHeader: BinaryCodable {
// New (Swift won't generate this automatically)
init(version: Version, width: UInt16, height: UInt16, numberOfGlobalColorTableEntries: Int, colorTableEntriesSortedByImportance: Bool, colorResolution: UInt8, hasGlobalColorTable: Bool, backgroundColorIndex: UInt8, aspectRatio: UInt8) {
self.version = version
self.screenWidth = width
self.screenHeight = height
self.numberOfGlobalColorTableEntries = numberOfGlobalColorTableEntries
self.colorTableEntriesSortedByImportance = colorTableEntriesSortedByImportance
self.colorResolution = colorResolution
self.hasGlobalColorTable = hasGlobalColorTable
self.backgroundColorIndex = backgroundColorIndex
self.aspectRatio = aspectRatio
}
func encode(to encoder: BinaryEncoder) throws {
}
}
// Use these three lines of code to debug your implementation as we go along.
let header = GIFHeader(version: .gif89a, width: 46, height: 37, numberOfGlobalColorTableEntries: 4, colorTableEntriesSortedByImportance: false, colorResolution: 8, hasGlobalColorTable: true, backgroundColorIndex: 0, aspectRatio: 0)
let encoder = BinaryDataEncoder()
let encodedData = try encoder.encode(header)
Like our decoder, we first need to create a container view. This will let us encode bytes sequentially.
import BinaryCodable
struct GIFHeader: BinaryCodable {
func encode(to encoder: BinaryEncoder) throws {
// New
var container = encoder.container()
}
}
Encoding the GIF header is straightforward:
import BinaryCodable
struct GIFHeader: BinaryCodable {
func encode(to encoder: BinaryEncoder) throws {
var container = encoder.container()
// New
try container.encode("GIF", encoding: .ascii, terminator: nil)
}
}
Encoding our enum requires that we first mark the enum as BinaryCodable as well. We can then easily encode the enum value.
import BinaryCodable
struct GIFHeader: BinaryCodable {
// Changed
enum Version: String, BinaryCodable {
case gif87a = "87a"
case gif89a = "89a"
}
func encode(to encoder: BinaryEncoder) throws {
var container = encoder.container()
try container.encode("GIF", encoding: .ascii, terminator: nil)
// New
try container.encode(version)
}
}
Encoding width and height is also straightforward:
import BinaryCodable
struct GIFHeader: BinaryCodable {
func encode(to encoder: BinaryEncoder) throws {
var container = encoder.container()
try container.encode("GIF", encoding: .ascii, terminator: nil)
try container.encode(version)
// New
try container.encode(screenWidth)
try container.encode(screenHeight)
}
}
Encoding the packed byte requires some bit-fiddling again and modifying the Packed type to conform to BinaryCodable
:
import BinaryCodable
struct GIFHeader: BinaryCodable {
// Changed
struct Packed: OptionSet, BinaryCodable
func encode(to encoder: BinaryEncoder) throws {
var container = encoder.container()
try container.encode("GIF", encoding: .ascii, terminator: nil)
try container.encode(version)
try container.encode(screenWidth)
try container.encode(screenHeight)
// New
var packed = Packed()
packed.formUnion(Packed(rawValue: UInt8((numberOfGlobalColorTableEntries >> 1) - 1)))
if colorTableEntriesSortedByImportance {
packed.insert(.colorTableSort)
}
packed.formUnion(Packed(rawValue: (colorResolution - 1) << 4))
if hasGlobalColorTable {
packed.insert(.globalColorTable)
}
try container.encode(packed)
}
}
But the remainder of the header is straightforward:
import BinaryCodable
struct GIFHeader: BinaryCodable {
func encode(to encoder: BinaryEncoder) throws {
var container = encoder.container()
try container.encode("GIF", encoding: .ascii, terminator: nil)
try container.encode(version)
try container.encode(screenWidth)
try container.encode(screenHeight)
var packed = Packed()
packed.formUnion(Packed(rawValue: UInt8((numberOfGlobalColorTableEntries >> 1) - 1)))
if colorTableEntriesSortedByImportance {
packed.insert(.colorTableSort)
}
packed.formUnion(Packed(rawValue: (colorResolution - 1) << 4))
if hasGlobalColorTable {
packed.insert(.globalColorTable)
}
try container.encode(packed)
// New
try container.encode(backgroundColorIndex)
try container.encode(aspectRatio)
}
}
And that's it! We've now added full binary coding support to our GIFHeader type. Check out the full tested implementation of this documentation in Tests/BinaryCodableTests/GIFHeaderTests.swift.