|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | +"""Huffman Codec - Huffman coding content compression. |
| 3 | +
|
| 4 | +Huffman coding, devised by David A. Huffman in 1952, gives the frequent symbols shorter codes than the rare ones, with |
| 5 | + an optimal prefix code (no code being the beginning of another) built by merging the two least frequent groups of |
| 6 | + symbols until a single one remains. This codec builds the code of the bytes of the text and writes it as a canonical |
| 7 | + Huffman code (the codes being assigned in order of length, then of symbol, hence given by their lengths only), in a |
| 8 | + self-describing format: |
| 9 | +- the number of distinct bytes minus 1 (1 byte) |
| 10 | +- for each of them, the byte and the length of its code (2 bytes) |
| 11 | +- the number of padding bits at the end of the last byte (1 byte) |
| 12 | +- the codes of the bytes of the text, from the most significant bit (a single distinct byte having the code "0") |
| 13 | +
|
| 14 | +An empty text gives an empty output. As with the other compression codecs, malformed data is reported (or, with a |
| 15 | + non-strict errors handling, replaced, dropped or left as is), but, as the length of the text is not stored, data |
| 16 | + truncated at the end of a code gives a truncated text. |
| 17 | +
|
| 18 | +NB: Not an encoding properly speaking. |
| 19 | +
|
| 20 | +This codec: |
| 21 | +- en/decodes strings from str to str |
| 22 | +- en/decodes strings from bytes to bytes |
| 23 | +- decodes file content to str (read) |
| 24 | +- encodes file content from str to bytes (write) |
| 25 | +
|
| 26 | +Reference: https://en.wikipedia.org/wiki/Canonical_Huffman_code |
| 27 | +""" |
| 28 | +import heapq |
| 29 | +from collections import Counter |
| 30 | + |
| 31 | +from ..__common__ import * |
| 32 | + |
| 33 | + |
| 34 | +__examples__ = { |
| 35 | + 'enc(huffman)': {'': "", 'a': "\x00a\x01\x07\x00", 'aab': "\x01a\x01b\x01\x05 ", |
| 36 | + 'abracadabra': "\x04a\x01b\x03c\x03d\x03r\x03\x01N\xac\x9c"}, |
| 37 | + 'dec(huffman)': {'\x00a\x01\x07\x00': "a", '\x01a\x01b\x01\x05 ': "aab", '\x01a\x01a\x01\x05 ': None, |
| 38 | + '\x01a\x01b\x02\x05 ': None, '\x01a\x01b\x01\x08 ': None, '\x01a\x01b\x01': None, |
| 39 | + '\x05': None, '\x01a\x01b\x00\x05 ': None, '\x00a\x02\x07\x00': None}, |
| 40 | + 'enc-dec(huffman)': ["", "aaaa", "This is a test", "@random{1,256,2048}"], |
| 41 | +} |
| 42 | + |
| 43 | + |
| 44 | +def _lengths(data): |
| 45 | + """ Lengths of the Huffman codes of the bytes of the data, merging the least frequent groups (the ties being broken |
| 46 | + by the order of creation, the bytes being taken in increasing order). """ |
| 47 | + counts = Counter(data) |
| 48 | + if len(counts) == 1: |
| 49 | + return {next(iter(counts)): 1} |
| 50 | + heap, lengths = [(n, i, [s]) for i, (s, n) in enumerate(sorted(counts.items()))], dict.fromkeys(counts, 0) |
| 51 | + heapq.heapify(heap) |
| 52 | + i = len(heap) |
| 53 | + while len(heap) > 1: |
| 54 | + n1, _, g1 = heapq.heappop(heap) |
| 55 | + n2, _, g2 = heapq.heappop(heap) |
| 56 | + for s in g1 + g2: |
| 57 | + lengths[s] += 1 |
| 58 | + heapq.heappush(heap, (n1 + n2, i, g1 + g2)) |
| 59 | + i += 1 |
| 60 | + return lengths |
| 61 | + |
| 62 | + |
| 63 | +def _canonical(lengths): |
| 64 | + """ Canonical codes (as (code, length)) of the bytes, assigned in order of length, then of byte. """ |
| 65 | + codes, code, prev = {}, 0, 0 |
| 66 | + for s, n in sorted(lengths.items(), key=lambda x: (x[1], x[0])): |
| 67 | + code <<= n - prev |
| 68 | + codes[s], code, prev = (code, n), code + 1, n |
| 69 | + return codes |
| 70 | + |
| 71 | + |
| 72 | +def huffman_compress(text, errors="strict"): |
| 73 | + data = b(ensure_str(text)) |
| 74 | + if not data: |
| 75 | + return b"", len(text) |
| 76 | + lengths = _lengths(data) |
| 77 | + codes, header = _canonical(lengths), bytes([len(lengths) - 1]) |
| 78 | + for s in sorted(lengths): |
| 79 | + header += bytes([s, lengths[s]]) |
| 80 | + acc, nbits = 0, 0 |
| 81 | + for x in data: |
| 82 | + c, n = codes[x] |
| 83 | + acc, nbits = acc << n | c, nbits + n |
| 84 | + pad = -nbits % 8 |
| 85 | + # the compressed data is given as latin-1 text (or bytes), so that it is read back as is |
| 86 | + return (header + bytes([pad]) + (acc << pad).to_bytes((nbits + pad) // 8, "big")).decode("latin-1"), len(text) |
| 87 | + |
| 88 | + |
| 89 | +def _error(msg): |
| 90 | + raise HuffmanDecodeError(f"'huffman' codec can't decode: {msg}") |
| 91 | + |
| 92 | + |
| 93 | +def _decode(data): |
| 94 | + """ Decode Huffman-coded data, raising an error with a precise message if it is malformed. """ |
| 95 | + if not data: |
| 96 | + return b"" |
| 97 | + n = data[0] + 1 |
| 98 | + if len(data) < 2 + 2 * n: |
| 99 | + _error("truncated header") |
| 100 | + lengths = {} |
| 101 | + for i in range(n): |
| 102 | + s, length = data[1 + 2 * i], data[2 + 2 * i] |
| 103 | + if s in lengths or length == 0: |
| 104 | + _error("bad code length" if length == 0 else f"duplicate byte {s}") |
| 105 | + lengths[s] = length |
| 106 | + # the code lengths must give a complete prefix code (Kraft's equality), a single byte having a code of 1 bit |
| 107 | + m = max(lengths.values()) |
| 108 | + if (sum(1 << (m - length) for length in lengths.values()) != 1 << m) if n > 1 else m != 1: |
| 109 | + _error("code lengths not giving a complete prefix code") |
| 110 | + pad, payload = data[1 + 2 * n], data[2 + 2 * n:] |
| 111 | + if pad > 7 or not payload or 8 * len(payload) - pad < min(lengths.values()): |
| 112 | + _error("bad padding") |
| 113 | + table = {(c, l): s for s, (c, l) in _canonical(lengths).items()} |
| 114 | + bits, r, code, length = int.from_bytes(payload, "big") >> pad, bytearray(), 0, 0 |
| 115 | + # as the code is complete, any sequence of bits is a sequence of codes, possibly followed by a truncated one |
| 116 | + for i in range(8 * len(payload) - pad - 1, -1, -1): |
| 117 | + code, length = code << 1 | bits >> i & 1, length + 1 |
| 118 | + if (code, length) in table: |
| 119 | + r.append(table[code, length]) |
| 120 | + code, length = 0, 0 |
| 121 | + if length: |
| 122 | + _error("truncated code at the end") |
| 123 | + return bytes(r) |
| 124 | + |
| 125 | + |
| 126 | +def huffman_decompress(text, errors="strict"): |
| 127 | + data = b(ensure_str(text)) |
| 128 | + try: |
| 129 | + return _decode(data), len(text) |
| 130 | + except HuffmanDecodeError: |
| 131 | + # as with gzip and zstd, malformed data is reported (with a precise message), or replaced, dropped or left as is |
| 132 | + if errors == "strict": |
| 133 | + raise |
| 134 | + return handle_error("huffman", errors, decode=True)(data[:1].decode("latin-1"), 0), len(text) |
| 135 | + |
| 136 | + |
| 137 | +add("huffman", huffman_compress, huffman_decompress, guess=None) |
0 commit comments