This blog post describes how to implement PlantUML Text Encoding in Swift.
Technically it showcases
- Compressing Data with Buffer Compression
- Swift implementation of Base64 algorithm
The code is part of my project SwiftPlantUML
Specification
According to PlantUML Text Encoding the text diagram need to be
- Encoded in UTF-8
- Compressed using Deflate (or Brotli) algorithm
- Reencoded in ASCII using a transformation close to base64
Implementation
I created a value type PlantUMLText conforming to RawRepresentable. This synthesizes an initializer accepting the raw value. The encoded text shall be accessible through a computed property.
import Foundation
struct PlantUMLText: RawRepresentable {
var rawValue: String
var encodedValue: String {
}
}
Encoding a string in UTF-8 is pretty simple in Swift so I don't list this as an explicit step. Let's focus on
- deflate
- reencode in ASCII with base64 algorithm but a different mapping array.
Deflate
The Compression framework from Apple implements the zlib encoder (at level 5 only). The encoded format is the raw DEFLATE format as described in IETF RFC 1951.
Note to myself: do not forget to import the framework.
import Compression
Then I created the convenience function deflate as an extension of PlantUMLText.
The implementation is from Apple's article about Compressing and Decompressing Data with Buffer Compression.
private extension PlantUMLText {
func deflate(_ text: String) -> NSData {
let sourceString = text
let sourceBuffer = Array(sourceString.utf8)
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: sourceString.count)
let algorithm = COMPRESSION_ZLIB
let compressedSize = compression_encode_buffer(destinationBuffer, sourceString.count,
sourceBuffer, sourceString.count,
nil,
algorithm)
return NSData(bytesNoCopy: destinationBuffer,
length: compressedSize)
}
}
Base 64 algorithm
I also created a convenience API to encode the compressed data.
internal extension PlantUMLText {
func base64plantuml(_ compressedData: NSData) -> String {
}
}
The implementation is more tricky. It follows the bae64 algorithm but a different mapping table is used. What does this mean?
Base64 encodes binary data according to the ASCII standard and represents them through 64 characters. The standard alphabet comprises 10 digits, 26 lowercase characters, 26 uppercase characters, the Plus sign (+) and the Forward Slash (/). There is also a 65th character known as a pad, the Equal sign (=).
So you can look up a character based on a decimal value. For example, with the decimal value 45, you can find the Minus sign (-).

For the standard alphabet the mapping array for values 0-63 is:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
For PlantUML, the mapping array for values 0-63 is:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_
Let's built up the mapping array from 8-bit unsigned integers with 64 entries.
let encodingAlphabetTable: [UInt8] = [
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
97,
98,
99,
100,
101,
102,
103,
104,
105,
106,
107,
108,
109,
110,
111,
112,
113,
114,
115,
116,
117,
118,
119,
120,
121,
122,
45,
95,
61,
]
Now that we have the mapping array let's go over the implementation. I took it from Doug Richardson and his project SwiftyBase64.
let bytes = [UInt8](compressedData)
var encodedBytes: [UInt8] = []
let padding = encodingAlphabetTable[64]
var i = 0
let count = bytes.count
while i + 3 <= count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4) | ((bytes[i + 1] & 0b1111_0000) >> 4)
let three = ((bytes[i + 1] & 0b0000_1111) << 2) | ((bytes[i + 2] & 0b1100_0000) >> 6)
let four = bytes[i + 2] & 0b0011_1111
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(encodingAlphabetTable[Int(three)])
encodedBytes.append(encodingAlphabetTable[Int(four)])
i += 3
}
if i + 2 == count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4) | ((bytes[i + 1] & 0b1111_0000) >> 4)
let three = ((bytes[i + 1] & 0b0000_1111) << 2)
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(encodingAlphabetTable[Int(three)])
encodedBytes.append(padding)
} else if i + 1 == count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4)
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(padding)
encodedBytes.append(padding)
} else {
assert(i == count)
}
return String(decoding: encodedBytes, as: Unicode.UTF8.self)
If you struggle to understand the implementation, then you are not alone :)
It helps to read up on the Base64 encoding specification.
The encoding process represents 24-bit groups of input bits as output
strings of 4 encoded characters. Proceeding from left to right, a
24-bit input group is formed by concatenating 3 8-bit input groups.
These 24 bits are then treated as 4 concatenated 6-bit groups, each
of which is translated into a single character in the base 64
alphabet.
Each 6-bit group is used as an index into an array of 64 printable
characters.
That's the job of the while loop.
while i + 3 <= count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4) | ((bytes[i + 1] & 0b1111_0000) >> 4)
let three = ((bytes[i + 1] & 0b0000_1111) << 2) | ((bytes[i + 2] & 0b1100_0000) >> 6)
let four = bytes[i + 2] & 0b0011_1111
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(encodingAlphabetTable[Int(three)])
encodedBytes.append(encodingAlphabetTable[Int(four)])
i += 3
}
Bitwise operators are used to do the transformation. I highly recommend to read Computer science in JavaScript: Base64 encoding which explains super well how bitwise operators can be used for base64 encoding and you don't have to know JavaScript to understand it.
Special processing is performed if fewer than 24 bits are available
at the end of the data being encoded. A full encoding quantum is
always completed at the end of a quantity. When fewer than 24 input
bits are available in an input group, bits with value zero are added
(on the right) to form an integral number of 6-bit groups. Padding
at the end of the data is performed using the '=' character.
That's handled after the while loop.
if i + 2 == count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4) | ((bytes[i + 1] & 0b1111_0000) >> 4)
let three = ((bytes[i + 1] & 0b0000_1111) << 2)
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(encodingAlphabetTable[Int(three)])
encodedBytes.append(padding)
} else if i + 1 == count {
let one = bytes[i] >> 2
let two = ((bytes[i] & 0b11) << 4)
encodedBytes.append(encodingAlphabetTable[Int(one)])
encodedBytes.append(encodingAlphabetTable[Int(two)])
encodedBytes.append(padding)
encodedBytes.append(padding)
} else {
assert(i == count)
}
As the final step, let's assemble the encoded string.
Each 6-bit group is used as an index into an array of 64 printable
characters. The character referenced by the index is placed in the
output string.
return String(decoding: encodedBytes, as: Unicode.UTF8.self)
Complete source can be found on GitHub.