#include "title.h" #include #include #include #include #include #include #include #include #include #include FT_FREETYPE_H #define MAX_IMG 16384 /* ---------- helpers ---------- */ struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; static void my_error_exit(j_common_ptr cinfo) { struct my_error_mgr *m = (struct my_error_mgr *)cinfo->err; longjmp(m->setjmp_buffer, 1); } static void rgb2yuv(int r, int g, int b, int *Y, int *Cb, int *Cr) { *Y = (int)(0.299f * r + 0.587f * g + 0.114f * b); *Cb = (int)(128.0f - 0.169f * r - 0.331f * g + 0.500f * b); *Cr = (int)(128.0f + 0.500f * r - 0.419f * g - 0.081f * b); if (*Y < 0) *Y = 0; if (*Y > 255) *Y = 255; if (*Cb < 0) *Cb = 0; if (*Cb > 255) *Cb = 255; if (*Cr < 0) *Cr = 0; if (*Cr > 255) *Cr = 255; } static int png_is(const unsigned char m[8]) { static const unsigned char sig[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; return memcmp(m, sig, 8) == 0; } static int jpg_is(const unsigned char m[3]) { return m[0] == 0xFF && m[1] == 0xD8; } /* Decode PNG or JPEG (by magic bytes) into RGBA8 (straight alpha). * Caller frees *rgba with free(). Returns 0 on success. */ static int decode_image(const char *path, int *w, int *h, uint8_t **rgba) { FILE *fp = fopen(path, "rb"); if (!fp) { fprintf(stderr, "title: cannot open %s\n", path); return -1; } unsigned char magic[8]; if (fread(magic, 1, 8, fp) != 8) { fclose(fp); return -1; } fseek(fp, 0, SEEK_SET); if (png_is(magic)) { png_structp pp = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); png_infop ip = pp ? png_create_info_struct(pp) : NULL; if (!pp || !ip) { if (pp) png_destroy_read_struct(&pp, NULL, NULL); fclose(fp); return -1; } if (setjmp(png_jmpbuf(pp))) { png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; } png_init_io(pp, fp); png_read_info(pp, ip); png_set_strip_16(pp); png_set_expand(pp); png_set_tRNS_to_alpha(pp); if (png_get_color_type(pp, ip) == PNG_COLOR_TYPE_RGB) png_set_filler(pp, 0xFF, PNG_FILLER_AFTER); png_read_update_info(pp, ip); int W = (int)png_get_image_width(pp, ip); int H = (int)png_get_image_height(pp, ip); if (W <= 0 || H <= 0 || W > MAX_IMG || H > MAX_IMG) { png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; } size_t stride = png_get_rowbytes(pp, ip); uint8_t *buf = malloc((size_t)W * H * 4); if (!buf) { png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; } png_bytep *rows = malloc(sizeof(png_bytep) * H); if (!rows) { free(buf); png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; } for (int y = 0; y < H; y++) rows[y] = buf + (size_t)y * stride; png_read_image(pp, rows); png_read_end(pp, NULL); free(rows); png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); *w = W; *h = H; *rgba = buf; return 0; } if (jpg_is(magic)) { struct my_error_mgr jerr; struct jpeg_decompress_struct cinfo; cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; } jpeg_create_decompress(&cinfo); jpeg_stdio_src(&cinfo, fp); if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; } cinfo.out_color_space = JCS_RGB; jpeg_start_decompress(&cinfo); int W = cinfo.output_width, H = cinfo.output_height; if (W <= 0 || H <= 0 || W > MAX_IMG || H > MAX_IMG) { jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; } uint8_t *buf = malloc((size_t)W * H * 4); if (!buf) { jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; } JSAMPROW row = malloc(W * 3); if (!row) { free(buf); jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; } for (int y = 0; y < H; y++) { jpeg_read_scanlines(&cinfo, &row, 1); for (int x = 0; x < W; x++) { uint8_t *p = buf + ((size_t)y * W + x) * 4; p[0] = row[x * 3 + 0]; p[1] = row[x * 3 + 1]; p[2] = row[x * 3 + 2]; p[3] = 255; } } free(row); jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(fp); *w = W; *h = H; *rgba = buf; return 0; } fclose(fp); fprintf(stderr, "title: unsupported image format: %s\n", path); return -1; } /* Bilinear scale RGBA (premultiplied to avoid fringes) into dst. */ static void scale_rgba(const uint8_t *src, int sw, int sh, uint8_t *dst, int dw, int dh) { int xs = (dw > 1) ? dw - 1 : 1; int ys = (dh > 1) ? dh - 1 : 1; for (int y = 0; y < dh; y++) { float fy = ((float)y + 0.5f) * sh / (float)ys - 0.5f; if (fy < 0) fy = 0; int y0 = (int)fy; int y1 = y0 + 1; if (y1 >= sh) y1 = y0; float wy = fy - y0; for (int x = 0; x < dw; x++) { float fx = ((float)x + 0.5f) * sw / (float)xs - 0.5f; if (fx < 0) fx = 0; int x0 = (int)fx; int x1 = x0 + 1; if (x1 >= sw) x1 = x0; float wx = fx - x0; float pr = 0, pg = 0, pb = 0, pa = 0; const uint8_t *p00 = src + ((size_t)y0 * sw + x0) * 4; const uint8_t *p01 = src + ((size_t)y0 * sw + x1) * 4; const uint8_t *p10 = src + ((size_t)y1 * sw + x0) * 4; const uint8_t *p11 = src + ((size_t)y1 * sw + x1) * 4; float w00 = (1 - wx) * (1 - wy), w01 = wx * (1 - wy); float w10 = (1 - wx) * wy, w11 = wx * wy; float a00 = p00[3] / 255.0f, a01 = p01[3] / 255.0f; float a10 = p10[3] / 255.0f, a11 = p11[3] / 255.0f; pr = p00[0] * a00 * w00 + p01[0] * a01 * w01 + p10[0] * a10 * w10 + p11[0] * a11 * w11; pg = p00[1] * a00 * w00 + p01[1] * a01 * w01 + p10[1] * a10 * w10 + p11[1] * a11 * w11; pb = p00[2] * a00 * w00 + p01[2] * a01 * w01 + p10[2] * a10 * w10 + p11[2] * a11 * w11; pa = a00 * w00 + a01 * w01 + a10 * w10 + a11 * w11; uint8_t *o = dst + ((size_t)y * dw + x) * 4; if (pa > 0.001f) { o[0] = (uint8_t)(pr / pa); o[1] = (uint8_t)(pg / pa); o[2] = (uint8_t)(pb / pa); } else { o[0] = o[1] = o[2] = 0; } o[3] = (uint8_t)(pa * 255.0f); } } } /* Blend one RGBA image (premultiplied inside) onto the NV12 bar at (x0,y0). * opacity 0-255 scales the whole logo's translucency. */ static void blend_logo(uint8_t *map, int out_w, int out_h, int bar_h, const uint8_t *rgba, int w, int h, int x0, int y0, int opacity) { uint8_t *Y = map; uint8_t *UV = map + (size_t)out_w * out_h; float op = (opacity > 255) ? 255.0f : (opacity < 0 ? 0.0f : (float)opacity); for (int j = 0; j < h; j++) { int y = y0 + j; if (y < 0 || y >= bar_h) continue; for (int i = 0; i < w; i++) { int x = x0 + i; if (x < 0 || x >= out_w) continue; const uint8_t *p = rgba + ((size_t)j * w + i) * 4; float a = (p[3] / 255.0f) * (op / 255.0f); int r = p[0], g = p[1], b = p[2]; int yy, cb, cr; rgb2yuv(r, g, b, &yy, &cb, &cr); int wa = (int)(a * 255.0f); uint8_t *yp = &Y[(size_t)y * out_w + x]; *yp = (uint8_t)(*yp + ((yy - *yp) * wa) / 255); if ((x & 1) == 0 && (y & 1) == 0) { uint8_t *uvp = &UV[(size_t)y / 2 * out_w + x]; *uvp = (uint8_t)(*uvp + ((cb - *uvp) * wa) / 255); if (x + 1 < out_w) { uint8_t *uvp2 = uvp + 1; *uvp2 = (uint8_t)(*uvp2 + ((cr - *uvp2) * wa) / 255); } } } } } /* ---------- UTF-8 ---------- */ static int utf8_decode(const uint8_t *s, uint32_t *cp) { if (s[0] < 0x80) { *cp = s[0]; return 1; } if ((s[0] & 0xE0) == 0xC0 && (s[1] & 0xC0) == 0x80) { *cp = ((s[0] & 0x1F) << 6) | (s[1] & 0x3F); return 2; } if ((s[0] & 0xF0) == 0xE0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) { *cp = ((s[0] & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); return 3; } if ((s[0] & 0xF8) == 0xF0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) { *cp = ((s[0] & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); return 4; } return 0; } /* ---------- FreeType with process-wide caching ---------- * FT_Init_FreeType + FT_New_Face parse the (multi-MB) font file on every * call; rendering used to happen per frame for the title strip and NO * SIGNAL text. Cache the library and one face per font path, and cache * rasterized text bitmaps so per-frame cost is a single alpha blit. * All title_* calls come from the main thread only -> no locking needed. */ static FT_Library g_ft_lib = NULL; typedef struct { char path[512]; FT_Face face; int size; /* currently set pixel size, -1 = unset */ } FaceCache; static FaceCache g_face = { .path = "", .face = NULL, .size = -1 }; static FT_Face get_face(const char *font, int size) { if (!g_ft_lib) { if (FT_Init_FreeType(&g_ft_lib) != 0) return NULL; } if (!g_face.face || strcmp(g_face.path, font) != 0) { if (g_face.face) { FT_Done_Face(g_face.face); g_face.face = NULL; } if (FT_New_Face(g_ft_lib, font, 0, &g_face.face) != 0) { /* Configured font missing on this board (e.g. wqy-microhei not * installed): fall back to the first available default font so * the title strip still renders text. */ const char *fb = title_default_font(); int fb_ok = fb && strcmp(fb, font) != 0 && FT_New_Face(g_ft_lib, fb, 0, &g_face.face) == 0; if (!fb_ok) { g_face.face = NULL; return NULL; } fprintf(stderr, "title: font %s not found, using %s\n", font, fb); font = fb; } snprintf(g_face.path, sizeof(g_face.path), "%s", font); g_face.size = -1; } if (g_face.size != size) { if (FT_Set_Pixel_Sizes(g_face.face, 0, size) != 0) return NULL; g_face.size = size; } return g_face.face; } /* ---------- rasterized text bitmap cache ---------- */ #define TEXT_CACHE_SLOTS 6 typedef struct { /* key */ char text[256]; char font[64]; /* short tag: last path component */ int size; unsigned color; int opacity; /* bitmap: A8 coverage, origin at (ox, oy); baseline sits at y = oy */ uint8_t *cov; int w, h, ox, oy; long advance; /* total advance width in px */ uint64_t last_use; } TextBitmap; static TextBitmap g_tcache[TEXT_CACHE_SLOTS]; static uint64_t g_tcache_clock = 0; static TextBitmap *text_cache_find(const char *font, const char *text, int size, unsigned color, int opacity) { const char *tag = strrchr(font, '/'); tag = tag ? tag + 1 : font; for (int i = 0; i < TEXT_CACHE_SLOTS; i++) { TextBitmap *t = &g_tcache[i]; if (t->cov && t->size == size && t->color == color && t->opacity == opacity && t->advance >= 0 && strcmp(t->text, text) == 0 && strcmp(t->font, tag) == 0) return t; } return NULL; } /* Rasterize `text` into a fresh cache slot. Returns slot or NULL. */ static TextBitmap *text_cache_rasterize(const char *font, const char *text, int size, unsigned color, int opacity) { FT_Face face = get_face(font, size); if (!face) return NULL; long total = 0; const uint8_t *s = (const uint8_t *)text; while (*s) { uint32_t cp; int n = utf8_decode(s, &cp); if (n <= 0) { s++; continue; } if (FT_Load_Char(face, cp, FT_LOAD_NO_BITMAP) == 0) total += face->glyph->advance.x >> 6; s += n; } if (total <= 0) return NULL; /* find a free slot (or evict least recently used) */ TextBitmap *t = &g_tcache[0]; for (int i = 0; i < TEXT_CACHE_SLOTS; i++) { if (!g_tcache[i].cov) { t = &g_tcache[i]; break; } if (g_tcache[i].last_use < t->last_use) t = &g_tcache[i]; } free(t->cov); memset(t, 0, sizeof(*t)); /* generous box: pad covers negative bearings/overhangs */ int pad = size; t->w = (int)total + 2 * pad; t->h = 2 * size + 8; t->ox = pad; t->oy = size + 4; t->cov = calloc((size_t)t->w * t->h, 1); if (!t->cov) { memset(t, 0, sizeof(*t)); return NULL; } snprintf(t->text, sizeof(t->text), "%s", text); const char *tag = strrchr(font, '/'); tag = tag ? tag + 1 : font; snprintf(t->font, sizeof(t->font), "%s", tag); t->size = size; t->color = color; t->opacity = opacity < 0 ? 0 : (opacity > 255 ? 255 : opacity); t->advance = total; s = (const uint8_t *)text; int pen_x = t->ox; while (*s) { uint32_t cp; int n = utf8_decode(s, &cp); if (n <= 0) { s++; continue; } if (FT_Load_Char(face, cp, FT_LOAD_RENDER) == 0 && face->glyph->bitmap.buffer) { FT_Bitmap *bm = &face->glyph->bitmap; int gx = pen_x + face->glyph->bitmap_left; int gy = t->oy - face->glyph->bitmap_top; for (unsigned j = 0; j < bm->rows; j++) { int yy = gy + (int)j; if (yy < 0 || yy >= t->h) continue; const uint8_t *row = bm->buffer + (size_t)j * bm->pitch; for (unsigned i = 0; i < bm->width; i++) { int xx = gx + (int)i; if (xx < 0 || xx >= t->w) continue; int v = row[i]; if (v > t->cov[(size_t)yy * t->w + xx]) t->cov[(size_t)yy * t->w + xx] = (uint8_t)v; } } pen_x += face->glyph->advance.x >> 6; } s += n; } return t; } /* Blend a cached A8 text bitmap onto NV12 at baseline (bx, by), * clipped to [y_top, y_bot). Same Y/UV blending as draw_glyph. */ static void blit_text_bitmap(uint8_t *map, int out_w, int out_h, int y_top, int y_bot, const TextBitmap *t, int bx, int by, unsigned color, int opacity) { if (!t || !t->cov) return; uint8_t *Y = map; uint8_t *UV = map + (size_t)out_w * out_h; int Yw, Cbw, Crw; rgb2yuv((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, &Yw, &Cbw, &Crw); int x0 = bx - t->ox; int y0 = by - t->oy; for (int j = 0; j < t->h; j++) { int y = y0 + j; if (y < y_top || y >= y_bot || y >= out_h) continue; const uint8_t *crow = t->cov + (size_t)j * t->w; uint8_t *yrow = Y + (size_t)y * out_w; uint8_t *uvrow = UV + (size_t)(y / 2) * out_w; for (int i = 0; i < t->w; i++) { int cov = (crow[i] * opacity) / 255; if (cov <= 0) continue; int x = x0 + i; if (x < 0 || x >= out_w) continue; uint8_t *dst = &yrow[x]; *dst = (uint8_t)(*dst + ((Yw - *dst) * cov) / 255); if ((x & 1) == 0 && (y & 1) == 0) { uint8_t *uvp = &uvrow[x]; *uvp = (uint8_t)(*uvp + ((Cbw - *uvp) * cov) / 255); if (x + 1 < out_w) uvp[1] = (uint8_t)(uvp[1] + ((Crw - uvp[1]) * cov) / 255); } } } } static void text_cache_touch(TextBitmap *t) { t->last_use = ++g_tcache_clock; } /* Measure the advance width (px) of a UTF-8 text line. Returns width or -1. */ static long measure_text(const char *font, const char *text, int size) { FT_Face face = get_face(font, size); if (!face) return -1; const uint8_t *s = (const uint8_t *)text; long total = 0; while (*s) { uint32_t cp; int n = utf8_decode(s, &cp); if (n <= 0) { s++; continue; } if (FT_Load_Char(face, cp, FT_LOAD_NO_BITMAP) == 0) total += face->glyph->advance.x >> 6; s += n; } return total; } /* Render a UTF-8 text line into the NV12 buffer, clipped to [y_top, y_bot). * Text is left-aligned at left_x (<=0: centered on the whole width) and its * vertical center sits at center_y. opacity 0-255 scales translucency. * Uses the rasterized-text cache: repeated draws are a single blit. * Returns the total advance width (px) or -1 on error. */ static long render_text(uint8_t *map, int out_w, int out_h, int y_top, int y_bot, const char *font, const char *text, int size, unsigned color, int left_x, int center_y, int opacity) { if (opacity < 0) opacity = 0; if (opacity > 255) opacity = 255; TextBitmap *t = text_cache_find(font, text, size, color, opacity); if (!t) t = text_cache_rasterize(font, text, size, color, opacity); if (!t || !t->cov) return -1; text_cache_touch(t); if (left_x <= 0) { left_x = out_w / 2 - (int)t->advance / 2; if (left_x < 0) left_x = 0; } if (center_y <= 0) center_y = y_top + (y_bot - y_top) / 2; FT_Face face = get_face(font, size); if (!face) return -1; long asc = face->size->metrics.ascender >> 6; long desc = face->size->metrics.descender >> 6; int baseline_y = center_y + (int)((asc + desc) / 2); blit_text_bitmap(map, out_w, out_h, y_top, y_bot, t, left_x, baseline_y, color, opacity); return t->advance; } /* Return the first available WQY font (microhei preferred, zenhei fallback). */ const char *title_default_font(void) { static const char *const candidates[] = { "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", }; for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) if (access(candidates[i], R_OK) == 0) return candidates[i]; return candidates[0]; } /* ---------- public ---------- */ /* Draw logo + text as one centered group into the bar (bar_h rows). */ static int render_logo_text(uint8_t *map, int out_w, int out_h, int bar_h, const TitleCfg *cfg) { int ok = 0; int w = 0, h = 0, logo_dw = 0, logo_dh = 0; uint8_t *rgba = NULL, *scaled = NULL; if (cfg->logo[0]) { if (decode_image(cfg->logo, &w, &h, &rgba) == 0) { int box = bar_h - 2 * cfg->logo_pad; if (box < 4) box = 4; float sc = (float)box / (w > h ? w : h); logo_dw = (int)(w * sc + 0.5f); logo_dh = (int)(h * sc + 0.5f); if (logo_dw < 1) logo_dw = 1; if (logo_dh < 1) logo_dh = 1; scaled = malloc((size_t)logo_dw * logo_dh * 4); if (scaled) scale_rgba(rgba, w, h, scaled, logo_dw, logo_dh); } } const char *font = cfg->font[0] ? cfg->font : title_default_font(); long tw = 0; if (cfg->text[0]) { tw = measure_text(font, cfg->text, cfg->font_size); if (tw < 0) tw = 0; } int pad = cfg->logo_pad > 0 ? cfg->logo_pad : 0; int gap = pad; /* space between logo and text */ long group_w = (logo_dw > 0 ? (long)pad + logo_dw + gap : 0) + tw; long start_x = (out_w - group_w) / 2; if (start_x < 0) start_x = 0; int op = cfg->opacity > 0 ? cfg->opacity : 255; if (scaled) { blend_logo(map, out_w, out_h, bar_h, scaled, logo_dw, logo_dh, (int)start_x + pad, (bar_h - logo_dh) / 2, op); ok = 1; } if (cfg->text[0] && tw > 0) { long r = render_text(map, out_w, out_h, 0, bar_h, font, cfg->text, cfg->font_size, cfg->color, (int)start_x + pad + logo_dw + gap, bar_h / 2, op); if (r >= 0) ok = 1; else fprintf(stderr, "title: text render failed\n"); } free(scaled); free(rgba); return ok; } int title_render(uint8_t *map, int out_w, int out_h, const TitleCfg *cfg) { if (!map || !cfg) return -1; int bar_h = cfg->height; if (bar_h <= 0 || bar_h > out_h) bar_h = out_h; if (bar_h & 1) bar_h--; /* keep UV rows aligned */ if (bar_h < 2) bar_h = 2; uint8_t *Y = map; uint8_t *UV = map + (size_t)out_w * out_h; /* dark bar background */ memset(Y, 0x10, (size_t)out_w * bar_h); memset(UV, 0x80, (size_t)out_w * (bar_h / 2)); int ok = render_logo_text(map, out_w, out_h, bar_h, cfg); if (!ok && !cfg->logo[0] && !cfg->text[0]) return -1; return ok ? 0 : -1; } int title_render_overlay(uint8_t *map, int out_w, int out_h, const TitleCfg *cfg) { if (!map || !cfg) return -1; int bar_h = cfg->height; if (bar_h <= 0 || bar_h > out_h) bar_h = out_h; if (bar_h & 1) bar_h--; /* keep UV rows aligned */ if (bar_h < 2) bar_h = 2; /* Semi-transparent dark strip over the live frame: blend each channel * halfway toward dark (Y=0x10) / neutral (UV=0x80). */ uint8_t *Y = map; uint8_t *UV = map + (size_t)out_w * out_h; for (int y = 0; y < bar_h; y++) { uint8_t *row = Y + (size_t)y * out_w; for (int x = 0; x < out_w; x++) row[x] = (uint8_t)((row[x] >> 1) + 8); } int uvh = bar_h / 2; for (int y = 0; y < uvh; y++) { uint8_t *row = UV + (size_t)y * out_w; for (int x = 0; x < out_w; x++) row[x] = (uint8_t)((row[x] >> 1) + 0x40); } int ok = render_logo_text(map, out_w, out_h, bar_h, cfg); if (!ok && !cfg->logo[0] && !cfg->text[0]) return -1; return ok ? 0 : -1; } int title_render_text(uint8_t *map, int out_w, int out_h, const char *font, const char *text, int size, unsigned color, int center_x, int center_y) { if (!map || !text || !text[0]) return -1; const char *f = font && font[0] ? font : title_default_font(); int left_x = center_x; if (center_x > 0) { /* center on an explicit x */ long tw = measure_text(f, text, size); if (tw < 0) tw = 0; left_x = center_x - (int)(tw / 2); } long r = render_text(map, out_w, out_h, 0, out_h, f, text, size, color, left_x, center_y, 255); return r >= 0 ? 0 : -1; } long title_measure_text(const char *font, const char *text, int size) { const char *f = font && font[0] ? font : title_default_font(); return measure_text(f, text, size); } /* ---------- PLC connection status badge ---------- */ /* Draw a small translucent badge in the top-right corner of an NV12 buffer: * a colored status dot (green = connected, red = disconnected) plus a label. * Returns 0 on success, -1 on error. */ int title_render_plc_status(uint8_t *map, int out_w, int out_h, const char *font, int connected) { if (!map || out_w < 80 || out_h < 50) return -1; const char *st = connected ? "PLC 已连接" : "PLC 断开"; int text_size = 18; long tw = title_measure_text(font, st, text_size); if (tw < 0) tw = 0; int dot_r = 6; int pad_x = 10, pad_y = 6; int dot_zone = dot_r * 2 + 8; /* dot + gap to label */ int bad_w = pad_x + dot_zone + pad_x + (int)tw + pad_x; int bad_h = pad_y + text_size + pad_y + 4; if (bad_h < dot_zone + pad_y + 4) bad_h = dot_zone + pad_y + 4; int x0 = out_w - 12 - bad_w; /* right margin 12 */ int y0 = 12; if (x0 < 0) { x0 = 0; bad_w = out_w - 12; } /* Build the RGBA badge canvas: dark rounded pill + status dot. */ uint8_t *rgba = malloc((size_t)bad_w * bad_h * 4); if (!rgba) return -1; for (int j = 0; j < bad_h; j++) { for (int i = 0; i < bad_w; i++) { int rc = 0; /* distance to the nearest of the 4 rounded corners (r=8) */ int cx = (i < 8) ? 7 : (i >= bad_w - 8 ? bad_w - 8 - 1 : i); int cy = (j < 8) ? 7 : (j >= bad_h - 8 ? bad_h - 8 - 1 : j); int dx = i - cx, dy = j - cy; if (i < 8 || i >= bad_w - 8 || j < 8 || j >= bad_h - 8) if (dx * dx + dy * dy > 8 * 8) rc = 1; if (rc) { /* outside pill: transparent */ rgba[(size_t)(j * bad_w + i) * 4 + 3] = 0; continue; } rgba[(size_t)(j * bad_w + i) * 4 + 0] = 0x24; rgba[(size_t)(j * bad_w + i) * 4 + 1] = 0x24; rgba[(size_t)(j * bad_w + i) * 4 + 2] = 0x24; rgba[(size_t)(j * bad_w + i) * 4 + 3] = 190; /* ~75% */ } } /* status dot (1px ring in light gray, fill in status color) */ int dot_cx = pad_x + dot_r + 1; int dot_cy = bad_h / 2; int dcr, dcg, dcb; if (connected) { dcr = 60; dcg = 205; dcb = 64; /* green */ } else { dcr = 224; dcg = 48; dcb = 40; /* red */ } for (int j = dot_cy - dot_r - 1; j <= dot_cy + dot_r + 1; j++) { for (int i = dot_cx - dot_r - 1; i <= dot_cx + dot_r + 1; i++) { if (i < 0 || i >= bad_w || j < 0 || j >= bad_h) continue; int d2 = (i - dot_cx) * (i - dot_cx) + (j - dot_cy) * (j - dot_cy); if (d2 <= dot_r * dot_r) { /* fill */ uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4; px[0] = (uint8_t)dcr; px[1] = (uint8_t)dcg; px[2] = (uint8_t)dcb; px[3] = 255; } else if (d2 <= (dot_r + 1) * (dot_r + 1)) { /* ring */ uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4; px[0] = 0xE8; px[1] = 0xE8; px[2] = 0xE8; px[3] = 230; } } } blend_logo(map, out_w, out_h, out_h, rgba, bad_w, bad_h, x0, y0, 255); free(rgba); int text_cx = x0 + pad_x + dot_zone + pad_x - 2 + (int)(tw / 2); int text_cy = y0 + bad_h / 2; return title_render_text(map, out_w, out_h, font, st, text_size, 0xFFFFFF, text_cx, text_cy); } /* Draw a translucent pill with text, bottom-left corner. */ int title_render_ip(uint8_t *map, int out_w, int out_h, const char *font, const char *text) { if (!map || out_w < 80 || out_h < 50) return -1; const char *st = (text && text[0]) ? text : "IP 无地址"; int text_size = 18; long tw = title_measure_text(font, st, text_size); if (tw < 0) tw = 0; int pad_x = 10, pad_y = 6; int bad_w = pad_x + (int)tw + pad_x; int bad_h = pad_y + text_size + pad_y + 4; int x0 = 12; /* left margin 12 */ int y0 = out_h - 12 - bad_h; /* bottom-left corner */ if (y0 < 0) y0 = 0; if (x0 + bad_w > out_w) bad_w = out_w - x0; if (bad_w <= 0) return -1; uint8_t *rgba = malloc((size_t)bad_w * bad_h * 4); if (!rgba) return -1; for (int j = 0; j < bad_h; j++) { for (int i = 0; i < bad_w; i++) { int rc = 0; /* distance to the nearest of the 4 rounded corners (r=8) */ int cx = (i < 8) ? 7 : (i >= bad_w - 8 ? bad_w - 8 - 1 : i); int cy = (j < 8) ? 7 : (j >= bad_h - 8 ? bad_h - 8 - 1 : j); int dx = i - cx, dy = j - cy; if (i < 8 || i >= bad_w - 8 || j < 8 || j >= bad_h - 8) if (dx * dx + dy * dy > 8 * 8) rc = 1; uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4; if (rc) { px[3] = 0; continue; } /* outside pill: transparent */ px[0] = 0x24; px[1] = 0x24; px[2] = 0x24; px[3] = 190; } } blend_logo(map, out_w, out_h, out_h, rgba, bad_w, bad_h, x0, y0, 255); free(rgba); int text_cx = x0 + bad_w / 2; int text_cy = y0 + bad_h / 2; return title_render_text(map, out_w, out_h, font, st, text_size, 0xFFFFFF, text_cx, text_cy); }