pybmp2 1.0__tar.gz → 2.0.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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ build/
4
+ dist/
5
+ *.egg-info/
6
+ .venv/
7
+ .coverage
8
+ coverage.xml
9
+ htmlcov/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
@@ -0,0 +1,67 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [2.0.0] - unreleased
8
+
9
+ A full rewrite of the library.
10
+
11
+ ### Fixed
12
+ - `PIXELARRAY` now holds the whole stored pixel array. 1.x sliced the
13
+ hexlified file using byte counts, so it returned only half the image. Thanks
14
+ to [@buthanoid](https://github.com/buthanoid) for reporting and first fixing
15
+ this in [PR #1](https://github.com/dannybritto96/pybmp/pull/1). 2.0 also reads
16
+ the pixel array from the header's pixel-data offset (`bfOffBits`), not a
17
+ hard-coded position, so files with palettes or bit-field masks work, and
18
+ fixes the same problem for the core and V4 headers.
19
+ - `RAWIMGSIZE` was read from 3 bytes, which shifted `HORIZONTALRES`,
20
+ `VERTICALRES`, `NCOLORS` and `NIMPCOLORS`. The V4 colour-space endpoints
21
+ were read as 41 bytes instead of 36, which shifted the gamma fields.
22
+ - Integers are always decoded as little-endian (1.x used the host byte
23
+ order). Width and height are signed, so top-down images report a negative
24
+ `HEIGHT` instead of a huge unsigned number.
25
+ - The library no longer sets the `pybmp2` logger to `DEBUG` at import time and
26
+ no longer logs at `INFO` for every file.
27
+
28
+ ### Added
29
+ - Pixel decoding: `to_rgb()`, `to_rgba()`, `to_indices()`, `to_numpy()` and
30
+ `get_pixel()` for 1/2/4/8/16/24/32-bit images with `BI_RGB`, `BI_RLE8`,
31
+ `BI_RLE4`, `BI_BITFIELDS` and `BI_ALPHABITFIELDS`.
32
+ - Header versions 16, 52, 56, 64 (OS/2) and 124 (V5), in addition to 12, 40
33
+ and 108.
34
+ - Encoding: `pybmp.encode()`, `pybmp.write()`, `BMP.from_pixels()`,
35
+ `BMP.save()` and `BMP.convert()`.
36
+ - `pybmp.open()`, `BMP.open()`, `BMP.from_bytes()` and `BMP(data=...)`.
37
+ - A typed `BMPHeader` dataclass (`BMP.header`), the `Compression` enum and
38
+ `parse_header()`.
39
+ - An exception hierarchy: `BMPError`, `InvalidBMPError`, `TruncatedFileError`,
40
+ `HeaderNotSupported` and `UnsupportedFormatError`, all `ValueError`s.
41
+ - The `pybmp` command-line tool (`info`, `convert`) and `python -m pybmp`.
42
+ - Type hints (`py.typed`), a pytest suite with coverage, ruff, mypy, and
43
+ GitHub Actions CI and release workflows.
44
+
45
+ ### Changed
46
+ - Packaging moved to `pyproject.toml` (hatchling) with a `src/` layout.
47
+ Python 3.10+ is required.
48
+ - `HEIGHT` and `SHAPE[1]` keep the on-disk sign (negative means top-down).
49
+ Use `BMP.height` or `BMP.size` for positive values.
50
+ - `REDMASK`, `GREENMASK`, `BLUEMASK` and `ALPHAMASK` are the 4 little-endian
51
+ bytes as stored, as before. Integer values are on `BMP.header`.
52
+ - Passing more than one of `filename`, `hexdata` and `data` raises
53
+ `ValueError`. 1.x silently used the first.
54
+ - Malformed input raises `InvalidBMPError` or `TruncatedFileError`, not a
55
+ `UnicodeDecodeError`, `binascii.Error` or silently wrong values.
56
+ - `compression_method()` returns the name and logs at `DEBUG`.
57
+
58
+ ### Deprecated
59
+ - `BMP.unhexlify()` and `BMP.bytes2int()`. They emit `DeprecationWarning`.
60
+
61
+ ### Removed
62
+ - `setup.py` and the committed `dist/` and `pybmp2.egg-info/` build artifacts.
63
+
64
+ ## [1.0] - 2019
65
+
66
+ - First release: parses BMP file and DIB headers (12, 40 and 108 bytes) from a
67
+ file or a hexlified string.
File without changes
pybmp2-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.5
2
+ Name: pybmp2
3
+ Version: 2.0.0
4
+ Summary: Fast, dependency-free reader and writer for Windows BMP images.
5
+ Project-URL: Homepage, https://github.com/dannybritto96/pybmp
6
+ Project-URL: Issues, https://github.com/dannybritto96/pybmp/issues
7
+ Project-URL: Changelog, https://github.com/dannybritto96/pybmp/blob/master/CHANGELOG.md
8
+ Author-email: Danie Britto <dannybritto96@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: bitmap,bmp,decoder,dib,encoder,image
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1; extra == 'dev'
26
+ Requires-Dist: mypy>=1.10; extra == 'dev'
27
+ Requires-Dist: numpy>=1.23; extra == 'dev'
28
+ Requires-Dist: pillow>=9; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=4; extra == 'dev'
30
+ Requires-Dist: pytest>=7; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6; extra == 'dev'
32
+ Requires-Dist: twine>=5; extra == 'dev'
33
+ Provides-Extra: numpy
34
+ Requires-Dist: numpy>=1.23; extra == 'numpy'
35
+ Provides-Extra: test
36
+ Requires-Dist: numpy>=1.23; extra == 'test'
37
+ Requires-Dist: pillow>=9; extra == 'test'
38
+ Requires-Dist: pytest-cov>=4; extra == 'test'
39
+ Requires-Dist: pytest>=7; extra == 'test'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # pybmp
43
+
44
+ [![CI](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/pybmp2.svg)](https://pypi.org/project/pybmp2/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/pybmp2.svg)](https://pypi.org/project/pybmp2/)
47
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
48
+
49
+ A fast, dependency-free Python library and command-line tool for reading and
50
+ writing Windows BMP (DIB) images.
51
+
52
+ pybmp parses every BMP header version, decodes pixels to plain RGB/RGBA
53
+ bytes (or palette indices), and encodes images back to BMP. The hot paths use
54
+ C-level bulk operations (`bytes.translate`, extended slice assignment,
55
+ `memoryview`, big-integer bit operations) instead of per-pixel Python loops,
56
+ so it is fast without any dependencies. numpy is optional: if it is installed,
57
+ `to_numpy()` is available and unusual bit-field layouts decode faster.
58
+
59
+ The distribution is called **`pybmp2`** on PyPI. The import name is `pybmp`.
60
+
61
+ ## Features
62
+
63
+ - Reads every DIB header version: `BITMAPCOREHEADER` (12 bytes), OS/2
64
+ `OS22XBITMAPHEADER` (16 and 64 bytes), `BITMAPINFOHEADER` (40),
65
+ `BITMAPV2INFOHEADER` (52), `BITMAPV3INFOHEADER` (56), `BITMAPV4HEADER` (108)
66
+ and `BITMAPV5HEADER` (124).
67
+ - Decodes 1, 2, 4, 8, 16, 24 and 32 bits per pixel.
68
+ - Decodes `BI_RGB`, `BI_RLE8`, `BI_RLE4`, `BI_BITFIELDS` and `BI_ALPHABITFIELDS`,
69
+ including arbitrary channel masks such as RGB565, ARGB4444 and A2R10G10B10.
70
+ - Handles bottom-up and top-down (negative height) images, row padding for any
71
+ width, palettes of any size, and the pixel-data offset from the file header.
72
+ - Encodes 1/2/4/8-bit indexed, 16-bit (X1R5G5B5), 24-bit and 32-bit RGB, and
73
+ 32-bit RGBA, with a core, info, V4 or V5 header, top-down or bottom-up.
74
+ - Gives typed header access (`BMPHeader` dataclass), specific exceptions, a
75
+ `pybmp` command-line tool, and full type hints (`py.typed`).
76
+ - Stays backwards compatible with the pybmp 1.x attributes (`SHAPE`,
77
+ `PIXELARRAY`, `RAWIMGSIZE` and the rest).
78
+
79
+ ### Unsupported
80
+
81
+ These raise `UnsupportedFormatError` when you decode pixels. Headers still parse.
82
+
83
+ - `BI_JPEG` and `BI_PNG` embedded images. `pixel_array` returns the embedded
84
+ stream, so you can hand it to a JPEG or PNG decoder.
85
+ - `BI_CMYK`, `BI_CMYKRLE8` and `BI_CMYKRLE4` (Windows Metafile only).
86
+ - OS/2 Huffman 1D and RLE24 compression.
87
+ - ICC colour profiles and gamma in V4/V5 headers are parsed but not applied.
88
+ - Multi-image OS/2 bitmap arrays (`BA`) are read as a single image.
89
+ - Writing RLE-compressed files, and converting true-colour images to an
90
+ indexed palette (quantisation).
91
+
92
+ ## Install
93
+
94
+ ```shell
95
+ pip install pybmp2 # pure Python, no dependencies
96
+ pip install "pybmp2[numpy]" # adds the optional numpy fast path and to_numpy()
97
+ ```
98
+
99
+ From source:
100
+
101
+ ```shell
102
+ git clone https://github.com/dannybritto96/pybmp.git
103
+ cd pybmp
104
+ pip install . # or: pip install -e ".[dev]" for development
105
+ ```
106
+
107
+ pybmp needs Python 3.10 or newer.
108
+
109
+ ## Quick start
110
+
111
+ ```python
112
+ import pybmp
113
+
114
+ # Write a 3x2 RGB image (rows top-down, 3 bytes per pixel)...
115
+ pixels = bytes([
116
+ 255, 0, 0, 0, 255, 0, 0, 0, 255,
117
+ 255, 255, 0, 0, 255, 255, 255, 255, 255,
118
+ ])
119
+ pybmp.write("example.bmp", 3, 2, pixels)
120
+
121
+ # ...and read it back.
122
+ img = pybmp.open("example.bmp")
123
+ print(img) # <BMP 3x2 24-bit BI_RGB BITMAPINFOHEADER>
124
+ print(img.size, img.mode) # (3, 2) RGB
125
+ assert img.to_rgb() == pixels
126
+ print(img.get_pixel(2, 0)) # (0, 0, 255)
127
+ ```
128
+
129
+ ## Usage
130
+
131
+ ### Reading
132
+
133
+ ```python
134
+ import io
135
+ from pathlib import Path
136
+
137
+ from pybmp import BMP
138
+
139
+ data = Path("example.bmp").read_bytes()
140
+
141
+ img = BMP("example.bmp") # path (str or Path)
142
+ img = BMP.open(io.BytesIO(data)) # binary file object
143
+ img = BMP.from_bytes(data) # bytes already in memory
144
+
145
+ h = img.header # frozen BMPHeader dataclass
146
+ print(h.header_name, h.bits_per_pixel, h.compression_name, h.pixel_offset)
147
+ print(img.width, img.height, img.top_down, img.palette)
148
+
149
+ rgb = img.to_rgb() # bytes, 3 per pixel, rows top-down
150
+ rgba = img.to_rgba() # bytes, 4 per pixel (alpha = 255 if none stored)
151
+ raw = img.pixel_array # stored bytes exactly as on disk, from the pixel offset
152
+ print(img.info()) # JSON-serialisable summary of every header field
153
+ ```
154
+
155
+ With numpy installed:
156
+
157
+ ```python
158
+ from pybmp import BMP
159
+
160
+ arr = BMP("example.bmp").to_numpy() # shape (height, width, 3), dtype uint8
161
+ print(arr.shape) # (2, 3, 3)
162
+ ```
163
+
164
+ ### Indexed (palette) images
165
+
166
+ ```python
167
+ import pybmp
168
+
169
+ palette = [(0, 0, 0), (255, 255, 255), (255, 0, 0), (0, 0, 255)]
170
+ indices = bytes([0, 1, 2, 3, 3, 2, 1, 0]) # one palette index per pixel
171
+ data = pybmp.encode(4, 2, indices, mode="P", bits=2, palette=palette)
172
+
173
+ img = pybmp.BMP(data=data)
174
+ assert img.to_indices() == indices
175
+ print(img.bits_per_pixel, len(img.palette)) # 2 4
176
+ ```
177
+
178
+ ### Writing
179
+
180
+ ```python
181
+ import pybmp
182
+
183
+ rgba = bytes([255, 0, 0, 128] * 4) # 2x2, half-transparent red
184
+ pybmp.write("alpha.bmp", 2, 2, rgba, mode="RGBA") # 32-bit BITMAPV4HEADER
185
+
186
+ rgb = bytes(range(18))
187
+ small = pybmp.encode(3, 2, rgb, bits=16) # 16-bit X1R5G5B5
188
+ top = pybmp.encode(3, 2, rgb, top_down=True, header="v5")
189
+
190
+ img = pybmp.BMP.from_pixels(3, 2, rgb) # build a BMP object directly
191
+ img.convert(bits=32, top_down=True).save("converted.bmp")
192
+ ```
193
+
194
+ ### Errors
195
+
196
+ All errors derive from `pybmp.BMPError`, which subclasses `ValueError`:
197
+
198
+ | Exception | Raised when |
199
+ |---|---|
200
+ | `InvalidBMPError` | the data is not a well-formed BMP (bad signature, width, offset, hex) |
201
+ | `TruncatedFileError` | the file ends inside a header, palette or the pixel array |
202
+ | `HeaderNotSupported` | the DIB header size is unknown |
203
+ | `UnsupportedFormatError` | the file is valid but uses an unsupported feature |
204
+
205
+ ```python
206
+ import pybmp
207
+
208
+ try:
209
+ pybmp.BMP(data=b"definitely not a bitmap")
210
+ except pybmp.InvalidBMPError as exc:
211
+ print("not a BMP:", exc)
212
+ ```
213
+
214
+ ### pybmp 1.x compatibility
215
+
216
+ The 1.x constructor and upper-case attributes still work:
217
+
218
+ ```python
219
+ import binascii
220
+ from pybmp import BMP
221
+
222
+ with open("example.bmp", "rb") as f:
223
+ hexdata = binascii.hexlify(f.read())
224
+
225
+ img = BMP(hexdata=hexdata) # or BMP(filename="example.bmp")
226
+ print(img.SHAPE, img.RAWIMGSIZE, len(img.PIXELARRAY)) # (3, 2) 24 24
227
+ ```
228
+
229
+ ## Command line
230
+
231
+ ```shell
232
+ $ pybmp info example.bmp
233
+ example.bmp: 3x2 24-bit BI_RGB BITMAPINFOHEADER (bottom-up, RGB, palette=0, offset=54)
234
+
235
+ $ pybmp info --json example.bmp # every header field as JSON
236
+ $ pybmp convert in.bmp out.bmp --bits 32 --top-down --header v5
237
+ $ python -m pybmp --version
238
+ ```
239
+
240
+ `convert` accepts `--bits {1,2,4,8,16,24,32}`, `--top-down` or `--bottom-up`,
241
+ and `--header {core,info,v4,v5}`. Indexed targets need an indexed source.
242
+
243
+ ## Benchmarks
244
+
245
+ Measured with `python benchmarks/bench.py` on Python 3.13.5, Linux x86_64
246
+ (best of 5 runs, milliseconds). "1.x parse" is the pybmp 1.x code on `master`
247
+ (after PR #1), which hexlifies the whole file. "2.x parse" builds a `BMP` and
248
+ reads `PIXELARRAY`. "naive loop" is a reference per-pixel Python decoder, not
249
+ part of either version. Your numbers will vary by machine.
250
+
251
+ | Image (24-bit) | File MB | 1.x parse | 2.x parse | Speed-up | 2.x `to_rgb` | Pillow `open+tobytes` | naive loop decode |
252
+ |---|---|---|---|---|---|---|---|
253
+ | 64x64 | 0.01 | 0.03 | 0.017 | 2x | 0.03 | 0.03 | 0.7 |
254
+ | 512x512 | 0.79 | 2.21 | 0.103 | 21x | 0.55 | 0.43 | 43.1 |
255
+ | 1024x1024 | 3.15 | 7.33 | 0.500 | 15x | 2.38 | 1.64 | 179.5 |
256
+ | 2048x2048 | 12.58 | 32.83 | 1.626 | 20x | 9.16 | 6.35 | - |
257
+ | 4096x4096 | 50.33 | 87.87 | 42.668 | 2x | 144.09 | 91.92 | - |
258
+
259
+ Decode and encode at 2048x2048 (4.2 megapixels), milliseconds:
260
+
261
+ | Format | 2.x decode to RGB(A) | Pillow decode | 2.x encode |
262
+ |---|---|---|---|
263
+ | 1-bit indexed | 15.9 | 4.9 | 36.6 |
264
+ | 4-bit indexed | 15.9 | 5.0 | 34.1 |
265
+ | 8-bit indexed | 12.4 | 4.3 | 24.4 |
266
+ | 16-bit RGB555 | 21.5 | 12.3 | 30.0 |
267
+ | 24-bit RGB | 8.1 | 6.7 | 10.2 |
268
+ | 32-bit RGBA (V4) | 13.7 | 5.4 | 13.7 |
269
+
270
+ pybmp 1.x could not decode pixels at all, so there is no 1.x column for
271
+ decoding. Pure-Python 2.x decoding is 20-80x faster than a per-pixel loop and
272
+ within 1.2-3.5x of Pillow's C decoder.
273
+
274
+ ## Development
275
+
276
+ ```shell
277
+ pip install -e ".[dev]"
278
+ pytest --cov # tests, with coverage (CI requires >= 90%)
279
+ ruff check . && ruff format --check .
280
+ mypy # strict type checking of src/ and tests/
281
+ python benchmarks/bench.py # benchmarks against the 1.x code on master
282
+ python -m build && twine check dist/*
283
+ ```
284
+
285
+ The test suite covers every module: header parsing, decoding of every bit
286
+ depth and compression, round trips across depths, header versions,
287
+ orientations and padded widths, malformed and truncated files, the CLI, the
288
+ 1.x compatibility layer, cross-checks against Pillow, and the code examples
289
+ in this README.
290
+
291
+ ## Contributing
292
+
293
+ Bug reports and pull requests are welcome.
294
+
295
+ 1. Fork the repository and create a branch from `master`.
296
+ 2. Install the dev extras: `pip install -e ".[dev]"`.
297
+ 3. Add tests for your change. For decoder fixes, a small hand-built file in
298
+ `tests/helpers.py` style is ideal.
299
+ 4. Make sure `pytest --cov`, `ruff check .`, `ruff format --check .` and `mypy`
300
+ all pass. CI runs them on Python 3.10 to 3.13.
301
+ 5. Open a pull request describing the change.
302
+
303
+ ## What changed in 2.0
304
+
305
+ 2.0 is a full rewrite. The [CHANGELOG](CHANGELOG.md) has the details.
306
+
307
+ - **Correct pixel data.** 1.x sliced the hexlified file with byte counts, so
308
+ `PIXELARRAY` held only half the image. This was reported and first fixed by
309
+ [@buthanoid](https://github.com/buthanoid) in
310
+ [PR #1](https://github.com/dannybritto96/pybmp/pull/1). Thank you! 2.0
311
+ completes that fix. Pixels are read from the header's pixel-data offset with
312
+ real byte lengths for every header version, and the misaligned header fields
313
+ (`RAWIMGSIZE`, resolution, colour counts, V4 colour space and gamma) are read
314
+ from the right positions.
315
+ - Pixel decoding (`to_rgb`, `to_rgba`, `to_indices`, `to_numpy`), encoding
316
+ (`encode`, `write`, `save`, `convert`) and a CLI are new.
317
+ - More header versions, RLE, bit fields, palettes and top-down images are supported.
318
+ - `src/` layout, `pyproject.toml` packaging, type hints, a pytest suite, and
319
+ CI with ruff, mypy and coverage.
320
+
321
+ Reference: <https://en.wikipedia.org/wiki/BMP_file_format>
322
+
323
+ ## License
324
+
325
+ MIT. See [LICENSE](LICENSE).
pybmp2-2.0.0/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # pybmp
2
+
3
+ [![CI](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pybmp2.svg)](https://pypi.org/project/pybmp2/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/pybmp2.svg)](https://pypi.org/project/pybmp2/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ A fast, dependency-free Python library and command-line tool for reading and
9
+ writing Windows BMP (DIB) images.
10
+
11
+ pybmp parses every BMP header version, decodes pixels to plain RGB/RGBA
12
+ bytes (or palette indices), and encodes images back to BMP. The hot paths use
13
+ C-level bulk operations (`bytes.translate`, extended slice assignment,
14
+ `memoryview`, big-integer bit operations) instead of per-pixel Python loops,
15
+ so it is fast without any dependencies. numpy is optional: if it is installed,
16
+ `to_numpy()` is available and unusual bit-field layouts decode faster.
17
+
18
+ The distribution is called **`pybmp2`** on PyPI. The import name is `pybmp`.
19
+
20
+ ## Features
21
+
22
+ - Reads every DIB header version: `BITMAPCOREHEADER` (12 bytes), OS/2
23
+ `OS22XBITMAPHEADER` (16 and 64 bytes), `BITMAPINFOHEADER` (40),
24
+ `BITMAPV2INFOHEADER` (52), `BITMAPV3INFOHEADER` (56), `BITMAPV4HEADER` (108)
25
+ and `BITMAPV5HEADER` (124).
26
+ - Decodes 1, 2, 4, 8, 16, 24 and 32 bits per pixel.
27
+ - Decodes `BI_RGB`, `BI_RLE8`, `BI_RLE4`, `BI_BITFIELDS` and `BI_ALPHABITFIELDS`,
28
+ including arbitrary channel masks such as RGB565, ARGB4444 and A2R10G10B10.
29
+ - Handles bottom-up and top-down (negative height) images, row padding for any
30
+ width, palettes of any size, and the pixel-data offset from the file header.
31
+ - Encodes 1/2/4/8-bit indexed, 16-bit (X1R5G5B5), 24-bit and 32-bit RGB, and
32
+ 32-bit RGBA, with a core, info, V4 or V5 header, top-down or bottom-up.
33
+ - Gives typed header access (`BMPHeader` dataclass), specific exceptions, a
34
+ `pybmp` command-line tool, and full type hints (`py.typed`).
35
+ - Stays backwards compatible with the pybmp 1.x attributes (`SHAPE`,
36
+ `PIXELARRAY`, `RAWIMGSIZE` and the rest).
37
+
38
+ ### Unsupported
39
+
40
+ These raise `UnsupportedFormatError` when you decode pixels. Headers still parse.
41
+
42
+ - `BI_JPEG` and `BI_PNG` embedded images. `pixel_array` returns the embedded
43
+ stream, so you can hand it to a JPEG or PNG decoder.
44
+ - `BI_CMYK`, `BI_CMYKRLE8` and `BI_CMYKRLE4` (Windows Metafile only).
45
+ - OS/2 Huffman 1D and RLE24 compression.
46
+ - ICC colour profiles and gamma in V4/V5 headers are parsed but not applied.
47
+ - Multi-image OS/2 bitmap arrays (`BA`) are read as a single image.
48
+ - Writing RLE-compressed files, and converting true-colour images to an
49
+ indexed palette (quantisation).
50
+
51
+ ## Install
52
+
53
+ ```shell
54
+ pip install pybmp2 # pure Python, no dependencies
55
+ pip install "pybmp2[numpy]" # adds the optional numpy fast path and to_numpy()
56
+ ```
57
+
58
+ From source:
59
+
60
+ ```shell
61
+ git clone https://github.com/dannybritto96/pybmp.git
62
+ cd pybmp
63
+ pip install . # or: pip install -e ".[dev]" for development
64
+ ```
65
+
66
+ pybmp needs Python 3.10 or newer.
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import pybmp
72
+
73
+ # Write a 3x2 RGB image (rows top-down, 3 bytes per pixel)...
74
+ pixels = bytes([
75
+ 255, 0, 0, 0, 255, 0, 0, 0, 255,
76
+ 255, 255, 0, 0, 255, 255, 255, 255, 255,
77
+ ])
78
+ pybmp.write("example.bmp", 3, 2, pixels)
79
+
80
+ # ...and read it back.
81
+ img = pybmp.open("example.bmp")
82
+ print(img) # <BMP 3x2 24-bit BI_RGB BITMAPINFOHEADER>
83
+ print(img.size, img.mode) # (3, 2) RGB
84
+ assert img.to_rgb() == pixels
85
+ print(img.get_pixel(2, 0)) # (0, 0, 255)
86
+ ```
87
+
88
+ ## Usage
89
+
90
+ ### Reading
91
+
92
+ ```python
93
+ import io
94
+ from pathlib import Path
95
+
96
+ from pybmp import BMP
97
+
98
+ data = Path("example.bmp").read_bytes()
99
+
100
+ img = BMP("example.bmp") # path (str or Path)
101
+ img = BMP.open(io.BytesIO(data)) # binary file object
102
+ img = BMP.from_bytes(data) # bytes already in memory
103
+
104
+ h = img.header # frozen BMPHeader dataclass
105
+ print(h.header_name, h.bits_per_pixel, h.compression_name, h.pixel_offset)
106
+ print(img.width, img.height, img.top_down, img.palette)
107
+
108
+ rgb = img.to_rgb() # bytes, 3 per pixel, rows top-down
109
+ rgba = img.to_rgba() # bytes, 4 per pixel (alpha = 255 if none stored)
110
+ raw = img.pixel_array # stored bytes exactly as on disk, from the pixel offset
111
+ print(img.info()) # JSON-serialisable summary of every header field
112
+ ```
113
+
114
+ With numpy installed:
115
+
116
+ ```python
117
+ from pybmp import BMP
118
+
119
+ arr = BMP("example.bmp").to_numpy() # shape (height, width, 3), dtype uint8
120
+ print(arr.shape) # (2, 3, 3)
121
+ ```
122
+
123
+ ### Indexed (palette) images
124
+
125
+ ```python
126
+ import pybmp
127
+
128
+ palette = [(0, 0, 0), (255, 255, 255), (255, 0, 0), (0, 0, 255)]
129
+ indices = bytes([0, 1, 2, 3, 3, 2, 1, 0]) # one palette index per pixel
130
+ data = pybmp.encode(4, 2, indices, mode="P", bits=2, palette=palette)
131
+
132
+ img = pybmp.BMP(data=data)
133
+ assert img.to_indices() == indices
134
+ print(img.bits_per_pixel, len(img.palette)) # 2 4
135
+ ```
136
+
137
+ ### Writing
138
+
139
+ ```python
140
+ import pybmp
141
+
142
+ rgba = bytes([255, 0, 0, 128] * 4) # 2x2, half-transparent red
143
+ pybmp.write("alpha.bmp", 2, 2, rgba, mode="RGBA") # 32-bit BITMAPV4HEADER
144
+
145
+ rgb = bytes(range(18))
146
+ small = pybmp.encode(3, 2, rgb, bits=16) # 16-bit X1R5G5B5
147
+ top = pybmp.encode(3, 2, rgb, top_down=True, header="v5")
148
+
149
+ img = pybmp.BMP.from_pixels(3, 2, rgb) # build a BMP object directly
150
+ img.convert(bits=32, top_down=True).save("converted.bmp")
151
+ ```
152
+
153
+ ### Errors
154
+
155
+ All errors derive from `pybmp.BMPError`, which subclasses `ValueError`:
156
+
157
+ | Exception | Raised when |
158
+ |---|---|
159
+ | `InvalidBMPError` | the data is not a well-formed BMP (bad signature, width, offset, hex) |
160
+ | `TruncatedFileError` | the file ends inside a header, palette or the pixel array |
161
+ | `HeaderNotSupported` | the DIB header size is unknown |
162
+ | `UnsupportedFormatError` | the file is valid but uses an unsupported feature |
163
+
164
+ ```python
165
+ import pybmp
166
+
167
+ try:
168
+ pybmp.BMP(data=b"definitely not a bitmap")
169
+ except pybmp.InvalidBMPError as exc:
170
+ print("not a BMP:", exc)
171
+ ```
172
+
173
+ ### pybmp 1.x compatibility
174
+
175
+ The 1.x constructor and upper-case attributes still work:
176
+
177
+ ```python
178
+ import binascii
179
+ from pybmp import BMP
180
+
181
+ with open("example.bmp", "rb") as f:
182
+ hexdata = binascii.hexlify(f.read())
183
+
184
+ img = BMP(hexdata=hexdata) # or BMP(filename="example.bmp")
185
+ print(img.SHAPE, img.RAWIMGSIZE, len(img.PIXELARRAY)) # (3, 2) 24 24
186
+ ```
187
+
188
+ ## Command line
189
+
190
+ ```shell
191
+ $ pybmp info example.bmp
192
+ example.bmp: 3x2 24-bit BI_RGB BITMAPINFOHEADER (bottom-up, RGB, palette=0, offset=54)
193
+
194
+ $ pybmp info --json example.bmp # every header field as JSON
195
+ $ pybmp convert in.bmp out.bmp --bits 32 --top-down --header v5
196
+ $ python -m pybmp --version
197
+ ```
198
+
199
+ `convert` accepts `--bits {1,2,4,8,16,24,32}`, `--top-down` or `--bottom-up`,
200
+ and `--header {core,info,v4,v5}`. Indexed targets need an indexed source.
201
+
202
+ ## Benchmarks
203
+
204
+ Measured with `python benchmarks/bench.py` on Python 3.13.5, Linux x86_64
205
+ (best of 5 runs, milliseconds). "1.x parse" is the pybmp 1.x code on `master`
206
+ (after PR #1), which hexlifies the whole file. "2.x parse" builds a `BMP` and
207
+ reads `PIXELARRAY`. "naive loop" is a reference per-pixel Python decoder, not
208
+ part of either version. Your numbers will vary by machine.
209
+
210
+ | Image (24-bit) | File MB | 1.x parse | 2.x parse | Speed-up | 2.x `to_rgb` | Pillow `open+tobytes` | naive loop decode |
211
+ |---|---|---|---|---|---|---|---|
212
+ | 64x64 | 0.01 | 0.03 | 0.017 | 2x | 0.03 | 0.03 | 0.7 |
213
+ | 512x512 | 0.79 | 2.21 | 0.103 | 21x | 0.55 | 0.43 | 43.1 |
214
+ | 1024x1024 | 3.15 | 7.33 | 0.500 | 15x | 2.38 | 1.64 | 179.5 |
215
+ | 2048x2048 | 12.58 | 32.83 | 1.626 | 20x | 9.16 | 6.35 | - |
216
+ | 4096x4096 | 50.33 | 87.87 | 42.668 | 2x | 144.09 | 91.92 | - |
217
+
218
+ Decode and encode at 2048x2048 (4.2 megapixels), milliseconds:
219
+
220
+ | Format | 2.x decode to RGB(A) | Pillow decode | 2.x encode |
221
+ |---|---|---|---|
222
+ | 1-bit indexed | 15.9 | 4.9 | 36.6 |
223
+ | 4-bit indexed | 15.9 | 5.0 | 34.1 |
224
+ | 8-bit indexed | 12.4 | 4.3 | 24.4 |
225
+ | 16-bit RGB555 | 21.5 | 12.3 | 30.0 |
226
+ | 24-bit RGB | 8.1 | 6.7 | 10.2 |
227
+ | 32-bit RGBA (V4) | 13.7 | 5.4 | 13.7 |
228
+
229
+ pybmp 1.x could not decode pixels at all, so there is no 1.x column for
230
+ decoding. Pure-Python 2.x decoding is 20-80x faster than a per-pixel loop and
231
+ within 1.2-3.5x of Pillow's C decoder.
232
+
233
+ ## Development
234
+
235
+ ```shell
236
+ pip install -e ".[dev]"
237
+ pytest --cov # tests, with coverage (CI requires >= 90%)
238
+ ruff check . && ruff format --check .
239
+ mypy # strict type checking of src/ and tests/
240
+ python benchmarks/bench.py # benchmarks against the 1.x code on master
241
+ python -m build && twine check dist/*
242
+ ```
243
+
244
+ The test suite covers every module: header parsing, decoding of every bit
245
+ depth and compression, round trips across depths, header versions,
246
+ orientations and padded widths, malformed and truncated files, the CLI, the
247
+ 1.x compatibility layer, cross-checks against Pillow, and the code examples
248
+ in this README.
249
+
250
+ ## Contributing
251
+
252
+ Bug reports and pull requests are welcome.
253
+
254
+ 1. Fork the repository and create a branch from `master`.
255
+ 2. Install the dev extras: `pip install -e ".[dev]"`.
256
+ 3. Add tests for your change. For decoder fixes, a small hand-built file in
257
+ `tests/helpers.py` style is ideal.
258
+ 4. Make sure `pytest --cov`, `ruff check .`, `ruff format --check .` and `mypy`
259
+ all pass. CI runs them on Python 3.10 to 3.13.
260
+ 5. Open a pull request describing the change.
261
+
262
+ ## What changed in 2.0
263
+
264
+ 2.0 is a full rewrite. The [CHANGELOG](CHANGELOG.md) has the details.
265
+
266
+ - **Correct pixel data.** 1.x sliced the hexlified file with byte counts, so
267
+ `PIXELARRAY` held only half the image. This was reported and first fixed by
268
+ [@buthanoid](https://github.com/buthanoid) in
269
+ [PR #1](https://github.com/dannybritto96/pybmp/pull/1). Thank you! 2.0
270
+ completes that fix. Pixels are read from the header's pixel-data offset with
271
+ real byte lengths for every header version, and the misaligned header fields
272
+ (`RAWIMGSIZE`, resolution, colour counts, V4 colour space and gamma) are read
273
+ from the right positions.
274
+ - Pixel decoding (`to_rgb`, `to_rgba`, `to_indices`, `to_numpy`), encoding
275
+ (`encode`, `write`, `save`, `convert`) and a CLI are new.
276
+ - More header versions, RLE, bit fields, palettes and top-down images are supported.
277
+ - `src/` layout, `pyproject.toml` packaging, type hints, a pytest suite, and
278
+ CI with ruff, mypy and coverage.
279
+
280
+ Reference: <https://en.wikipedia.org/wiki/BMP_file_format>
281
+
282
+ ## License
283
+
284
+ MIT. See [LICENSE](LICENSE).