《ESP8266墨水屏阅读器开发实战》——使用readguy库显示Img2LCD生成的点阵数据

原文链接:https://mp.weixin.qq.com/s/tSdABeNaBxmIidHYIrB7ww

本文实现的功能是基于GitHub上readguy开源代码的二次开发与精简,项目原始作者是friendshipender,项目地址:https://github.com/fsender/readguy

上一篇介绍了如何显示PCtoLCD2002生成的字模,减少FLASH占用。这篇继续分享如何使用readguy库显示Img2LCD生成的点阵数据。
先看效果:
《ESP8266墨水屏阅读器开发实战》——使用readguy库显示Img2LCD生成的点阵数据
准备一张图片(最好单色),宽高一致。
《ESP8266墨水屏阅读器开发实战》——使用readguy库显示Img2LCD生成的点阵数据
(可灵AI生成)
打开图片,按如下设置:水平扫描、单色、宽高=200、勾选高位在前。根据图片可调整亮度、对比度。保存*.c文件。
《ESP8266墨水屏阅读器开发实战》——使用readguy库显示Img2LCD生成的点阵数据
新建pic.h文件,复制刚保存的数据,在变量名后面加上PROGMEM
#ifndef PIC_H#define PIC_H
#include const unsigned char gImage_cat[5000] PROGMEM={ /* 0X10,0X01,0X00,0XC8,0X00,0XC8, */0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,0X00,//........//..........#endif
新建icons.h,生成绘制图标函数drawIcon_PROGMEM
#ifndef ICONS_H#define ICONS_H
#include 
/* ============================================================ * icons.h - 图标/图片绘制工具函数库 v2.0 * * 功能列表: *   ✅ drawIcon_PROGMEM()       : 基础绘图 (手动指定尺寸) *   ✅ parseImageHeader()       : 解析IMG2LCD头信息 *   ✅ printImageInfo()         : 调试工具 (打印详细信息) *   ✅ drawIconAuto_PROGMEM()   : ⭐智能绘图 (自动检测或反推尺寸) * * 适用场景: *   - 显示 IMG2LCD 生成的单色位图数据 *   - 支持有头信息和无头信息的图片 *   - 自动处理字节对齐问题 *   - 智能反推图片尺寸 (当头信息不可用) * * 使用方法: *   #include "icons.h" * *   方法1: 手动指定 (最可靠) *     drawIcon_PROGMEM(gfx, x, y, data, width, height, color); * *   方法2: 自动模式 (推荐!) ⭐ *     drawIconAuto_PROGMEM(gfx, x, y, data, color, sizeof(data)); * *   方法3: 调试模式 *     printImageInfo("name", data, sizeof(data)); * * 版本: 2.0.0 (增强版 - 支持无头信息图片) * ============================================================ */
/** * @brief 绘制图标 (支持任意尺寸,自动处理字节对齐) * @param gfx               图形对象引用 * @param x, y              显示位置 (左上角坐标) * @param data              图像数据指针 (必须存储在 PROGMEM 中) * @param w, h              图像宽高 (像素) * @param color             颜色 (TFT_BLACK 或 TFT_WHITE) * @param byteWidthOverride 可选: 手动指定每行字节数 (-1=自动计算) * * @note 技术细节: *   - 使用 pgm_read_byte() 从 PROGMEM 读取数据 (节省 RAM) *   - 自动进行边界检查,防止数组越界 *   - 支持 DEBUG_ICON 宏开启调试输出 * */template <typename T>void drawIcon_PROGMEM(T &gfx, int32_t x, int32_t y, const uint8_t *data,                      int32_t w, int32_t h, uint32_t color,                      int32_t byteWidthOverride = -1){    // 计算理论上的每行字节数    if (!data || w <= 0 || h <= 0)   return;    int32_t byteWidthCalc = (w + 7) / 8;
    // 使用用户指定的值或计算值    int32_t byteWidth = (byteWidthOverride > 0) ? byteWidthOverride : byteWidthCalc;
    uint8_t *sprBuf = (uint8_t *)gfx.getBuffer();    int32_t sprWidth = (gfx.drvWidth() + 7) / 8;
#ifdef DEBUG_ICON    Serial.printf("[drawIcon] %dx%d, calcBW=%d, useBW=%d\n",                  w, h, byteWidthCalc, byteWidth);#endif
    for (int32_t row = 0; row < h; row++)    {        for (int32_t col = 0; col < w; col++)        {            // 从 PROGMEM 读取图像数据            uint8_t byteVal = pgm_read_byte(data + row * byteWidth + col / 8);
            // 检查该像素是否应该绘制            if (byteVal & (0x80 >> (col & 7)))            {                int32_t px = x + col;                int32_t py = y + row;
                // 边界检查 (防止越界访问)                if (px >= 0 && px < gfx.width() && py >= 0 && py < gfx.height())                {                    int32_t bufIdx = py * sprWidth + px / 8;
                    // 设置像素颜色                    if (color == TFT_BLACK)                        sprBuf[bufIdx] &= ~(0x80 >> (px & 7));                    else                        sprBuf[bufIdx] |= (0x80 >> (px & 7));                }            }        }    }}

// ==================== 头信息解析函数 ====================/** * @brief 从 IMG2LCD 数据中解析真实的图像尺寸 * @param data        图像数据指针 (PROGMEM) * @param outWidth    输出: 真实宽度 * @param outHeight   输出: 真实高度 * @return true=成功解析, false=解析失败 *  * IMG2LCD 头信息格式 (6字节): *   Byte[0]: 标记 (通常 0x10) *   Byte[1]: 版本 (通常 0x01)  *   Byte[2-3]: 宽度 (大端序, 高字节在前) *   Byte[4-5]: 高度 (大端序, 高字节在前) */bool parseImageHeader(const uint8_t *data, int32_t &outWidth, int32_t &outHeight){    if (!data)        return false;
    uint8_t b2 = pgm_read_byte(data + 2);    uint8_t b3 = pgm_read_byte(data + 3);    uint8_t b4 = pgm_read_byte(data + 4);    uint8_t b5 = pgm_read_byte(data + 5);
    outWidth = (b2 << 8) | b3;    outHeight = (b4 << 8) | b5;
    // 合理性检查 (宽度/高度应该在 1~2000 之间)    return (outWidth > 0 && outWidth <= 2000 &&            outHeight > 0 && outHeight <= 2000);}/** * @brief 打印图像详细信息 (用于串口调试) * @param name      图片名称标识 * @param data      图像数据 (PROGMEM) * @param dataSize  数组总大小 (使用 sizeof()) */void printImageInfo(const char *name, const uint8_t *data, int32_t dataSize){  Serial.printf("\n========== 图片信息: %s ==========\n", name);  Serial.printf("数据大小: %d 字节\n", dataSize);
  if (dataSize >= 16) {    Serial.println("\n前16字节原始数据:");    for (int i = 0; i < 16; i++) {      Serial.printf("%02X "pgm_read_byte(data + i));      if ((i + 1) % 8 == 0) Serial.println();    }    Serial.println();  }
  int32_t headerW = 0, headerH = 0;  bool parsed = parseImageHeader(data, headerW, headerH);
  if (parsed && dataSize >= 6) {    uint8_t b0 = pgm_read_byte(data + 0);    uint8_t b1 = pgm_read_byte(data + 1);    uint8_t b2 = pgm_read_byte(data + 2);    uint8_t b3 = pgm_read_byte(data + 3);    uint8_t b4 = pgm_read_byte(data + 4);    uint8_t b5 = pgm_read_byte(data + 5);
    Serial.printf("✅ 头信息解析成功:\n");    Serial.printf("   标记: [0x%02X][0x%02X]\n", b0, b1);    Serial.printf("   尺寸: %d × %d 像素 (0x%02X%02X × 0x%02X%02X)\n"                  headerW, headerH, b2, b3, b4, b5);
    int32_t byteWidth = (headerW + 7) / 8;    int32_t calcSize = byteWidth * headerH;    Serial.printf("   每行字节数: %d\n", byteWidth);    Serial.printf("   理论大小: %d 字节 (%d行 × %d字节/行)\n", calcSize, headerH, byteWidth);
    if (calcSize == dataSize) {      Serial.println("   ✅ 数据大小完全匹配! 头信息可靠!");      Serial.println("\n📋 正确调用方式:");      Serial.printf("   drawIcon_PROGMEM(myDisplay, x, y, %s, %d, %d, TFT_BLACK);\n",                    name, headerW, headerH);    } else {      Serial.println("   ⚠️ 数据大小不匹配!");      Serial.printf("   实际: %d 字节, 理论: %d 字节 (差值: %d)\n"                    dataSize, calcSize, dataSize - calcSize);      Serial.println("   可能原因: IMG2LCD进行了字节对齐填充");    }  } else {    Serial.println("❌ 无法从头信息解析!");    Serial.println("   可能原因: 头信息不存在或在注释中");  }
  Serial.println("\n💡 从数据大小反推可能的尺寸:");  int found = 0;  for (int tryH = 1; tryH <= 300 && found < 15; tryH++) {    int bw = dataSize / tryH;    if (bw > 0 && dataSize % tryH == 0) {      int approxW = bw * 8;      if (approxW > 0 && approxW <= 1000) {        Serial.printf("   → %4d × %-4d (byteWidth=%3d)\n", approxW, tryH, bw);        found++;      }    }  }
  if (found == 0) {    Serial.println("   (无法找到整除的组合)");  }
  Serial.println("\n提示: 如果显示'无法解析头信息',请查看 pic.h 文件中");  Serial.println("      数组定义行的注释,格式如: /* 0X10,0X01,0X00,0XBB,0X00,0XC8 */");  Serial.println("======================================\n");}

// ==================== 智能自动绘图函数 ====================
/** * @brief 自动绘制图标 ⭐最智能的版本! * @param gfx         图形对象 * @param x, y        显示位置 * @param data        图像数据 (PROGMEM) * @param color       颜色 * @param dataSize    数据总大小 (必须提供 sizeof()!) * @return true=成功绘制, false=失败 *  * 功能说明: *   1. 先尝试从头信息解析尺寸 *   2. 如果失败,从数据大小数学反推 *   3. 优先选择接近正方形的尺寸 *   4. 自动调用 drawIcon_PROGMEM 绘制 *  * @code * // 最简用法 - 不需要知道尺寸! * drawIconAuto_PROGMEM(myDisplay, 0, 0, gImage_cat02, TFT_BLACK, sizeof(gImage_cat02)); * @endcode */template<typename T>bool drawIconAuto_PROGMEM(T& gfx, int32_t x, int32_t y,                      const uint8_t* data, uint32_t color,                      int32_t dataSize) {  if (!data || dataSize <= 0) {    Serial.println("[drawIconAuto] ❌ 无效参数!");    return false;  }
  int32_t w = 0, h = 0;  bool fromHeader = parseImageHeader(data, w, h);
  if (!fromHeader) {    Serial.println("[drawIconAuto] ⚠️ 头信息不存在,尝试从数据大小反推...");
    int32_t bestW = 0, bestH = 0;    int32_t minDiff = dataSize;
    for (int tryH = 1; tryH <= 300; tryH++) {      int bw = dataSize / tryH;      if (bw > 0 && dataSize % tryH == 0) {        int tryW = bw * 8;        if (tryW > 0 && tryW <= 1000) {          int diff = abs(tryW - tryH);          if (diff < minDiff) {            minDiff = diff;            bestW = tryW;            bestH = tryH;            if (diff < 20break;          }        }      }    }
    if (bestW > 0 && bestH > 0) {      w = bestW;      h = bestH;      Serial.printf("[drawIconAuto] ✅ 反推成功: 可能是 %d × %d\n", w, h);    } else {      Serial.printf("[drawIconAuto] ❌ 无法确定尺寸! (dataSize=%d)\n", dataSize);      Serial.println("[drawIconAuto] 请使用 printImageInfo() 查看详情,然后手动指定尺寸");      return false;    }  } else {    Serial.printf("[drawIconAuto] ✅ 头信息解析成功: %d × %d\n", w, h);  }
  drawIcon_PROGMEM(gfx, x, y, data, w, h, color);  return true;}
#endif // ICONS_H
helloHINK.ino
#include   //arduino功能基础库. 在platformIO平台上此语句不可或缺#include "readguy.h"  //包含readguy_driver 基础驱动库#include "pic.h"#include "icons.h"
ReadguyDriver myDisplay;  //新建一个readguy对象, 用于显示驱动.void setup() {  // put your setup code here, to run once:  Serial.begin(115200);  //初始化串口  Serial.println("");
  myDisplay.init(1);
  myDisplay.setEpdDriver(truetrue);
  myDisplay.fillScreen(TFT_WHITE);  // 全屏填充颜色  myDisplay.setTextColor(01);     //设置显示的颜色. 0代表黑色, 1代表白色
  drawIcon_PROGMEM(myDisplay, 10050, gImage_cat, 200200, TFT_BLACK);
  drawIcon_PROGMEM(myDisplay, 00, gImage_cat02, 83100, TFT_BLACK);
  drawIconAuto_PROGMEM(myDisplay, 1000, gImage_cat02, TFT_BLACK,sizeof(gImage_cat02));  myDisplay.display();  //默认局部快刷  myDisplay.powerOffEPD();//关闭电源,保护屏幕  }void loop(){delay(10);}
下一篇介绍如何读取SD卡内容,并显示到屏幕上。
 

暂无评论,快来发表第一条评论吧!

📋 需求咨询