Skip to content

Instantly share code, notes, and snippets.

@Magicking
Created May 6, 2025 10:43
Show Gist options
  • Save Magicking/fa07cce01dfa6f1575cd1f518064339d to your computer and use it in GitHub Desktop.
Save Magicking/fa07cce01dfa6f1575cd1f518064339d to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
// Author: Sylvain Magicking Laurent
pragma solidity 0.8.21;
import "@openzeppelin/contracts/utils/Base64.sol";
// Error when the palette is full
error MaxPaletteReached();
error XoutOfBounds();
error YoutOfBounds();
contract BMPImage1BPC {
struct InfoHeader {
uint32 width; // width in pixels
uint32 height; // height in pixels
uint16 colorPlanes; // number of color planes (must be 1)
uint16 bitsPerPixel; // bits per pixel
uint32 compression; // compression method
uint32 imageSize; // image size in bytes
uint32 horizontalResolution; // horizontal resolution in pixels per meter
uint32 verticalResolution; // vertical resolution in pixels per meter
uint32 colorsInPalette; // number of colors in the color palette
uint32 importantColors; // number of important colors used
}
struct Image {
Header header;
InfoHeader infoHeader;
bytes palette;
bytes data;
}
function writeInfoHeader(InfoHeader memory h) public pure returns (bytes memory) {
bytes memory header = new bytes(40);
// write header size
header[0] = 0x28;
header[1] = 0;
header[2] = 0;
header[3] = 0;
// write image width
header[4] = bytes1(uint8(h.width));
header[5] = bytes1(uint8(h.width >> 8));
header[6] = bytes1(uint8(h.width >> 16));
header[7] = bytes1(uint8(h.width >> 24));
// write image height
header[8] = bytes1(uint8(h.height));
header[9] = bytes1(uint8(h.height >> 8));
header[10] = bytes1(uint8(h.height >> 16));
header[11] = bytes1(uint8(h.height >> 24));
// write color planes
header[12] = bytes1(uint8(h.colorPlanes));
header[13] = bytes1(uint8(h.colorPlanes >> 8));
// write bits per pixel
header[14] = bytes1(uint8(h.bitsPerPixel));
header[15] = bytes1(uint8(h.bitsPerPixel >> 8));
// write compression method
header[16] = bytes1(uint8(h.compression));
header[17] = bytes1(uint8(h.compression >> 8));
header[18] = bytes1(uint8(h.compression >> 16));
header[19] = bytes1(uint8(h.compression >> 24));
// write image size
uint32 imageSize = h.imageSize;
header[20] = bytes1(uint8(imageSize));
header[21] = bytes1(uint8(imageSize >> 8));
header[22] = bytes1(uint8(imageSize >> 16));
header[23] = bytes1(uint8(imageSize >> 24));
// write horizontal resolution
header[24] = bytes1(uint8(h.horizontalResolution));
header[25] = bytes1(uint8(h.horizontalResolution >> 8));
header[26] = bytes1(uint8(h.horizontalResolution >> 16));
header[27] = bytes1(uint8(h.horizontalResolution >> 24));
// write vertical resolution
header[28] = bytes1(uint8(h.verticalResolution));
header[29] = bytes1(uint8(h.verticalResolution >> 8));
header[30] = bytes1(uint8(h.verticalResolution >> 16));
header[31] = bytes1(uint8(h.verticalResolution >> 24));
// write colors in palette
header[32] = bytes1(uint8(h.colorsInPalette));
header[33] = bytes1(uint8(h.colorsInPalette >> 8));
header[34] = bytes1(uint8(h.colorsInPalette >> 16));
header[35] = bytes1(uint8(h.colorsInPalette >> 24));
// write important colors
header[36] = bytes1(uint8(h.importantColors));
header[37] = bytes1(uint8(h.importantColors >> 8));
header[38] = bytes1(uint8(h.importantColors >> 16));
header[39] = bytes1(uint8(h.importantColors >> 24));
return header;
}
struct Header {
uint16 signature; // signature (must be 0x4d42)
uint32 fileSize; // file size in bytes
uint32 reserved; // reserved (must be 0)
uint32 dataOffset; // offset to image data in bytes from beginning of file (54 bytes + XX bytes for the palette)
}
function writeHeader(Header memory h) public pure returns (bytes memory) {
bytes memory header = new bytes(14);
// write signature
header[0] = bytes1(uint8(h.signature));
header[1] = bytes1(uint8(h.signature >> 8));
// write file size
uint32 fileSize = h.fileSize;
header[2] = bytes1(uint8(fileSize));
header[3] = bytes1(uint8(fileSize >> 8));
header[4] = bytes1(uint8(fileSize >> 16));
header[5] = bytes1(uint8(fileSize >> 24));
// write reserved
header[6] = bytes1(uint8(h.reserved));
header[7] = bytes1(uint8(h.reserved >> 8));
header[8] = bytes1(uint8(h.reserved >> 16));
header[9] = bytes1(uint8(h.reserved >> 24));
// write data offset
header[10] = bytes1(uint8(h.dataOffset));
header[11] = bytes1(uint8(h.dataOffset >> 8));
header[12] = bytes1(uint8(h.dataOffset >> 16));
header[13] = bytes1(uint8(h.dataOffset >> 24));
return header;
}
function newImage(uint32 width, uint32 height) public pure returns (Image memory) {
Image memory image;
image.header.signature = 0x4d42;
image.header.fileSize = 54 + width * height + 2 /* number of colors*/ * 4 /* color size */;
image.header.reserved = 0;
image.header.dataOffset = 0x28 + 14 + 2 * 4;
image.infoHeader.width = width;
image.infoHeader.height = height;
image.infoHeader.colorPlanes = 1;
image.infoHeader.bitsPerPixel = 1;
image.infoHeader.compression = 0;
image.infoHeader.imageSize = width * height;
image.infoHeader.horizontalResolution = 0x00000b13;
image.infoHeader.verticalResolution = 0x00000b13;
/* Specifies the number of color indexes in the color table
actually used by the bitmap. If this value is zero, the bitmap uses the
maximum number of colors corresponding to the value of the biBitCount member.
For more information on the maximum sizes of the color table, see the
description of the BITMAPINFO structure earlier in this topic.
*/
image.infoHeader.colorsInPalette = 2;
/*
Specifies the number of color indexes that are considered
important for displaying the bitmap. If this value is zero, all colors are
important.
*/
image.infoHeader.importantColors = 0;
image.palette = new bytes(2 * 4);
// Initialize the palette with black and no transparency
image.palette[3] = 0xff;
image.palette[7] = 0xff;
// Calculate the number of bytes per row, rounded up to the nearest multiple of 4
uint32 bytesPerRow = ((width + 3) >> 2) << 2;
image.data = new bytes(bytesPerRow * height);
return image;
}
// Human pixel layout
/**
*
* 0,0 *
* *
* *
* w-1;h-1 *
*
*/
// Canvas pixel layout
/**
*
* w-1;h-1 *
* *
* *
* 0,0 *
*
*/
// setPixelAt convert (x,y) from human to canvas layout
// 1 bit per pixel
/**
7 00000000: 424d 4600 0000 0000 0000 3e00 0000 2800 BMF.......>...(.
6 ||||||||| | offset |
5 00000010: 0000 0800 0000 0200 0000 0100 0100 0000 ................
4 | width | height | | bpp|
3 00000020: 0000 0800 0000 c40e 0000 c40e 0000 0200 ................
2 | size+pad| | |
1 00000030: 0000 0200 0000 0000 0000 ffff ffff 7e00 ................
0 color pal |imp.col | black | white |
1 00000040: 0000 7e00 0000 0a .......
-----------------------------
[......]|01111110
00000000|01111110|00000000|00000000|00000000|00001010
Image layout:
10000001 1 = black, 0 = white (see output_image2.bmp)
10000001
TODO: add a function to convert (x,y) from human to canvas layout with palette index
* */
function setPixelAt(Image memory image, uint32 x, uint32 y, uint8 r, uint8 g, uint8 b, uint8 a)
public
pure
returns (Image memory)
{
uint32 width = image.infoHeader.width;
uint32 height = image.infoHeader.height;
if (!(x < width)) revert XoutOfBounds();
if (!(y < height)) revert YoutOfBounds();
uint8 colorIndex = PaletteIndex(image, r, g, b);
// Calculate bytes per row, ensuring 4-byte alignment (BMP specification)
uint32 bytesPerRow = ((width + uint32(7)) / uint32(8) + uint32(3)) & ~uint32(3);
// Find the byte index where the pixel is located
uint32 byteIndex = x / 8 + ((height - y - 1) * bytesPerRow);
// Find the bit position within the byte (7 = MSB, 0 = LSB)
uint8 bitPosition = uint8(7 - (x % 8));
bytes memory mem = image.data;
// Write the color index bit to the correct position
assembly {
// Get pointer to the byte that contains our pixel
let bytePtr := add(add(mem, 0x20), byteIndex)
// Load current byte value
let currentByte := shr(248, mload(bytePtr))
// If colorIndex is 1, set the bit; if 0, clear the bit
switch colorIndex
case 0 {
// Clear the bit at bitPosition (AND with inverse of bit mask)
let mask := shl(bitPosition, 1)
let invertedMask := not(mask)
currentByte := and(currentByte, invertedMask)
}
case 1 {
// Set the bit at bitPosition (OR with bit mask)
let mask := shl(bitPosition, 1)
currentByte := or(currentByte, mask)
}
// Store the modified byte back
mstore8(bytePtr, currentByte)
}
return image;
}
// Draw bitfield
/*
Canvas content:
0b00000000000000000000000000000000000000000000000000000000000000000000011111111110000000000000000011110000000000000000000100000000
0b00000111111111000000000000000000000000111111111000000000000000000001110000000000000000000000011110000000000000000000101000000000
0b00111100000001111000000000000000011111100000001110000000000000000001111111111111100000000001110000000000000000000011100000000000
0b00100000000000001111111111111111110000000000000011100000110000000000000000000011100000000011000000000000001111000000000000000000
0b00000000000000000000000000000000000000000000000000111111100000000111111111111110001110000111111111111111111000000000000000000000
0b00000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000001000000000000
in hex 256 * 3:
0x000000000000000007fe0000f000010007fc000003fe00001c00000780000a00
0x3c0780007e0380001fff801c000038002000ffffc000e0c000038030003c0000
0x0000000000003f807ffe387fffe0000000000000000000000000400000001000
*/
function draw128pxLinesBitfield(Image memory image, uint32 x, uint32 y, uint256 colors, uint256[] memory parts)
public
pure
returns (Image memory)
{
uint8 r = uint8((colors >> 16) & 0xFF);
uint8 g = uint8((colors >> 8) & 0xFF);
uint8 b = uint8(colors & 0xFF);
uint8 a = /* = r = g = b */ 255;
uint32 width = image.infoHeader.width;
uint32 height = image.infoHeader.height;
uint8 line = uint8(parts.length * 2);
if (!(x + 128 <= width)) revert XoutOfBounds();
if (!(y + uint32(line) <= height)) revert YoutOfBounds();
for (uint8 i = 0; i < line; i++) {
for (uint8 j = 0; j < 128; j++) {
uint256 bitField = parts[uint256(i) / 2];
uint256 bit = (bitField >> (255 - ((uint256(j) + uint256(i) * 128) % 256))) & 0x1;
if (bit == 1) {
image = setPixelAt(image, x + j, y + i, r, g, b, a);
} /*
// Uncomment to have a black background
else {
image = setPixelAt(image, x + j, y + i, 0, 0, 0, 255);
}*/
}
}
return image;
}
// Draw dotted line
function drawSkipLine(
Image memory image,
uint32 x1,
uint32 y1,
uint32 x2,
uint32 y2,
uint8 r,
uint8 g,
uint8 b,
uint8 a,
uint32 skip
) public pure returns (Image memory) {
uint32 width = image.infoHeader.width;
uint32 height = image.infoHeader.height;
require(x1 < width, "x1 out of bounds");
require(y1 < height, "y1 out of bounds");
require(x2 < width, "x2 out of bounds");
require(y2 < height, "y2 out of bounds");
uint32 dx = x2 - x1;
uint32 dy = y2 - y1;
uint32 steps = 0;
if (dx > dy) {
steps = dx;
} else {
steps = dy;
}
uint32 xInc = dx / steps;
uint32 yInc = dy / steps;
uint32 x = x1;
uint32 y = y1;
for (uint32 i = 0; i < steps; i++) {
if (skip > 0) {
if (i % skip == 0) {
image = setPixelAt(image, x, y, r, g, b, a);
}
} else {
image = setPixelAt(image, x, y, r, g, b, a);
}
x += xInc;
y += yInc;
}
return image;
}
function drawLine(
Image memory image,
uint32 x1,
uint32 y1,
uint32 x2,
uint32 y2,
uint8 r,
uint8 g,
uint8 b,
uint8 a
) public pure returns (Image memory) {
return drawSkipLine(image, x1, y1, x2, y2, r, g, b, a, 0);
}
function PaletteIndex(Image memory image, uint8 _r, uint8 _g, uint8 _b) public pure returns (uint8) {
if (image.palette[4] == 0 && image.palette[5] == 0 && image.palette[6] == 0) {
// Set the palette color
image.palette[4] = bytes1(_r);
image.palette[5] = bytes1(_g);
image.palette[6] = bytes1(_b);
return 1;
}
if (image.palette[4] == bytes1(_r) && image.palette[5] == bytes1(_g) && image.palette[6] == bytes1(_b)) {
return 1;
}
if (_r == 0 && _g == 0 && _b == 0) {
return 0;
}
revert MaxPaletteReached();
}
function getCharacter(uint8 index) public pure returns (uint8[15] memory) {
require(index < 26, "a-z out of range");
uint256 fontDataA2N = 0x2bedd75ce48f5b779a7f348e5aedf6f497e9356eb6493df6d; // Font data encoded in an uint256 A -> N
uint256 fontDataM2Z = 0x5f6d76d7ef81db36badf11cba4b5d65b6fd6df6ab5eddf9cf; // Font data encoded in an uint256 M -> Z
uint8[15] memory ret;
// Use the second font data if the index is greater than 13
if (index >= 13) {
index -= 13;
fontDataA2N = fontDataM2Z;
}
for (uint8 i = 0; i < 15; i++) {
uint256 shift = (195 - index * 15) - i - 1;
if (shift == 0) {
ret[i] = uint8(fontDataA2N & 0x1);
continue;
}
ret[i] = uint8((fontDataA2N >> shift) & 0x1);
}
return ret;
}
function drawString(Image memory img, uint32 x, uint32 y, string memory s)
public
pure
returns (Image memory ret)
{
uint8 r = 255;
uint8 g = 255;
uint8 b = 255;
uint8 a = 255;
ret = img;
for (uint8 j = 0; j < bytes(s).length; j++) {
// Convert from A-Z or a-z to 0-25
uint8 c = uint8(bytes(s)[j]);
// Skip non-alphabet characters
if (!((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a))) {
// if digit draw it
if (c >= 0x30 && c <= 0x39) {
ret = writeN(ret, x + j * 4, y, c - 0x30);
}
continue;
}
c = (c | 0x60) - 0x61;
uint8[15] memory fontChar = getCharacter(c);
for (uint8 i = 0; i < 15; i++) {
if (fontChar[i] == 1) {
ret = setPixelAt(ret, x + (i % 3) + j * 4, y + (i / 3), r, g, b, a);
} else {
ret = setPixelAt(ret, x + (i % 3) + j * 4, y + (i / 3), 0, 0, 0, a);
}
}
}
return ret;
}
function writeN(Image memory img, uint32 x, uint32 y, uint8 n) public pure returns (Image memory ret) {
require(n < 10, "N must be less than 10");
uint8 r = 255;
uint8 g = 255;
uint8 b = 255;
uint8 a = 255;
ret = img;
/*
uint8[10*15] memory fontData = [
0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, // 0
010
101
101 0
101
010
010
110
010 1
010
111
...
110001010100111 2
110001010001110 3
101101111001001 4
111100110001110 5
011100111101011 6
...
111001010100100011101011101011011101111001011
0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1,
1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0,
1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1,
1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1,
1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0,
0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1,
0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1];
101111101101101011101101101011111101111100000011101101100110111101111101101111000010100111111101001010111110101110101100111101101101101110101101101111 101101010101101 011110110111011111100111001111
101111101101101011101101101011111101111100000011101101100110 110101110101101 111000010100111111101001010111110101110101100111101101101101110101101101111101101010101101011110110111011111100111001111
101111101101101011101101101011111101111100000011101101100110110101110101101111000010100111111101001010111110101110101100111101101101101110101101101111101101010101101011110110111011111100111001111
*/
// bitfield encoded in a uint256
uint256 fontData = 0x15b52c97c54f8a3ade4f98e73d7ca91d75bbcb; // Font data encoded in an uint256 0 -> 9
uint8 pix;
for (uint8 i = 0; i < 15; i++) {
uint256 shift = (150 - n * 15) - i - 1;
if (shift == 0) {
pix = uint8(fontData & 0x1);
continue;
}
pix = uint8((fontData >> shift) & 0x1);
if (pix > 0) {
ret = setPixelAt(ret, x + (i % 3), y + (i / 3), r, g, b, a);
} else {
ret = setPixelAt(ret, x + (i % 3), y + (i / 3), 0, 0, 0, a);
}
}
}
function drawFrenchFlags(Image memory img, uint32 x, uint32 y) public pure returns (Image memory ret) {
// Write a blue, white and red line pixel by pixel
ret = img;
for (uint32 i = 0; i < 3; i++) {
ret = setPixelAt(ret, x + 2, y + i, 255, 0, 0, 255);
}
for (uint32 i = 0; i < 3; i++) {
ret = setPixelAt(ret, x + 1, y + i, 255, 255, 255, 255);
}
for (uint32 i = 0; i < 3; i++) {
ret = setPixelAt(ret, x + 0, y + i, 0, 0, 255, 255);
}
}
function drawNumber(Image memory img, uint32 x, uint32 y, uint256 bn, uint256 digit)
public
pure
returns (Image memory ret)
{
ret = img;
uint32 j = 0;
for (uint8 i = 0; i < digit; i++) {
uint256 n = uint256(bn / (10 ** uint256((digit - 1) - i))) % 10;
ret = writeN(ret, x + j * 4, y, uint8(n));
j++;
}
}
function drawDuration(Image memory img, uint32 x, uint32 y, uint256 remainingSeconds)
public
pure
returns (Image memory ret)
{
ret = img;
uint256 day = remainingSeconds / 86400;
remainingSeconds -= day * 86400;
uint256 hour = remainingSeconds / 3600;
remainingSeconds -= hour * 3600;
uint256 minute = remainingSeconds / 60;
remainingSeconds -= minute * 60;
ret = drawNumber(img, x, y, day, 1);
ret = drawString(img, x + 1 * 4, y, "d");
ret = drawNumber(img, x + 3 * 4, y, hour, 2);
ret = drawString(img, x + 5 * 4, y, "h");
ret = drawNumber(img, x + 7 * 4, y, minute, 2);
ret = drawString(img, x + 9 * 4, y, "m");
ret = drawNumber(img, x + 11 * 4, y, remainingSeconds, 2);
ret = drawString(img, x + 13 * 4, y, "s");
}
function encode(Image memory img) public pure returns (bytes memory file) {
bytes memory header = writeHeader(img.header);
bytes memory infoHeader = writeInfoHeader(img.infoHeader);
file = new bytes(header.length + infoHeader.length + img.palette.length + img.data.length);
for (uint256 i = 0; i < header.length; i++) {
file[i] = header[i];
}
for (uint256 i = 0; i < infoHeader.length; i++) {
file[header.length + i] = infoHeader[i];
}
for (uint256 i = 0; i < img.palette.length; i++) {
file[header.length + infoHeader.length + i] = img.palette[i];
}
for (uint256 i = 0; i < img.data.length; i++) {
file[header.length + infoHeader.length + img.palette.length + i] = img.data[i];
}
return file;
}
function B64MimeEncode(string memory mime, bytes memory data) public pure returns (string memory) {
return string(abi.encodePacked("data:", mime, ";base64,", Base64.encode(data)));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment