Wrong BL=21 caused black screen; INT is separate from backlight and stays INPUT after reset. Co-authored-by: Cursor <cursoragent@cursor.com>
684 lines
17 KiB
C++
684 lines
17 KiB
C++
#include "photo_view.h"
|
|
#include "pins.h"
|
|
|
|
#include <AnimatedGIF.h>
|
|
#include <Arduino.h>
|
|
#include <JPEGDEC.h>
|
|
#include <PNGdec.h>
|
|
#include <SD.h>
|
|
#include <string.h>
|
|
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
|
|
// Half-res canvas (fits ESP32 DRAM). UI shows it at 2x -> 320x240.
|
|
/* Cap decode canvas on all boards to fit 3.5" DRAM. */
|
|
static constexpr uint16_t CANVAS_W = 128;
|
|
static constexpr uint16_t CANVAS_H = 96;
|
|
static constexpr size_t CANVAS_PIXELS = (size_t)CANVAS_W * (size_t)CANVAS_H;
|
|
|
|
static uint16_t *g_pixels = nullptr; // buffer A (heap)
|
|
static uint16_t *g_buf_b = nullptr; // buffer B (heap, GIF double-buffer)
|
|
static uint16_t *g_show = nullptr; // LVGL reads this
|
|
static uint16_t *g_draw = nullptr; // 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 = nullptr; // 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;
|
|
}
|
|
if (!g_pixels) {
|
|
g_pixels = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t));
|
|
if (!g_pixels) {
|
|
Serial.println("photo_view: canvas alloc failed");
|
|
return false;
|
|
}
|
|
g_show = g_draw = g_draw_dst = g_pixels;
|
|
}
|
|
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;
|
|
}
|
|
if (g_pixels) {
|
|
free(g_pixels);
|
|
g_pixels = nullptr;
|
|
}
|
|
g_show = g_draw = g_draw_dst = nullptr;
|
|
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;
|
|
}
|