switching colors in PROGMEM stored icons

Hello,

as part of my ongoing Arduino trip computer project, I have been toying with the idea of implementing a “nightttime” design of the screen. At the moment, all the numbers on the 1.44’’ Adafruit TFT screen are displayed in black on white, about like this:

The numbers - and all the icons - are all stored as const uint16_t arrays in PROGMEM, like this:

const uint16_t s1[192] PROGMEM = {
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffdf, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xbdf7, 0xb5b6, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xdefb, 0x1082, 0xce59, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x39c7, 0x0000, 0xdefb, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x0861, 0x0000, 0xf79e, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffdf, 0x0000, 0x0020, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffdf, 0x2945, 0x18e3, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xdefb, 0xc638, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xd6ba, 0xd6ba, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xd6ba, 0x1082, 0x52aa, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x94b2, 0x0000, 0x632c, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x8410, 0x0000, 0x7bcf, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x8430, 0x0000, 0x8c71, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xf79e, 0x18e3, 0xa534, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xbdd7, 0xbdd7, 0xffff, 
    0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffdf, 0xffff
};

To put the numbers on the screen, I use the following functions:

//------------- Image and Text Drawing Functions

void drawProgmemIcon(const uint16_t* icon, int16_t x, int16_t y, uint8_t width, uint8_t height) {
  uint16_t count = 0;
  // Set up a window the right size to stream pixels into
  tft.setAddrWindow(x, y, x + width - 1, y + height - 1);

  for (uint16_t i = 0; i < height; i++) {
    for (uint16_t j = 0; j < width; j++) {
  tft.pushColor(pgm_read_word(&icon[count++]));
    }
  }
}


void plotFloat(float value, int poX, int poY, uint16_t font)
{

  if (value < -20) return;    // Temperature sensors only support temperatures of -20°C
  if (value > 149.9) return; // This function handles values in range 0.0 - 149.9

  char str[13];
  byte index = 0;
  byte digit = 0;
  byte bmpWidth  = 0;
  byte bmpHeight = 0;

  // Convert value to a string of length 4 with 1 decimal place

  if (font == "large")  dtostrf(value, 4, 1, str);

  else if (font == "small") dtostrf(value, 5, 1, str);


  while (str[index]) {
    if ( str[index] == ' ') // Is it a 'space' character
    {

      if (font == "large") {
        bmpWidth  = pgm_read_byte( &cWidths[0]);
        bmpHeight = pgm_read_byte( &cHeights[0]);
      }

      else if (font == "small") {
        bmpWidth  = pgm_read_byte( &sWidths[0]);
        bmpHeight = pgm_read_byte( &sHeights[0]);
      }
      // Use fill rectangle as there is no 'space' character in the bitmap set
      tft.fillRect(poX, poY, bmpWidth, bmpHeight, ST7735_WHITE); // Draw a 'space'
      // drawIcon(aBMPmap[0], poX, poY, bmpWidth, bmpHeight); // Leading zero instead of space (alternative to above line)
      poX += bmpWidth; // Width of a space same as 0 digit to over-write previous numbers
    }
    else
    {
      digit = str[index];          // Fetch ascii code
      if (digit == '.') digit = 10; // Check for decimal point, set to bitmap 10
      else if (digit == '-') digit = 11; // allow for negative values
      else digit -= '0';            // Shift to range 0-9

      if (font == "large") {
        bmpWidth  = pgm_read_byte( &cWidths[digit]);
        bmpHeight = pgm_read_byte( &cHeights[digit]);
        // Draw the bitmap
        drawProgmemIcon(cBMPmap[digit], poX, poY, bmpWidth, bmpHeight);
      }

      else if (font == "small") {


        bmpWidth  = pgm_read_byte( &sWidths[digit]);
        bmpHeight = pgm_read_byte( &sHeights[digit]);
        // Draw the bitmap
        drawProgmemIcon(sBMPmap[digit], poX, poY, bmpWidth, bmpHeight);
      }

      // Move x coord plot position
      poX += bmpWidth;
    }
    index++;  // Increment pointer to next character
  }
}


void plotInteger(long value, int poX, int poY, uint16_t font)
{
  char str[12];
  byte index = 0; // Array index
  byte digit = 0;
  byte bmpWidth  = 0;
  byte bmpHeight = 0;

  // Convert value to a string
  ltoa(value, str, 10);

  // While character pointed to is not a null (strings are zero terminated)
  while (str[index]) {
    digit = str[index++] - '0'; // Fetch ascii code, shift to range 0-9 and increment index

    if (digit == '-') digit = 11; // allow for negative values

    bmpWidth  = pgm_read_byte( &cWidths[digit]); // Get width
    bmpHeight = pgm_read_byte( &cHeights[digit]); //Get height

    // Draw the bitmap
    drawProgmemIcon(cBMPmap[digit], poX, poY, bmpWidth, bmpHeight);
    // Move x coord plot position
    poX += bmpWidth;
  }
}

My Atmega1284P which will power the display is already brimming with PROGMEM icons by now, as all you can see in the above picture comes from PROGMEM.

To implement some sort of “nighttime” display, which will be easier on the eyes in the dark, I’ve had the idea of just swapping out black and white pixels against each other and drawing them on a black background, because again, the Atmega1284P is “full” and won’t take up another complete set of “nighttime” icons and numbers.

The main reason why I chose the Atmega1284P to power the display was its huge memory and that I will be able to have near-unnoticeable refresh latencies compared to loading all the icons and digits from SD, and that my trip computer display will remain fully operational if there is a SD card read failure. But what remains of the Atmega’s memory now, I will need for my actual program code.

One thing I’ve already tried was to alter the drawProgmemIcon function like this:

 for (uint16_t i = 0; i < height; i++) {
    for (uint16_t j = 0; j < width; j++) {

      uint16_t currentColor = pgm_read_word(&icon[count++]);

      String currentColorString = String(currentColor);
      currentColorString.replace("ffff", "0000");

      uint16_t currentColorShow = currentColorString.toInt();
      
      tft.pushColor(currentColorShow);
     
    }
  }

This did not work, the display looks the same, and all it does is slow down the screen refresh time considerably.

Is there any way that my “pixel swap” idea could work as part of this code?

You can simply map a different colour Palette. In practice, I bet that your icons have no more than 16 colours. Certainly they will have less than 256 distinct colours.

Instead of storing uint16_t for each pixel, you use uint8_t for an index into your Palette. IrfanView can transform your JPEG or BMP into the necessary format.

Your 1284P has got enough SRAM to process JPEGs. Or to store a 256 colour Palette. Even a Uno can manage a 16 colour palette.

David.

Right... I think we've already talked about 16 vs. 256 colors for my icons and numbers in another thread. The thing is, with 16 colors, you just won't get the smooth edges and the graphics blending in with the background like they do with 256 colors, even if only a handful of them will actually be used. Just to have more colors to choose from makes it look more "smooth".

Like when you're describing something in a sentence, maybe in a foreign language. Surely that sentence will not contain all the words in that language that you know, but just having many different, nuanced words available inside your head for use in that sentence will give you the possibility of describing something much more accurately and in much more detail than if you only knew a handful of words in that language at all.

It's that whole thing about anti-aliasing, if you remember... I've tried 8-bit colors on this project, and I was a bit disappointed at the way they looked, to be honest.

So is there really no way you can swap out values from a const uint16_t array for use with my drawing functions?

Yes, I was very impressed by the anti-aliasing. It really improved the look of your monochrome icons.

From a memory point of view. Each 16x16 icon has 256 pixels taking 512 bytes (as 65536-colour). Or 256 bytes as 256-colour. But you need 512 bytes to store the Palette.

If you have 20 icons. 20 x 512 = 10240.
Or 20 x 256 + single Palette = 5120 + 512.

If you have a Daytime Palette and a Nighttime Palette the flash storage is trivial. If you copy the Palette to SRAM, it does take up valuable memory.

I suggest that you play with your icons in IrfanView. See if you can tell the difference between 256-colour bitmap. I make no claims for 16 colour. See for yourself.

David.

Edit. I have attached a ZIP with the same bitmap in different formats. You can display on your PC. Display on TFT with my MCUFRIEND_kbv library example. Edit with IrfanView.

carguy.zip (29.2 KB)

Thanks for creating those examples... sorry I didn't get back to you sooner... been busy with other stuff... :(

I don't use IrfanView but GIMP, but it has a feature where you can convert images into different color spaces, for example "indexed 255".

You are right in that the differences are negligible between 256-color and 65536-color. If it comes at the benefit of being able to create a nighttime palette that I can load right from PROGMEM, then it's a compromise worth making.

I haven't had time to convert all my fonts and icons to 256-color. But if I wanted to use "LCD Image converter" to do the job, how would I go about creating the PROGMEM code for them? Is it as simple as setting the block size to "8-bit" in the "Options" menu?

Your existing code:

    for (uint16_t i = 0; i < height; i++) {
        for (uint16_t j = 0; j < width; j++) {
            tft.pushColor(pgm_read_word(&icon[count++]));
        }
    }

You can just alter the color brightness at nighttime (brutally)

    for (uint16_t i = 0; i < height; i++) {
        for (uint16_t j = 0; j < width; j++) {
            uint16_t color = pgm_read_word(&icon[count++]));
            if (night_time) color &= ~0x8410; //remove msb of each colour
            tft.pushColor(color);
        }
    }

Or you could use a lookup table.

From an efficiency point of view, I would probably use two Palette tables.

You can use IrfanView to process JPEGs, BMPs etc. And BMP format supports Palettes.
I would be pretty sure that ANY respectable Image program will support resizing, colour reduction etc.

I would copy the working Palette into SRAM. It makes the colour indexing simpler.
But you could do the indexing from PROGMEM. After all, most Fonts are rendered from PROGMEM.

David.

silly question maybe, but how do I convert an image to 8-bit, 256-color hex code which I can then use in my Arduino code?

LCD Image Converter only seems to do 16-bit encoding.

So I have found a way to convert my 16-bit images into 8-bit. I saved one of them as a 256-color GIF as a test and then extracted the hex values from it with a hex editor, which I have put into a PROGMEM array:

const uint8_t headergif[3456] PROGMEM = {

0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x80, 0x00, 0x1b, 0x00, 0xe7, 0xff, 0x00, 0xa1, 0x00, 0x02, 0xa3, 0x02, 0x0c, 0x91, 0x08, 
0x08, 0x94, 0x0e, 0x12, 0x9f, 0x0e, 0x0a, 0x97, 0x12, 0x14, 0xa1, 0x12, 0x0c, 0x98, 0x15, 0x1b, 0xa2, 0x14, 0x14, 0xa3, 0x16, 
0x14, 0xa5, 0x18, 0x15, 0xa6, 0x19, 0x16, 0xa7, 0x1b, 0x17, 0xa7, 0x1b, 0x1d, 0x98, 0x20, 0x1c, 0xaa, 0x1f, 0x20, 0x9c, 0x25, 

// you get the idea


}
;

I’ve attached the PROGMEM icon as a .txt file as it was too large to be included in this post.

But now what do I do with that? I didn’t find a function in the Adafruit GFX library that prints out 8-bit color images on the screen.

I have modified my drawProgmemIcon function like this:

void drawProgmemIcon8bit(const uint8_t* icon, int16_t x, int16_t y, uint8_t width, uint8_t height) {
  uint16_t count = 0;
  // Set up a window the right size to stream pixels into
  tft.setAddrWindow(x, y, x + width - 1, y + height - 1);

  for (uint16_t i = 0; i < height; i++) {
    for (uint16_t j = 0; j < width; j++) {

      tft.pushColor(pgm_read_word(&icon[count++]));
    }
  }
}

which I then call like this:

drawProgmemIcon8bit(headergif, 0, 0, 128, 27);

but, while it dumps out a bunch of pixels onto the screen, this does not display an 8-bit image correctly.

I suppose that function does not work with 8-bit images?

progmemicon.txt (17 KB)

header.gif

I have rendered many BMP formats on a TFT.
I have rendered JPEG on a TFT.

I have never tried a GIF. It appears that SmartMatrix library can render 32x32 GIFs. But I have not tried it.

BMP is not compressed at all. Ok, the colour Palette formats are smaller than 24-bit or 16-bit formats.
JPEG compresses very well. You need at least a 128kB AVR to decode it. ARM is a lot faster.

If you have an SD card, size is not an issue. You can display a (non-Palette) BMP with a 8kB AVR.

Is there any reason for your choice of GIF?

David.

It just dawned on me that I may have made an error in thinking when I stored the image file as a gif.

CompuServe GIF is a format with 8 bits per pixel, but the file itself does not contain (uncompressed) pixel information. It contains 8-bit hex values, but they are the result of running the pixel color information through the LZW algorithm when saving a file.

I have also tried saving images as 8-bit bitmaps with MS Paint, but the results were quite disappointing.

So I guess I am kind of back at square one.

The reason why I want to have so many images in PROGMEM is that I want the trip computer to work even when there is an SD card read fail, and because access latencies just seem to be much smaller when an image comes from PROGMEM. Particularly when I parse sensor values into bitmaps that are then displayed on the screen. Saving images as .raw on the SD card did bring some improvement, but still nothing seems to beat getting your graphics straight from the chip’s own memory.

I am using an Atmega1284P for this project. So there is indeed a fair bit of memory to be had.

Yes, I can see the attraction of storing all the graphics in AVR Flash memory if possible. Which comes down to raw bitmaps or some form of compressed format.

Even 16-colour bitmaps with external Palette takes a lot of memory. e.g. 39kB for 240x320. About 82kB for 256-colour.

You can store many more images in JPEG. I don't know about GIF. I might havve a play with the SmartMatrix library one day.

You can manipulate images with regular PC programs. Load from SD card. When happy, you can store in program memory.

David.

ok I have now stored an image as an 8-bit bitmap using GIMP. According to the file properties that Windows Explorer now shows me, it is indeed an 8-bit image with 4.49 kb (127x27 px, see attached image).

Where do I go from here if I want to store that image in PROGMEM and display it on my TFT screen as an 8-bit color image? I have downloaded the latest Adafruit GFX library (mine was from over a year ago), but it only contains functions to display 8-bit grayscale images.

headerbmp.zip (2.87 KB)

Look at the showBMP examples in my MCUFRIEND_kbv library.

The sketch should work with most TFT libraries. Note that a 256 colour Palette will take up almost as much memory space (1024B) as the actual bitmap (3456B).

I can post a sketch to render BMP from Flash if you have difficulties. I understand that you are using Adafruit_ST7735 library.

David.

Yes, I am using the ST7735. It's an Adafruit 1.44'' TFT.

I have downloaded and installed your library. The showBMP example looks promising, as it does include the possibility of displaying 8-bit images.

Just to be clear - storing an image as 8-bit in PROGMEM will still only use half the memory as the same image stored as 16-bit, right?

I'm still not sure how to extract the 8-bit hex color information from an 8-bit bitmap so that only it will be in the PROGMEM array. You'd somehow have to get rid of the header, wouldn't you...

I will post a sketch tomorrow with BMP embedded in Flash. It is my bedtime.

I have been doing some reading on 8-bit indexed colors. Apparently, the way it works is that you’ve got a color palette as part of an indexed-color image file that contains the color palette values as an array of up to 256 colors. The pixels are then stored one by one in another array which contains values from 0 to 255 which correspond to values in the RGB color definition array, is that correct?

It turns out that GIMP lets you create a complete .h file with all the relevant information. I’ve made one for a 256-color version of the “Adafruit Parrot” image. I’ve enclosed both the image and the .h file at the bottom of this post.

As part of this .h file, the color information is in this form:

/*  GIMP header image file format (INDEXED): D:\parrot-h.h  */

static unsigned int width = 128;
static unsigned int height = 128;

/*  Call this macro repeatedly.  After each use, the pixel data can be extracted  */

#define HEADER_PIXEL(data,pixel) {\
pixel[0] = header_data_cmap[(unsigned char)data[0]][0]; \
pixel[1] = header_data_cmap[(unsigned char)data[0]][1]; \
pixel[2] = header_data_cmap[(unsigned char)data[0]][2]; \
data ++; }

static char header_data_cmap[256][3] = {
 { 24, 21, 26},
 { 46, 16, 18},
 { 28, 31, 17},
 { 32, 30, 20},
 { 36, 40, 24},
 { 37, 41, 19},
 { 45, 39, 19},
 { 43, 39, 27},
 { 32, 46, 23},
 { 38, 45, 16},
 { 46, 44, 16},
 { 45, 52, 20},
 { 49, 52, 29},
 { 39, 56, 30},
 { 55, 52, 24},
 { 39, 57, 23},
 { 49, 53, 35},
 { 39, 56, 37},
 { 57, 51, 36},


// .... you get the idea...

And then there’s a separate array with each pixel:

static char header_data[] = {
 254,252,242,216,105,185,133,22,55,189,234,190,36,11,33,117,
 220,189,102,142,167,188,121,87,84,48,58,77,151,121,56,17,
 13,13,32,60,105,148,172,236,242,244,214,213,237,231,203,179,
 167,194,213,146,77,73,73,188,150,52,37,18,53,82,86,85,
 55,52,36,18,18,16,9,12,18,38,46,46,60,48,45,130,
 221,190,84,45,48,150,188,142,99,77,45,15,9,9,30,88,
 102,15,4,11,42,42,20,8,4,5,5,8,12,40,70,133,
 56,9,2,2,2,4,4,5,5,5,30,88,102,78,48,36,
 219,253,235,219,195,190,55,11,107,220,240,108,11,33,52,117,

// ...you get the idea...

Looking at your code in the showBMP sketch, apparently this RGB color information needs to be parsed into RGB565:

switch (bmpDepth) {          // Convert pixel from BMP to TFT format
                        case 24:
                            b = sdbuffer[buffidx++];
                            g = sdbuffer[buffidx++];
                            r = sdbuffer[buffidx++];
                            color = tft.color565(r, g, b);
                            break;

Would there be any value in converting the RGB color palette in header_data_cmap[256][3] into RGB565 in the first place? Would that save time in printing the pixels out onto the screen?

Also, if you take a design like this one here:

Let’s say the digits and icons on the screen all come from PROGMEM arrays that index a color palette like the one above. Wouldn’t that mean that I would only have to define one color palette once, for all the PROGMEM images on the screen?

parrot.zip (14.1 KB)

parrot-h1.h (61.4 KB)

EDIT:

I have tried to implement what I said in my last post, but so far, it’s not working as such. It compiles, but just dumps out a bunch of garbled pixels on the screen. Here’s my sketch code:

#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library
#include <SPI.h>
#include <avr/pgmspace.h>

// Including the test image from testimage.h
#include "testimage.h"

#define TFT_CS  12
#define TFT_RST 13
#define TFT_DC  14

#define SD_CS 4

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS,  TFT_DC, TFT_RST);

#define TFT_SCLK 7
#define TFT_MOSI 5

// converting RGB  values to RGB565
uint16_t rgb_to_565(uint16_t red, uint16_t green, uint16_t blue){
    
  uint16_t rgb565 = ((((red>>3)<<11) | ((green>>2)<<5) | (blue>>3)));

  return rgb565;
}


void drawProgmemIconIndexed(const char* const icon, int16_t x, int16_t y, uint8_t width, uint8_t height) {
  uint16_t count = 0;
  // Set up a window the right size to stream pixels into
  tft.setAddrWindow(x, y, x + width - 1, y + height - 1);

  for (uint16_t i = 0; i < height; i++) {
    for (uint16_t j = 0; j < width; j++) {

      byte whichColor = pgm_read_word(&icon[count++]);

      byte r = pgm_read_word(test_cmap[whichColor][0]);
      byte g = pgm_read_word(test_cmap[whichColor][1]);
      byte b = pgm_read_word(test_cmap[whichColor][2]);


      uint16_t color_565 = rgb_to_565(r, g, b);

      tft.pushColor(color_565);

     
    }
  }
}

void setup() {
  tft.initR(INITR_144GREENTAB);
  
  tft.setRotation(1);
 tft.fillRect(0, 99, 128, 128, ST7735_WHITE);
}

void loop() {

  drawProgmemIconIndexed(0, 0, 0, 25, 25);
}

You can also find this test sketch attached to this post, together with the “testimage.h” header file.

Can you give me a few pointers why my sketch isn’t working yet?

testsketch.ino (1.41 KB)

testimage.h (7.28 KB)

My apologies. I spent too much time in the pub on Sunday. And I went to a funeral on Monday.

I have attached an example sketch that renders some standard BMP files on a ST7735 screen.

The BMPs were generated by IrfanView from a regular 24-bit JPEG / BMP / PNG image.
And then the colour resolution reduced to 256 colours (8-bit palette) and 4-bit (16 colour palette)

The 16-colour does not look too bad. 256-colour looks good. IrfanView or GIMP might be able to do a better job if you configure carefully.

I have only tested on a Seeeduino and a Due. You might run into PROGMEM issues with your mega1284.

I have not looked at your code yet. I strongly advise against using decimal numbers in your data arrays. Hex is a lot easier to read.

David.

show_ladies_P.zip (98.9 KB)

No problem. In the mean time, I have busied myself getting a grasp of the indexed-color concept.

As I said, GIMP lets you export an indexed-color image as an .h header file, with the RGB color palette information in one array within that file, and an array of references to those colors in another array which then actually make up the image pixels.

But my idea is now that I don't have to supply a distinct color palette for each and every image. In GIMP, you can extract the color palette of one image, for example 256 colors in "indexed" mode, and apply that very same color palette to any number of other images that you will then also create. In other words, the subsequent images can be made to fit within that once-defined 256-color space. So in theory, you could have any number of images in PROGMEM which each only consist of 8-bit array elements per pixel, and which reference one common 16-bit color array/table.

If my thinking isn't entirely wrong, I would thus be able to store both a "daytime" and "nighttime" set of images and numbers on my 1284, at nearly the same amount of memory space that one set of fully defined 16-bit color images now occupies. True, I would still need two color palette arrays at 2x512 bytes for up to 256 colors defined as 16-bit, for day and night, but I could still cut in half the PROGMEM space that any one image occupies now.

I tried to implement that in the sketch and .h file in my last post. If I can get this to work in principle, it's probably indeed going to be best to convert all my RGB values from the three-dimensional color table array into a one-dimensional uint16_t array. For some reason, GIMP does not do that, so I am going to have to see if there's a tool available as software or online which can quickly convert a data dump, as it were, of those 256 different sets of RGB values into 16-bit hex values just one time so I can copy and paste them in my code.

Sadly, my current sketch from my last post isn't quite doing so far what I've just outlined; the test image in the .h file should look something like this:

But the sketch just throws out a bunch of mashed up pixels onto the screen.

EDIT: I have now simplified the sketch, and I’ve put everything into one .ino file.

I’ve managed to convert my RGB color definitions into what I believe to now be correct RGB565 16-bit colors.

Sadly, this sketch does not compile:

// 256-color indexed palette in RGB565 file format

const uint16_t colorTableDaytime[256] PROGMEM = {

0x0000, 0x2000, 0x4000, 0x3800, 0x881, 0x4000, 0x4000, 0x4800, 0x1082, 0x4801, 0x4021, 0x3841, 0x5020, 0x5801, 0x10A2, 0x4841, 
0x18C3, 0x18E3, 0x4081, 0x6060, 0x1903, 0x5083, 0x48A3, 0x50A2, 0x40C3, 0x6861, 0x48C2, 0x9041, 0x2124, 0x58C3, 0x50C4, 0x50E4, 
0x2945, 0x9062, 0x70C2, 0x4905, 0x5103, 0x2965, 0x4945, 0x3185, 0x98A3, 0x5944, 0x7124, 0x31A6, 0x98E3, 0x39C6, 0x6184, 0x9924, 
0x6186, 0x39E7, 0x59A6, 0x59C6, 0x9945, 0x81A4, 0x4228, 0x69E9, 0x71E6, 0x91C4, 0x4A49, 0xA186, 0x7207, 0x6A26, 0x4A69, 0xA1A7, 
0x6248, 0x7248, 0x7228, 0x6A49, 0x8208, 0x6A67, 0x52AA, 0xAA07, 0xAA08, 0x8289, 0x9A67, 0x72A9, 0x7AA7, 0x7AAA, 0x7AC8, 0x5AEB, 
0xAA69, 0xAA67, 0x7ACB, 0x632C, 0x8306, 0x8308, 0xA2C8, 0xAAC8, 0x92EA, 0xB2CB, 0x6B6D, 0xA30A, 0x932C, 0x7B4D, 0x834B, 0xB2EB, 
0x836D, 0x8B67, 0x73AE, 0xB30C, 0xBB29, 0xC32A, 0xB34A, 0x7BEF, 0x9BC7, 0x9BAD, 0x83CF, 0xBB6D, 0xAB8D, 0x9BE8, 0x8BCF, 0x7C0F, 
0x9BEB, 0xB3AD, 0x9C0A, 0xBBAE, 0x8430, 0x940E, 0x8C10, 0x9C28, 0x942E, 0xBBED, 0x8450, 0x9C30, 0x8C51, 0xBBEF, 0xAC48, 0xC3EF, 
0x9451, 0xCBED, 0xC40D, 0x8C71, 0xAC4F, 0xCC0E, 0xC42E, 0xBC4E, 0xA48B, 0xAC6C, 0xC430, 0x94B2, 0xD42E, 0xD42E, 0xB471, 0xA491, 
0xA4B2, 0xCC50, 0xB4A9, 0xC46F, 0xD44F, 0x94D2, 0xCC6F, 0xC490, 0xB4EA, 0x9CF3, 0xB4ED, 0xB4D3, 0xACF0, 0xC4D1, 0x9D13, 0xCCB2, 
0xD4B0, 0xA514, 0xBD2D, 0xBD2B, 0xD4D2, 0xA534, 0xC513, 0xB534, 0xAD55, 0xBD6E, 0xD514, 0xAD75, 0xB555, 0xC56C, 0xC58D, 0xAD95, 
0xC574, 0xD534, 0xBD96, 0xD573, 0xD574, 0xBDB3, 0xB5B6, 0xCDAD, 0xDD75, 0xDD73, 0xCD95, 0xB5D6, 0xDD96, 0xBDF7, 0xCDD6, 0xD60D, 
0xBDF8, 0xCDF6, 0xDDB7, 0xCE10, 0xD60E, 0xDDD7, 0xC617, 0xD630, 0xE5F7, 0xC638, 0xDE14, 0xCE36, 0xC658, 0xCE59, 0xE638, 0xCE99, 
0xE696, 0xE6B1, 0xEE79, 0xDE97, 0xD6BA, 0xDEB1, 0xDED2, 0xD6DA, 0xEE99, 0xE6D4, 0xE6BA, 0xDEFB, 0xEEBB, 0xDF1B, 0xF6DB, 0xE71B, 
0xEF34, 0xE737, 0xE73C, 0xEF1C, 0xEF55, 0xEF56, 0xEF58, 0xE73C, 0xE75C, 0xF73D, 0xEF5D, 0xF779, 0xF798, 0xEF7D, 0xF798, 0xFF7D, 
0xF79B, 0xFF99, 0xEF9D, 0xF7B9, 0xF79E, 0xEFBE, 0xFFBE, 0xF7BE, 0xFFDE, 0xF7DF, 0xFFDF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF


};


// image data as references to array elements of colorTableNighttime[256]

const uint8_t* const test[400] PROGMEM = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,25,19,10,0,0,0,0,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,3,5,0,0,0,0,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,7,29,15,15,15,5,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,1,3,21,21,17,6,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,3,5,0,0,0,0,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,13,12,11,11,11,10,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,7,17,18,18,25,6,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,3,31,2,2,2,4,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,8,16,5,5,5,2,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,1,8,3,3,3,22,
  0,0,0,0,
  0,0,0,0,0,0,0,0,0,1,8,16,9,9,9,2,
  0,0,0,0,
  0,2,9,0,0,0,0,0,10,1,13,20,0,0,0,0,
  0,9,2,0,
  23,1,1,13,15,16,5,0,18,1,1,28,4,9,16,32,
  13,1,1,19,
  24,14,27,33,17,13,14,0,21,1,1,23,4,11,7,17,
  26,27,14,24,
  0,0,0,0,10,20,2,0,6,19,19,6,0,2,20,10,
  0,0,0,4,
  0,2,5,0,0,0,2,30,0,2,4,0,20,4,0,0,
  0,30,10,0,
  2,24,1,25,11,12,8,1,3,14,14,3,1,7,12,11,
  18,1,23,4,
  4,31,22,28,1,8,12,6,26,7,7,26,6,12,8,1,
  29,22,6,4,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0
  };



#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library
#include <SPI.h>
#include <avr/pgmspace.h>

// Including the test image from testimage.h


#define TFT_CS  12
#define TFT_RST 13
#define TFT_DC  14

#define SD_CS 4

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS,  TFT_DC, TFT_RST);

#define TFT_SCLK 7
#define TFT_MOSI 5




void drawProgmemIconIndexed(const uint8_t* icon, int16_t x, int16_t y, uint8_t width, uint8_t height) {
  uint16_t count = 0;
  // Set up a window the right size to stream pixels into
  tft.setAddrWindow(x, y, x + width - 1, y + height - 1);


  for (uint16_t i = 0; i < height; i++) {
    for (uint16_t j = 0; j < width; j++) {

      byte color_reference = pgm_read_byte(&icon[count++]);

      uint16_t color_565 = pgm_read_word(colorTableDaytime[color_reference]);

      tft.pushColor(color_565);


    }
  }
}

void setup() {
  tft.initR(INITR_144GREENTAB);

  tft.setRotation(1);
  tft.fillRect(0, 99, 128, 128, ST7735_WHITE);
}

void loop() {

  drawProgmemIconIndexed(test, 0, 0, 20, 20);
}

This is what I get as an error message:

testsketch:119: error: cannot convert 'const uint8_t* const* {aka const unsigned char* const*}' to 'const uint8_t* {aka const unsigned char*}' for argument '1' to 'void drawProgmemIconIndexed(const uint8_t*, int16_t, int16_t, uint8_t, uint8_t)'

   drawProgmemIconIndexed(test, 0, 0, 20, 20);

                                            ^

exit status 1
cannot convert 'const uint8_t* const* {aka const unsigned char* const*}' to 'const uint8_t* {aka const unsigned char*}' for argument '1' to 'void drawProgmemIconIndexed(const uint8_t*, int16_t, int16_t, uint8_t, uint8_t)'

I’ve done some math, and if I can manage to store all my desired icons, numbers, and whatever else in PROGMEM as two sets of distinct daytime and nighttime icons which each contain only 8-bit information which then references one of two 16-bit color tables, I will come in at around 55 to 56 KB of PROGMEM occupied by my graphics.

I have run all my graphics through a color palette filter, which means that any icon from the “daytime” set will have the same color palette as any other “daytime” icon. And the same for the nighttime icons.

To illustrate - this is my daytime “reference” screen image:

And this is my nighttime reference screen:

“Reference” meaning, all icons and number digits will be derived from a reference color palette that I have obtained from converting these screen images to 256-color. That way, I’ll be able to define just two different color palettes for both sets of images in PROGMEM, and won’t have to put a new color palette for every different PROGMEM image.

Such as the daytime and nighttime coolant temperature icons: