#include "Adafruit_ImageReader_ThinkInk.h" #ifdef __AVR__ #define BUFPIXELS 24 ///< 24 * 5 = 120 bytes #else #define BUFPIXELS 200 ///< 200 * 5 = 1000 bytes #endif // KEEP THIS VALUE CURRENT WITH THE LARGEST MAJOR AXIS AMONG ALL PANELS IN // Adafruit_EPD (currently 400 px from the ThinkInk_420_* panels). #define EPD_AXIS_MAX 400 // These are error accumulation buffers used for diffusion dithering: static int8_t vRed[EPD_AXIS_MAX], vGreen[EPD_AXIS_MAX], vBlue[EPD_AXIS_MAX]; static int8_t hRed, hGreen, hBlue; // Palettes are weird but are needed for error diffusion dithering, and the // tricolor palette is used in the quantize() function. Values here are the // perceived RGB565 values corresponding to each ink color (derived from // the Photoshop palettes used in dither guide). static const uint8_t palette_mono[][4] = {{47 >> 3, 36 >> 2, 41 >> 3, EPD_BLACK}, {242 >> 3, 244 >> 2, 239 >> 3, EPD_WHITE}}, palette_tricolor[][4] = {{47 >> 3, 36 >> 2, 41 >> 3, EPD_BLACK}, {242 >> 3, 244 >> 2, 239 >> 3, EPD_WHITE}, {215 >> 3, 38 >> 2, 39 >> 3, EPD_RED}}, palette_grayscale4[][4] = {{47 >> 3, 36 >> 2, 41 >> 3, EPD_BLACK}, {112 >> 3, 105 >> 2, 107 >> 3, EPD_DARK}, {177 >> 3, 175 >> 2, 173 >> 3, EPD_LIGHT}, {242 >> 3, 244 >> 2, 239 >> 3, EPD_WHITE}}; /*! @brief Quantize RGB565 color to one of the fixed EPD colod indices, whatever's a closest match for the EPD display type. @param rgb RGB565 input color. @param mode One of the thinkinkmode_t types enumerated in Adafruit_EPD library (e.g. THINKINK_MONO). @return Color index into palette appropriate for mode. This is the palette index, NOT the RGB value. The calling function may or may not want to look up the RGB color on its own (e.g. when doing error diffusion). */ static uint8_t quantize(uint16_t rgb, thinkinkmode_t mode) { uint8_t r = rgb >> 11; uint8_t g = (rgb >> 5) & 0x3F; uint8_t b = rgb & 0x1F; if (mode == THINKINK_MONO) { // RGB-to-gray weightings here are fixed-point equivalents to // R=0.2989, G=0.587, B=0.114, factoring in that green is a 6-bit // value covering a larger range compared to red and blue's 5 bits. return (uint8_t)((r * 631 + g * 611 + b * 241) >> 15); // 0 or 1 } else if (mode == THINKINK_GRAYSCALE4) { // Same deal re: RGB-to-gray weightings return (uint8_t)((r * 631 + g * 611 + b * 241) >> 14); // 0 to 3 } else { // THINKINK_TRICOLOR // For the moment, doing a brute-force compare against each color // in the tricolor palette, returning the index of the closest match // (using distance in linear RGB space for comparison). uint8_t i, closest_index = 0; uint32_t closest_dist = 0xFFFFFFFF; for (i = 0; i < sizeof palette_tricolor / sizeof palette_tricolor[0]; i++) { int8_t dr = (r - palette_tricolor[i][0]) * 2; // Red dist int8_t dg = g - palette_tricolor[i][1]; // Green dist int8_t db = (b - palette_tricolor[i][2]) * 2; // Blue dist uint32_t dist = dr * dr + dg * dg + db * db; // Dist^2 if (dist < closest_dist) { // No sqrt needed, because relative compare closest_dist = dist; closest_index = i; } } return closest_index; } } /*! @brief Reset error accumulation buffers prior to diffusion dithering (before reading BMP or rendering via draw()). @param rgb If true, display has some colors (not mono or gray). @return None (void). */ static void dither_reset(bool rgb) { memset(vRed, 0, sizeof vRed); hRed = 0; if (rgb) { memset(vGreen, 0, sizeof vGreen); memset(vBlue, 0, sizeof vBlue); hGreen = hBlue = 0; } } /*! @brief Draw span of pixels from source buffer to EPD display, applying quantization and dithering as requested. Clipping is already handled in calling function; coordinates can safely be assumed fully in-image at this point. This ONLY does a horizontal span, not a 2D rect. Input data will ALWAYS be 16-bit RGB565 at this point (bitmaps have been expanded). @param src Source data; array of pixels in 16-bit RGB565 format. @param epd Screen to draw to (any Adafruit_EPD-derived class). @param x Horizontal offset in pixels; left edge = 0, positive = right. Screen rotation setting is observed. @param y Vertical offset in pixels; top edge = 0, positive = down. @param width Width of span to draw, in pixels. @param mode One of the thinkinkmode_t types enumerated in Adafruit_EPD library (e.g. THINKINK_MONO). @param dither One of the dither_t values enumerated in header - DITHER_NONE, DITHER_ORDERED or DITHER_DIFFUSION. @return None (void). */ static void span(uint16_t *src, Adafruit_EPD *epd, int16_t x, int16_t y, int16_t width, thinkinkmode_t mode, dither_t dither) { uint8_t *palette; if (mode == THINKINK_MONO) { palette = (uint8_t *)palette_mono; } else if (mode == THINKINK_GRAYSCALE4) { palette = (uint8_t *)palette_grayscale4; } else { palette = (uint8_t *)palette_tricolor; } if (dither == DITHER_NONE) { while (width--) { epd->drawPixel(x++, y, palette[quantize(*src++, mode) * 4 + 3]); } } else if (dither == DITHER_PATTERN) { // 4x4 ordered dither goes here } else { // DITHER_DIFFUSION while (width--) { uint16_t rgb = *src++; uint8_t r = rgb >> 11; uint8_t g = (rgb >> 5) & 0x3F; uint8_t b = rgb & 0x1F; if (mode == THINKINK_TRICOLOR) { } else { // See notes in quantize() about the math here uint16_t gray = r * 631 + g * 611 + b * 241; // 0-65535ish } } } } /*! @brief Draw an image in RAM to an Adafruit ThinkInk display. @param epd Screen to draw to (any Adafruit_EPD-derived class). @param x Horizontal offset in pixels; left edge = 0, positive = right. Value is signed, image will be clipped if all or part is off the screen edges. Screen rotation setting is observed. @param y Vertical offset in pixels; top edge = 0, positive = down. @param mode One of the thinkinkmode_t types enumerated in Adafruit_EPD library (e.g. THINKINK_MONO). @param dither One of the dither_t values enumerated in header - DITHER_NONE, DITHER_ORDERED or DITHER_DIFFUSION. @return None (void). */ void Adafruit_Image_ThinkInk::draw(Adafruit_EPD &epd, int16_t x, int16_t y, thinkinkmode_t mode, dither_t dither) { if ((x >= epd.width()) || (y >= epd.height())) { return; // Reject off right/bottom } if (format == IMAGE_1) { int x2 = x + canvas.canvas1->width() - 1; int y2 = y + canvas.canvas1->height() - 1; if ((x2 < 0) || (y2 < 0)) { return; // Reject off left/top } // Vertical clipping is achieved with an offset into the canvas buffer. uint8_t *buffer = canvas.canvas1->getBuffer(); if (y < 0) { // Top clip buffer -= y * ((canvas.canvas1->width() + 7) / 8); y = 0; } if (y2 >= epd.height()) { // Bottom clip y2 = epd.height() - 1; } // Horizontal clipping is peculiar by comparison...because source data // is bit packed, need to keep track of the initial byte offset (at the // start of each row) and bit index of the first pixel... uint16_t initial_offset; uint8_t initial_bit; if (x < 0) { // Left clip initial_offset = -x / 8; initial_bit = 7 - (-x & 7); // 7 to 0 x = 0; } else { initial_offset = initial_bit = 0; } if (x2 >= epd.width()) { // Right clip x2 = epd.width() - 1; } epd.startWrite(); if (dither == DITHER_DIFFUSION) { dither_reset((mode == THINKINK_TRICOLOR)); } uint16_t epdbuf[BUFPIXELS]; // Temp space for buffering EPD data uint16_t destidx = 0; for (; y <= y2; y++) { // For each row... uint16_t offset = initial_offset; uint8_t bit = initial_bit; int span_x = x; for (int16_t col = x; col <= x2; col++) { epdbuf[destidx++] = palette[(buffer[offset] >> bit) & 1]; if (bit) { bit--; } else { bit = 7; offset++; } // If last pixel of row, or if epdbuf is full... if ((col >= x2) || (destidx >= BUFPIXELS)) { // Pass epdbuf data to span-drawing function... span(epdbuf, &epd, span_x, y, destidx, mode, dither); span_x += destidx; // Next span starts here destidx = 0; // Reset epdbuf } } buffer += (canvas.canvas1->width() + 7) / 8; // Offset to next row } } else if (format == IMAGE_8) { // BMP reader doesn't currently handle palettized images } else if (format == IMAGE_16) { int x2 = x + canvas.canvas16->width() - 1; int y2 = y + canvas.canvas16->height() - 1; if ((x2 < 0) || (y2 < 0)) { return; // Reject off left/top } uint16_t *buffer = canvas.canvas16->getBuffer(); if (y < 0) { // Top clip buffer -= y * canvas.canvas16->width(); // Offset to first scanline y = 0; } if (y2 >= epd.height()) { // Bottom clip y2 = epd.height() - 1; } if (x < 0) { // Left clip buffer += x; // Offset to first column x = 0; } if (x2 >= epd.width()) { // Right clip x2 = epd.width() - 1; } epd.startWrite(); if (dither == DITHER_DIFFUSION) { dither_reset((mode == THINKINK_TRICOLOR)); } int16_t width = x2 - x + 1; for (; y <= y2; y++) { // For each row... // Call span function, passing pointer into RGB565 image span(buffer, &epd, x, y, width, mode, dither); buffer += canvas.canvas16->width(); // Offset to next scanline } } // end IMAGE_16 epd.endWrite(); } // ADAFRUIT_IMAGEREADER_THINKINK CLASS ************************************* // Loads images from SD card to screen or RAM. /*! @brief Constructor. @return Adafruit_ImageReader object. @param fs FAT filesystem associated with this Adafruit_ImageReader instance. Any images to load will come from this filesystem; if multiple filesystems are required, each will require its own Adafruit_ImageReader object. The filesystem does NOT need to be initialized yet when passed in here (since this will often be in pre-setup() declaration, but DOES need initializing before any of the image loading or size functions are called! */ Adafruit_ImageReader_ThinkInk::Adafruit_ImageReader_ThinkInk(FatFileSystem &fs) : Adafruit_ImageReader(fs) {} /*! @brief Loads BMP image file from SD card directly to Adafruit_ThinkInk display. @param filename Name of BMP image file to load. @param epd Screen to draw to (any Adafruit_EPD-derived class). @param x Horizontal offset in pixels; left edge = 0, positive = right. Value is signed, image will be clipped if all or part is off the screen edges. Screen rotation setting is observed. @param y Vertical offset in pixels; top edge = 0, positive = down. @param mode One of the thinkinkmode_t types enumerated in Adafruit_EPD library (e.g. THINKINK_MONO). @param dither One of the dither_t values enumerated in header - DITHER_NONE, DITHER_ORDERED or DITHER_DIFFUSION. @param transact Pass 'true' if TFT and SD are on the same SPI bus, in which case SPI transactions are necessary. If separate peripherals, can pass 'false'. @return One of the ImageReturnCode values (IMAGE_SUCCESS on successful completion, other values on failure). */ ImageReturnCode Adafruit_ImageReader_ThinkInk::drawBMP( char *filename, Adafruit_EPD &epd, int16_t x, int16_t y, thinkinkmode_t mode, dither_t dither, boolean transact) { uint16_t epdbuf[BUFPIXELS]; // Temp space for buffering EPD data // Call core BMP-reading function, passing address to EPD object, // EPD working buffer, and X & Y position of top-left corner (image // will be cropped on load if necessary). Image pointer is NULL when // reading to EPD, and transact argument is passed through. return coreBMP(filename, &epd, epdbuf, x, y, NULL, mode, dither, transact); } /*! @brief BMP-reading function common both to the draw function (to EPD) and load function (to canvas object in RAM). BMP code has been centralized here so if/when more BMP format variants are added in the future, it doesn't need to be implemented, debugged and kept in sync in two places. @param filename Name of BMP image file to load. @param epd Screen to draw to (any Adafruit_EPD-derived class) if loading to screen, or NULL otherwise. @param dest Working buffer for loading 16-bit TFT pixel data, if loading to screen, else NULL. @param x Horizontal offset in pixels (if loading to screen). @param y Vertical offset in pixels (if loading to screen). @param img Pointer to Adafruit_Image_ThinkInk object, if loading to RAM (or NULL if loading to screen). @param mode One of the thinkinkmode_t types enumerated in Adafruit_EPD library (e.g. THINKINK_MONO). @param dither One of the dither_t values enumerated in header - DITHER_NONE, DITHER_ORDERED or DITHER_DIFFUSION (IGNORED if loading image to canvas). @param transact Use SPI transactions; 'true' is needed only if loading to screen and it's on the same SPI bus as the SD card. Other situations can use 'false'. @return One of the ImageReturnCode values (IMAGE_SUCCESS on successful completion, other values on failure). */ ImageReturnCode Adafruit_ImageReader_ThinkInk::coreBMP( char *filename, // SD file to load Adafruit_EPD *epd, // Pointer to TFT object, or NULL if to image uint16_t *dest, // EPD working buffer, or NULL if to canvas int16_t x, // Position if loading to EPD (else ignored) int16_t y, Adafruit_Image_ThinkInk *img, // NULL if load-to-screen thinkinkmode_t mode, dither_t dither, boolean transact) { // SD & EPD sharing bus, use transactions ImageReturnCode status = IMAGE_ERR_FORMAT; // IMAGE_SUCCESS on valid file uint32_t offset; // Start of image data in file uint32_t headerSize; // Indicates BMP version int bmpWidth, bmpHeight; // BMP width & height in pixels uint8_t planes; // BMP planes uint8_t depth; // BMP bit depth uint32_t compression = 0; // BMP compression mode uint32_t colors = 0; // Number of colors in palette uint16_t *quantized = NULL; // EPD Color palette uint32_t rowSize; // >bmpWidth if scanline padding uint8_t sdbuf[3 * BUFPIXELS]; // BMP read buf (R+G+B/pixel) int16_t epd_col, epd_row; #if ((3 * BUFPIXELS) <= 255) uint8_t srcidx = sizeof sdbuf; // Current position in sdbuf #else uint16_t srcidx = sizeof sdbuf; #endif uint32_t destidx = 0; uint8_t *dest1 = NULL; // Dest ptr for 1-bit BMPs to img boolean flip = true; // BMP is stored bottom-to-top uint32_t bmpPos = 0; // Next pixel position in file int loadWidth, loadHeight, // Region being loaded (clipped) loadX, loadY; // " int row, col; // Current pixel pos. uint8_t r, g, b, color; // Current pixel color uint8_t bitIn = 0; // Bit number for 1-bit data in uint8_t bitOut = 0; // Column mask for 1-bit data out // If an Adafruit_Image object is passed and currently contains anything, // free its contents as it's about to be overwritten with new stuff. if (img) img->dealloc(); // If BMP is being drawn off the right or bottom edge of the screen, // nothing to do here. NOT an error, just a trivial clip operation. if (epd && ((x >= epd->width()) || (y >= epd->height()))) return IMAGE_SUCCESS; // Open requested file on SD card if (!(file = filesys->open(filename, FILE_READ))) { return IMAGE_ERR_FILE_NOT_FOUND; } // Parse BMP header. 0x4D42 (ASCII 'BM') is the Windows BMP signature. // There are other values possible in a .BMP file but these are super // esoteric (e.g. OS/2 struct bitmap array) and NOT supported here! if (readLE16() == 0x4D42) { // BMP signature (void)readLE32(); // Read & ignore file size (void)readLE32(); // Read & ignore creator bytes offset = readLE32(); // Start of image data // Read DIB header headerSize = readLE32(); bmpWidth = readLE32(); bmpHeight = readLE32(); // If bmpHeight is negative, image is in top-down order. // This is not canon but has been observed in the wild. if (bmpHeight < 0) { bmpHeight = -bmpHeight; flip = false; } planes = readLE16(); depth = readLE16(); // Bits per pixel // Compression mode is present in later BMP versions (default = none) if (headerSize > 12) { compression = readLE32(); (void)readLE32(); // Raw bitmap data size; ignore (void)readLE32(); // Horizontal resolution, ignore (void)readLE32(); // Vertical resolution, ignore colors = readLE32(); // Number of colors in palette, or 0 for 2^depth (void)readLE32(); // Number of colors used (ignore) // File position should now be at start of palette (if present) } if (!colors) colors = 1 << depth; loadWidth = bmpWidth; loadHeight = bmpHeight; loadX = 0; loadY = 0; if (epd) { // Crop area to be loaded (if destination is EPD) if (x < 0) { loadX = -x; loadWidth += x; x = 0; } if (y < 0) { loadY = -y; loadHeight += y; y = 0; } if ((x + loadWidth) > epd->width()) loadWidth = epd->width() - x; if ((y + loadHeight) > epd->height()) loadHeight = epd->height() - y; } if ((planes == 1) && (compression == 0)) { // Only uncompressed is handled // BMP rows are padded (if needed) to 4-byte boundary rowSize = ((depth * bmpWidth + 31) / 32) * 4; // CURRENTLY ONLY 24-BIT BGR AND 1-BIT BMPS ARE SUPPORTED. // 8-bit grayscale, 8- or 4-bit paletted, etc. are NOT HANDLED. if ((depth == 24) || (depth == 1)) { if (img) { // Loading to RAM -- allocate GFX canvas status = IMAGE_ERR_MALLOC; // Assume won't fit to start // Future: handle other depths. if (depth == 24) { if ((img->canvas.canvas16 = new GFXcanvas16(bmpWidth, bmpHeight))) { dest = img->canvas.canvas16->getBuffer(); } } else { if ((img->canvas.canvas1 = new GFXcanvas1(bmpWidth, bmpHeight))) { dest1 = img->canvas.canvas1->getBuffer(); } } } // else loading to screen -- 'dest' pointer was passed in if (dest || dest1) { // Supported format, alloc OK, etc. status = IMAGE_SUCCESS; if ((loadWidth > 0) && (loadHeight > 0)) { // Clip top/left if (epd) { // If loading to display... epd->startWrite(); // Start SPI (regardless of transact) epd_col = x; epd_row = y; } else { // Future: handle other depths. if (depth == 1) { img->format = IMAGE_1; // Is a GFX 1-bit canvas type } else { img->format = IMAGE_16; // Is a GFX 16-bit canvas type } } if ((depth >= 16) || (quantized = (uint16_t *)malloc(colors * sizeof(uint16_t)))) { if (depth < 16) { // Load color table, quantied to RGB565. This is NOT // converted to EPD color indices yet -- that's done // during draw operation, as there may yet be // dithering operations to perform. for (uint16_t c = 0; c < colors; c++) { b = file.read(); g = file.read(); r = file.read(); (void)file.read(); // Ignore 4th byte quantized[c] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } } if (dither == DITHER_DIFFUSION) { dither_reset((mode == THINKINK_TRICOLOR)); } for (row = 0; row < loadHeight; row++) { // EACH SCANLINE ------------------------------------------- yield(); // Keep ESP8266 happy // Seek to start of scan line. It might seem labor-intensive // to be doing this on every line, but this method covers a // lot of gritty details like cropping, flip and scanline // padding. Also, the seek only takes place if the file // position actually needs to change (avoids a lot of cluster // math in SD library). if (flip) // Bitmap is stored bottom-to-top order (normal BMP) bmpPos = offset + (bmpHeight - 1 - (row + loadY)) * rowSize; else // Bitmap is stored top-to-bottom bmpPos = offset + (row + loadY) * rowSize; if (depth == 24) { bmpPos += loadX * 3; } else { bmpPos += loadX / 8; bitIn = 7 - (loadX & 7); bitOut = 0x80; if (img) { // If loading 1-bit image to RAM destidx = ((bmpWidth + 7) / 8) * row; } } if (file.position() != bmpPos) { // Need seek? if (transact) { epd->endWrite(); // End EPD SPI transaction } file.seek(bmpPos); // Seek = SD transaction srcidx = sizeof sdbuf; // Force buffer reload } int span_x = epd_col; for (col = 0; col < loadWidth; col++) { // EACH PIXEL ON SCANLINE -------------------------------- if (srcidx >= sizeof sdbuf) { // Time to load more data? if (epd && transact) { epd->endWrite(); // End display SPI transaction } #if defined(ARDUINO_NRF52_ADAFRUIT) // NRF52840 seems to have trouble reading more than 512 // bytes across certain boundaries. Workaround for now // is to break the read into smaller chunks... int32_t bytesToGo = sizeof sdbuf, bytesRead = 0, bytesThisPass; while (bytesToGo > 0) { bytesThisPass = min(bytesToGo, 512); file.read(&sdbuf[bytesRead], bytesThisPass); bytesRead += bytesThisPass; bytesToGo -= bytesThisPass; } #else file.read(sdbuf, sizeof sdbuf); // Load from SD #endif if (epd && transact) { epd->startWrite(); // Start next display SPI transact } srcidx = 0; // Reset bmp buf index } if (depth == 24) { // Convert RGB pixel to 565 format, save in dest. // Makes no difference if going to screen or canvas. b = sdbuf[srcidx++]; g = sdbuf[srcidx++]; r = sdbuf[srcidx++]; dest[destidx++] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } else { // 1-bit // Extract bit (color index) from BMP... uint8_t n = (sdbuf[srcidx] >> bitIn) & 1; if (!bitIn) { srcidx++; bitIn = 7; } else { bitIn--; } if (epd) { // Loading 1-bit image to display... // RGB565 color from palette goes in EPD dest buffer... dest[destidx++] = quantized[n]; // Even though source image is 1-bit, and display might // be 1-bit, it's not always the case that the source // image is black & white (could be any two RGB colors). // Hence the expansion to RGB565. The span-rendering // function then quantizes and/or dithers this to what // the display can best handle. In some cases that's // fantastically bloaty (converting 1-bit to 16-bit RGB // and then back to 1-bit later in the span function), // but the alternative is a TON of special case code. // Since the EPD is slow to update anyway, we'll just // accept it, the code isn't really the bottleneck. } else { // Loading 1-bit image to RAM... // Store bit in canvas1 buffer if (n) dest1[destidx] |= bitOut; else dest1[destidx] &= ~bitOut; bitOut >>= 1; if (!bitOut) { bitOut = 0x80; destidx++; } } } // If loading to display, and either we're on the last pixel // of the current row, OR if the dest buffer is full... if (epd && ((col == (loadWidth - 1)) || (destidx >= BUFPIXELS))) { // Issue a span of pixels to the display... span(dest, epd, span_x, epd_row + row, destidx, mode, dither); span_x += destidx; // Next span will start here destidx = 0; // Reset dest buffer counter } } // end pixel (column) loop ------------------------------- } // end scanline (row) loop ------------------------------- if (quantized) { // Was an RGB565 palette allocated? if (epd) // If image was loaded to display, free(quantized); // palette is no longer needed else img->palette = quantized; // Keep palette with img } } // end depth>24 or quantized malloc OK } // end top/left clip } // end malloc check } // end depth check } // end planes/compression check } // end signature file.close(); return status; }