#include "photo_view.h" #include "pins.h" #include #include #include #include #include #include #include #include // Half-res canvas (fits ESP32 DRAM). UI shows it at 2x -> 320x240. static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2; static constexpr uint16_t CANVAS_H = UI_VER_RES / 2; static constexpr size_t CANVAS_PIXELS = (size_t)CANVAS_W * (size_t)CANVAS_H; static uint16_t g_pixels[CANVAS_PIXELS]; // buffer A static uint16_t *g_buf_b = nullptr; // buffer B (heap, GIF double-buffer) static uint16_t *g_show = g_pixels; // LVGL reads this static uint16_t *g_draw = g_pixels; // GIF decoder writes this static bool g_ready = false; static lv_image_dsc_t g_dsc; static uint16_t g_img_w = 0; static uint16_t g_img_h = 0; static JPEGDEC *g_jpeg = nullptr; static PNG *g_png = nullptr; static AnimatedGIF *g_gif = nullptr; static File g_file; static int g_off_x = 0; static int g_off_y = 0; static int g_src_w = 0; static int g_src_h = 0; static int g_dst_w = 0; static int g_dst_h = 0; // GIF worker (core 0): decode must not block LVGL/touch on core 1. static TaskHandle_t g_gif_task = nullptr; static uint16_t *g_draw_dst = g_pixels; // alias used by gif_draw static char g_gif_path[PHOTO_PATH_MAX]; static volatile bool g_gif_active = false; static volatile bool g_gif_busy = false; static volatile bool g_gif_stop = false; static volatile bool g_frame_ready = false; static void canvas_clear_buf(uint16_t *buf, uint16_t color) { if (!buf) { return; } for (size_t i = 0; i < CANVAS_PIXELS; i++) { buf[i] = color; } } static void canvas_clear(uint16_t color) { g_show = g_pixels; g_draw = g_pixels; g_draw_dst = g_pixels; canvas_clear_buf(g_pixels, color); } static void canvas_sync_dsc() { g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC; g_dsc.header.cf = LV_COLOR_FORMAT_RGB565; g_dsc.header.flags = 0; g_dsc.header.w = CANVAS_W; g_dsc.header.h = CANVAS_H; g_dsc.header.stride = CANVAS_W * 2; g_dsc.data_size = CANVAS_PIXELS * 2; g_dsc.data = (const uint8_t *)g_show; g_dsc.reserved = nullptr; } static void free_jpeg() { if (g_jpeg) { delete g_jpeg; g_jpeg = nullptr; } } static void free_png() { if (g_png) { delete g_png; g_png = nullptr; } } // Tightest JPEGDEC scale that still covers the canvas (center-cropped). static int jpeg_scale_cover(int w, int h, int *dw, int *dh) { const int scales[] = {0, JPEG_SCALE_HALF, JPEG_SCALE_QUARTER, JPEG_SCALE_EIGHTH}; const int divs[] = {1, 2, 4, 8}; int best = 0; int best_dw = w; int best_dh = h; int best_area = 0x7fffffff; bool found = false; for (int i = 0; i < 4; i++) { const int dw_i = w / divs[i]; const int dh_i = h / divs[i]; if (dw_i < 1 || dh_i < 1) { continue; } if (dw_i >= (int)CANVAS_W && dh_i >= (int)CANVAS_H) { const int area = dw_i * dh_i; if (area < best_area) { best_area = area; best = scales[i]; best_dw = dw_i; best_dh = dh_i; found = true; } } } if (!found) { best = 0; best_dw = w; best_dh = h; } *dw = best_dw; *dh = best_dh; return best; } static void *jpeg_open(const char *filename, int32_t *size) { g_file = SD.open(filename); if (!g_file) { return nullptr; } *size = (int32_t)g_file.size(); return &g_file; } static void jpeg_close(void *handle) { if (handle) { ((File *)handle)->close(); } } static int32_t jpeg_read(JPEGFILE *handle, uint8_t *buffer, int32_t length) { if (!handle || !handle->fHandle) { return 0; } return (int32_t)((File *)handle->fHandle)->read(buffer, length); } static int32_t jpeg_seek(JPEGFILE *handle, int32_t position) { if (!handle || !handle->fHandle) { return 0; } return ((File *)handle->fHandle)->seek(position) ? position : 0; } static int jpeg_draw(JPEGDRAW *p) { if (!p || !p->pPixels) { return 0; } for (int row = 0; row < p->iHeight; row++) { const int y = p->y + row; if (y < 0 || y >= (int)CANVAS_H) { continue; } const uint16_t *src = p->pPixels + row * p->iWidth; for (int col = 0; col < p->iWidth; col++) { const int x = p->x + col; if (x < 0 || x >= (int)CANVAS_W) { continue; } g_pixels[(size_t)y * CANVAS_W + (size_t)x] = src[col]; } } return 1; } static void *png_open(const char *filename, int32_t *size) { g_file = SD.open(filename); if (!g_file) { return nullptr; } *size = (int32_t)g_file.size(); return &g_file; } static void png_close(void *handle) { if (handle) { ((File *)handle)->close(); } } static int32_t png_read(PNGFILE *handle, uint8_t *buffer, int32_t length) { if (!handle || !handle->fHandle) { return 0; } return (int32_t)((File *)handle->fHandle)->read(buffer, length); } static int32_t png_seek(PNGFILE *handle, int32_t position) { if (!handle || !handle->fHandle) { return 0; } return ((File *)handle->fHandle)->seek(position) ? position : 0; } static int png_draw(PNGDRAW *p) { if (!p || !g_png || g_src_w <= 0 || g_dst_w <= 0) { return 0; } static uint16_t line[640]; if (p->iWidth > 640) { return 0; } g_png->getLineAsRGB565(p, line, PNG_RGB565_LITTLE_ENDIAN, 0x0000); int dy = 0; while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < p->y) { dy++; } while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == p->y) { const int y = g_off_y + dy; if (y >= 0 && y < (int)CANVAS_H) { for (int dx = 0; dx < g_dst_w; dx++) { const int sx = (dx * g_src_w) / g_dst_w; if (sx < 0 || sx >= p->iWidth) { continue; } const int x = g_off_x + dx; if (x < 0 || x >= (int)CANVAS_W) { continue; } g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx]; } } dy++; } return 1; } static void *gif_open(const char *fname, int32_t *pSize) { g_file = SD.open(fname); if (!g_file) { return nullptr; } *pSize = (int32_t)g_file.size(); return &g_file; } static void gif_close(void *pHandle) { if (pHandle) { ((File *)pHandle)->close(); } } static int32_t gif_read(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) { if (!pFile || !pFile->fHandle) { return 0; } int32_t n = (int32_t)((File *)pFile->fHandle)->read(pBuf, iLen); pFile->iPos = (int32_t)((File *)pFile->fHandle)->position(); return n; } static int32_t gif_seek(GIFFILE *pFile, int32_t iPosition) { if (!pFile || !pFile->fHandle) { return 0; } ((File *)pFile->fHandle)->seek(iPosition); pFile->iPos = (int32_t)((File *)pFile->fHandle)->position(); return pFile->iPos; } // GIF disposal tracking (apply previous frame's disposal at start of next frame). static uint8_t g_gif_pend_disposal = 0; static int16_t g_gif_pend_x = 0; static int16_t g_gif_pend_y = 0; static int16_t g_gif_pend_w = 0; static int16_t g_gif_pend_h = 0; static uint16_t g_gif_bg565 = 0x0000; static void gif_fill_rect(uint16_t *dst, int x0, int y0, int w, int h, uint16_t color) { if (!dst || w <= 0 || h <= 0) { return; } for (int y = y0; y < y0 + h; y++) { if (y < 0 || y >= (int)CANVAS_H) { continue; } for (int x = x0; x < x0 + w; x++) { if (x < 0 || x >= (int)CANVAS_W) { continue; } dst[(size_t)y * CANVAS_W + (size_t)x] = color; } } } // 1:1 blit into canvas (centered / clipped). No per-frame downscale — that // caused moving-region flicker with disposal + transparency. static void gif_draw(GIFDRAW *pDraw) { if (!pDraw || !g_draw_dst || g_src_w <= 0) { return; } if (g_gif_stop) { return; } uint16_t *dst = g_draw_dst; if (pDraw->y == 0) { // Restore previous frame rect to background before drawing this frame. if (g_gif_pend_disposal == 2) { gif_fill_rect(dst, g_gif_pend_x, g_gif_pend_y, g_gif_pend_w, g_gif_pend_h, g_gif_bg565); } g_gif_pend_disposal = pDraw->ucDisposalMethod; g_gif_pend_x = (int16_t)(g_off_x + pDraw->iX); g_gif_pend_y = (int16_t)(g_off_y + pDraw->iY); g_gif_pend_w = (int16_t)pDraw->iWidth; g_gif_pend_h = (int16_t)pDraw->iHeight; if (pDraw->pPalette) { g_gif_bg565 = pDraw->pPalette[pDraw->ucBackground]; } } const int y = g_off_y + pDraw->iY + pDraw->y; if (y < 0 || y >= (int)CANVAS_H) { return; } const int x0 = g_off_x + pDraw->iX; for (int i = 0; i < pDraw->iWidth; i++) { const int x = x0 + i; if (x < 0 || x >= (int)CANVAS_W) { continue; } const uint8_t idx = pDraw->pPixels[i]; if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) { continue; } dst[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx]; } } static void gif_worker_cleanup() { if (g_gif) { g_gif->close(); delete g_gif; g_gif = nullptr; } g_show = g_pixels; g_draw = g_pixels; g_draw_dst = g_pixels; g_dsc.data = (const uint8_t *)g_show; g_frame_ready = false; g_gif_busy = false; g_gif_active = false; } static void gif_task(void * /*arg*/) { for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); g_gif_busy = true; g_gif_stop = false; g_frame_ready = false; if (!g_buf_b) { g_buf_b = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t)); if (!g_buf_b) { Serial.println("GIF: back buffer alloc failed"); g_gif_busy = false; g_gif_active = false; continue; } } g_gif = new AnimatedGIF(); if (!g_gif) { gif_worker_cleanup(); continue; } g_gif->begin(GIF_PALETTE_RGB565_LE); if (!g_gif->open(g_gif_path, gif_open, gif_close, gif_read, gif_seek, gif_draw)) { Serial.printf("GIF open fail: %s\n", g_gif_path); gif_worker_cleanup(); continue; } g_src_w = g_gif->getCanvasWidth(); g_src_h = g_gif->getCanvasHeight(); if (g_src_w <= 0 || g_src_h <= 0) { gif_worker_cleanup(); continue; } // 1:1 into canvas (center; clip if GIF larger than 160x120). g_off_x = ((int)CANVAS_W - g_src_w) / 2; g_off_y = ((int)CANVAS_H - g_src_h) / 2; g_dst_w = g_src_w; g_dst_h = g_src_h; g_img_w = CANVAS_W; g_img_h = CANVAS_H; g_gif_pend_disposal = 0; g_gif_pend_w = 0; g_gif_pend_h = 0; // Double-buffer: show A, decode into B (B starts as copy of A). g_show = g_pixels; g_draw = g_buf_b; canvas_clear_buf(g_show, 0x0000); memcpy(g_draw, g_show, CANVAS_PIXELS * sizeof(uint16_t)); g_draw_dst = g_draw; g_dsc.data = (const uint8_t *)g_show; g_gif_active = true; while (!g_gif_stop) { int delay_ms = 0; // bSync=false: never delay() inside the library (that froze touch). g_gif->playFrame(false, &delay_ms); if (g_gif_stop) { break; } g_frame_ready = true; // Wait until UI swapped buffers (or abort). while (g_frame_ready && !g_gif_stop) { vTaskDelay(pdMS_TO_TICKS(1)); } if (g_gif_stop) { break; } // Keep GIF timing; floor avoids hammering LVGL refresh. if (delay_ms < 40) { delay_ms = 40; } if (delay_ms > 200) { delay_ms = 200; } int waited = 0; while (waited < delay_ms && !g_gif_stop) { vTaskDelay(pdMS_TO_TICKS(5)); waited += 5; } } gif_worker_cleanup(); } } static void gif_ensure_task() { if (g_gif_task) { return; } xTaskCreatePinnedToCore(gif_task, "gif", 8192, nullptr, 1, &g_gif_task, 0); } static void gif_stop_and_wait() { g_gif_stop = true; g_frame_ready = false; const uint32_t t0 = millis(); while (g_gif_busy && (millis() - t0) < 800) { vTaskDelay(pdMS_TO_TICKS(5)); } // If worker hung, force local cleanup of flags (file may still be open briefly) if (g_gif_busy) { Serial.println("GIF: stop timeout"); g_gif_busy = false; g_gif_active = false; } } static bool load_jpeg(const char *path) { gif_stop_and_wait(); free_png(); free_jpeg(); g_jpeg = new JPEGDEC(); if (!g_jpeg) { return false; } if (!g_jpeg->open(path, jpeg_open, jpeg_close, jpeg_read, jpeg_seek, jpeg_draw)) { Serial.printf("JPEG open fail: %s err=%d\n", path, g_jpeg->getLastError()); free_jpeg(); return false; } const int nw = g_jpeg->getWidth(); const int nh = g_jpeg->getHeight(); int dw = 0; int dh = 0; const int scale = jpeg_scale_cover(nw, nh, &dw, &dh); g_img_w = CANVAS_W; g_img_h = CANVAS_H; // Center; negative offsets crop overflow into the canvas (cover). const int xoff = ((int)CANVAS_W - dw) / 2; const int yoff = ((int)CANVAS_H - dh) / 2; canvas_clear(0x0000); g_jpeg->setPixelType(RGB565_LITTLE_ENDIAN); const bool ok = g_jpeg->decode(xoff, yoff, scale); g_jpeg->close(); free_jpeg(); if (!ok) { Serial.printf("JPEG decode fail: %s\n", path); } return ok; } static bool load_png(const char *path) { gif_stop_and_wait(); free_jpeg(); free_png(); g_png = new PNG(); if (!g_png) { return false; } const int rc = g_png->open(path, png_open, png_close, png_read, png_seek, png_draw); if (rc != PNG_SUCCESS) { Serial.printf("PNG open fail: %s rc=%d\n", path, rc); free_png(); return false; } g_src_w = g_png->getWidth(); g_src_h = g_png->getHeight(); if (g_src_w <= 0 || g_src_h <= 0 || g_src_w > 640) { Serial.printf("PNG bad size %dx%d\n", g_src_w, g_src_h); g_png->close(); free_png(); return false; } // Sample source into every canvas pixel (cover / fill). g_dst_w = CANVAS_W; g_dst_h = CANVAS_H; g_img_w = CANVAS_W; g_img_h = CANVAS_H; g_off_x = 0; g_off_y = 0; canvas_clear(0x0000); const int drc = g_png->decode(nullptr, 0); g_png->close(); free_png(); if (drc != PNG_SUCCESS) { Serial.printf("PNG decode fail: %s rc=%d\n", path, drc); return false; } return true; } static bool load_gif(const char *path) { gif_stop_and_wait(); free_jpeg(); free_png(); gif_ensure_task(); strncpy(g_gif_path, path, sizeof(g_gif_path) - 1); g_gif_path[sizeof(g_gif_path) - 1] = '\0'; canvas_clear(0x0000); canvas_sync_dsc(); g_gif_stop = false; xTaskNotifyGive(g_gif_task); // Wait for first composed frame, then publish via pointer swap (no mid-frame tear). const uint32_t t0 = millis(); while ((millis() - t0) < 300) { if (g_frame_ready) { break; } if ((millis() - t0) > 40 && !g_gif_busy && !g_gif_active) { return false; } vTaskDelay(pdMS_TO_TICKS(5)); } if (g_frame_ready) { photo_view_gif_tick(); } canvas_sync_dsc(); return g_gif_active || g_gif_busy; } bool photo_view_init() { if (g_ready) { return true; } canvas_clear(0x0000); canvas_sync_dsc(); g_ready = true; gif_ensure_task(); Serial.printf("photo_view: canvas %ux%u (%u bytes), free heap %u\n", (unsigned)CANVAS_W, (unsigned)CANVAS_H, (unsigned)(CANVAS_PIXELS * 2), (unsigned)ESP.getFreeHeap()); return true; } void photo_view_deinit() { photo_view_stop(); free_jpeg(); free_png(); if (g_buf_b) { free(g_buf_b); g_buf_b = nullptr; } g_show = g_pixels; g_draw = g_pixels; g_draw_dst = g_pixels; g_ready = false; g_dsc.data = nullptr; } void photo_view_stop() { gif_stop_and_wait(); } bool photo_view_show(const PhotoEntry *entry) { if (!entry || !photo_view_init()) { return false; } bool ok = false; switch (entry->kind) { case PHOTO_JPG: ok = load_jpeg(entry->path); break; case PHOTO_PNG: ok = load_png(entry->path); break; case PHOTO_GIF: ok = load_gif(entry->path); break; } canvas_sync_dsc(); return ok; } const lv_image_dsc_t *photo_view_dsc() { return g_ready ? &g_dsc : nullptr; } uint16_t photo_view_canvas_w() { return CANVAS_W; } uint16_t photo_view_canvas_h() { return CANVAS_H; } uint16_t photo_view_img_w() { return g_img_w; } uint16_t photo_view_img_h() { return g_img_h; } bool photo_view_is_gif() { return g_gif_active || g_gif_busy; } bool photo_view_gif_tick() { if (!g_frame_ready || !g_buf_b) { return false; } // Publish completed frame by swapping pointers (display never sees a half-copy). uint16_t *tmp = g_show; g_show = g_draw; g_draw = tmp; g_dsc.data = (const uint8_t *)g_show; g_draw_dst = g_draw; // Next decode must start from the frame we just showed (disposal / transparency). memcpy(g_draw, g_show, CANVAS_PIXELS * sizeof(uint16_t)); g_frame_ready = false; return true; }