#!/usr/bin/env python3 """ Generate test BMP images for verifying Bitmap.cpp format support. Creates BMP files at 480x800 (CrossPoint display in portrait orientation). Test images use patterns designed to reveal dithering artifacts: - Checkerboard: sharp edges between gray levels, dithering adds noise at boundaries - Fine lines: thin 1px lines on contrasting background, dithering smears them - Mixed blocks: small rectangles of alternating grays, dithering blurs transitions - Gradient band: smooth transition in the middle, clean grays top/bottom Formats generated: - 1-bit: black & white (baseline, never dithered) - 2-bit: 4-level grayscale (non-standard CrossPoint extension, won't open on PC) - 4-bit: 4-color grayscale palette (standard BMP, new support) - 8-bit: 4-color grayscale palette (colorsUsed=4, should skip dithering) - 8-bit: 256-color grayscale (full palette, should be dithered) - 24-bit: RGB grayscale gradient (should be dithered) Usage: python generate_test_bmps.py [output_dir] Default output_dir: ./test_bmps/ """ import struct import os import sys WIDTH = 480 HEIGHT = 800 # The 4 e-ink gray levels (luminance values) GRAY_LEVELS = [0, 85, 170, 255] def write_bmp_file_header(f, pixel_data_offset, file_size): f.write(b'BM') f.write(struct.pack('> 3] |= (0x80 >> (x & 7)) f.write(row) print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)") def generate_2bit(path): """2-bit BMP: 4-level grayscale test pattern (non-standard, CrossPoint extension).""" bpp = 2 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette) for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): idx = get_test_pattern_index(x, y, WIDTH, HEIGHT) byte_pos = x >> 2 bit_shift = 6 - ((x & 3) * 2) row[byte_pos] |= (idx << bit_shift) f.write(row) print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)") def generate_4bit(path): """4-bit BMP: 4-color grayscale test pattern (standard, should skip dithering).""" bpp = 4 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette) for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): idx = get_test_pattern_index(x, y, WIDTH, HEIGHT) byte_pos = x >> 1 if x & 1: row[byte_pos] |= idx else: row[byte_pos] |= (idx << 4) f.write(row) print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)") def generate_8bit_4colors(path): """8-bit BMP with only 4 palette entries (colorsUsed=4, should skip dithering).""" bpp = 8 palette = GRAY_LEVELS row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette) for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): row[x] = get_test_pattern_index(x, y, WIDTH, HEIGHT) f.write(row) print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)") def generate_8bit_256colors(path): """8-bit BMP with full 256 palette (should be dithered normally).""" bpp = 8 palette = list(range(256)) row_bytes = (WIDTH * bpp + 31) // 32 * 4 palette_size = len(palette) * 4 pixel_offset = 14 + 40 + palette_size file_size = pixel_offset + row_bytes * HEIGHT with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, len(palette)) write_palette(f, palette) for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): row[x] = get_test_pattern_lum(x, y, WIDTH, HEIGHT) f.write(row) print(f" Created: {path} ({bpp}-bit, {len(palette)} colors)") def generate_24bit(path): """24-bit BMP: RGB grayscale test pattern (should be dithered normally).""" bpp = 24 row_bytes = (WIDTH * bpp + 31) // 32 * 4 pixel_offset = 14 + 40 file_size = pixel_offset + row_bytes * HEIGHT with open(path, 'wb') as f: write_bmp_file_header(f, pixel_offset, file_size) write_bmp_dib_header(f, WIDTH, HEIGHT, bpp, 0) for y in range(HEIGHT): row = bytearray(row_bytes) for x in range(WIDTH): gray = get_test_pattern_lum(x, y, WIDTH, HEIGHT) offset = x * 3 row[offset] = gray # B row[offset + 1] = gray # G row[offset + 2] = gray # R f.write(row) print(f" Created: {path} ({bpp}-bit)") def main(): output_dir = sys.argv[1] if len(sys.argv) > 1 else './test_bmps' os.makedirs(output_dir, exist_ok=True) print(f"Generating test BMPs in {output_dir}/") print(f"Resolution: {WIDTH}x{HEIGHT}") print() generate_1bit(os.path.join(output_dir, 'test_1bit_bw.bmp')) generate_2bit(os.path.join(output_dir, 'test_2bit_4gray.bmp')) generate_4bit(os.path.join(output_dir, 'test_4bit_4gray.bmp')) generate_8bit_4colors(os.path.join(output_dir, 'test_8bit_4colors.bmp')) generate_8bit_256colors(os.path.join(output_dir, 'test_8bit_256gray_gradient.bmp')) generate_24bit(os.path.join(output_dir, 'test_24bit_gradient.bmp')) print() print("Test pattern layout (4 horizontal bands):") print(" Band 1 (top): 16x16 checkerboard cycling all 4 gray levels") print(" Band 2: Fine 1px horizontal lines alternating grays") print(" Band 3: 4 stripes with nested 4x4 checkerboard detail") print(" Band 4 (bottom): Diagonal bands cycling all 4 levels") print() print("What to look for:") print(" Direct mapping: Sharp, clean edges between gray blocks") print(" Dithering: Noisy/speckled boundaries, smeared fine lines") print() print("Expected results on device:") print(" 1-bit: Clean B&W checkerboard, no dithering") print(" 2-bit: Clean 4-gray pattern, no dithering (non-standard BMP, won't open on PC)") print(" 4-bit: Clean 4-gray pattern, no dithering (standard BMP, viewable on PC)") print(" 8-bit (4 colors): Clean 4-gray pattern, no dithering (standard BMP, viewable on PC)") print(" 8-bit (256 colors): Same layout but with intermediate grays, WITH dithering") print(" 24-bit: Same layout but with intermediate grays, WITH dithering") print() print("Note: 2-bit BMP is a non-standard CrossPoint extension. Standard image viewers") print("will not open it. Use the 4-bit BMP instead for images created with standard tools") print("(e.g. ImageMagick: convert input.png -colorspace Gray -colors 4 -depth 4 BMP3:output.bmp)") print() print("Copy files to /sleep/ folder on SD card to test as custom sleep screen images.") if __name__ == '__main__': main()