How do I stream a picture from SD to the screen buffer?

Possibly a simple question: How do I stream a picture from SD to the screen buffer?

Mmmm … not sure why I couldn’t remember this. For anyone else who wants to know, here is how I did it. I am using Mode 15 (220 x 176 , 16 colour). You will have to change the dimensions and the divisor for other modes. If using 256 colours you do not need to divide by 2 at all. If you are using 4 colour then divide by 4.

    File file;

    if (file.openRO("music/title.img")) {
        file.read(PD::getBuffer(), 2 + (220 * 176 / 2));
    }
4 Likes

How do you create that image?

I haven’t tested this, it may have worked some time in the past, I cut/pasted it out of an old project…
It’s using mode 13 (8bit 220x88) and loads a standard windows bitmap also saved as 8bit. There is no error checking or any sort of verification, so if the format is not correct you will get a mess.

#include "Pokitto.h"
#include "SDFileSystem.h"

unsigned short pal[256]; // assign a 256 entry array to hold the palette
unsigned char texture[110*88] __attribute__ ((aligned (4))); // texture for covers

void readimage(const char * filename){

    uint32_t myFile = fileOpen("bitmap.bmp", FILE_MODE_READONLY);
    if( !myFile ){

        char temp=0;
        for(int t=0; t<54; t++){
            fileReadBytes(&temp, 1);
        }

        unsigned char col[3];
        for(int temp=0; temp<256; temp++){
            fileReadBytes(&col[0], 3);
            pal[temp] = (col[0]>>3) | ((col[1] >> 2) << 5) | ((col[2] >> 3) << 11);
        }
        Pokitto::Display::load565Palette(&pal[0]); // load a palette the same way as any other palette in any other screen mode

        Pokitto::Display::print("Thing\n");
        Pokitto::Display::print("Read Pic\n");
		fread(texture, 1, 110*88, file);
        Pokitto::Display::print("Draw Pic\n");
		Pokitto::Display::drawBitmap(0, 0, texture);

        fileClose();
    } // if myFile

}   


int main(){
    using PC=Pokitto::Core;
    using PD=Pokitto::Display;
    PC::begin();
    PD::persistence = true;
    PD::invisiblecolor = 0;

    readimage("bitmap.bmp");
    
    while( PC::isRunning() ){
        if( !PC::update() ) 
            continue;
    }
    
    return 0;
}
1 Like

I create them using a script that I think you provided! If it wasn’t you, then it was @carbonacat … I have actually forgotten who gave it to me.

1 Like

I will publish the code that I am using with apologies to the author (I am sure if they gave it to me they are happy for others to use it as well :slight_smile: ) In addition to Node.js, you will need to install canvas using the command $ npm install canvas

/* Usage: node gfx.js file.png w h output */
const fs = require("fs"); const Canvas = require('canvas'); const Image = Canvas.Image;
/* Pico-8 */
const palette = [ 
0, 0, 0, 
29, 43, 83, 
126, 37, 83, 
0, 135, 81,
171, 82, 54, 
95, 87, 79, 
194, 195, 199, 
255, 241, 232,
255, 0, 77, 
255, 163, 0, 
255, 236, 39, 
0, 228, 54,
41, 173, 255, 
131, 118, 156, 
255, 119, 168, 
255, 204, 170
]; 

let file = process.argv[2]; w = parseInt(process.argv[3]); h = parseInt(process.argv[4]);


if( !file ) { 
  console.log("Usage: node gfx.js file.png w h output"); 
}
else { 
  if (process.argv[5] == "") { 
    fs.writeFileSync( `${file.replace(/\..*$/, '')}.img`, Buffer.from( img4bpp( file, w, h ) ) ); 
  } 
  else { 
    fs.writeFileSync(process.argv[5], Buffer.from( img4bpp( file, w, h ) ) );
  } 
  console.log( `${file.replace(/\..*$/, '')}.img`); 
}

function img4bpp( path, w, h ){ 

	const canvas = Canvas.createCanvas(w, h); 
	const ctx = canvas.getContext('2d'); 
	const image = new Image(); image.src = fs.readFileSync( path );

    ctx.drawImage( image, 0, 0, w, h ); 
    const buf = canvas.toBuffer('raw'); 
    const pixel = [0,0,0]; 
    const infl = [];

	infl.push( w / 16 ); infl.push( w % 16 ); infl.push( h / 16 ); infl.push( h % 16 );

    let line = '';

    for( let i=0; i<buf.length; ++i ){

		let j;

		pixel[2] = buf[i++]; 
		pixel[1] = buf[i++]; 
		pixel[0] = buf[i++];


		let minind = 999;

		for( j=0; j<palette.length / 3; ++j ){

			if (Math.abs(pixel[0] - palette[j * 3]) < 4 && Math.abs(pixel[1] - palette[(j * 3) + 1]) < 4 && Math.abs(pixel[2] - palette[(j * 3) + 2]) < 4) {

				minind = j; 
				break; 
				
			}

		}

		if (minind == 999) { 
		  console.log("%i %i %i", w, h, buf.length); console.log("I: %i, X: %i, Y: %i - %i,%i,%i", i, 		((i/4)%w), ((i/4)/w), pixel[0], pixel[1], pixel[2] );

		}

		infl.push( minind );
		line += minind.toString(16); if (line.length == w ){ if (w % 2 == 1) {

        infl.push( 0 ); }

		// console.log(line);
		line = ''; }

    }

    const out = [];

    for( let i=0; i<infl.length; ) { 
      let hi = infl[i++]; 
      let lo = infl[i++]; 
      out.push( (hi<<4) | lo ); 
    }

    return out;

}

As you can see at the top, I have hard coded the Pico-8 palette but you can alter this to your own needs.

If a colour cannot be matched, it logs out the coordinates of the pixel and continues on. This allows you to go back into the picture in FemtoIDE and recolour the area with a colour from the palette.

I vaguely recall the original using a ‘close enough’ algorithm when correlating colours from the image to indexes in the palette. I recall that I was getting incorrect results so I removed that logic.

Finally, this version of the code puts the width and height as the first two bytes of the output file which makes sense if you are reading the file into an array and then are using the PD:drawBitmap() command. If you are rendering it directly to the screen buffer, remove the width and height from the output stream.

Alternatively, you can just ‘burn’ the first two bytes like this:

    if (file.openRO("music/Title.img")) {
        file.read(PD::getBuffer(), 2);
        file.read(PD::getBuffer(), (220 * 176 / 2));
    }
2 Likes

Definitely not from me!

(Also, technically you don’t need that canvas nodejs dependency, you can just create a canvas element for such an operation - @FManga does it in various scripts including the one converting all .png to .h files in femtoide)

Yup, probably was me, just I forgot. But the original author was @FManga I suppose, it comes from Abbayes porting effort.
I must check if I add this kind of output also to my img2pok tool…

2 Likes

Ahh … I thought it came from you but when you asked the question, I figured I had it wrong.

Some people could … but not me. I know so little about NodeHS!

1 Like

The next step could be find a way to load a picture from sd to flash memory, and so have variable resources loaded on demand.
I think @FManga exposed this idea a long time ago.

Its easy to load a picture into flash memory.

File file;
uint8_t imgBuffer[2 + (220 * 176 / 2)];

if (file.openRO("music/title.img")) {
    file.read(buffer, 2 + (220 * 176 / 2));
}

I assume this is what you mean? As for loading them ‘on demand’ you could wrap the loading into a function and pass a value representing the image you want.


enum class Images : uint8_t {
    Splash,
    Title,
    ...
};

Images bufferedImage

function drawBitmap(Images imageToRender, int16_t x, int16_t y, uint8_t w, uint8_t h) {

    File file;
    uint8_t imgBuffer[2 + (220 * 176 / 2)];

    char fileName[19] = { 'm', 'u', 's', 'i', 'c', '/', 'G', 'A', 'M', 'E', '_', 0', '0', '0', '.', 'i', 'm', 'g', ' ' };
    

    // Only refresh the image buffer if it is not already loaded ..

    if (bufferedImage != imageIndex) {

        uint8_t imageIndex = static_cast<uint8_t>(imageToRender);
        fileName[11] = 48 + (imageIndex / 100);
        fileName[12] = 48 + ((imageIndex - ((imageIndex / 100) * 100)) / 10);
        fileName[13] = 48 + (imageIndex % 10);
        fileName[18] = 0;

        if (file.openRO(filename)) {
           file.read(buffer, 2 + (w * h/ 2));
        }

       bufferedImage = imageToRender;

    }

    PD::drawBitmap(x, y, buffer);
}

Totally untested - it may not even compile!

1 Like

For that to work you’d have to make the buffer statically allocated (i.e. global or a static local), otherwise the array will just be overwritten with nonsense from the stack. (Not to mention an array that size could potentially upset the stack.)

While I’m at it, you could also save a bit of effort by initialising the file name with a string literal (which will be copied into the array, and the size of the array will be inferred) and slightly improve flexibility by using std::size with negative offsets to index the array, so anything prior to the numeric suffix can be edited without breaking the code.

E.g.

// Need a header of some kind for std::size
#include <array>

enum class Image : std::uint8_t
{
    None,
    Splash,
    Title,
    // ...
};

// Need a 'none' value to indicate 'nothing loaded yet'
Image bufferedImage = Image::None;
std::uint8_t buffer[2 + (220 * 176 / 2)] {};

void drawBitmap(Image imageToRender, std::int16_t x, std::int16_t y, std::uint8_t width, std::uint8_t height)
{
    using Pokitto::Display;

    // Only refresh the image buffer if it is not already loaded ...
    if (bufferedImage != imageIndex)
    {
        uint8_t imageIndex = static_cast<std::uint8_t>(imageToRender);

        char fileName[] = "music/GAME_000.img";

        fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 100) % 10));
        fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
        fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 1) % 10));

        File file;

        if (file.openRO(filename))
           file.read(buffer, 2 + (width * height / 2));

       bufferedImage = imageToRender;
    }

    Display::drawBitmap(x, y, buffer);
}

Also untested of course, but I can’t immediately see a reason why it wouldn’t work.
(If you’re worried about speed with the BCD conversion, try a “double-dabble”.)

I would have it as a global along with the bufferedImage var.

The solution might change further based on the naming convention so indexing the filename from the front or negatively would be appropriate in certain cases.

Pretty sure

fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 100) % 10));
fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 1) % 10));

needs to actually be:

fileName[std::size(fileName) - 9] = ('0' + ((imageIndex / 100) % 10));
fileName[std::size(fileName) - 8] = ('0' + ((imageIndex / 10) % 10));
fileName[std::size(fileName) - 7] = ('0' + ((imageIndex / 1) % 10));

Well sure, if you want to do it the boring old ‘most significant digit first’ way. :P

Yes, I guess its a silly convention isn’t it?

1 Like