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



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,//........//..........}
/* ============================================================* 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;Serial.printf("[drawIcon] %dx%d, calcBW=%d, useBW=%d\n",w, h, byteWidthCalc, byteWidth);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));elsesprBuf[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 < 20) break;}}}}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;}
ReadguyDriver myDisplay; //新建一个readguy对象, 用于显示驱动.void setup() {// put your setup code here, to run once:Serial.begin(115200); //初始化串口Serial.println("");myDisplay.init(1);myDisplay.setEpdDriver(true, true);myDisplay.fillScreen(TFT_WHITE); // 全屏填充颜色myDisplay.setTextColor(0, 1); //设置显示的颜色. 0代表黑色, 1代表白色drawIcon_PROGMEM(myDisplay, 100, 50, gImage_cat, 200, 200, TFT_BLACK);drawIcon_PROGMEM(myDisplay, 0, 0, gImage_cat02, 83, 100, TFT_BLACK);drawIconAuto_PROGMEM(myDisplay, 100, 0, gImage_cat02, TFT_BLACK,sizeof(gImage_cat02));myDisplay.display(); //默认局部快刷myDisplay.powerOffEPD();//关闭电源,保护屏幕}void loop(){delay(10);}