UltraGrid
UltraGrid Low-Latency High-Resolution Transmission System
Loading...
Searching...
No Matches
text.c
Go to the documentation of this file.
1
5/*
6 * Copyright (c) 2014-2023 CESNET, z. s. p. o.
7 * All rights reserved.
8 *
9 * Redistribution and use in source and binary forms, with or without
10 * modification, is permitted provided that the following conditions
11 * are met:
12 *
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 *
16 * 2. Redistributions in binary form must reproduce the above copyright
17 * notice, this list of conditions and the following disclaimer in the
18 * documentation and/or other materials provided with the distribution.
19 *
20 * 3. Neither the name of CESNET nor the names of its contributors may be
21 * used to endorse or promote products derived from this software without
22 * specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
25 * "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
26 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
27 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
28 * EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
29 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
30 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
34 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
35 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 */
45#ifdef HAVE_CONFIG_H
46#include "config.h" // WAND7
47#include "config_unix.h"
48#include "config_win32.h"
49#endif /* HAVE_CONFIG_H */
50
51#pragma GCC diagnostic push
52#pragma GCC diagnostic ignored "-Wtype-limits"
53#ifdef WAND7
54#include <MagickWand/MagickWand.h>
55#else
56#pragma GCC diagnostic warning "-Wignored-qualifiers"
57#include <wand/magick_wand.h>
58#endif
59#pragma GCC diagnostic pop
60
61#include "capture_filter.h"
62#include "debug.h"
63#include "lib_common.h"
64#include "video.h"
65#include "video_display.h"
66#include "vo_postprocess.h"
67#include "utils/color_out.h"
68#include "utils/macros.h"
69#include "utils/string.h" // replace_all
70#include "utils/text.h"
71
72struct state_text {
73 struct video_frame *in;
74 char *data;
75 size_t data_size;
76 char *text;
78 int width; // pixels
81
82 // font size
84
86 int text_height_px; // actual px height (with descender)
87
89
90 DrawingWand *dw;
91 MagickWand *wand_bg;
92 MagickWand *wand_text;
93};
94
95static bool text_get_property(void *state, int property, void *val, size_t *len)
96{
99 UNUSED(val);
100 UNUSED(len);
101
102 return false;
103}
104
105// all three macros are divisors of the actual video pixel dimensions
106#define TEXT_H_DIV 30
107#define MARGIN_X_DIV 60
108#define MARGIN_Y_DIV 60
109
110static pthread_once_t vo_postprocess_text_initialized = PTHREAD_ONCE_INIT;
111
112static void init_text() {
113 MagickWandGenesis();
114 atexit(MagickWandTerminus);
115}
116
117
118static void * text_init(const char *config) {
120
121 struct state_text *s;
122
123 if (strlen(config) == 0 || strcmp(config, "help") == 0) {
124 char desc[] = TBOLD("text") " video postprocess takes as a parameter text to be drawed. "
125 "Colons in text must be escaped with a backslash (see Examples). Spaces may be escaped or the whole argument should be enclosed by quotation marks.\n";
127 color_printf("\nUsage:\n");
128 color_printf("\t" TBOLD("-p text:<text>") "\n");
129 color_printf("\t" TBOLD("-p text:x=<x>:y=<y>:h=<text_height>:t=<text>") "\n");
130 color_printf("\nExamples:\n");
131 color_printf(TBOLD("\t-p text:stream1\n"
132 "\t-p text:x=100:y=100:h=20:t=text\n"
133 "\t-p \"text:Video stream from location XY\"\n"
134 "\t-p \"text:Text can also contains escaped colons - \\:\"\n")
135 "\n");
136 return NULL;
137 }
138
139 s = calloc(1, sizeof *s);
140 s->req_x = s->req_y = s->req_h = -1;
141
142 char *config_copy = strdup(config);
143 replace_all(config_copy, ESCAPED_COLON, DELDEL);
144 char *tmp = config_copy, *item, *save_ptr;
145 while ((item = strtok_r(tmp, ":", &save_ptr))) {
146 if (strstr(item, "x=") != NULL) {
147 s->req_x = atoi(item + 2);
148 } else if (strstr(item, "y=") != NULL) {
149 s->req_y = atoi(item + 2);
150 } else if (strstr(item, "h=") != NULL) {
151 s->req_h = atoi(item + 2);
152 } else if (strstr(item, "t=") != NULL) {
153 replace_all(item + 2, DELDEL, ":");
154 s->text = strdup(item + 2);
155 } else {
156 replace_all(item, DELDEL, ":");
157 s->text = strdup(item);
158 }
159 tmp = NULL;
160 }
161 free(config_copy);
162
163 if(!s->text || strlen(s->text) == 0){
164 log_msg(LOG_LEVEL_ERROR, "[text] Text can not be empty!\n");
165 free(s);
166 return NULL;
167 }
168
169 if (s->req_h < -1) {
170 log_msg(LOG_LEVEL_ERROR, "[text] Text height cannot be negative!\n");
171 free(s);
172 return NULL;
173 }
174
175 return s;
176}
177
178static int cf_text_init(struct module *parent, const char *cfg, void **state)
179{
180 (void) parent;
181 void *s = text_init(cfg);
182 if (!s) {
183 return 1;
184 } else {
185 *state = s;
186 return 0;
187 }
188}
189
190static bool
192{
193 struct state_text *s = (struct state_text *) state;
194
195 vf_free(s->in);
196 if (s->wand_text)
197 DestroyMagickWand(s->wand_text);
198 if (s->wand_bg)
199 DestroyMagickWand(s->wand_bg);
200 if (s->dw)
201 DestroyDrawingWand(s->dw);
202 s->in = 0;
203 s->wand_bg = 0;
204 s->wand_text = 0;
205 s->dw = 0;
206
208
209 s->margin_x = s->req_x == -1 ? (int) desc.width / MARGIN_X_DIV : s->req_x;
210 s->margin_y = s->req_y == -1 ? (int) desc.height / MARGIN_Y_DIV : s->req_y;
211 s->text_height_pt = s->req_h == -1 ? (int) desc.height / TEXT_H_DIV : s->req_h;
212 s->width = MIN(s->margin_x + strlen(s->text) * s->text_height_pt, desc.width);
213 s->text_base_y = MIN(s->margin_y + s->text_height_pt, (int) desc.height);
214
215
216 const char *color;
217 const char *color_outline;
218 const char *colorspace;
219 if (desc.color_spec == RGBA) {
220 color = "#333333FF";
221 color_outline = "#FFFFFFFF";
222 colorspace = "rgba";
223 } else if (desc.color_spec == RGB) {
224 color = "#333333FF";
225 color_outline = "#FFFFFFFF";
226 colorspace = "rgb";
227 } else if (desc.color_spec == UYVY) {
228 color = "#228080FF";
229 color_outline = "#EB8080FF";
230 colorspace = "UYVY";
231 } else {
232 bug_msg(LOG_LEVEL_ERROR, "[text vo_pp.] Codec not supported! ");
233 return false;
234 }
235
236 s->dw = NewDrawingWand();
237 DrawSetFontSize(s->dw, s->text_height_pt);
238 MagickBooleanType status = DrawSetFont(s->dw, "helvetica");
239 if(status != MagickTrue) {
240 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] DraweSetFont failed!\n");
241 return false;
242 }
243 {
244 PixelWand *pw = NewPixelWand();
245 PixelSetColor(pw, color);
246 DrawSetFillColor(s->dw, pw);
247 PixelSetColor(pw, color_outline);
248 DrawSetStrokeColor(s->dw, pw);
249 DestroyPixelWand(pw);
250 }
251
252 s->wand_bg = NewMagickWand();
253 status = MagickSetFormat(s->wand_bg, colorspace);
254 s->wand_text = NewMagickWand();
255 status &= MagickSetFormat(s->wand_text, colorspace);
256 if(status != MagickTrue) {
257 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickSetFormat failed!\n");
258 return false;
259 }
260
261
262 status = MagickSetDepth(s->wand_bg, 8);
263 status &= MagickSetDepth(s->wand_text, 8);
264 assert(status == MagickTrue && "[text vo_pp.] MagickSetDepth failed");
265
266 PixelWand *transparent_bg = NewPixelWand();
267 // PixelSetColor(transparent_bg, "#cccccc80"); // for debugging
268 PixelSetColor(transparent_bg, "#00000000");
269
270 // still need a dummy canvas to query font metrics
271 MagickNewImage(s->wand_text, 1, 1, transparent_bg);
272
273 // glyph w, glyph h, ascender, descender, txt w, txt h
274 double *metrics = MagickQueryFontMetrics(s->wand_text, s->dw, s->text);
275 if(!metrics) {
276 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickQueryFontMetrics failed!\n");
277 return false;
278 }
279 double descender = metrics[3]; // has negative value
280 s->text_width_px = metrics[4];
281 s->text_height_px = metrics[5]; // whole height, including the descender
282 free(metrics);
283
284 MagickRemoveImage(s->wand_text);
285 status = MagickNewImage(s->wand_text, s->text_width_px, s->text_height_px, transparent_bg);
286 if (!status) {
287 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickNewImage failed!\n");
288 return false;
289 }
290
291 double x_off = 0;
292 double y_off_baseline = s->text_height_px - (-descender) + 1;
293 double rot = 0;
294 status = MagickAnnotateImage(s->wand_text, s->dw, x_off, y_off_baseline, rot, s->text);
295 if (!status) {
296 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickAnnotateImage failed!\n\n"
297 "Perhaps the text contains invalid characters?\n");
298 return false;
299 }
300
301 DestroyPixelWand(transparent_bg);
302
303 return true;
304}
305
306static struct video_frame * text_getf(void *state)
307{
308 struct state_text *s = (struct state_text *) state;
309
310 return s->in;
311}
312
313static bool text_postprocess(void *state, struct video_frame *in, struct video_frame *out, int req_pitch)
314{
315 MagickBooleanType status;
316 struct state_text *s = (struct state_text *) state;
317
318 int dst_linesize = vc_get_linesize(s->width, in->color_spec);
319 int src_linesize = vc_get_linesize(in->tiles[0].width, in->color_spec);
320
321 int im_height_px = in->tiles[0].height;
322
323 // copy over just the relevant "stripe"
324 // cannot copy just the rectangle because pixel formats
325 // are not necessarily byte aligned
326 char *stripe = malloc((size_t) s->text_height_px * dst_linesize);
327 assert(stripe != NULL && "Malloc failed");
328 for (ptrdiff_t y = 0; y < s->text_height_px; y++) {
329 // skip what is out-of-bounds
330 if (y + s->margin_y < 0) {
331 y += -s->margin_y - 1;
332 continue;
333 }
334 if (y + s->margin_y >= im_height_px)
335 break;
336
337 // copy what isn't
338 memcpy(stripe + (y) * dst_linesize,
339 in->tiles[0].data + (y + s->margin_y) * src_linesize,
340 dst_linesize);
341 }
342
343 // read image stripe into ImageMagick
344 MagickRemoveImage(s->wand_bg);
345
346 status = MagickSetSize(s->wand_bg, s->width, s->text_height_px);
347 assert(status == MagickTrue && "[text vo_pp.] MagickSetSize failed -- ");
348
349 status = MagickReadImageBlob(s->wand_bg, stripe, s->text_height_px * dst_linesize);
350 if (status != MagickTrue) {
351 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickReadImageBlob failed!\n");
352 return false;
353 }
354
355 // compose it with text
356#ifdef WAND7
357 status = MagickCompositeImage(s->wand_bg, s->wand_text, OverCompositeOp, true, s->margin_x, 0);
358#else
359 status = MagickCompositeImage(s->wand_bg, s->wand_text, OverCompositeOp, s->margin_x, 0);
360#endif
361 if (status != MagickTrue) {
362 log_msg(LOG_LEVEL_WARNING, "[text vo_pp.] MagickCompositeImage failed!\n");
363 return false;
364 }
365
366 // extract the composed data
367 unsigned char *data;
368 size_t data_len;
369 data = MagickGetImageBlob(s->wand_bg, &data_len);
370 assert(data != NULL && "[text vo_pp.] MagickGetImageBlob failed!");
371
372 // send the input stream...
373 memcpy(out->tiles[0].data, in->tiles[0].data, in->tiles[0].data_len);
374
375 // ...and the text stripe on top of it
376 // if ((int) data_len == s->text_height_pt * dstlinesize) {
377 for (int y = 0; y < s->text_height_px; y++) {
378 // again, skip what is oob
379 if (y + s->margin_y < 0) {
380 y += - s->margin_y - 1; // 1 == don't y++ on continue
381 continue;
382 }
383 if (y + s->margin_y >= im_height_px) {
384 break;
385 }
386
387 memcpy(out->tiles[0].data + (y + s->margin_y) * req_pitch,
388 data + y * dst_linesize,
389 dst_linesize);
390 }
391 // }
392
393 free(data);
394
395 free(stripe);
396 return true;
397}
398
399static struct video_frame *cf_text_filter(void *state, struct video_frame *f)
400{
401 struct state_text *s = (struct state_text *) state;
402
406 } else {
407 log_msg(LOG_LEVEL_WARNING, "[text] Cannot reinitialize!\n");
408 return NULL;
409 }
410 }
411
412 struct video_frame *out = vf_alloc_desc_data(s->saved_desc);
414 if (text_postprocess(state, f, out, vc_get_linesize(f->tiles[0].width, f->color_spec))) {
416 return out;
417 } else {
419 vf_free(out);
420 return NULL;
421 }
422}
423
424static void text_done(void *state)
425{
426 struct state_text *s = (struct state_text *) state;
427
428 vf_free(s->in);
429
430 DestroyMagickWand(s->wand_bg);
431 DestroyMagickWand(s->wand_text);
432 DestroyDrawingWand(s->dw);
433
434 free(s->data);
435 free(s->text);
436 free(s);
437}
438
439static void text_get_out_desc(void *state, struct video_desc *out, int *in_display_mode, int *out_frames)
440{
441 struct state_text *s = (struct state_text *) state;
442
443 *out = video_desc_from_frame(s->in);
444
445 *in_display_mode = DISPLAY_PROPERTY_VIDEO_MERGED;
446 *out_frames = 1;
447}
448
458
464
465
468
#define UNUSED(x)
void ** state
int void * val
struct video_desc desc
int void size_t * len
int property
void vf_free(struct video_frame *buf)
Frees video_frame structure.
struct video_desc video_desc_from_frame(const struct video_frame *frame)
Returns struct video_desc from video frame.
int vc_get_linesize(unsigned int width, codec_t codec)
struct video_frame * vf_alloc_desc_data(struct video_desc desc)
#define CAPTURE_FILTER_ABI_VERSION
int color_printf(const char *format,...)
#define TBOLD(x)
Definition color_out.h:70
void log_msg(int level, const char *format,...)
Definition debug.cpp:130
void bug_msg(int level, const char *format,...)
prints the error with the advice to report the bug
Definition debug.cpp:379
#define LOG_LEVEL_ERROR
general errors
Definition debug.h:59
#define LOG_LEVEL_WARNING
less severe errors
Definition debug.h:60
@ DISPLAY_PROPERTY_VIDEO_MERGED
monolithic framebuffer
@ LIBRARY_CLASS_VIDEO_POSTPROCESS
Definition lib_common.h:83
@ LIBRARY_CLASS_CAPTURE_FILTER
Definition lib_common.h:75
#define REGISTER_MODULE(name, info, lclass, abi)
Registers module to global modules' registry.
Definition lib_common.h:144
#define MIN(a, b)
Definition macros.h:63
const char * parent
Definition settings.cpp:198
void replace_all(char *in, const char *from, const char *to)
Replaces all occurencies of 'from' to 'to' in string 'in'.
Definition string.c:71
#define DELDEL
Definition string.h:50
#define ESCAPED_COLON
Definition string.h:51
Definition hd-rum.c:27
char * data
Definition text.c:74
int text_base_y
Definition text.c:79
int margin_x
Definition text.c:80
int req_h
Definition text.c:77
struct video_frame * in
Definition text.c:73
char * text
Definition text.c:76
int req_x
Definition text.c:77
int width
Definition text.c:78
MagickWand * wand_bg
Definition text.c:91
size_t data_size
Definition text.c:75
int text_width_px
Definition text.c:85
int text_height_pt
Definition text.c:83
int margin_y
Definition text.c:80
int text_height_px
Definition text.c:86
MagickWand * wand_text
Definition text.c:92
int req_y
Definition text.c:77
struct video_desc saved_desc
Definition text.c:88
DrawingWand * dw
Definition text.c:90
unsigned int width
Definition types.h:280
char * data
Raw tile data. Pointer must be at least 4B aligned.
Definition types.h:287
unsigned int data_len
length of the data
Definition types.h:288
unsigned int height
Definition types.h:281
codec_t color_spec
Definition types.h:182
unsigned int width
width of every tile (!)
Definition types.h:179
unsigned int height
height of every tile (!)
Definition types.h:180
void(* dispose)(struct video_frame *)
Definition types.h:245
Struct video_frame represents a video frame and contains video description.
Definition types.h:305
struct tile tiles[FLEXIBLE_ARRAY_MEMBER]
Definition types.h:345
struct video_frame_callbacks callbacks
Definition types.h:324
codec_t color_spec
Definition types.h:306
@ RGB
RGB 8-bit (packed into 24-bit word)
Definition types.h:80
@ UYVY
YCbCr 422 8-bit - Cb Y0 Cr Y1.
Definition types.h:71
@ RGBA
RGBA 8-bit, big-endian.
Definition types.h:70
char * wrap_paragraph(char *text)
Definition text.c:204
This is an umbrella header for video functions.
bool video_desc_eq(struct video_desc a, struct video_desc b)
Compares two video descriptions.
#define VIDEO_FRAME_DISPOSE(frame)
Definition video_frame.h:76
static const struct capture_filter_info capture_filter_text_info
Definition text.c:459
static bool text_postprocess(void *state, struct video_frame *in, struct video_frame *out, int req_pitch)
Definition text.c:313
static const struct vo_postprocess_info vo_pp_text_info
Definition text.c:449
static void text_done(void *state)
Definition text.c:424
#define TEXT_H_DIV
Definition text.c:106
static void * text_init(const char *config)
Definition text.c:118
static bool text_postprocess_reconfigure(void *state, struct video_desc desc)
Definition text.c:191
static int cf_text_init(struct module *parent, const char *cfg, void **state)
Definition text.c:178
static pthread_once_t vo_postprocess_text_initialized
Definition text.c:110
static struct video_frame * cf_text_filter(void *state, struct video_frame *f)
Definition text.c:399
static void init_text()
Definition text.c:112
static void text_get_out_desc(void *state, struct video_desc *out, int *in_display_mode, int *out_frames)
Definition text.c:439
static bool text_get_property(void *state, int property, void *val, size_t *len)
Definition text.c:95
static struct video_frame * text_getf(void *state)
Definition text.c:306
#define MARGIN_Y_DIV
Definition text.c:108
#define MARGIN_X_DIV
Definition text.c:107
#define VO_PP_ABI_VERSION