TxSprite.fromImageBytes constructor
Create a TxSprite from any image bytes that can be decoded by img.decode() If it's an indexed image, quantize down to 14 colors if larger (plus black, white as palette entries 0 and 1.) If it's an RGB image, also quantize to 14 colors (then prepend black and white) Scale to 640x400 preserving aspect ratio for 1- or 2-bit images Scale to ~128k pixels preserving aspect ratio for 4-bit images TODO improve quantization - quality, speed If quantizing, since neuralNet seems to give the black in entry 0 and white in the last entry We just leave 0 (black) and swap palette entries 1 and 15 (and remap those pixels)
Implementation
TxSprite.fromImageBytes({required super.msgCode, required Uint8List imageBytes}) {
var image = img.decodeImage(imageBytes);
if (image == null) throw Exception('Unable to decode image file');
// the number of colors in the image (after optional quantization), must be 16 or fewer
final int numColors;
bool quantized = false;
if ((image.hasPalette && image.palette!.numColors > 16) || !image.hasPalette) {
_log.fine('quantizing image');
// note, even though we ask for only numberOfColors here, neuralNet gives us back a palette of 256
// with only the first numberOfColors populated, ordered by increasing luminance (TODO and always containing black, then colors, then white last)
// we'll trim that palette array down to 16*3 later
image = img.quantize(image, numberOfColors: 16, method: img.QuantizeMethod.neuralNet, dither: img.DitherKernel.none);
numColors = 16;
quantized = true;
}
else {
_log.fine('16 or fewer colors in an indexed image, no quantizing required');
numColors = image.palette!.numColors;
}
if (image.width > 640 || image.height > 400) {
_log.fine('scaling down oversized image');
if (image.palette!.numColors <= 4) {
_log.fine('Low bit depth image, scale to 640x400');
// 1- or 2-bit images can take as much of 640x400 as needed, preserving aspect ratio
// use nearest interpolation, we can't use any interpolation that averages colors
image = img.copyResize(image,
width: 640,
height: 400,
maintainAspect: true,
interpolation: img.Interpolation.nearest);
}
else {
_log.fine('4-bit image, scale to max 128k pixels');
// 4-bit images need to be smaller or else we run out of memory. Limit to 64kb, or 128k pixels
int numSrcPixels = image.height * image.width;
if (numSrcPixels > 128000) {
double scaleFactor = sqrt(128000 / numSrcPixels);
_log.fine(() => 'scaling down by $scaleFactor');
image = img.copyResize(image,
width: (image.width * scaleFactor).toInt().clamp(1, 640),
height: (image.height * scaleFactor).toInt().clamp(1, 400),
maintainAspect: true,
interpolation: img.Interpolation.nearest);
}
else {
_log.fine('small 4-bit image, no need to scale down pixels, just max extent in height or width');
// TODO does this scale up if the image is smaller? It doesn't need to do this.
image = img.copyResize(image,
width: 640,
height: 400,
maintainAspect: true,
interpolation: img.Interpolation.nearest);
}
}
}
_width = image.width;
_height = image.height;
_pixelData = image.data!.toUint8List();
// use a temporary palette in case we need to expand it to add VOID at the start
Uint8List? initialPalette;
// we can process RGB or RGBA format palettes, but any others we just exclude here
if (image.palette!.numChannels == 3) {
_log.fine('3-channel palette');
// take the sublist because neuralNet quantized images have the number of colors we want
// but packaged in a length 256 palette (a bug)
initialPalette = image.palette!.toUint8List().sublist(0, 3 * numColors);
}
else if (image.palette!.numChannels == 4) {
_log.fine('4-channel palette');
// strip out the alpha channel from the palette
// take the sublist because neuralNet quantized images have the number of colors we want
// but packaged in a length 256 palette (a bug)
initialPalette = _extractRGB(image.palette!.toUint8List().sublist(0, 4 * numColors));
}
else {
throw Exception('Image colors must have 3 or 4 channels to be converted to a sprite');
}
// Frame uses palette entry 0 for VOID.
// If we can fit another color, we can add VOID at the start and shift every pixel up by one.
// If we can't fit any more colors, for now just set 0 to black (otherwise the rest of the display
// will be lit)
// We move the white palette index to 1 so that regular white text displays OK
// TODO in future we could:
// - find an alpha color from the palette (if it exists), and swap it with the color in 0
// - find black from the palette (if it exists), and swap it with the color in 0
// - find the darkest luminance color and swap it with the color in 0
// - neuralNet quantizer seems to set 0 to black and max color to white.
if (image.palette!.numColors < 16) {
_log.fine('fewer than 16 colors');
// if the first RGB color of the palette is not black/void, we need to
// insert another color (which may promote the image to 2-bit or 4-bit)
// but no need to if the palette already has black at the start
if (initialPalette[0] != 0 || initialPalette[1] != 0 || initialPalette[2] != 0) {
// TODO consider checking for whether we can move/add white to the #1 slot without
// increasing the palette size to the next bitness
_log.fine('insert black at the front of the palette');
_numColors = image.palette!.numColors + 1;
_paletteData = Uint8List(initialPalette.length + 3);
_paletteData.setAll(3, initialPalette);
_log.fine(initialPalette);
_log.fine(_paletteData);
// update all the pixels to refer to the new palette index
for (int i=0; i<_pixelData.length; i++) {
_pixelData[i] += 1;
}
}
else {
// palette already has black at the start, just copy it over
_log.fine('black already at the start of the palette');
_numColors = image.palette!.numColors;
_paletteData = initialPalette;
}
}
else {
_log.fine('exactly 16 colors');
if (!quantized) {
// 16 colors in palette set by user's file, might not have black in slot 0
_log.fine('16 colors exactly, make sure 0 is set to black');
_numColors = image.palette!.numColors;
// can't fit any more colors, set entry 0 to black
_paletteData = initialPalette;
_paletteData[0] = 0;
_paletteData[1] = 0;
_paletteData[2] = 0;
_log.fine(initialPalette);
_log.fine(_paletteData);
}
else {
_log.fine('16 colors coming from quantizer');
_numColors = 16;
_paletteData = initialPalette;
_log.fine(initialPalette);
// black is already in #0 [0, 1, 2]
// swap color in #1 [3, 4, 5] with white, which is in #15 [45, 46, 47]
var swapR = _paletteData[3];
var swapG = _paletteData[4];
var swapB = _paletteData[5];
_paletteData[3] = 255;
_paletteData[4] = 255;
_paletteData[5] = 255;
_paletteData[45] = swapR;
_paletteData[46] = swapG;
_paletteData[47] = swapB;
_log.fine(_paletteData);
// update all the pixels to swap the palette index for white and the color that was in #1
for (int i=0; i<_pixelData.length; i++) {
if (_pixelData[i] == 1) {
_pixelData[i] = 15;
}
else if (_pixelData[i] == 15) {
_pixelData[i] = 1;
}
}
}
_log.fine(() => 'Sprite: $_width x $_height, $_numColors cols, ${pack().length} bytes');
}
}