Fix photo stripes and keep GIF playback off the UI thread.

Use cover/stretch display, solid sample assets, core0 GIF decode without blocking touch, and always-commit project rule.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 18:18:03 +08:00
parent 800e168b98
commit 6cf42d56a7
14 changed files with 337 additions and 178 deletions

View File

@@ -0,0 +1,12 @@
---
description: Always create a local git commit after finishing a task
alwaysApply: true
---
# Git commit after work
After completing a user-requested change (code, docs, firmware, assets), always:
1. Update `CHANGELOG.md` when behavior changes
2. `git add` relevant files and create a local commit (do not push unless asked)
3. Keep the working tree clean before ending the turn

View File

@@ -1,5 +1,19 @@
# Changelog
## 2026-10-19 — 去掉图后条纹
- 样张改为纯色底,去掉装饰横纹。
- 画面 cover 铺满画布;LVGL 全屏 STRETCH,避免缩放留缝/条带。
## 2026-10-19 — GIF 不阻塞触控
- GIF 解码改到 core0 任务;`playFrame(false)` 去掉库内 `delay()`。
- 切换相片先 `photo_view_stop()`,避免点按卡几秒。
## 2026-10-19 — 每次改完提交
- 项目规则:完成任务后必须本地 git commit。
## 2026-10-19 — Photo album
- CYD LVGL 壳层来自 tftest(触摸映射冻结,TF + SoftSPI)。

View File

@@ -7,8 +7,8 @@ ESP32-2432S028R 电子相册:从 TF 卡读取 **JPG / PNG / GIF**,触控切
## 功能
- 扫描 TF 卡 `/photos` 与根目录的 `.jpg` / `.jpeg` / `.png` / `.gif`
- 半分辨率画布 160×120,界面 2× 放大到 320×240(适配无 PSRAM 的 DRAM)
- JPG 过大自动 1/2、1/4、1/8 缩放;PNG / GIF 等比缩入画布
- 半分辨率画布 160×120,全屏拉伸到 320×240(适配无 PSRAM 的 DRAM)
- JPG / PNG / GIF 按 cover 铺满画布(可裁边)
- **点左半屏 = 上一张,点右半屏 = 下一张**;底栏 `<` / `>` 同样切换
- GIF 自动播帧;右上角显示 TF 状态
- 串口 `#PUT` 传文件到卡(`tools/sd_put.py`)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -1,5 +1,5 @@
name: CYD Photo album (cyd)
git: 99618a4
git: 800e168
date: 2026-10-19
board: ESP32-2432S028R (ILI9341)
features: jpg/png/gif album, SoftSPI touch, TF card
@@ -12,5 +12,5 @@ offsets:
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin CB21DB25D8AAC041B39C10AA2E63E00D2401D1A7EA4F956F9B4985D1EA87C8EB
firmware.bin 8BAFA46DA3DF4BF3A15D5B67E3062B80E0D8576A2F59BE2101431747E24438AF
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

Binary file not shown.

View File

@@ -11,6 +11,9 @@ void photo_view_deinit();
// Decode entry into half-res RGB565 canvas (160x120, shown 2x).
bool photo_view_show(const PhotoEntry *entry);
// Abort GIF worker immediately so touch/switch stays responsive.
void photo_view_stop();
const lv_image_dsc_t *photo_view_dsc();
uint16_t photo_view_canvas_w();
uint16_t photo_view_canvas_h();
@@ -18,5 +21,5 @@ uint16_t photo_view_img_w();
uint16_t photo_view_img_h();
bool photo_view_is_gif();
// Advance GIF frame if due; returns true when pixels changed.
// Publish a ready GIF frame to the visible canvas (non-blocking).
bool photo_view_gif_tick();

View File

@@ -72,14 +72,15 @@ void setup() {
}
void loop() {
if (sd_put_poll()) {
refresh_sd_ui(true);
}
// Touch/UI first — GIF decode runs on core0 and must not starve this.
lv_timer_handler();
lv_tick_inc(5);
ui_tick();
if (sd_put_poll()) {
refresh_sd_ui(true);
}
const uint32_t now = millis();
if (now - last_refresh_ms >= REFRESH_MS) {
last_refresh_ms = now;
@@ -87,5 +88,5 @@ void loop() {
refresh_sd_ui(false);
}
delay(5);
delay(photo_view_is_gif() ? 2 : 5);
}

View File

@@ -8,6 +8,9 @@
#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.
static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2;
static constexpr uint16_t CANVAS_H = UI_VER_RES / 2;
@@ -24,8 +27,6 @@ static PNG *g_png = nullptr;
static AnimatedGIF *g_gif = nullptr;
static File g_file;
static bool g_gif_open = false;
static uint32_t g_gif_next_ms = 0;
static int g_off_x = 0;
static int g_off_y = 0;
static int g_src_w = 0;
@@ -33,12 +34,24 @@ static int g_src_h = 0;
static int g_dst_w = 0;
static int g_dst_h = 0;
static void canvas_clear(uint16_t color) {
// GIF worker (core 0): decode must not block LVGL/touch on core 1.
static TaskHandle_t g_gif_task = nullptr;
static uint16_t *g_back = nullptr; // decode target (heap)
static uint16_t *g_draw_dst = g_pixels; // where gif_draw writes
static char g_gif_path[PHOTO_PATH_MAX];
static volatile bool g_gif_active = false; // session open (UI-visible)
static volatile bool g_gif_busy = false; // worker running
static volatile bool g_gif_stop = false; // request abort
static volatile bool g_frame_ready = false; // back buffer has a new frame
static void canvas_clear_buf(uint16_t *buf, uint16_t color) {
for (size_t i = 0; i < CANVAS_PIXELS; i++) {
g_pixels[i] = color;
buf[i] = color;
}
}
static void canvas_clear(uint16_t color) { 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;
@@ -65,47 +78,40 @@ static void free_png() {
}
}
static void gif_close_session() {
if (g_gif_open && g_gif) {
g_gif->close();
g_gif_open = false;
// 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 (g_gif) {
delete g_gif;
g_gif = nullptr;
if (!found) {
best = 0;
best_dw = w;
best_dh = h;
}
}
static void fit_size(int nw, int nh, int *dw, int *dh) {
*dw = nw;
*dh = nh;
if (*dw <= (int)CANVAS_W && *dh <= (int)CANVAS_H) {
return;
}
const float sx = (float)CANVAS_W / (float)nw;
const float sy = (float)CANVAS_H / (float)nh;
const float s = sx < sy ? sx : sy;
*dw = (int)(nw * s + 0.5f);
*dh = (int)(nh * s + 0.5f);
if (*dw < 1) {
*dw = 1;
}
if (*dh < 1) {
*dh = 1;
}
}
static int jpeg_scale_for(int w, int h) {
if (w > CANVAS_W * 4 || h > CANVAS_H * 4) {
return JPEG_SCALE_EIGHTH;
}
if (w > CANVAS_W * 2 || h > CANVAS_H * 2) {
return JPEG_SCALE_QUARTER;
}
if (w > CANVAS_W || h > CANVAS_H) {
return JPEG_SCALE_HALF;
}
return 0;
*dw = best_dw;
*dh = best_dh;
return best;
}
static void *jpeg_open(const char *filename, int32_t *size) {
@@ -192,34 +198,32 @@ static int png_draw(PNGDRAW *p) {
return 0;
}
// Line buffer for source width (cap to avoid stack blow)
static uint16_t line[640];
if (p->iWidth > 640) {
return 0;
}
g_png->getLineAsRGB565(p, line, PNG_RGB565_LITTLE_ENDIAN, 0x0000);
// Nearest-neighbor: which dest rows map from this source row?
for (int dy = 0; dy < g_dst_h; dy++) {
const int sy = (dy * g_src_h) / g_dst_h;
if (sy != p->y) {
continue;
}
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) {
continue;
}
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;
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];
}
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;
}
@@ -258,41 +262,182 @@ static int32_t gif_seek(GIFFILE *pFile, int32_t iPosition) {
}
static void gif_draw(GIFDRAW *pDraw) {
if (!pDraw || g_src_w <= 0 || g_dst_w <= 0) {
if (!pDraw || !g_draw_dst || g_src_w <= 0 || g_dst_w <= 0) {
return;
}
// Map source frame lines into scaled dest canvas
if (g_gif_stop) {
return;
}
const int src_y = pDraw->iY + pDraw->y;
for (int dy = 0; dy < g_dst_h; dy++) {
const int sy = (dy * g_src_h) / g_dst_h;
if (sy != src_y) {
continue;
}
const int y = g_off_y + dy;
uint16_t *dst = g_draw_dst;
// Fast path: 1:1 into canvas region
if (g_src_w == g_dst_w && g_src_h == g_dst_h) {
const int y = g_off_y + src_y;
if (y < 0 || y >= (int)CANVAS_H) {
continue;
return;
}
for (int dx = 0; dx < g_dst_w; dx++) {
const int sx = (dx * g_src_w) / g_dst_w;
const int lx = sx - pDraw->iX;
if (lx < 0 || lx >= pDraw->iWidth) {
continue;
}
const uint8_t idx = pDraw->pPixels[lx];
uint16_t *row = dst + (size_t)y * CANVAS_W + (size_t)g_off_x + pDraw->iX;
for (int x = 0; x < pDraw->iWidth; x++) {
const uint8_t idx = pDraw->pPixels[x];
if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) {
continue;
}
const int x = g_off_x + dx;
if (x < 0 || x >= (int)CANVAS_W) {
row[x] = pDraw->pPalette[idx];
}
return;
}
// Scaled: only dest rows that sample this source line
int dy = 0;
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < src_y) {
dy++;
}
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == src_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;
const int lx = sx - pDraw->iX;
if (lx < 0 || lx >= pDraw->iWidth) {
continue;
}
const uint8_t idx = pDraw->pPixels[lx];
if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) {
continue;
}
const int x = g_off_x + dx;
if (x < 0 || x >= (int)CANVAS_W) {
continue;
}
dst[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx];
}
}
dy++;
}
}
static void gif_worker_cleanup() {
if (g_gif) {
g_gif->close();
delete g_gif;
g_gif = nullptr;
}
g_draw_dst = g_pixels;
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_back) {
g_back = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t));
if (!g_back) {
Serial.println("GIF: back buffer alloc failed");
g_gif_busy = false;
g_gif_active = false;
continue;
}
g_pixels[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx];
}
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;
}
// Cover full canvas (sample source into every dest pixel).
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_buf(g_back, 0x0000);
g_draw_dst = g_back;
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 consumed the frame (or abort).
while (g_frame_ready && !g_gif_stop) {
vTaskDelay(pdMS_TO_TICKS(2));
}
if (g_gif_stop) {
break;
}
if (delay_ms < 30) {
delay_ms = 30;
}
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_close_session();
gif_stop_and_wait();
free_png();
free_jpeg();
@@ -310,22 +455,13 @@ static bool load_jpeg(const char *path) {
const int nw = g_jpeg->getWidth();
const int nh = g_jpeg->getHeight();
const int scale = jpeg_scale_for(nw, nh);
int dw = nw;
int dh = nh;
if (scale == JPEG_SCALE_HALF) {
dw /= 2;
dh /= 2;
} else if (scale == JPEG_SCALE_QUARTER) {
dw /= 4;
dh /= 4;
} else if (scale == JPEG_SCALE_EIGHTH) {
dw /= 8;
dh /= 8;
}
int dw = 0;
int dh = 0;
const int scale = jpeg_scale_cover(nw, nh, &dw, &dh);
g_img_w = (uint16_t)dw;
g_img_h = (uint16_t)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;
@@ -341,7 +477,7 @@ static bool load_jpeg(const char *path) {
}
static bool load_png(const char *path) {
gif_close_session();
gif_stop_and_wait();
free_jpeg();
free_png();
@@ -367,11 +503,13 @@ static bool load_png(const char *path) {
return false;
}
fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h);
g_img_w = (uint16_t)g_dst_w;
g_img_h = (uint16_t)g_dst_h;
g_off_x = ((int)CANVAS_W - g_dst_w) / 2;
g_off_y = ((int)CANVAS_H - g_dst_h) / 2;
// 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);
@@ -385,44 +523,37 @@ static bool load_png(const char *path) {
}
static bool load_gif(const char *path) {
gif_stop_and_wait();
free_jpeg();
free_png();
gif_close_session();
g_gif = new AnimatedGIF();
if (!g_gif) {
return false;
}
g_gif->begin(GIF_PALETTE_RGB565_LE);
if (!g_gif->open(path, gif_open, gif_close, gif_read, gif_seek, gif_draw)) {
Serial.printf("GIF open fail: %s\n", path);
gif_close_session();
return false;
}
g_src_w = g_gif->getCanvasWidth();
g_src_h = g_gif->getCanvasHeight();
if (g_src_w <= 0 || g_src_h <= 0) {
gif_close_session();
return false;
}
fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h);
g_img_w = (uint16_t)g_dst_w;
g_img_h = (uint16_t)g_dst_h;
g_off_x = ((int)CANVAS_W - g_dst_w) / 2;
g_off_y = ((int)CANVAS_H - g_dst_h) / 2;
g_gif_open = true;
gif_ensure_task();
strncpy(g_gif_path, path, sizeof(g_gif_path) - 1);
g_gif_path[sizeof(g_gif_path) - 1] = '\0';
// Show black immediately; worker fills frames async.
canvas_clear(0x0000);
int delay_ms = 0;
g_gif->playFrame(true, &delay_ms);
if (delay_ms < 20) {
delay_ms = 100;
canvas_sync_dsc();
g_gif_stop = false;
xTaskNotifyGive(g_gif_task);
// Brief wait for first frame (non-blocking beyond a short timeout).
const uint32_t t0 = millis();
while ((millis() - t0) < 250) {
if (g_frame_ready) {
break;
}
// Worker finished with no frame => open/decode failed
if ((millis() - t0) > 40 && !g_gif_busy && !g_gif_active) {
return false;
}
vTaskDelay(pdMS_TO_TICKS(5));
}
g_gif_next_ms = millis() + (uint32_t)delay_ms;
return true;
if (g_frame_ready && g_back) {
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
g_frame_ready = false;
}
return g_gif_active || g_gif_busy;
}
bool photo_view_init() {
@@ -432,6 +563,7 @@ bool photo_view_init() {
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());
@@ -439,13 +571,19 @@ bool photo_view_init() {
}
void photo_view_deinit() {
gif_close_session();
photo_view_stop();
free_jpeg();
free_png();
if (g_back) {
free(g_back);
g_back = 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;
@@ -476,21 +614,13 @@ 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_open; }
bool photo_view_is_gif() { return g_gif_active || g_gif_busy; }
bool photo_view_gif_tick() {
if (!g_gif_open || !g_gif) {
if (!g_frame_ready || !g_back) {
return false;
}
const uint32_t now = millis();
if ((int32_t)(now - g_gif_next_ms) < 0) {
return false;
}
int delay_ms = 0;
g_gif->playFrame(true, &delay_ms);
if (delay_ms < 20) {
delay_ms = 100;
}
g_gif_next_ms = now + (uint32_t)delay_ms;
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
g_frame_ready = false;
return true;
}

View File

@@ -110,6 +110,9 @@ static void go_delta(int delta) {
if (n == 0) {
return;
}
// Abort GIF worker first so the click is not stuck behind decode/delay.
photo_view_stop();
int16_t next = (int16_t)(g_index + delta);
while (next < 0) {
next = (int16_t)(next + (int16_t)n);
@@ -117,9 +120,6 @@ static void go_delta(int delta) {
while (next >= (int16_t)n) {
next = (int16_t)(next - (int16_t)n);
}
if (next == g_index && n > 1) {
// still reload if single? skip
}
g_index = next;
show_current();
}
@@ -185,9 +185,12 @@ void ui_create() {
lv_obj_add_event_cb(tap, on_tap_zone, LV_EVENT_CLICKED, nullptr);
img_photo = lv_image_create(scr);
lv_obj_set_size(img_photo, photo_view_canvas_w(), photo_view_canvas_h());
lv_image_set_scale(img_photo, 512); // 256=1x, 512=2x -> full 320x240
lv_obj_align(img_photo, LV_ALIGN_CENTER, 0, 0);
// Full-screen widget + stretch avoids scale-transform gaps/stripes behind the photo.
lv_obj_set_size(img_photo, UI_HOR_RES, UI_VER_RES);
lv_obj_set_pos(img_photo, 0, 0);
lv_image_set_inner_align(img_photo, LV_IMAGE_ALIGN_STRETCH);
lv_obj_set_style_bg_color(img_photo, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(img_photo, LV_OPA_COVER, 0);
lv_obj_clear_flag(img_photo, LV_OBJ_FLAG_CLICKABLE);
// Top bar

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate 320x240 JPG/PNG/GIF samples into assets/photos."""
"""Generate 320x240 JPG/PNG/GIF samples into assets/photos (solid bg, no stripes)."""
from __future__ import annotations
from pathlib import Path
@@ -27,16 +27,13 @@ def font(size: int = 28) -> ImageFont.ImageFont:
def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None:
im = Image.new("RGB", (W, H), bg)
d = ImageDraw.Draw(im)
for i in range(0, H, 12):
c = tuple(max(0, min(255, v + (i % 24) - 12)) for v in accent)
d.rectangle((0, i, W, i + 6), fill=c)
d.rectangle((16, 40, W - 16, H - 40), fill=bg)
d.rectangle((16, 40, W - 16, H - 40), outline=accent, width=3)
d.text((28, 70), title, fill=(234, 240, 246), font=font(36))
d.text((28, 130), subtitle, fill=(122, 139, 156), font=font(20))
d.text((28, 170), "CYD ALBUM TEST", fill=accent, font=font(16))
# Solid panel only — no horizontal stripe decoration
d.rounded_rectangle((20, 36, W - 20, H - 36), radius=12, fill=bg, outline=accent, width=3)
d.text((36, 70), title, fill=(234, 240, 246), font=font(36))
d.text((36, 130), subtitle, fill=(160, 175, 190), font=font(20))
d.text((36, 170), "CYD ALBUM TEST", fill=accent, font=font(16))
if fmt == "JPEG":
im.save(path, "JPEG", quality=90)
im.save(path, "JPEG", quality=92)
else:
im.save(path, "PNG")
print(f"wrote {path} ({path.stat().st_size} bytes)")
@@ -45,7 +42,7 @@ def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None:
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
card((10, 16, 22), (79, 195, 247), "01 JPG", "tap left / right", OUT / "01_blue.jpg", "JPEG")
card((20, 12, 28), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG")
card((28, 12, 18), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG")
card((8, 28, 20), (61, 220, 151), "03 PNG", "green sample", OUT / "03_green.png", "PNG")
frames = []
@@ -53,10 +50,9 @@ def main() -> None:
im = Image.new("RGB", (W, H), (10, 16, 22))
d = ImageDraw.Draw(im)
d.text((24, 24), "04 GIF", fill=(234, 240, 246), font=font(32))
d.text((24, 70), "animated", fill=(122, 139, 156), font=font(18))
d.text((24, 70), "animated", fill=(160, 175, 190), font=font(18))
x = 20 + i * 40
d.rectangle((x, 120, x + 80, 180), fill=(255, 200, 40))
d.rectangle((20, 200, 300, 210), fill=(36, 49, 64))
d.rounded_rectangle((x, 120, x + 80, 180), radius=8, fill=(255, 200, 40))
frames.append(im)
gif_path = OUT / "04_bounce.gif"
frames[0].save(