Python Pillow – RLE compression of BMP image
Question:
I’m working on a script, which builds an image, combines it with another image and saves it locally as an 8-bit BMP-file.
The image is then read by a ESP32 microcontroller, but the problem is that due to memorylimitations, the allowed file size is somewhat limited.
As a consequence, I made a BMP decoder for the ESP32, which supports RLE. In theory, the allowed number of bytes can still be exceeded, but only text and simple icons are to be read, so it will most likely never happen.
It uses Pillow for image processing, which now supports RLE-compression from version 9.1.0
https://github.com/python-pillow/Pillow/blob/main/docs/handbook/image-file-formats.rst
Pillow reads and writes Windows and OS/2 BMP files containing 1, L, P,
or RGB data. 16-colour images are read as P images. Support for
reading 8-bit run-length encoding was added in Pillow 9.1.0. Support
for reading 4-bit run-length encoding was added in Pillow 9.3.0.
Here’s the part of the code, that combines two existing images into a new one and saves them:
img_buf = io.BytesIO() # Start converting from Matplotlib to PIL
# Supported: eps, jpeg, jpg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff, webp
plt.savefig(img_buf, format='png', transparent=True)
graph = Image.open(img_buf)
# Create empty, 8-bit canvas
new_image = Image.new('P',(600,448), (255,255,255)) # P = 8-bit indexed
new_image.paste(img,(0,0)) # Insert image 1 into canvas
new_image.paste(graph,(0,200)) # Insert image 2 into canvas at y:200
new_image.save("../output/priceeast.bmp", compression=1) # According to the docs, 1 = RLE
It saves the image, alright, but not RLE-encoded and I can’t work out, how to enable it… or is RLE only supported when reading BMP, not saving?
UPDATE:
I added this line below:
subprocess.call('magick ../output/priceeast.png -type palette -compress RLE ../output/priceeast.bmp ', shell=True)
Answers:
Pillow does not support writing BMP files with compression, which can be determined by investigating the source. BmpImagePlugin._write
:
# bitmap info header
fp.write(
o32(header) # info header size
+ o32(im.size[0]) # width
+ o32(im.size[1]) # height
+ o16(1) # planes
+ o16(bits) # depth
+ o32(0) # compression (0=uncompressed)
+ o32(image) # size of bitmap
+ o32(ppm[0]) # resolution
+ o32(ppm[1]) # resolution
+ o32(colors) # colors used
+ o32(colors) # colors important
)
We can see here that the compression field in the output file header is hard-coded to none, indicating that compression is not supported when writing a file.
If you would like a work-around, ImageMagick can convert any image format to 8-bit RLE BMP like this:
magick INPUTIMAGE.xxx -type palette -compress RLE result.bmp # where XXX is PNG, JPG, TIFF, GIF, TGA etc
Check the result with exiftool
like this:
exiftool -filename -filesize -compression result.bmp
File Name : result.bmp
File Size : 1168 bytes
Compression : 8-Bit RLE
Note that there are Python bindings to ImageMagick via wand, so you can achieve the same effect in Python like this:
#!/usr/bin/env python3
# You may need this before running on macOS
# export MAGICK_HOME=/opt/homebrew
from wand.image import Image
# Load image , or create pseudo image
with Image(filename='gradient:red-blue') as img:
img.type = 'palette'
img.compression = 'rle'
img.save(filename='result.bmp')
I’m working on a script, which builds an image, combines it with another image and saves it locally as an 8-bit BMP-file.
The image is then read by a ESP32 microcontroller, but the problem is that due to memorylimitations, the allowed file size is somewhat limited.
As a consequence, I made a BMP decoder for the ESP32, which supports RLE. In theory, the allowed number of bytes can still be exceeded, but only text and simple icons are to be read, so it will most likely never happen.
It uses Pillow for image processing, which now supports RLE-compression from version 9.1.0
https://github.com/python-pillow/Pillow/blob/main/docs/handbook/image-file-formats.rst
Pillow reads and writes Windows and OS/2 BMP files containing 1, L, P,
or RGB data. 16-colour images are read as P images. Support for
reading 8-bit run-length encoding was added in Pillow 9.1.0. Support
for reading 4-bit run-length encoding was added in Pillow 9.3.0.
Here’s the part of the code, that combines two existing images into a new one and saves them:
img_buf = io.BytesIO() # Start converting from Matplotlib to PIL
# Supported: eps, jpeg, jpg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff, webp
plt.savefig(img_buf, format='png', transparent=True)
graph = Image.open(img_buf)
# Create empty, 8-bit canvas
new_image = Image.new('P',(600,448), (255,255,255)) # P = 8-bit indexed
new_image.paste(img,(0,0)) # Insert image 1 into canvas
new_image.paste(graph,(0,200)) # Insert image 2 into canvas at y:200
new_image.save("../output/priceeast.bmp", compression=1) # According to the docs, 1 = RLE
It saves the image, alright, but not RLE-encoded and I can’t work out, how to enable it… or is RLE only supported when reading BMP, not saving?
UPDATE:
I added this line below:
subprocess.call('magick ../output/priceeast.png -type palette -compress RLE ../output/priceeast.bmp ', shell=True)
Pillow does not support writing BMP files with compression, which can be determined by investigating the source. BmpImagePlugin._write
:
# bitmap info header
fp.write(
o32(header) # info header size
+ o32(im.size[0]) # width
+ o32(im.size[1]) # height
+ o16(1) # planes
+ o16(bits) # depth
+ o32(0) # compression (0=uncompressed)
+ o32(image) # size of bitmap
+ o32(ppm[0]) # resolution
+ o32(ppm[1]) # resolution
+ o32(colors) # colors used
+ o32(colors) # colors important
)
We can see here that the compression field in the output file header is hard-coded to none, indicating that compression is not supported when writing a file.
If you would like a work-around, ImageMagick can convert any image format to 8-bit RLE BMP like this:
magick INPUTIMAGE.xxx -type palette -compress RLE result.bmp # where XXX is PNG, JPG, TIFF, GIF, TGA etc
Check the result with exiftool
like this:
exiftool -filename -filesize -compression result.bmp
File Name : result.bmp
File Size : 1168 bytes
Compression : 8-Bit RLE
Note that there are Python bindings to ImageMagick via wand, so you can achieve the same effect in Python like this:
#!/usr/bin/env python3
# You may need this before running on macOS
# export MAGICK_HOME=/opt/homebrew
from wand.image import Image
# Load image , or create pseudo image
with Image(filename='gradient:red-blue') as img:
img.type = 'palette'
img.compression = 'rle'
img.save(filename='result.bmp')