| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- #include "title.h"
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <setjmp.h>
- #include <math.h>
- #include <png.h>
- #include <jpeglib.h>
- #include <ft2build.h>
- #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);
- }
|