The book’s text is cut into pieces of 4,096 bytes, and each piece is compressed on its
own into one record, so a reader can open any page without decompressing the whole
book. The text records are records 1 to textRecordCount, as given by the
PalmDOC header. To read a record, a
reader first removes the trailing entries from its end, then decrypts it if the book has
DRM, then decompresses it. Joined together, the records give the book as one HTML
document.
Trailing entries
When the MOBI header’s extraDataFlags
are not 0, every text record ends with extra entries that are not part of the
compressed text. They are stored in bit order after the text: the multibyte entry
(bit 0) first, then the entry for bit 1, then bit 2 and so on, so the entry of the
highest set bit is last. A reader removes them from the end:
- For each set bit from 15 down to 1: read a backward-encoded integer ending at the last byte of the record. It is the size of that entry, including the integer itself. Remove that many bytes from the end.
- If bit 0 is set: the last remaining byte is the multibyte entry’s size byte. Its low two bits give n, 0 to 3; remove n + 1 bytes. The n bytes before the size byte are the start of a multibyte character that the next record finishes; the same bytes appear at the start of the next record’s text too. They are not compressed but are encrypted.
The entry for bit 1 is the trailing byte sequence (TBS), which tells a Kindle which table of contents entries start, end or continue in this record, for quick navigation. Its layout varies with the index type and is only partly understood; readers that only show the text skip it. The contents of the bit 2 entry, uncrossable breaks, are not documented.
Variable-width integers
MOBI uses a big-endian variable-width integer in trailing entries and indexes. Each byte carries 7 bits of the value in its low bits; bit 7 marks one end of the number.
- Forward encoded, read from the first byte onwards: bit 7 is set only on the last byte. Used in index entries and CNCX strings.
- Backward encoded, read from the last byte backwards: bit 7 is set only on the first byte. Used for the sizes of trailing entries, which are read from the end of a record.
For example, 0x11111 is 04 22 91 forward and 84 22 11
backward. A value below 128 is one byte with bit 7 set either way: 5 is
85.
PalmDOC compression
Compression type 2 is a byte-oriented LZ77 made for the slow processors of Palm handhelds. Each record decompresses to at most 4,096 bytes, and back references never reach into a previous record. The decoder reads one byte at a time:
| Byte | Meaning |
|---|---|
00 | A literal: output the byte 0. |
01–08 | A run of literals: the byte is a count n; copy the next n bytes to the output as they are. This is how bytes 0x01 to 0x08 and 0x80 to 0xFF are stored. |
09–7F | A literal: output the byte. |
80–BF | A back reference: this byte and the next form a 16-bit big-endian value. Bits 15–14 are 10; bits 13–3 are the distance, 1 to 2,047, back from the end of the output; bits 2–0 are the length − 3, so 3 to 10 bytes are copied. Copy byte by byte, since the source may overlap what is being written. |
C0–FF | A space and a character: output a space (0x20), then the byte XOR 0x80 (a character from 0x40 to 0x7F). |
A compressor looks back up to 2,047 bytes for a match of 3 or more bytes. Some old PalmDOC tools also XORed every byte with 0xA5 before storing it; a decoder that gets nonsense can try undoing that first.
Example
74 68 65 E3 61 74 F4 80 43
| Bytes | Output |
|---|---|
74 68 65 | the, three literals |
E3 | a space and 0xE3 XOR 0x80 = 0x63, c: the c |
61 74 | at: the cat |
F4 | a space and t: the cat t (9 bytes) |
80 43 | 0x8043: distance (0x8043 >> 3) & 0x7FF = 8, length (0x8043 & 7) + 3 = 6. Copy 6 bytes starting 8 back, he cat: the cat the cat |
HUFF/CDIC compression
Compression type 17480 (DH) codes the text with a Huffman code whose
symbols are phrases from a dictionary. The tables are in the records that the MOBI
header’s huffmanRecordOffset and huffmanRecordCount give: one
HUFF record, then one or more CDIC records. The method is not
documented by Amazon; this description follows the decoders in calibre and
KindleUnpack.
HUFF record
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0 | 4 | char[4] | identifier | HUFF. |
4 | 4 | UInt32 | headerLength | 24. |
8 | 4 | UInt32 | cacheOffset | Where the first table, 256 × UInt32, starts in this record. |
12 | 4 | UInt32 | baseOffset | Where the second table, 64 × UInt32, starts. |
16 | 8 | bytes | unknown | Not used by decoders. |
The first table is indexed by the next 8 bits of the input. Each 32-bit entry holds:
bits 0–4, the code length (1 to 32, never 0); bit 7, a flag that the code is complete
within 8 bits (always set when the length is 8 or less); bits 8–31, the
maxcode for that length, from which a decoder computes
((maxcode + 1) << (32 − length)) − 1.
The second table holds 32 pairs, one per code length 1 to 32: the smallest code of that length, then the largest. A decoder shifts each left by (32 − length), adding ones below the largest, as for the first table.
CDIC records
| Offset | Size | Type | Name | Description |
|---|---|---|---|---|
0 | 4 | char[4] | identifier | CDIC. |
4 | 4 | UInt32 | headerLength | 16. |
8 | 4 | UInt32 | phraseCount | The total number of phrases in all CDIC records together. |
12 | 4 | UInt32 | bits | Each CDIC record holds at most 2bits phrases. |
16 | 2 × n | UInt16[] | offsets | One offset per phrase in this record, counted from byte 16. n is 2bits, or the phrases still left for the last record. |
At each offset (from byte 16) is a phrase: a UInt16 whose bits 0–14 are its length and whose bit 15 is a flag, then the phrase’s bytes. With the flag set, the bytes are plain text. With it clear, the bytes are themselves Huffman-coded and must be decoded the same way before use; a decoder then keeps the result. The phrases of all CDIC records are numbered in one sequence.
Decoding
- Read the record as a stream of bits, most significant first, and look at the next 32 bits as an unsigned number, code.
- Look up the top 8 bits of code in the first table to get a length, the flag and maxcode. If the flag is clear, increase the length while code is less than the second table’s smallest code for that length, then take maxcode from the second table.
- The phrase number is (maxcode − code) >> (32 − length). Output that phrase and move on by length bits.
- Stop when fewer bits are left than the next code needs; the end of the record is padding.
The text
Decompressed and joined, the text records make one HTML document in the MOBI header’s encoding. It is the HTML of the source book, simplified, with a few changes:
| Markup | Meaning |
|---|---|
<a filepos=0000012345> | A link to a byte position in the uncompressed text, written as ten decimal digits, unquoted. It replaces the href of internal links; the target is any element starting at that position. |
<img recindex="00001"> | An image: the number counts from 1 at the MOBI header’s firstImageRecord. Also seen as hirecindex and lowrecindex for high and low resolution versions. |
<mbp:pagebreak/> | A page break. Other mbp: elements, such as mbp:section and mbp:frameset, come from Mobipocket’s own extensions. |
<guide><reference type="toc" filepos=…> | The book’s guide, as in an OPF package: the table of contents, the start of the text and other landmarks, by position. |
<idx:entry>, <idx:orth> | In dictionaries, a headword entry and its spelling, indexed into the orthographic index. |
Only HTML 3.2-level formatting is supported: font, b,
i, blockquote, table and the like, with
attributes such as width and height in place of CSS. There are
no style sheets.