Skip to content

Commit

Permalink
Add RGB565 and RGB888 color support to Quantum Painter (qmk#19382)
Browse files Browse the repository at this point in the history
  • Loading branch information
infinityis authored Jan 14, 2023
1 parent 5873fbe commit 45851a1
Show file tree
Hide file tree
Showing 21 changed files with 227 additions and 44 deletions.
59 changes: 38 additions & 21 deletions docs/quantum_painter.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ Supported devices:

## Quantum Painter Configuration :id=quantum-painter-config

| Option | Default | Purpose |
|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
| `QUANTUM_PAINTER_LOAD_FONTS_TO_RAM` | `FALSE` | Whether or not fonts should be loaded to RAM. Relevant for fonts stored in off-chip persistent storage, such as external flash. |
| `QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE` | `32` | The limit of the amount of pixel data that can be transmitted in one transaction to the display. Higher values require more RAM on the MCU. |
| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |
| Option | Default | Purpose |
|------------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
| `QUANTUM_PAINTER_LOAD_FONTS_TO_RAM` | `FALSE` | Whether or not fonts should be loaded to RAM. Relevant for fonts stored in off-chip persistent storage, such as external flash. |
| `QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE` | `32` | The limit of the amount of pixel data that can be transmitted in one transaction to the display. Higher values require more RAM on the MCU. |
| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
| `QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS` | `FALSE` | If native color range is supported. Requires significantly more RAM on the MCU. |
| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |

Drivers have their own set of configurable options, and are described in their respective sections.

Expand All @@ -63,7 +64,7 @@ options:
-d, --no-deltas Disables the use of delta frames when encoding animations.
-r, --no-rle Disables the use of RLE when encoding images.
-f FORMAT, --format FORMAT
Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
Output format, valid types: rgb888, rgb565, pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
-o OUTPUT, --output OUTPUT
Specify output directory. Defaults to same directory as input.
-i INPUT, --input INPUT
Expand All @@ -77,16 +78,18 @@ The `OUTPUT` argument needs to be a directory, and will default to the same dire

The `FORMAT` argument can be any of the following:

| Format | Meaning |
|-----------|-----------------------------------------------------------------------|
| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `pal16` | 16-color palette |
| `pal4` | 4-color palette |
| `pal2` | 2-color palette |
| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `mono16` | 16-shade grayscale |
| `mono4` | 4-shade grayscale |
| `mono2` | 2-shade grayscale |
| Format | Meaning |
|-----------|-------------------------------------------------------------------------------------------|
| `rgb888` | 16,777,216 colors in 8-8-8 RGB format (requires `QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS`) |
| `rgb565` | 65,536 colors in 5-6-5 RGB format (requires `QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS`) |
| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `pal16` | 16-color palette |
| `pal4` | 4-color palette |
| `pal2` | 2-color palette |
| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `mono16` | 16-shade grayscale |
| `mono4` | 4-shade grayscale |
| `mono2` | 2-shade grayscale |

**Examples**:

Expand Down Expand Up @@ -154,7 +157,7 @@ options:
-w, --raw Writes out the QFF file as raw data instead of c/h combo.
-r, --no-rle Disable the use of RLE to minimise converted image size.
-f FORMAT, --format FORMAT
Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
Output format, valid types: rgb565, pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
-u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
Also generate the specified unicode glyphs.
-n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
Expand Down Expand Up @@ -215,6 +218,8 @@ The maximum number of displays can be configured by changing the following in yo
#define GC9A01_NUM_DEVICES 3
```

Native color format rgb565 is compatible with GC9A01

#### ** ILI9163 **

Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`:
Expand All @@ -239,6 +244,8 @@ The maximum number of displays can be configured by changing the following in yo
#define ILI9163_NUM_DEVICES 3
```

Native color format rgb565 is compatible with ILI9163

#### ** ILI9341 **

Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`:
Expand All @@ -263,6 +270,8 @@ The maximum number of displays can be configured by changing the following in yo
#define ILI9341_NUM_DEVICES 3
```

Native color format rgb565 is compatible with ILI9341

#### ** ILI9488 **

Enabling support for the ILI9488 in Quantum Painter is done by adding the following to `rules.mk`:
Expand All @@ -287,6 +296,8 @@ The maximum number of displays can be configured by changing the following in yo
#define ILI9488_NUM_DEVICES 3
```

Native color format rgb888 is compatible with ILI9488

#### ** SSD1351 **

Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:
Expand All @@ -311,6 +322,8 @@ The maximum number of displays can be configured by changing the following in yo
#define SSD1351_NUM_DEVICES 3
```

Native color format rgb565 is compatible with SSD1351

#### ** ST7735 **

Enabling support for the ST7735 in Quantum Painter is done by adding the following to `rules.mk`:
Expand All @@ -335,6 +348,8 @@ The maximum number of displays can be configured by changing the following in yo
#define ST7735_NUM_DEVICES 3
```

Native color format rgb565 is compatible with ST7735

!> Some ST7735 devices are known to have different drawing offsets -- despite being a 132x162 pixel display controller internally, some display panels are only 80x160, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.

#### ** ST7789 **
Expand All @@ -361,6 +376,8 @@ The maximum number of displays can be configured by changing the following in yo
#define ST7789_NUM_DEVICES 3
```

Native color format rgb565 is compatible with ST7789

!> Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.

<!-- tabs:end -->
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/gc9a01/qp_gc9a01.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = {
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
7 changes: 7 additions & 0 deletions drivers/painter/generic/qp_rgb565_surface.c
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ static bool qp_rgb565_surface_append_pixels_rgb565(painter_device_t device, uint
return true;
}

// Append data to the target location
static bool qp_rgb565_surface_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
target_buffer[pixdata_offset] = pixdata_byte;
return true;
}

const struct painter_driver_vtable_t rgb565_surface_driver_vtable = {
.init = qp_rgb565_surface_init,
.power = qp_rgb565_surface_power,
Expand All @@ -173,6 +179,7 @@ const struct painter_driver_vtable_t rgb565_surface_driver_vtable = {
.viewport = qp_rgb565_surface_viewport,
.palette_convert = qp_rgb565_surface_palette_convert_rgb565_swapped,
.append_pixels = qp_rgb565_surface_append_pixels_rgb565,
.append_pixdata = qp_rgb565_surface_append_pixdata,
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/ili9xxx/qp_ili9163.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable =
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/ili9xxx/qp_ili9341.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable =
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/ili9xxx/qp_ili9488.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t ili9488_driver_vtable =
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb888,
.append_pixels = qp_tft_panel_append_pixels_rgb888,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/ssd1351/qp_ssd1351.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable =
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 1,
.swap_window_coords = true,
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/st77xx/qp_st7735.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t st7735_driver_vtable = {
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
1 change: 1 addition & 0 deletions drivers/painter/st77xx/qp_st7789.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = {
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert_rgb565_swapped,
.append_pixels = qp_tft_panel_append_pixels_rgb565,
.append_pixdata = qp_tft_panel_append_pixdata,
},
.num_window_bytes = 2,
.swap_window_coords = false,
Expand Down
5 changes: 5 additions & 0 deletions drivers/painter/tft_panel/qp_tft_panel.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ bool qp_tft_panel_append_pixels_rgb888(painter_device_t device, uint8_t *target_
}
return true;
}

bool qp_tft_panel_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
target_buffer[pixdata_offset] = pixdata_byte;
return true;
}
2 changes: 2 additions & 0 deletions drivers/painter/tft_panel/qp_tft_panel.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,5 @@ bool qp_tft_panel_palette_convert_rgb888(painter_device_t device, int16_t palett

bool qp_tft_panel_append_pixels_rgb565(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
bool qp_tft_panel_append_pixels_rgb888(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);

bool qp_tft_panel_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte);
80 changes: 75 additions & 5 deletions lib/python/qmk/painter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@

# The list of valid formats Quantum Painter supports
valid_formats = {
'rgb888': {
'image_format': 'IMAGE_FORMAT_RGB888',
'bpp': 24,
'has_palette': False,
'num_colors': 16777216,
'image_format_byte': 0x09, # see qp_internal_formats.h
},
'rgb565': {
'image_format': 'IMAGE_FORMAT_RGB565',
'bpp': 16,
'has_palette': False,
'num_colors': 65536,
'image_format_byte': 0x08, # see qp_internal_formats.h
},
'pal256': {
'image_format': 'IMAGE_FORMAT_PALETTE',
'bpp': 8,
Expand Down Expand Up @@ -144,19 +158,33 @@ def convert_requested_format(im, format):
ncolors = format["num_colors"]
image_format = format["image_format"]

# Ensure we have a valid number of colors for the palette
if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
raise ValueError("Number of colors must be 2, 4, 16, or 256.")

# Work out where we're getting the bytes from
if image_format == 'IMAGE_FORMAT_GRAYSCALE':
# Ensure we have a valid number of colors for the palette
if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
raise ValueError("Number of colors must be 2, 4, 16, or 256.")
# If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
im = ImageOps.grayscale(im)
im = im.convert("RGB")
elif image_format == 'IMAGE_FORMAT_PALETTE':
# Ensure we have a valid number of colors for the palette
if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
raise ValueError("Number of colors must be 2, 4, 16, or 256.")
# If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
im = im.convert("RGB")
im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
elif image_format == 'IMAGE_FORMAT_RGB565':
# Ensure we have a valid number of colors for the palette
if ncolors != 65536:
raise ValueError("Number of colors must be 65536.")
# If color, convert input to RGB
im = im.convert("RGB")
elif image_format == 'IMAGE_FORMAT_RGB888':
# Ensure we have a valid number of colors for the palette
if ncolors != 1677216:
raise ValueError("Number of colors must be 16777216.")
# If color, convert input to RGB
im = im.convert("RGB")

return im

Expand All @@ -170,8 +198,12 @@ def convert_image_bytes(im, format):
image_format = format["image_format"]
shifter = int(math.log2(ncolors))
pixels_per_byte = int(8 / math.log2(ncolors))
bytes_per_pixel = math.ceil(math.log2(ncolors) / 8)
(width, height) = im.size
expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
if (pixels_per_byte != 0):
expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
else:
expected_byte_count = width * height * bytes_per_pixel

if image_format == 'IMAGE_FORMAT_GRAYSCALE':
# Take the red channel
Expand Down Expand Up @@ -212,6 +244,44 @@ def convert_image_bytes(im, format):
byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
bytearray.append(byte)

if image_format == 'IMAGE_FORMAT_RGB565':
# Take the red, green, and blue channels
image_bytes_red = im.tobytes("raw", "R")
image_bytes_green = im.tobytes("raw", "G")
image_bytes_blue = im.tobytes("raw", "B")
image_pixels_len = len(image_bytes_red)

# No palette
palette = None

bytearray = []
for x in range(image_pixels_len):
# 5 bits of red, 3 MSb of green
byte = ((image_bytes_red[x] >> 3 & 0x1F) << 3) + (image_bytes_green[x] >> 5 & 0x07)
bytearray.append(byte)
# 3 LSb of green, 5 bits of blue
byte = ((image_bytes_green[x] >> 2 & 0x07) << 5) + (image_bytes_blue[x] >> 3 & 0x1F)
bytearray.append(byte)

if image_format == 'IMAGE_FORMAT_RGB888':
# Take the red, green, and blue channels
image_bytes_red = im.tobytes("raw", "R")
image_bytes_green = im.tobytes("raw", "G")
image_bytes_blue = im.tobytes("raw", "B")
image_pixels_len = len(image_bytes_red)

# No palette
palette = None

bytearray = []
for x in range(image_pixels_len):
byte = image_bytes_red[x]
bytearray.append(byte)
byte = image_bytes_green[x]
bytearray.append(byte)
byte = image_bytes_blue[x]
bytearray.append(byte)

if len(bytearray) != expected_byte_count:
raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")

Expand Down
Loading

0 comments on commit 45851a1

Please sign in to comment.