xmhuffman 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xmhuffman-0.1.0/LICENSE +21 -0
- xmhuffman-0.1.0/PKG-INFO +224 -0
- xmhuffman-0.1.0/README.md +184 -0
- xmhuffman-0.1.0/include/xmhuffman_kernel.h +52 -0
- xmhuffman-0.1.0/pyproject.toml +21 -0
- xmhuffman-0.1.0/setup.cfg +4 -0
- xmhuffman-0.1.0/setup.py +35 -0
- xmhuffman-0.1.0/src/xmhuffman.egg-info/PKG-INFO +224 -0
- xmhuffman-0.1.0/src/xmhuffman.egg-info/SOURCES.txt +14 -0
- xmhuffman-0.1.0/src/xmhuffman.egg-info/dependency_links.txt +1 -0
- xmhuffman-0.1.0/src/xmhuffman.egg-info/not-zip-safe +1 -0
- xmhuffman-0.1.0/src/xmhuffman.egg-info/top_level.txt +1 -0
- xmhuffman-0.1.0/src/xmhuffman_kernel.c +147 -0
- xmhuffman-0.1.0/tests/test_basic.py +245 -0
- xmhuffman-0.1.0/tests/test_pbix.py +133 -0
- xmhuffman-0.1.0/xmhuffman.c +31594 -0
xmhuffman-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Cotruta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xmhuffman-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xmhuffman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cython bindings for Microsoft xVelocity/Vertipaq canonical-Huffman string decoding
|
|
5
|
+
Home-page: https://github.com/Hugoberry/xmhuffman-cython
|
|
6
|
+
Author: Igor Cotruta
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Igor Cotruta
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: Homepage, https://github.com/Hugoberry/xmhuffman-cython
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
+
Requires-Python: >=3.8
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
# xmhuffman
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/xmhuffman/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
|
|
46
|
+
A small, fast Cython extension that decodes the canonical-Huffman string
|
|
47
|
+
dictionary pages used by xVelocity / Vertipaq column stores — the storage
|
|
48
|
+
format inside Power BI `.pbix` files (the `DataModel` part) and Excel
|
|
49
|
+
Power Pivot workbooks (`xl/model/item.data`).
|
|
50
|
+
|
|
51
|
+
Provides a tight C kernel for what would otherwise be a per-symbol Python
|
|
52
|
+
loop. On real `.pbix` files this is **30–50× faster** than an equivalent
|
|
53
|
+
pure-Python implementation and removes a hot path that dominates table
|
|
54
|
+
extraction time in tools like [pbixray](https://github.com/Hugoberry/pbixray).
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install xmhuffman
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Building from source requires a C compiler and Cython ≥ 3.0:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/Hugoberry/xmhuffman-cython
|
|
66
|
+
cd xmhuffman-cython
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
The library exposes a tiny surface — one entry point for the common case
|
|
73
|
+
plus a few lower-level helpers.
|
|
74
|
+
|
|
75
|
+
### Decode a dictionary page
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import xmhuffman
|
|
79
|
+
|
|
80
|
+
# All inputs come straight from the Vertipaq dictionary page metadata:
|
|
81
|
+
# bitstream — compressed_string_buffer (bytes)
|
|
82
|
+
# encode_array_128 — 128-byte nibble-packed code-length array
|
|
83
|
+
# offsets — per-string start bit offsets (sequence of u32)
|
|
84
|
+
# total_bits — store_total_bits (end of last string)
|
|
85
|
+
strings: list[bytes] = xmhuffman.decode_page(
|
|
86
|
+
bitstream,
|
|
87
|
+
encode_array_128,
|
|
88
|
+
offsets,
|
|
89
|
+
total_bits,
|
|
90
|
+
swap=True, # apply the byte-pair swap inside the extension
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Output is `list[bytes]`. Charset interpretation is the caller's choice:
|
|
95
|
+
Vertipaq pages flag themselves as either single-charset (latin-1 / ANSI,
|
|
96
|
+
one Python `str` per record) or general (the byte stream is UTF-16LE).
|
|
97
|
+
|
|
98
|
+
### Lower-level building blocks
|
|
99
|
+
|
|
100
|
+
For callers that want to amortize table construction across pages, or
|
|
101
|
+
just to unit-test pieces:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Expand the 128-byte nibble-packed array to 256 plain bytes of lengths.
|
|
105
|
+
lengths = xmhuffman.decompress_encode_array(encode_array_128)
|
|
106
|
+
|
|
107
|
+
# Pair-swap a buffer (bytes 2k and 2k+1 swap; trailing odd byte left as-is).
|
|
108
|
+
swapped = xmhuffman.swap_bytes(raw)
|
|
109
|
+
|
|
110
|
+
# Build the flat decode table once, reuse it across decode calls.
|
|
111
|
+
table_bytes, max_len = xmhuffman.build_table(encode_array_128)
|
|
112
|
+
strings = xmhuffman.decode_with_table(
|
|
113
|
+
bitstream, table_bytes, max_len, offsets, total_bits, swap=True,
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Format notes
|
|
118
|
+
|
|
119
|
+
The on-disk format is documented publicly in Microsoft's open
|
|
120
|
+
specification [\[MS-XLDM\] §2.7.4 — Huffman
|
|
121
|
+
Compression](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9).
|
|
122
|
+
Each dictionary page is, schematically:
|
|
123
|
+
|
|
124
|
+
| Field | Description |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `encode_array` | 128 bytes, two 4-bit code lengths per byte (low nibble = symbol `2i`, high = `2i+1`). Value 0 means "symbol unused". Per [\[MS-XLDM\]](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9) codeword lengths are between 2 and 15 bits. |
|
|
127
|
+
| `uiDecodeBits` | Width of the on-disk primary lookup table (≤ 12). This decoder uses a single flat `2^max_len` table instead and ignores `uiDecodeBits`. |
|
|
128
|
+
| `compressed_string_buffer` | The bitstream itself, with adjacent bytes pair-swapped on disk. No padding between strings. |
|
|
129
|
+
| `store_total_bits` | Total logical bit length; end sentinel for the last string. |
|
|
130
|
+
| `vector_of_record_handle_structures` | Per-record `(bit_offset, page_id)`; sorted offsets per page give the per-string start boundaries. |
|
|
131
|
+
|
|
132
|
+
Codes are classical Huffman, encoded canonically by sorting
|
|
133
|
+
`(length, symbol)` ascending and incrementing the code with a left-shift
|
|
134
|
+
on length changes — exactly the reconstruction described in
|
|
135
|
+
[\[MS-XLDM\] §2.7.4.1.5](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9).
|
|
136
|
+
|
|
137
|
+
### Character-set modes
|
|
138
|
+
|
|
139
|
+
[\[MS-XLDM\] §2.7.4.1.4](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9)
|
|
140
|
+
distinguishes two modes per page:
|
|
141
|
+
|
|
142
|
+
- **Single character set** (`character_set_type_identifier = 0x000aba91`)
|
|
143
|
+
— only the low byte of each character is Huffman-encoded; the upper
|
|
144
|
+
(charset) byte is stored once on the page and must be reinserted by
|
|
145
|
+
the caller to recover the original 2-byte character stream.
|
|
146
|
+
- **Multiple character sets** (`0x000aba92`) — both bytes are encoded;
|
|
147
|
+
the output byte stream is consumed directly as UTF-16LE.
|
|
148
|
+
|
|
149
|
+
This decoder emits raw `bytes` either way; reassembly of UTF-16 characters
|
|
150
|
+
(including reinserting the single-charset upper byte) is the caller's
|
|
151
|
+
responsibility.
|
|
152
|
+
|
|
153
|
+
## Performance
|
|
154
|
+
|
|
155
|
+
Apples-to-apples against an equivalent pure-Python decoder on a few real
|
|
156
|
+
`.pbix` files:
|
|
157
|
+
|
|
158
|
+
| File | Strings | Python ref | xmhuffman | Speedup |
|
|
159
|
+
|---|---:|---:|---:|---:|
|
|
160
|
+
| Adventure Works DW 2020 | 191,489 | 449 ms | 10.0 ms | 45× |
|
|
161
|
+
| Sales & Marketing sample | 103,290 | 160 ms | 5.3 ms | 30× |
|
|
162
|
+
| Retail Analysis sample | 9 | 144 ms | 2.9 ms | 50× |
|
|
163
|
+
|
|
164
|
+
The kernel does one unaligned 64-bit big-endian load, one shift, one
|
|
165
|
+
mask, one table lookup and one byte store per output symbol. The decode
|
|
166
|
+
table is a flat `2^max_len` array of `uint16_t` (≤ 64 KB; usually 1–8 KB)
|
|
167
|
+
that fits comfortably in L1/L2.
|
|
168
|
+
|
|
169
|
+
The GIL is released around the inner work, so callers can decode
|
|
170
|
+
multiple pages or columns from worker threads without contention.
|
|
171
|
+
|
|
172
|
+
## Project layout
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
xmhuffman-cython/
|
|
176
|
+
├── xmhuffman.pyx # Cython surface
|
|
177
|
+
├── xmhuffman.pxd # C declarations
|
|
178
|
+
├── src/xmhuffman_kernel.c # C kernel
|
|
179
|
+
├── include/xmhuffman_kernel.h
|
|
180
|
+
├── tests/ # correctness tests
|
|
181
|
+
└── bench/ # micro-benchmark
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Testing
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pip install -e .
|
|
188
|
+
pip install pytest
|
|
189
|
+
pytest tests/ -v
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The basic test suite checks each helper against a pure-Python reference
|
|
193
|
+
implementation. An additional integration test (`tests/test_pbix.py`)
|
|
194
|
+
decodes pages out of real `.pbix` files and asserts byte-identity with
|
|
195
|
+
the reference; it is skipped automatically when fixtures aren't
|
|
196
|
+
available.
|
|
197
|
+
|
|
198
|
+
## Scope and non-goals
|
|
199
|
+
|
|
200
|
+
- **Not** a general-purpose Huffman library. Alphabets are fixed at 256
|
|
201
|
+
symbols, codeword lengths are capped at 15 bits, and the bitstream
|
|
202
|
+
convention is the one used by Vertipaq pages.
|
|
203
|
+
- **Not** an encoder. Round-tripping pages is out of scope.
|
|
204
|
+
- **No** charset conversion inside the extension. The decoder returns
|
|
205
|
+
raw `bytes`; the caller picks between `latin-1` and paired UTF-16LE
|
|
206
|
+
based on the page's character-set identifier.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT. See [LICENSE](LICENSE).
|
|
211
|
+
|
|
212
|
+
## Acknowledgements
|
|
213
|
+
|
|
214
|
+
This package is the third in a family of thin Cython wrappers around
|
|
215
|
+
Microsoft column-store / compression formats, alongside
|
|
216
|
+
[xpress8-python](https://github.com/Hugoberry/xpress8-python) and
|
|
217
|
+
[xpress9-python](https://github.com/Hugoberry/xpress9-python).
|
|
218
|
+
|
|
219
|
+
The format itself is documented publicly in Microsoft's open
|
|
220
|
+
specification [\[MS-XLDM\] — Spreadsheet Data Model File
|
|
221
|
+
Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/),
|
|
222
|
+
which the implementation here follows. The Python reference and
|
|
223
|
+
end-to-end test fixtures come from the
|
|
224
|
+
[pbixray](https://github.com/Hugoberry/pbixray) project.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# xmhuffman
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/xmhuffman/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A small, fast Cython extension that decodes the canonical-Huffman string
|
|
7
|
+
dictionary pages used by xVelocity / Vertipaq column stores — the storage
|
|
8
|
+
format inside Power BI `.pbix` files (the `DataModel` part) and Excel
|
|
9
|
+
Power Pivot workbooks (`xl/model/item.data`).
|
|
10
|
+
|
|
11
|
+
Provides a tight C kernel for what would otherwise be a per-symbol Python
|
|
12
|
+
loop. On real `.pbix` files this is **30–50× faster** than an equivalent
|
|
13
|
+
pure-Python implementation and removes a hot path that dominates table
|
|
14
|
+
extraction time in tools like [pbixray](https://github.com/Hugoberry/pbixray).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install xmhuffman
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Building from source requires a C compiler and Cython ≥ 3.0:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/Hugoberry/xmhuffman-cython
|
|
26
|
+
cd xmhuffman-cython
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
The library exposes a tiny surface — one entry point for the common case
|
|
33
|
+
plus a few lower-level helpers.
|
|
34
|
+
|
|
35
|
+
### Decode a dictionary page
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import xmhuffman
|
|
39
|
+
|
|
40
|
+
# All inputs come straight from the Vertipaq dictionary page metadata:
|
|
41
|
+
# bitstream — compressed_string_buffer (bytes)
|
|
42
|
+
# encode_array_128 — 128-byte nibble-packed code-length array
|
|
43
|
+
# offsets — per-string start bit offsets (sequence of u32)
|
|
44
|
+
# total_bits — store_total_bits (end of last string)
|
|
45
|
+
strings: list[bytes] = xmhuffman.decode_page(
|
|
46
|
+
bitstream,
|
|
47
|
+
encode_array_128,
|
|
48
|
+
offsets,
|
|
49
|
+
total_bits,
|
|
50
|
+
swap=True, # apply the byte-pair swap inside the extension
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Output is `list[bytes]`. Charset interpretation is the caller's choice:
|
|
55
|
+
Vertipaq pages flag themselves as either single-charset (latin-1 / ANSI,
|
|
56
|
+
one Python `str` per record) or general (the byte stream is UTF-16LE).
|
|
57
|
+
|
|
58
|
+
### Lower-level building blocks
|
|
59
|
+
|
|
60
|
+
For callers that want to amortize table construction across pages, or
|
|
61
|
+
just to unit-test pieces:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Expand the 128-byte nibble-packed array to 256 plain bytes of lengths.
|
|
65
|
+
lengths = xmhuffman.decompress_encode_array(encode_array_128)
|
|
66
|
+
|
|
67
|
+
# Pair-swap a buffer (bytes 2k and 2k+1 swap; trailing odd byte left as-is).
|
|
68
|
+
swapped = xmhuffman.swap_bytes(raw)
|
|
69
|
+
|
|
70
|
+
# Build the flat decode table once, reuse it across decode calls.
|
|
71
|
+
table_bytes, max_len = xmhuffman.build_table(encode_array_128)
|
|
72
|
+
strings = xmhuffman.decode_with_table(
|
|
73
|
+
bitstream, table_bytes, max_len, offsets, total_bits, swap=True,
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Format notes
|
|
78
|
+
|
|
79
|
+
The on-disk format is documented publicly in Microsoft's open
|
|
80
|
+
specification [\[MS-XLDM\] §2.7.4 — Huffman
|
|
81
|
+
Compression](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9).
|
|
82
|
+
Each dictionary page is, schematically:
|
|
83
|
+
|
|
84
|
+
| Field | Description |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `encode_array` | 128 bytes, two 4-bit code lengths per byte (low nibble = symbol `2i`, high = `2i+1`). Value 0 means "symbol unused". Per [\[MS-XLDM\]](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9) codeword lengths are between 2 and 15 bits. |
|
|
87
|
+
| `uiDecodeBits` | Width of the on-disk primary lookup table (≤ 12). This decoder uses a single flat `2^max_len` table instead and ignores `uiDecodeBits`. |
|
|
88
|
+
| `compressed_string_buffer` | The bitstream itself, with adjacent bytes pair-swapped on disk. No padding between strings. |
|
|
89
|
+
| `store_total_bits` | Total logical bit length; end sentinel for the last string. |
|
|
90
|
+
| `vector_of_record_handle_structures` | Per-record `(bit_offset, page_id)`; sorted offsets per page give the per-string start boundaries. |
|
|
91
|
+
|
|
92
|
+
Codes are classical Huffman, encoded canonically by sorting
|
|
93
|
+
`(length, symbol)` ascending and incrementing the code with a left-shift
|
|
94
|
+
on length changes — exactly the reconstruction described in
|
|
95
|
+
[\[MS-XLDM\] §2.7.4.1.5](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9).
|
|
96
|
+
|
|
97
|
+
### Character-set modes
|
|
98
|
+
|
|
99
|
+
[\[MS-XLDM\] §2.7.4.1.4](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/f70b41f2-ca64-44a1-9e6f-53e63f6a5ee9)
|
|
100
|
+
distinguishes two modes per page:
|
|
101
|
+
|
|
102
|
+
- **Single character set** (`character_set_type_identifier = 0x000aba91`)
|
|
103
|
+
— only the low byte of each character is Huffman-encoded; the upper
|
|
104
|
+
(charset) byte is stored once on the page and must be reinserted by
|
|
105
|
+
the caller to recover the original 2-byte character stream.
|
|
106
|
+
- **Multiple character sets** (`0x000aba92`) — both bytes are encoded;
|
|
107
|
+
the output byte stream is consumed directly as UTF-16LE.
|
|
108
|
+
|
|
109
|
+
This decoder emits raw `bytes` either way; reassembly of UTF-16 characters
|
|
110
|
+
(including reinserting the single-charset upper byte) is the caller's
|
|
111
|
+
responsibility.
|
|
112
|
+
|
|
113
|
+
## Performance
|
|
114
|
+
|
|
115
|
+
Apples-to-apples against an equivalent pure-Python decoder on a few real
|
|
116
|
+
`.pbix` files:
|
|
117
|
+
|
|
118
|
+
| File | Strings | Python ref | xmhuffman | Speedup |
|
|
119
|
+
|---|---:|---:|---:|---:|
|
|
120
|
+
| Adventure Works DW 2020 | 191,489 | 449 ms | 10.0 ms | 45× |
|
|
121
|
+
| Sales & Marketing sample | 103,290 | 160 ms | 5.3 ms | 30× |
|
|
122
|
+
| Retail Analysis sample | 9 | 144 ms | 2.9 ms | 50× |
|
|
123
|
+
|
|
124
|
+
The kernel does one unaligned 64-bit big-endian load, one shift, one
|
|
125
|
+
mask, one table lookup and one byte store per output symbol. The decode
|
|
126
|
+
table is a flat `2^max_len` array of `uint16_t` (≤ 64 KB; usually 1–8 KB)
|
|
127
|
+
that fits comfortably in L1/L2.
|
|
128
|
+
|
|
129
|
+
The GIL is released around the inner work, so callers can decode
|
|
130
|
+
multiple pages or columns from worker threads without contention.
|
|
131
|
+
|
|
132
|
+
## Project layout
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
xmhuffman-cython/
|
|
136
|
+
├── xmhuffman.pyx # Cython surface
|
|
137
|
+
├── xmhuffman.pxd # C declarations
|
|
138
|
+
├── src/xmhuffman_kernel.c # C kernel
|
|
139
|
+
├── include/xmhuffman_kernel.h
|
|
140
|
+
├── tests/ # correctness tests
|
|
141
|
+
└── bench/ # micro-benchmark
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Testing
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pip install -e .
|
|
148
|
+
pip install pytest
|
|
149
|
+
pytest tests/ -v
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The basic test suite checks each helper against a pure-Python reference
|
|
153
|
+
implementation. An additional integration test (`tests/test_pbix.py`)
|
|
154
|
+
decodes pages out of real `.pbix` files and asserts byte-identity with
|
|
155
|
+
the reference; it is skipped automatically when fixtures aren't
|
|
156
|
+
available.
|
|
157
|
+
|
|
158
|
+
## Scope and non-goals
|
|
159
|
+
|
|
160
|
+
- **Not** a general-purpose Huffman library. Alphabets are fixed at 256
|
|
161
|
+
symbols, codeword lengths are capped at 15 bits, and the bitstream
|
|
162
|
+
convention is the one used by Vertipaq pages.
|
|
163
|
+
- **Not** an encoder. Round-tripping pages is out of scope.
|
|
164
|
+
- **No** charset conversion inside the extension. The decoder returns
|
|
165
|
+
raw `bytes`; the caller picks between `latin-1` and paired UTF-16LE
|
|
166
|
+
based on the page's character-set identifier.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT. See [LICENSE](LICENSE).
|
|
171
|
+
|
|
172
|
+
## Acknowledgements
|
|
173
|
+
|
|
174
|
+
This package is the third in a family of thin Cython wrappers around
|
|
175
|
+
Microsoft column-store / compression formats, alongside
|
|
176
|
+
[xpress8-python](https://github.com/Hugoberry/xpress8-python) and
|
|
177
|
+
[xpress9-python](https://github.com/Hugoberry/xpress9-python).
|
|
178
|
+
|
|
179
|
+
The format itself is documented publicly in Microsoft's open
|
|
180
|
+
specification [\[MS-XLDM\] — Spreadsheet Data Model File
|
|
181
|
+
Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xldm/),
|
|
182
|
+
which the implementation here follows. The Python reference and
|
|
183
|
+
end-to-end test fixtures come from the
|
|
184
|
+
[pbixray](https://github.com/Hugoberry/pbixray) project.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#ifndef XMHUFFMAN_KERNEL_H
|
|
2
|
+
#define XMHUFFMAN_KERNEL_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
|
|
7
|
+
#if defined(_MSC_VER)
|
|
8
|
+
#include <BaseTsd.h>
|
|
9
|
+
typedef SSIZE_T xmh_ssize_t;
|
|
10
|
+
#else
|
|
11
|
+
#include <sys/types.h>
|
|
12
|
+
typedef ssize_t xmh_ssize_t;
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
#ifdef __cplusplus
|
|
16
|
+
extern "C" {
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#define XMH_MAX_CODE_LEN 15
|
|
20
|
+
#define XMH_TABLE_MAX_SIZE (1u << XMH_MAX_CODE_LEN) /* 32768 u16 entries */
|
|
21
|
+
|
|
22
|
+
/* Expand 128-byte nibble-packed code lengths into 256 plain bytes.
|
|
23
|
+
* Low nibble of byte i is the length for symbol 2i, high nibble for 2i+1. */
|
|
24
|
+
void xmh_decompress_encode_array(const uint8_t *in128, uint8_t *out256);
|
|
25
|
+
|
|
26
|
+
/* Pair-swap: bytes 2k and 2k+1 swap; a trailing odd byte is copied as-is.
|
|
27
|
+
* Safe to alias (in == out). */
|
|
28
|
+
void xmh_swap_pairs(const uint8_t *in_buf, uint8_t *out_buf, size_t n);
|
|
29
|
+
|
|
30
|
+
/* Build the flat canonical-Huffman decode table.
|
|
31
|
+
* lengths256 : 256 code lengths in [0, 15]
|
|
32
|
+
* table : caller-provided u16 buffer, must hold 1 << *out_max_len
|
|
33
|
+
* entries. Safe upper bound: XMH_TABLE_MAX_SIZE.
|
|
34
|
+
* Each entry packs (symbol << 8) | code_len.
|
|
35
|
+
* out_max_len: receives the max non-zero code length (0 if alphabet empty).
|
|
36
|
+
* Returns 0 on success, negative on invalid code (Kraft failure). */
|
|
37
|
+
int xmh_build_table(const uint8_t *lengths256,
|
|
38
|
+
uint16_t *table,
|
|
39
|
+
unsigned *out_max_len);
|
|
40
|
+
|
|
41
|
+
/* Decode one bit-slice [start_bit, end_bit) of a *swapped* bitstream into
|
|
42
|
+
* out[0..out_cap). Returns bytes written, or -1 on overflow. */
|
|
43
|
+
xmh_ssize_t xmh_decode_one(const uint8_t *swapped, size_t swapped_len,
|
|
44
|
+
const uint16_t *table, unsigned max_len,
|
|
45
|
+
uint64_t start_bit, uint64_t end_bit,
|
|
46
|
+
uint8_t *out, size_t out_cap);
|
|
47
|
+
|
|
48
|
+
#ifdef __cplusplus
|
|
49
|
+
}
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
#endif /* XMHUFFMAN_KERNEL_H */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel", "Cython>=3.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xmhuffman"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cython bindings for Microsoft xVelocity/Vertipaq canonical-Huffman string decoding"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Igor Cotruta" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/Hugoberry/xmhuffman-cython"
|
xmhuffman-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from setuptools import setup, Extension
|
|
3
|
+
from Cython.Build import cythonize
|
|
4
|
+
|
|
5
|
+
is_macos = sys.platform == "darwin"
|
|
6
|
+
is_windows = sys.platform == "win32"
|
|
7
|
+
is_linux = not is_macos and not is_windows
|
|
8
|
+
|
|
9
|
+
if is_windows:
|
|
10
|
+
extra_compile_args = ["/O2"]
|
|
11
|
+
elif is_linux:
|
|
12
|
+
extra_compile_args = ["-O3", "-fPIC"]
|
|
13
|
+
else:
|
|
14
|
+
extra_compile_args = ["-O3"]
|
|
15
|
+
|
|
16
|
+
xmhuffman_module = Extension(
|
|
17
|
+
"xmhuffman",
|
|
18
|
+
sources=["xmhuffman.pyx", "src/xmhuffman_kernel.c"],
|
|
19
|
+
include_dirs=["include"],
|
|
20
|
+
extra_compile_args=extra_compile_args,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
setup(
|
|
24
|
+
name="xmhuffman",
|
|
25
|
+
version="0.1.0",
|
|
26
|
+
description="Cython bindings for Microsoft xVelocity/Vertipaq canonical-Huffman string decoding (PBIX/Power Pivot)",
|
|
27
|
+
author="Igor Cotruta",
|
|
28
|
+
url="https://github.com/Hugoberry/xmhuffman-cython",
|
|
29
|
+
ext_modules=cythonize(
|
|
30
|
+
[xmhuffman_module],
|
|
31
|
+
compiler_directives={"language_level": "3"},
|
|
32
|
+
),
|
|
33
|
+
python_requires=">=3.8",
|
|
34
|
+
zip_safe=False,
|
|
35
|
+
)
|