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.
- pybmp2-2.0.0/.gitignore +12 -0
- pybmp2-2.0.0/CHANGELOG.md +67 -0
- {pybmp2-1.0 → pybmp2-2.0.0}/LICENSE +0 -0
- pybmp2-2.0.0/PKG-INFO +325 -0
- pybmp2-2.0.0/README.md +284 -0
- pybmp2-2.0.0/benchmarks/bench.py +159 -0
- pybmp2-2.0.0/pyproject.toml +85 -0
- pybmp2-2.0.0/src/pybmp/__init__.py +95 -0
- pybmp2-2.0.0/src/pybmp/__main__.py +5 -0
- pybmp2-2.0.0/src/pybmp/_bmp.py +427 -0
- pybmp2-2.0.0/src/pybmp/_constants.py +56 -0
- pybmp2-2.0.0/src/pybmp/_decode.py +392 -0
- pybmp2-2.0.0/src/pybmp/_encode.py +237 -0
- pybmp2-2.0.0/src/pybmp/_errors.py +36 -0
- pybmp2-2.0.0/src/pybmp/_header.py +223 -0
- pybmp2-2.0.0/src/pybmp/_util.py +16 -0
- pybmp2-2.0.0/src/pybmp/cli.py +77 -0
- pybmp2-2.0.0/src/pybmp/py.typed +0 -0
- pybmp2-2.0.0/tests/conftest.py +6 -0
- pybmp2-2.0.0/tests/helpers.py +79 -0
- pybmp2-2.0.0/tests/test_bmp.py +247 -0
- pybmp2-2.0.0/tests/test_cli.py +103 -0
- pybmp2-2.0.0/tests/test_decode.py +445 -0
- pybmp2-2.0.0/tests/test_encode.py +138 -0
- pybmp2-2.0.0/tests/test_header.py +227 -0
- pybmp2-2.0.0/tests/test_pillow.py +105 -0
- pybmp2-2.0.0/tests/test_readme.py +49 -0
- pybmp2-2.0.0/tests/test_roundtrip.py +138 -0
- pybmp2-1.0/PKG-INFO +0 -52
- pybmp2-1.0/README.md +0 -36
- pybmp2-1.0/pybmp/__init__.py +0 -123
- pybmp2-1.0/pybmp2.egg-info/PKG-INFO +0 -52
- pybmp2-1.0/pybmp2.egg-info/SOURCES.txt +0 -9
- pybmp2-1.0/pybmp2.egg-info/dependency_links.txt +0 -1
- pybmp2-1.0/pybmp2.egg-info/top_level.txt +0 -1
- pybmp2-1.0/pyproject.toml +0 -2
- pybmp2-1.0/setup.cfg +0 -4
- pybmp2-1.0/setup.py +0 -6
pybmp2-2.0.0/.gitignore
ADDED
|
@@ -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
|
+
[](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/pybmp2/)
|
|
46
|
+
[](https://pypi.org/project/pybmp2/)
|
|
47
|
+
[](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
|
+
[](https://github.com/dannybritto96/pybmp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pybmp2/)
|
|
5
|
+
[](https://pypi.org/project/pybmp2/)
|
|
6
|
+
[](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).
|