title.c 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. #include "title.h"
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. #include <setjmp.h>
  7. #include <math.h>
  8. #include <png.h>
  9. #include <jpeglib.h>
  10. #include <ft2build.h>
  11. #include FT_FREETYPE_H
  12. #define MAX_IMG 16384
  13. /* ---------- helpers ---------- */
  14. struct my_error_mgr {
  15. struct jpeg_error_mgr pub;
  16. jmp_buf setjmp_buffer;
  17. };
  18. static void my_error_exit(j_common_ptr cinfo) {
  19. struct my_error_mgr *m = (struct my_error_mgr *)cinfo->err;
  20. longjmp(m->setjmp_buffer, 1);
  21. }
  22. static void rgb2yuv(int r, int g, int b, int *Y, int *Cb, int *Cr) {
  23. *Y = (int)(0.299f * r + 0.587f * g + 0.114f * b);
  24. *Cb = (int)(128.0f - 0.169f * r - 0.331f * g + 0.500f * b);
  25. *Cr = (int)(128.0f + 0.500f * r - 0.419f * g - 0.081f * b);
  26. if (*Y < 0) *Y = 0;
  27. if (*Y > 255) *Y = 255;
  28. if (*Cb < 0) *Cb = 0;
  29. if (*Cb > 255) *Cb = 255;
  30. if (*Cr < 0) *Cr = 0;
  31. if (*Cr > 255) *Cr = 255;
  32. }
  33. static int png_is(const unsigned char m[8]) {
  34. static const unsigned char sig[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
  35. return memcmp(m, sig, 8) == 0;
  36. }
  37. static int jpg_is(const unsigned char m[3]) {
  38. return m[0] == 0xFF && m[1] == 0xD8;
  39. }
  40. /* Decode PNG or JPEG (by magic bytes) into RGBA8 (straight alpha).
  41. * Caller frees *rgba with free(). Returns 0 on success. */
  42. static int decode_image(const char *path, int *w, int *h, uint8_t **rgba) {
  43. FILE *fp = fopen(path, "rb");
  44. if (!fp) { fprintf(stderr, "title: cannot open %s\n", path); return -1; }
  45. unsigned char magic[8];
  46. if (fread(magic, 1, 8, fp) != 8) { fclose(fp); return -1; }
  47. fseek(fp, 0, SEEK_SET);
  48. if (png_is(magic)) {
  49. png_structp pp = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  50. png_infop ip = pp ? png_create_info_struct(pp) : NULL;
  51. if (!pp || !ip) { if (pp) png_destroy_read_struct(&pp, NULL, NULL); fclose(fp); return -1; }
  52. if (setjmp(png_jmpbuf(pp))) {
  53. png_destroy_read_struct(&pp, &ip, NULL);
  54. fclose(fp);
  55. return -1;
  56. }
  57. png_init_io(pp, fp);
  58. png_read_info(pp, ip);
  59. png_set_strip_16(pp);
  60. png_set_expand(pp);
  61. png_set_tRNS_to_alpha(pp);
  62. if (png_get_color_type(pp, ip) == PNG_COLOR_TYPE_RGB)
  63. png_set_filler(pp, 0xFF, PNG_FILLER_AFTER);
  64. png_read_update_info(pp, ip);
  65. int W = (int)png_get_image_width(pp, ip);
  66. int H = (int)png_get_image_height(pp, ip);
  67. if (W <= 0 || H <= 0 || W > MAX_IMG || H > MAX_IMG) {
  68. png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1;
  69. }
  70. size_t stride = png_get_rowbytes(pp, ip);
  71. uint8_t *buf = malloc((size_t)W * H * 4);
  72. if (!buf) { png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; }
  73. png_bytep *rows = malloc(sizeof(png_bytep) * H);
  74. if (!rows) { free(buf); png_destroy_read_struct(&pp, &ip, NULL); fclose(fp); return -1; }
  75. for (int y = 0; y < H; y++) rows[y] = buf + (size_t)y * stride;
  76. png_read_image(pp, rows);
  77. png_read_end(pp, NULL);
  78. free(rows);
  79. png_destroy_read_struct(&pp, &ip, NULL);
  80. fclose(fp);
  81. *w = W; *h = H; *rgba = buf;
  82. return 0;
  83. }
  84. if (jpg_is(magic)) {
  85. struct my_error_mgr jerr;
  86. struct jpeg_decompress_struct cinfo;
  87. cinfo.err = jpeg_std_error(&jerr.pub);
  88. jerr.pub.error_exit = my_error_exit;
  89. if (setjmp(jerr.setjmp_buffer)) {
  90. jpeg_destroy_decompress(&cinfo);
  91. fclose(fp);
  92. return -1;
  93. }
  94. jpeg_create_decompress(&cinfo);
  95. jpeg_stdio_src(&cinfo, fp);
  96. if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
  97. jpeg_destroy_decompress(&cinfo); fclose(fp); return -1;
  98. }
  99. cinfo.out_color_space = JCS_RGB;
  100. jpeg_start_decompress(&cinfo);
  101. int W = cinfo.output_width, H = cinfo.output_height;
  102. if (W <= 0 || H <= 0 || W > MAX_IMG || H > MAX_IMG) {
  103. jpeg_destroy_decompress(&cinfo); fclose(fp); return -1;
  104. }
  105. uint8_t *buf = malloc((size_t)W * H * 4);
  106. if (!buf) { jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; }
  107. JSAMPROW row = malloc(W * 3);
  108. if (!row) { free(buf); jpeg_destroy_decompress(&cinfo); fclose(fp); return -1; }
  109. for (int y = 0; y < H; y++) {
  110. jpeg_read_scanlines(&cinfo, &row, 1);
  111. for (int x = 0; x < W; x++) {
  112. uint8_t *p = buf + ((size_t)y * W + x) * 4;
  113. p[0] = row[x * 3 + 0];
  114. p[1] = row[x * 3 + 1];
  115. p[2] = row[x * 3 + 2];
  116. p[3] = 255;
  117. }
  118. }
  119. free(row);
  120. jpeg_finish_decompress(&cinfo);
  121. jpeg_destroy_decompress(&cinfo);
  122. fclose(fp);
  123. *w = W; *h = H; *rgba = buf;
  124. return 0;
  125. }
  126. fclose(fp);
  127. fprintf(stderr, "title: unsupported image format: %s\n", path);
  128. return -1;
  129. }
  130. /* Bilinear scale RGBA (premultiplied to avoid fringes) into dst. */
  131. static void scale_rgba(const uint8_t *src, int sw, int sh,
  132. uint8_t *dst, int dw, int dh) {
  133. int xs = (dw > 1) ? dw - 1 : 1;
  134. int ys = (dh > 1) ? dh - 1 : 1;
  135. for (int y = 0; y < dh; y++) {
  136. float fy = ((float)y + 0.5f) * sh / (float)ys - 0.5f;
  137. if (fy < 0) fy = 0;
  138. int y0 = (int)fy;
  139. int y1 = y0 + 1;
  140. if (y1 >= sh) y1 = y0;
  141. float wy = fy - y0;
  142. for (int x = 0; x < dw; x++) {
  143. float fx = ((float)x + 0.5f) * sw / (float)xs - 0.5f;
  144. if (fx < 0) fx = 0;
  145. int x0 = (int)fx;
  146. int x1 = x0 + 1;
  147. if (x1 >= sw) x1 = x0;
  148. float wx = fx - x0;
  149. float pr = 0, pg = 0, pb = 0, pa = 0;
  150. const uint8_t *p00 = src + ((size_t)y0 * sw + x0) * 4;
  151. const uint8_t *p01 = src + ((size_t)y0 * sw + x1) * 4;
  152. const uint8_t *p10 = src + ((size_t)y1 * sw + x0) * 4;
  153. const uint8_t *p11 = src + ((size_t)y1 * sw + x1) * 4;
  154. float w00 = (1 - wx) * (1 - wy), w01 = wx * (1 - wy);
  155. float w10 = (1 - wx) * wy, w11 = wx * wy;
  156. float a00 = p00[3] / 255.0f, a01 = p01[3] / 255.0f;
  157. float a10 = p10[3] / 255.0f, a11 = p11[3] / 255.0f;
  158. pr = p00[0] * a00 * w00 + p01[0] * a01 * w01 + p10[0] * a10 * w10 + p11[0] * a11 * w11;
  159. pg = p00[1] * a00 * w00 + p01[1] * a01 * w01 + p10[1] * a10 * w10 + p11[1] * a11 * w11;
  160. pb = p00[2] * a00 * w00 + p01[2] * a01 * w01 + p10[2] * a10 * w10 + p11[2] * a11 * w11;
  161. pa = a00 * w00 + a01 * w01 + a10 * w10 + a11 * w11;
  162. uint8_t *o = dst + ((size_t)y * dw + x) * 4;
  163. if (pa > 0.001f) {
  164. o[0] = (uint8_t)(pr / pa);
  165. o[1] = (uint8_t)(pg / pa);
  166. o[2] = (uint8_t)(pb / pa);
  167. } else {
  168. o[0] = o[1] = o[2] = 0;
  169. }
  170. o[3] = (uint8_t)(pa * 255.0f);
  171. }
  172. }
  173. }
  174. /* Blend one RGBA image (premultiplied inside) onto the NV12 bar at (x0,y0).
  175. * opacity 0-255 scales the whole logo's translucency. */
  176. static void blend_logo(uint8_t *map, int out_w, int out_h, int bar_h,
  177. const uint8_t *rgba, int w, int h, int x0, int y0,
  178. int opacity) {
  179. uint8_t *Y = map;
  180. uint8_t *UV = map + (size_t)out_w * out_h;
  181. float op = (opacity > 255) ? 255.0f : (opacity < 0 ? 0.0f : (float)opacity);
  182. for (int j = 0; j < h; j++) {
  183. int y = y0 + j;
  184. if (y < 0 || y >= bar_h) continue;
  185. for (int i = 0; i < w; i++) {
  186. int x = x0 + i;
  187. if (x < 0 || x >= out_w) continue;
  188. const uint8_t *p = rgba + ((size_t)j * w + i) * 4;
  189. float a = (p[3] / 255.0f) * (op / 255.0f);
  190. int r = p[0], g = p[1], b = p[2];
  191. int yy, cb, cr;
  192. rgb2yuv(r, g, b, &yy, &cb, &cr);
  193. int wa = (int)(a * 255.0f);
  194. uint8_t *yp = &Y[(size_t)y * out_w + x];
  195. *yp = (uint8_t)(*yp + ((yy - *yp) * wa) / 255);
  196. if ((x & 1) == 0 && (y & 1) == 0) {
  197. uint8_t *uvp = &UV[(size_t)y / 2 * out_w + x];
  198. *uvp = (uint8_t)(*uvp + ((cb - *uvp) * wa) / 255);
  199. if (x + 1 < out_w) {
  200. uint8_t *uvp2 = uvp + 1;
  201. *uvp2 = (uint8_t)(*uvp2 + ((cr - *uvp2) * wa) / 255);
  202. }
  203. }
  204. }
  205. }
  206. }
  207. /* ---------- UTF-8 ---------- */
  208. static int utf8_decode(const uint8_t *s, uint32_t *cp) {
  209. if (s[0] < 0x80) { *cp = s[0]; return 1; }
  210. if ((s[0] & 0xE0) == 0xC0 && (s[1] & 0xC0) == 0x80) {
  211. *cp = ((s[0] & 0x1F) << 6) | (s[1] & 0x3F); return 2;
  212. }
  213. if ((s[0] & 0xF0) == 0xE0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
  214. *cp = ((s[0] & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); return 3;
  215. }
  216. if ((s[0] & 0xF8) == 0xF0 && (s[1] & 0xC0) == 0x80 &&
  217. (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) {
  218. *cp = ((s[0] & 0x07) << 18) | ((s[1] & 0x3F) << 12) |
  219. ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); return 4;
  220. }
  221. return 0;
  222. }
  223. /* ---------- FreeType with process-wide caching ----------
  224. * FT_Init_FreeType + FT_New_Face parse the (multi-MB) font file on every
  225. * call; rendering used to happen per frame for the title strip and NO
  226. * SIGNAL text. Cache the library and one face per font path, and cache
  227. * rasterized text bitmaps so per-frame cost is a single alpha blit.
  228. * All title_* calls come from the main thread only -> no locking needed. */
  229. static FT_Library g_ft_lib = NULL;
  230. typedef struct {
  231. char path[512];
  232. FT_Face face;
  233. int size; /* currently set pixel size, -1 = unset */
  234. } FaceCache;
  235. static FaceCache g_face = { .path = "", .face = NULL, .size = -1 };
  236. static FT_Face get_face(const char *font, int size) {
  237. if (!g_ft_lib) {
  238. if (FT_Init_FreeType(&g_ft_lib) != 0) return NULL;
  239. }
  240. if (!g_face.face || strcmp(g_face.path, font) != 0) {
  241. if (g_face.face) { FT_Done_Face(g_face.face); g_face.face = NULL; }
  242. if (FT_New_Face(g_ft_lib, font, 0, &g_face.face) != 0) {
  243. /* Configured font missing on this board (e.g. wqy-microhei not
  244. * installed): fall back to the first available default font so
  245. * the title strip still renders text. */
  246. const char *fb = title_default_font();
  247. int fb_ok = fb && strcmp(fb, font) != 0 &&
  248. FT_New_Face(g_ft_lib, fb, 0, &g_face.face) == 0;
  249. if (!fb_ok) {
  250. g_face.face = NULL;
  251. return NULL;
  252. }
  253. fprintf(stderr, "title: font %s not found, using %s\n", font, fb);
  254. font = fb;
  255. }
  256. snprintf(g_face.path, sizeof(g_face.path), "%s", font);
  257. g_face.size = -1;
  258. }
  259. if (g_face.size != size) {
  260. if (FT_Set_Pixel_Sizes(g_face.face, 0, size) != 0) return NULL;
  261. g_face.size = size;
  262. }
  263. return g_face.face;
  264. }
  265. /* ---------- rasterized text bitmap cache ---------- */
  266. #define TEXT_CACHE_SLOTS 6
  267. typedef struct {
  268. /* key */
  269. char text[256];
  270. char font[64]; /* short tag: last path component */
  271. int size;
  272. unsigned color;
  273. int opacity;
  274. /* bitmap: A8 coverage, origin at (ox, oy); baseline sits at y = oy */
  275. uint8_t *cov;
  276. int w, h, ox, oy;
  277. long advance; /* total advance width in px */
  278. uint64_t last_use;
  279. } TextBitmap;
  280. static TextBitmap g_tcache[TEXT_CACHE_SLOTS];
  281. static uint64_t g_tcache_clock = 0;
  282. static TextBitmap *text_cache_find(const char *font, const char *text, int size,
  283. unsigned color, int opacity) {
  284. const char *tag = strrchr(font, '/');
  285. tag = tag ? tag + 1 : font;
  286. for (int i = 0; i < TEXT_CACHE_SLOTS; i++) {
  287. TextBitmap *t = &g_tcache[i];
  288. if (t->cov && t->size == size && t->color == color &&
  289. t->opacity == opacity && t->advance >= 0 &&
  290. strcmp(t->text, text) == 0 && strcmp(t->font, tag) == 0)
  291. return t;
  292. }
  293. return NULL;
  294. }
  295. /* Rasterize `text` into a fresh cache slot. Returns slot or NULL. */
  296. static TextBitmap *text_cache_rasterize(const char *font, const char *text,
  297. int size, unsigned color, int opacity) {
  298. FT_Face face = get_face(font, size);
  299. if (!face) return NULL;
  300. long total = 0;
  301. const uint8_t *s = (const uint8_t *)text;
  302. while (*s) {
  303. uint32_t cp;
  304. int n = utf8_decode(s, &cp);
  305. if (n <= 0) { s++; continue; }
  306. if (FT_Load_Char(face, cp, FT_LOAD_NO_BITMAP) == 0)
  307. total += face->glyph->advance.x >> 6;
  308. s += n;
  309. }
  310. if (total <= 0) return NULL;
  311. /* find a free slot (or evict least recently used) */
  312. TextBitmap *t = &g_tcache[0];
  313. for (int i = 0; i < TEXT_CACHE_SLOTS; i++) {
  314. if (!g_tcache[i].cov) { t = &g_tcache[i]; break; }
  315. if (g_tcache[i].last_use < t->last_use) t = &g_tcache[i];
  316. }
  317. free(t->cov);
  318. memset(t, 0, sizeof(*t));
  319. /* generous box: pad covers negative bearings/overhangs */
  320. int pad = size;
  321. t->w = (int)total + 2 * pad;
  322. t->h = 2 * size + 8;
  323. t->ox = pad;
  324. t->oy = size + 4;
  325. t->cov = calloc((size_t)t->w * t->h, 1);
  326. if (!t->cov) { memset(t, 0, sizeof(*t)); return NULL; }
  327. snprintf(t->text, sizeof(t->text), "%s", text);
  328. const char *tag = strrchr(font, '/');
  329. tag = tag ? tag + 1 : font;
  330. snprintf(t->font, sizeof(t->font), "%s", tag);
  331. t->size = size;
  332. t->color = color;
  333. t->opacity = opacity < 0 ? 0 : (opacity > 255 ? 255 : opacity);
  334. t->advance = total;
  335. s = (const uint8_t *)text;
  336. int pen_x = t->ox;
  337. while (*s) {
  338. uint32_t cp;
  339. int n = utf8_decode(s, &cp);
  340. if (n <= 0) { s++; continue; }
  341. if (FT_Load_Char(face, cp, FT_LOAD_RENDER) == 0 && face->glyph->bitmap.buffer) {
  342. FT_Bitmap *bm = &face->glyph->bitmap;
  343. int gx = pen_x + face->glyph->bitmap_left;
  344. int gy = t->oy - face->glyph->bitmap_top;
  345. for (unsigned j = 0; j < bm->rows; j++) {
  346. int yy = gy + (int)j;
  347. if (yy < 0 || yy >= t->h) continue;
  348. const uint8_t *row = bm->buffer + (size_t)j * bm->pitch;
  349. for (unsigned i = 0; i < bm->width; i++) {
  350. int xx = gx + (int)i;
  351. if (xx < 0 || xx >= t->w) continue;
  352. int v = row[i];
  353. if (v > t->cov[(size_t)yy * t->w + xx])
  354. t->cov[(size_t)yy * t->w + xx] = (uint8_t)v;
  355. }
  356. }
  357. pen_x += face->glyph->advance.x >> 6;
  358. }
  359. s += n;
  360. }
  361. return t;
  362. }
  363. /* Blend a cached A8 text bitmap onto NV12 at baseline (bx, by),
  364. * clipped to [y_top, y_bot). Same Y/UV blending as draw_glyph. */
  365. static void blit_text_bitmap(uint8_t *map, int out_w, int out_h,
  366. int y_top, int y_bot,
  367. const TextBitmap *t, int bx, int by,
  368. unsigned color, int opacity) {
  369. if (!t || !t->cov) return;
  370. uint8_t *Y = map;
  371. uint8_t *UV = map + (size_t)out_w * out_h;
  372. int Yw, Cbw, Crw;
  373. rgb2yuv((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF,
  374. &Yw, &Cbw, &Crw);
  375. int x0 = bx - t->ox;
  376. int y0 = by - t->oy;
  377. for (int j = 0; j < t->h; j++) {
  378. int y = y0 + j;
  379. if (y < y_top || y >= y_bot || y >= out_h) continue;
  380. const uint8_t *crow = t->cov + (size_t)j * t->w;
  381. uint8_t *yrow = Y + (size_t)y * out_w;
  382. uint8_t *uvrow = UV + (size_t)(y / 2) * out_w;
  383. for (int i = 0; i < t->w; i++) {
  384. int cov = (crow[i] * opacity) / 255;
  385. if (cov <= 0) continue;
  386. int x = x0 + i;
  387. if (x < 0 || x >= out_w) continue;
  388. uint8_t *dst = &yrow[x];
  389. *dst = (uint8_t)(*dst + ((Yw - *dst) * cov) / 255);
  390. if ((x & 1) == 0 && (y & 1) == 0) {
  391. uint8_t *uvp = &uvrow[x];
  392. *uvp = (uint8_t)(*uvp + ((Cbw - *uvp) * cov) / 255);
  393. if (x + 1 < out_w)
  394. uvp[1] = (uint8_t)(uvp[1] + ((Crw - uvp[1]) * cov) / 255);
  395. }
  396. }
  397. }
  398. }
  399. static void text_cache_touch(TextBitmap *t) {
  400. t->last_use = ++g_tcache_clock;
  401. }
  402. /* Measure the advance width (px) of a UTF-8 text line. Returns width or -1. */
  403. static long measure_text(const char *font, const char *text, int size) {
  404. FT_Face face = get_face(font, size);
  405. if (!face) return -1;
  406. const uint8_t *s = (const uint8_t *)text;
  407. long total = 0;
  408. while (*s) {
  409. uint32_t cp;
  410. int n = utf8_decode(s, &cp);
  411. if (n <= 0) { s++; continue; }
  412. if (FT_Load_Char(face, cp, FT_LOAD_NO_BITMAP) == 0)
  413. total += face->glyph->advance.x >> 6;
  414. s += n;
  415. }
  416. return total;
  417. }
  418. /* Render a UTF-8 text line into the NV12 buffer, clipped to [y_top, y_bot).
  419. * Text is left-aligned at left_x (<=0: centered on the whole width) and its
  420. * vertical center sits at center_y. opacity 0-255 scales translucency.
  421. * Uses the rasterized-text cache: repeated draws are a single blit.
  422. * Returns the total advance width (px) or -1 on error. */
  423. static long render_text(uint8_t *map, int out_w, int out_h, int y_top, int y_bot,
  424. const char *font, const char *text, int size,
  425. unsigned color, int left_x, int center_y, int opacity) {
  426. if (opacity < 0) opacity = 0;
  427. if (opacity > 255) opacity = 255;
  428. TextBitmap *t = text_cache_find(font, text, size, color, opacity);
  429. if (!t) t = text_cache_rasterize(font, text, size, color, opacity);
  430. if (!t || !t->cov) return -1;
  431. text_cache_touch(t);
  432. if (left_x <= 0) {
  433. left_x = out_w / 2 - (int)t->advance / 2;
  434. if (left_x < 0) left_x = 0;
  435. }
  436. if (center_y <= 0) center_y = y_top + (y_bot - y_top) / 2;
  437. FT_Face face = get_face(font, size);
  438. if (!face) return -1;
  439. long asc = face->size->metrics.ascender >> 6;
  440. long desc = face->size->metrics.descender >> 6;
  441. int baseline_y = center_y + (int)((asc + desc) / 2);
  442. blit_text_bitmap(map, out_w, out_h, y_top, y_bot,
  443. t, left_x, baseline_y, color, opacity);
  444. return t->advance;
  445. }
  446. /* Return the first available WQY font (microhei preferred, zenhei fallback). */
  447. const char *title_default_font(void) {
  448. static const char *const candidates[] = {
  449. "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
  450. "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
  451. };
  452. for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++)
  453. if (access(candidates[i], R_OK) == 0) return candidates[i];
  454. return candidates[0];
  455. }
  456. /* ---------- public ---------- */
  457. /* Draw logo + text as one centered group into the bar (bar_h rows). */
  458. static int render_logo_text(uint8_t *map, int out_w, int out_h, int bar_h,
  459. const TitleCfg *cfg) {
  460. int ok = 0;
  461. int w = 0, h = 0, logo_dw = 0, logo_dh = 0;
  462. uint8_t *rgba = NULL, *scaled = NULL;
  463. if (cfg->logo[0]) {
  464. if (decode_image(cfg->logo, &w, &h, &rgba) == 0) {
  465. int box = bar_h - 2 * cfg->logo_pad;
  466. if (box < 4) box = 4;
  467. float sc = (float)box / (w > h ? w : h);
  468. logo_dw = (int)(w * sc + 0.5f);
  469. logo_dh = (int)(h * sc + 0.5f);
  470. if (logo_dw < 1) logo_dw = 1;
  471. if (logo_dh < 1) logo_dh = 1;
  472. scaled = malloc((size_t)logo_dw * logo_dh * 4);
  473. if (scaled) scale_rgba(rgba, w, h, scaled, logo_dw, logo_dh);
  474. }
  475. }
  476. const char *font = cfg->font[0] ? cfg->font
  477. : title_default_font();
  478. long tw = 0;
  479. if (cfg->text[0]) {
  480. tw = measure_text(font, cfg->text, cfg->font_size);
  481. if (tw < 0) tw = 0;
  482. }
  483. int pad = cfg->logo_pad > 0 ? cfg->logo_pad : 0;
  484. int gap = pad; /* space between logo and text */
  485. long group_w = (logo_dw > 0 ? (long)pad + logo_dw + gap : 0) + tw;
  486. long start_x = (out_w - group_w) / 2;
  487. if (start_x < 0) start_x = 0;
  488. int op = cfg->opacity > 0 ? cfg->opacity : 255;
  489. if (scaled) {
  490. blend_logo(map, out_w, out_h, bar_h, scaled, logo_dw, logo_dh,
  491. (int)start_x + pad, (bar_h - logo_dh) / 2, op);
  492. ok = 1;
  493. }
  494. if (cfg->text[0] && tw > 0) {
  495. long r = render_text(map, out_w, out_h, 0, bar_h, font, cfg->text, cfg->font_size,
  496. cfg->color, (int)start_x + pad + logo_dw + gap,
  497. bar_h / 2, op);
  498. if (r >= 0) ok = 1;
  499. else fprintf(stderr, "title: text render failed\n");
  500. }
  501. free(scaled);
  502. free(rgba);
  503. return ok;
  504. }
  505. int title_render(uint8_t *map, int out_w, int out_h, const TitleCfg *cfg) {
  506. if (!map || !cfg) return -1;
  507. int bar_h = cfg->height;
  508. if (bar_h <= 0 || bar_h > out_h) bar_h = out_h;
  509. if (bar_h & 1) bar_h--; /* keep UV rows aligned */
  510. if (bar_h < 2) bar_h = 2;
  511. uint8_t *Y = map;
  512. uint8_t *UV = map + (size_t)out_w * out_h;
  513. /* dark bar background */
  514. memset(Y, 0x10, (size_t)out_w * bar_h);
  515. memset(UV, 0x80, (size_t)out_w * (bar_h / 2));
  516. int ok = render_logo_text(map, out_w, out_h, bar_h, cfg);
  517. if (!ok && !cfg->logo[0] && !cfg->text[0]) return -1;
  518. return ok ? 0 : -1;
  519. }
  520. int title_render_overlay(uint8_t *map, int out_w, int out_h, const TitleCfg *cfg) {
  521. if (!map || !cfg) return -1;
  522. int bar_h = cfg->height;
  523. if (bar_h <= 0 || bar_h > out_h) bar_h = out_h;
  524. if (bar_h & 1) bar_h--; /* keep UV rows aligned */
  525. if (bar_h < 2) bar_h = 2;
  526. /* Semi-transparent dark strip over the live frame: blend each channel
  527. * halfway toward dark (Y=0x10) / neutral (UV=0x80). */
  528. uint8_t *Y = map;
  529. uint8_t *UV = map + (size_t)out_w * out_h;
  530. for (int y = 0; y < bar_h; y++) {
  531. uint8_t *row = Y + (size_t)y * out_w;
  532. for (int x = 0; x < out_w; x++)
  533. row[x] = (uint8_t)((row[x] >> 1) + 8);
  534. }
  535. int uvh = bar_h / 2;
  536. for (int y = 0; y < uvh; y++) {
  537. uint8_t *row = UV + (size_t)y * out_w;
  538. for (int x = 0; x < out_w; x++)
  539. row[x] = (uint8_t)((row[x] >> 1) + 0x40);
  540. }
  541. int ok = render_logo_text(map, out_w, out_h, bar_h, cfg);
  542. if (!ok && !cfg->logo[0] && !cfg->text[0]) return -1;
  543. return ok ? 0 : -1;
  544. }
  545. int title_render_text(uint8_t *map, int out_w, int out_h,
  546. const char *font, const char *text, int size,
  547. unsigned color, int center_x, int center_y) {
  548. if (!map || !text || !text[0]) return -1;
  549. const char *f = font && font[0] ? font : title_default_font();
  550. int left_x = center_x;
  551. if (center_x > 0) { /* center on an explicit x */
  552. long tw = measure_text(f, text, size);
  553. if (tw < 0) tw = 0;
  554. left_x = center_x - (int)(tw / 2);
  555. }
  556. long r = render_text(map, out_w, out_h, 0, out_h, f, text, size, color,
  557. left_x, center_y, 255);
  558. return r >= 0 ? 0 : -1;
  559. }
  560. long title_measure_text(const char *font, const char *text, int size) {
  561. const char *f = font && font[0] ? font : title_default_font();
  562. return measure_text(f, text, size);
  563. }
  564. /* ---------- PLC connection status badge ---------- */
  565. /* Draw a small translucent badge in the top-right corner of an NV12 buffer:
  566. * a colored status dot (green = connected, red = disconnected) plus a label.
  567. * Returns 0 on success, -1 on error. */
  568. int title_render_plc_status(uint8_t *map, int out_w, int out_h,
  569. const char *font, int connected) {
  570. if (!map || out_w < 80 || out_h < 50) return -1;
  571. const char *st = connected ? "PLC 已连接" : "PLC 断开";
  572. int text_size = 18;
  573. long tw = title_measure_text(font, st, text_size);
  574. if (tw < 0) tw = 0;
  575. int dot_r = 6;
  576. int pad_x = 10, pad_y = 6;
  577. int dot_zone = dot_r * 2 + 8; /* dot + gap to label */
  578. int bad_w = pad_x + dot_zone + pad_x + (int)tw + pad_x;
  579. int bad_h = pad_y + text_size + pad_y + 4;
  580. if (bad_h < dot_zone + pad_y + 4) bad_h = dot_zone + pad_y + 4;
  581. int x0 = out_w - 12 - bad_w; /* right margin 12 */
  582. int y0 = 12;
  583. if (x0 < 0) { x0 = 0; bad_w = out_w - 12; }
  584. /* Build the RGBA badge canvas: dark rounded pill + status dot. */
  585. uint8_t *rgba = malloc((size_t)bad_w * bad_h * 4);
  586. if (!rgba) return -1;
  587. for (int j = 0; j < bad_h; j++) {
  588. for (int i = 0; i < bad_w; i++) {
  589. int rc = 0;
  590. /* distance to the nearest of the 4 rounded corners (r=8) */
  591. int cx = (i < 8) ? 7 : (i >= bad_w - 8 ? bad_w - 8 - 1 : i);
  592. int cy = (j < 8) ? 7 : (j >= bad_h - 8 ? bad_h - 8 - 1 : j);
  593. int dx = i - cx, dy = j - cy;
  594. if (i < 8 || i >= bad_w - 8 || j < 8 || j >= bad_h - 8)
  595. if (dx * dx + dy * dy > 8 * 8) rc = 1;
  596. if (rc) { /* outside pill: transparent */
  597. rgba[(size_t)(j * bad_w + i) * 4 + 3] = 0;
  598. continue;
  599. }
  600. rgba[(size_t)(j * bad_w + i) * 4 + 0] = 0x24;
  601. rgba[(size_t)(j * bad_w + i) * 4 + 1] = 0x24;
  602. rgba[(size_t)(j * bad_w + i) * 4 + 2] = 0x24;
  603. rgba[(size_t)(j * bad_w + i) * 4 + 3] = 190; /* ~75% */
  604. }
  605. }
  606. /* status dot (1px ring in light gray, fill in status color) */
  607. int dot_cx = pad_x + dot_r + 1;
  608. int dot_cy = bad_h / 2;
  609. int dcr, dcg, dcb;
  610. if (connected) { dcr = 60; dcg = 205; dcb = 64; /* green */ }
  611. else { dcr = 224; dcg = 48; dcb = 40; /* red */ }
  612. for (int j = dot_cy - dot_r - 1; j <= dot_cy + dot_r + 1; j++) {
  613. for (int i = dot_cx - dot_r - 1; i <= dot_cx + dot_r + 1; i++) {
  614. if (i < 0 || i >= bad_w || j < 0 || j >= bad_h) continue;
  615. int d2 = (i - dot_cx) * (i - dot_cx) + (j - dot_cy) * (j - dot_cy);
  616. if (d2 <= dot_r * dot_r) { /* fill */
  617. uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4;
  618. px[0] = (uint8_t)dcr; px[1] = (uint8_t)dcg; px[2] = (uint8_t)dcb;
  619. px[3] = 255;
  620. } else if (d2 <= (dot_r + 1) * (dot_r + 1)) { /* ring */
  621. uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4;
  622. px[0] = 0xE8; px[1] = 0xE8; px[2] = 0xE8; px[3] = 230;
  623. }
  624. }
  625. }
  626. blend_logo(map, out_w, out_h, out_h, rgba, bad_w, bad_h, x0, y0, 255);
  627. free(rgba);
  628. int text_cx = x0 + pad_x + dot_zone + pad_x - 2 + (int)(tw / 2);
  629. int text_cy = y0 + bad_h / 2;
  630. return title_render_text(map, out_w, out_h, font, st, text_size,
  631. 0xFFFFFF, text_cx, text_cy);
  632. }
  633. /* Draw a translucent pill with text, bottom-left corner. */
  634. int title_render_ip(uint8_t *map, int out_w, int out_h,
  635. const char *font, const char *text) {
  636. if (!map || out_w < 80 || out_h < 50) return -1;
  637. const char *st = (text && text[0]) ? text : "IP 无地址";
  638. int text_size = 18;
  639. long tw = title_measure_text(font, st, text_size);
  640. if (tw < 0) tw = 0;
  641. int pad_x = 10, pad_y = 6;
  642. int bad_w = pad_x + (int)tw + pad_x;
  643. int bad_h = pad_y + text_size + pad_y + 4;
  644. int x0 = 12; /* left margin 12 */
  645. int y0 = out_h - 12 - bad_h; /* bottom-left corner */
  646. if (y0 < 0) y0 = 0;
  647. if (x0 + bad_w > out_w) bad_w = out_w - x0;
  648. if (bad_w <= 0) return -1;
  649. uint8_t *rgba = malloc((size_t)bad_w * bad_h * 4);
  650. if (!rgba) return -1;
  651. for (int j = 0; j < bad_h; j++) {
  652. for (int i = 0; i < bad_w; i++) {
  653. int rc = 0;
  654. /* distance to the nearest of the 4 rounded corners (r=8) */
  655. int cx = (i < 8) ? 7 : (i >= bad_w - 8 ? bad_w - 8 - 1 : i);
  656. int cy = (j < 8) ? 7 : (j >= bad_h - 8 ? bad_h - 8 - 1 : j);
  657. int dx = i - cx, dy = j - cy;
  658. if (i < 8 || i >= bad_w - 8 || j < 8 || j >= bad_h - 8)
  659. if (dx * dx + dy * dy > 8 * 8) rc = 1;
  660. uint8_t *px = rgba + (size_t)(j * bad_w + i) * 4;
  661. if (rc) { px[3] = 0; continue; } /* outside pill: transparent */
  662. px[0] = 0x24; px[1] = 0x24; px[2] = 0x24; px[3] = 190;
  663. }
  664. }
  665. blend_logo(map, out_w, out_h, out_h, rgba, bad_w, bad_h, x0, y0, 255);
  666. free(rgba);
  667. int text_cx = x0 + bad_w / 2;
  668. int text_cy = y0 + bad_h / 2;
  669. return title_render_text(map, out_w, out_h, font, st, text_size,
  670. 0xFFFFFF, text_cx, text_cy);
  671. }