Blender V4.5
vse_effect_text.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
8
9#include <cmath>
10#include <mutex>
11
12#include "BKE_lib_id.hh"
13#include "BKE_library.hh"
14#include "BKE_main.hh"
15
16#include "BLI_map.hh"
17#include "BLI_math_base.hh"
18#include "BLI_math_rotation.h"
19#include "BLI_math_vector.h"
20#include "BLI_math_vector.hh"
21#include "BLI_path_utils.hh"
22#include "BLI_rect.h"
23#include "BLI_string.h"
24#include "BLI_string_utf8.h"
25#include "BLI_task.hh"
26#include "BLI_vector.hh"
27
28#include "BLF_api.hh"
29
31#include "DNA_scene_types.h"
32#include "DNA_sequence_types.h"
33#include "DNA_space_types.h"
34#include "DNA_vfont_types.h"
35
37#include "IMB_imbuf_types.hh"
38
39#include "SEQ_effects.hh"
40#include "SEQ_proxy.hh"
41#include "SEQ_render.hh"
42#include "SEQ_utils.hh"
43
44#include "effects.hh"
45
46namespace blender::seq {
47
48/* -------------------------------------------------------------------- */
49/* Sequencer font access.
50 *
51 * Text strips can access and use fonts from a background thread
52 * (when depsgraph evaluation copies the scene, or when prefetch renders
53 * frames with text strips in a background thread).
54 *
55 * To not interfere with what might be happening on the main thread, all
56 * fonts used by the sequencer are made unique via #BLF_load_unique
57 * #BLF_load_mem_unique, and there's a mutex to guard against
58 * sequencer itself possibly using the fonts from several threads.
59 */
60
61struct SeqFontMap {
62 /* File path -> font ID mapping for file-based fonts. */
64 /* Datablock name -> font ID mapping for memory (datablock) fonts. */
66
67 /* Font access mutex. Recursive since it is locked from
68 * text strip rendering, which can call into loading from within. */
69 std::recursive_mutex mutex;
70};
71
73
75{
76 for (const auto &item : g_font_map.path_to_file_font_id.items()) {
77 BLF_unload_id(item.value);
78 }
79 g_font_map.path_to_file_font_id.clear();
80 for (const auto &item : g_font_map.name_to_mem_font_id.items()) {
81 BLF_unload_id(item.value);
82 }
83 g_font_map.name_to_mem_font_id.clear();
84}
85
86static int strip_load_font_file(const std::string &path)
87{
88 std::lock_guard lock(g_font_map.mutex);
89 int fontid = g_font_map.path_to_file_font_id.add_or_modify(
90 path,
91 [&](int *fontid) {
92 /* New path: load font. */
93 *fontid = BLF_load_unique(path.c_str());
94 return *fontid;
95 },
96 [&](int *fontid) {
97 /* Path already in cache: add reference to already loaded font,
98 * or load a new one in case that
99 * font id was unloaded behind our backs. */
100 if (*fontid >= 0) {
101 if (BLF_is_loaded_id(*fontid)) {
102 BLF_addref_id(*fontid);
103 }
104 else {
105 *fontid = BLF_load_unique(path.c_str());
106 }
107 }
108 return *fontid;
109 });
110 return fontid;
111}
112
113static int strip_load_font_mem(const std::string &name, const uchar *data, int data_size)
114{
115 std::lock_guard lock(g_font_map.mutex);
116 int fontid = g_font_map.name_to_mem_font_id.add_or_modify(
117 name,
118 [&](int *fontid) {
119 /* New name: load font. */
120 *fontid = BLF_load_mem_unique(name.c_str(), data, data_size);
121 return *fontid;
122 },
123 [&](int *fontid) {
124 /* Name already in cache: add reference to already loaded font,
125 * or (if we're on the main thread) load a new one in case that
126 * font id was unloaded behind our backs. */
127 if (*fontid >= 0) {
128 if (BLF_is_loaded_id(*fontid)) {
129 BLF_addref_id(*fontid);
130 }
131 else {
132 *fontid = BLF_load_mem_unique(name.c_str(), data, data_size);
133 }
134 }
135 return *fontid;
136 });
137 return fontid;
138}
139
140static void strip_unload_font(int fontid)
141{
142 std::lock_guard lock(g_font_map.mutex);
143 bool unloaded = BLF_unload_id(fontid);
144 /* If that was the last usage of the font and it got unloaded: remove
145 * it from our maps. */
146 if (unloaded) {
147 g_font_map.path_to_file_font_id.remove_if([&](auto item) { return item.value == fontid; });
148 g_font_map.name_to_mem_font_id.remove_if([&](auto item) { return item.value == fontid; });
149 }
150}
151
152/* -------------------------------------------------------------------- */
155
157{
158 /* `data->text[0] == 0` is ignored on purpose in order to make it possible to edit. */
159
160 TextVars *data = static_cast<TextVars *>(strip->effectdata);
161 if (data->text_size < 1.0f ||
162 ((data->color[3] == 0.0f) &&
163 (data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) &&
164 (data->outline_color[3] == 0.0f || data->outline_width <= 0.0f ||
165 (data->flag & SEQ_TEXT_OUTLINE) == 0)))
166 {
167 return false;
168 }
169 return true;
170}
171
172static void init_text_effect(Strip *strip)
173{
174 if (strip->effectdata) {
175 MEM_freeN(strip->effectdata);
176 }
177
178 TextVars *data = MEM_callocN<TextVars>("textvars");
179 strip->effectdata = data;
180
181 data->text_font = nullptr;
182 data->text_blf_id = -1;
183 data->text_size = 60.0f;
184
185 copy_v4_fl(data->color, 1.0f);
186 data->shadow_color[3] = 0.7f;
187 data->shadow_angle = DEG2RADF(65.0f);
188 data->shadow_offset = 0.04f;
189 data->shadow_blur = 0.0f;
190 data->box_color[0] = 0.2f;
191 data->box_color[1] = 0.2f;
192 data->box_color[2] = 0.2f;
193 data->box_color[3] = 0.7f;
194 data->box_margin = 0.01f;
195 data->box_roundness = 0.0f;
196 data->outline_color[3] = 0.7f;
197 data->outline_width = 0.05f;
198
199 STRNCPY(data->text, "Text");
200
201 data->loc[0] = 0.5f;
202 data->loc[1] = 0.5f;
203 data->anchor_x = SEQ_TEXT_ALIGN_X_CENTER;
204 data->anchor_y = SEQ_TEXT_ALIGN_Y_CENTER;
206 data->wrap_width = 1.0f;
207}
208
209void effect_text_font_unload(TextVars *data, const bool do_id_user)
210{
211 if (data == nullptr) {
212 return;
213 }
214
215 /* Unlink the VFont */
216 if (do_id_user && data->text_font != nullptr) {
217 id_us_min(&data->text_font->id);
218 data->text_font = nullptr;
219 }
220
221 /* Unload the font. */
222 if (data->text_blf_id >= 0) {
223 strip_unload_font(data->text_blf_id);
224 data->text_blf_id = -1;
225 }
226}
227
228void effect_text_font_load(TextVars *data, const bool do_id_user)
229{
230 VFont *vfont = data->text_font;
231 if (vfont == nullptr) {
232 return;
233 }
234
235 if (do_id_user) {
236 id_us_plus(&vfont->id);
237 }
238
239 if (vfont->packedfile != nullptr) {
240 PackedFile *pf = vfont->packedfile;
241 /* Create a name that's unique between library data-blocks to avoid loading
242 * a font per strip which will load fonts many times.
243 *
244 * WARNING: this isn't fool proof!
245 * The #VFont may be renamed which will cause this to load multiple times,
246 * in practice this isn't so likely though. */
247 char name[MAX_ID_FULL_NAME];
248 BKE_id_full_name_get(name, &vfont->id, 0);
249
250 data->text_blf_id = strip_load_font_mem(name, static_cast<const uchar *>(pf->data), pf->size);
251 }
252 else {
253 char filepath[FILE_MAX];
254 STRNCPY(filepath, vfont->filepath);
255
256 BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&vfont->id));
257 data->text_blf_id = strip_load_font_file(filepath);
258 }
259}
260
261static void free_text_effect(Strip *strip, const bool do_id_user)
262{
263 TextVars *data = static_cast<TextVars *>(strip->effectdata);
264 effect_text_font_unload(data, do_id_user);
265
266 if (data) {
267 MEM_delete(data->runtime);
269 strip->effectdata = nullptr;
270 }
271}
272
273static void load_text_effect(Strip *strip)
274{
275 TextVars *data = static_cast<TextVars *>(strip->effectdata);
277}
278
279static void copy_text_effect(Strip *dst, const Strip *src, const int flag)
280{
282 TextVars *data = static_cast<TextVars *>(dst->effectdata);
283
284 data->runtime = nullptr;
285 data->text_blf_id = -1;
287}
288
289static int num_inputs_text()
290{
291 return 0;
292}
293
294static StripEarlyOut early_out_text(const Strip *strip, float /*fac*/)
295{
296 if (!effects_can_render_text(strip)) {
298 }
300}
301
302/* Simplified version of gaussian blur specifically for text shadow blurring:
303 * - Data is only the alpha channel,
304 * - Skips blur outside of shadow rectangle. */
305static void text_gaussian_blur_x(const Span<float> gaussian,
306 int half_size,
307 int start_line,
308 int width,
309 int height,
310 const uchar *rect,
311 uchar *dst,
312 const rcti &shadow_rect)
313{
314 dst += int64_t(start_line) * width;
315 for (int y = start_line; y < start_line + height; y++) {
316 for (int x = 0; x < width; x++) {
317 float accum(0.0f);
318 if (x >= shadow_rect.xmin && x <= shadow_rect.xmax) {
319 float accum_weight = 0.0f;
320 int xmin = math::max(x - half_size, shadow_rect.xmin);
321 int xmax = math::min(x + half_size, shadow_rect.xmax);
322 for (int nx = xmin, index = (xmin - x) + half_size; nx <= xmax; nx++, index++) {
323 float weight = gaussian[index];
324 int offset = y * width + nx;
325 accum += rect[offset] * weight;
326 accum_weight += weight;
327 }
328 accum *= (1.0f / accum_weight);
329 }
330
331 *dst = accum;
332 dst++;
333 }
334 }
335}
336
337static void text_gaussian_blur_y(const Span<float> gaussian,
338 int half_size,
339 int start_line,
340 int width,
341 int height,
342 const uchar *rect,
343 uchar *dst,
344 const rcti &shadow_rect)
345{
346 dst += int64_t(start_line) * width;
347 for (int y = start_line; y < start_line + height; y++) {
348 for (int x = 0; x < width; x++) {
349 float accum(0.0f);
350 if (x >= shadow_rect.xmin && x <= shadow_rect.xmax) {
351 float accum_weight = 0.0f;
352 int ymin = math::max(y - half_size, shadow_rect.ymin);
353 int ymax = math::min(y + half_size, shadow_rect.ymax);
354 for (int ny = ymin, index = (ymin - y) + half_size; ny <= ymax; ny++, index++) {
355 float weight = gaussian[index];
356 int offset = ny * width + x;
357 accum += rect[offset] * weight;
358 accum_weight += weight;
359 }
360 accum *= (1.0f / accum_weight);
361 }
362 *dst = accum;
363 dst++;
364 }
365 }
366}
367
368static void clamp_rect(int width, int height, rcti &r_rect)
369{
370 r_rect.xmin = math::clamp(r_rect.xmin, 0, width - 1);
371 r_rect.xmax = math::clamp(r_rect.xmax, 0, width - 1);
372 r_rect.ymin = math::clamp(r_rect.ymin, 0, height - 1);
373 r_rect.ymax = math::clamp(r_rect.ymax, 0, height - 1);
374}
375
376static void initialize_shadow_alpha(int width,
377 int height,
378 int2 offset,
379 const rcti &shadow_rect,
380 const uchar *input,
381 Array<uchar> &r_shadow_mask)
382{
383 const IndexRange shadow_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
384 threading::parallel_for(shadow_y_range, 8, [&](const IndexRange y_range) {
385 for (const int64_t y : y_range) {
386 const int64_t src_y = math::clamp<int64_t>(y + offset.y, 0, height - 1);
387 for (int x = shadow_rect.xmin; x <= shadow_rect.xmax; x++) {
388 int src_x = math::clamp(x - offset.x, 0, width - 1);
389 size_t src_offset = width * src_y + src_x;
390 size_t dst_offset = width * y + x;
391 r_shadow_mask[dst_offset] = input[src_offset * 4 + 3];
392 }
393 }
394 });
395}
396
397static void composite_shadow(int width,
398 const rcti &shadow_rect,
399 const float4 &shadow_color,
400 const Array<uchar> &shadow_mask,
401 uchar *output)
402{
403 const IndexRange shadow_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
404 threading::parallel_for(shadow_y_range, 8, [&](const IndexRange y_range) {
405 for (const int64_t y : y_range) {
406 size_t offset = y * width + shadow_rect.xmin;
407 uchar *dst = output + offset * 4;
408 for (int x = shadow_rect.xmin; x <= shadow_rect.xmax; x++, offset++, dst += 4) {
409 uchar a = shadow_mask[offset];
410 if (a == 0) {
411 /* Fully transparent, leave output pixel as is. */
412 continue;
413 }
414 float4 col1 = load_premul_pixel(dst);
415 float4 col2 = shadow_color * (a * (1.0f / 255.0f));
416 /* Blend under the output. */
417 float fac = 1.0f - col1.w;
418 float4 col = col1 + fac * col2;
420 }
421 }
422 });
423}
424
426 const RenderData *context, const TextVars *data, int line_height, const rcti &rect, ImBuf *out)
427{
428 const int width = context->rectx;
429 const int height = context->recty;
430 /* Blur value of 1.0 applies blur kernel that is half of text line height. */
431 const float blur_amount = line_height * 0.5f * data->shadow_blur;
432 bool do_blur = blur_amount >= 1.0f;
433
434 Array<uchar> shadow_mask(size_t(width) * height, 0);
435
436 const int2 offset = int2(cosf(data->shadow_angle) * line_height * data->shadow_offset,
437 sinf(data->shadow_angle) * line_height * data->shadow_offset);
438
439 rcti shadow_rect = rect;
440 BLI_rcti_translate(&shadow_rect, offset.x, -offset.y);
441 BLI_rcti_pad(&shadow_rect, 1, 1);
442 clamp_rect(width, height, shadow_rect);
443
444 /* Initialize shadow by copying existing text/outline alpha. */
445 initialize_shadow_alpha(width, height, offset, shadow_rect, out->byte_buffer.data, shadow_mask);
446
447 if (do_blur) {
448 /* Create blur kernel weights. */
449 const int half_size = int(blur_amount + 0.5f);
450 Array<float> gaussian = make_gaussian_blur_kernel(blur_amount, half_size);
451
452 BLI_rcti_pad(&shadow_rect, half_size + 1, half_size + 1);
453 clamp_rect(width, height, shadow_rect);
454
455 /* Horizontal blur: blur shadow_mask into blur_buffer. */
456 Array<uchar> blur_buffer(size_t(width) * height, NoInitialization());
457 IndexRange blur_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
458 threading::parallel_for(blur_y_range, 8, [&](const IndexRange y_range) {
459 const int y_first = y_range.first();
460 const int y_size = y_range.size();
461 text_gaussian_blur_x(gaussian,
462 half_size,
463 y_first,
464 width,
465 y_size,
466 shadow_mask.data(),
467 blur_buffer.data(),
468 shadow_rect);
469 });
470
471 /* Vertical blur: blur blur_buffer into shadow_mask. */
472 threading::parallel_for(blur_y_range, 8, [&](const IndexRange y_range) {
473 const int y_first = y_range.first();
474 const int y_size = y_range.size();
475 text_gaussian_blur_y(gaussian,
476 half_size,
477 y_first,
478 width,
479 y_size,
480 blur_buffer.data(),
481 shadow_mask.data(),
482 shadow_rect);
483 });
484 }
485
486 /* Composite shadow under regular output. */
487 float4 color = data->shadow_color;
488 color.x *= color.w;
489 color.y *= color.w;
490 color.z *= color.w;
491 composite_shadow(width, shadow_rect, color, shadow_mask, out->byte_buffer.data);
492}
493
494/* Text outline calculation is done by Jump Flooding Algorithm (JFA).
495 * This is similar to inpaint/jump_flooding in Compositor, also to
496 * "The Quest for Very Wide Outlines", Ben Golus 2020
497 * https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9 */
498
499constexpr uint16_t JFA_INVALID = 0xFFFF;
500
501struct JFACoord {
502 uint16_t x;
503 uint16_t y;
504};
505
508 int2 size,
509 IndexRange x_range,
510 IndexRange y_range,
511 int step_size)
512{
513 threading::parallel_for(y_range, 8, [&](const IndexRange sub_y_range) {
514 for (const int64_t y : sub_y_range) {
515 size_t index = y * size.x;
516 for (const int64_t x : x_range) {
517 float2 coord = float2(x, y);
518
519 /* For each pixel, sample 9 pixels at +/- step size pattern,
520 * and output coordinate of closest to the boundary. */
521 JFACoord closest_texel{JFA_INVALID, JFA_INVALID};
522 float minimum_squared_distance = std::numeric_limits<float>::max();
523 for (int dy = -step_size; dy <= step_size; dy += step_size) {
524 int yy = y + dy;
525 if (yy < 0 || yy >= size.y) {
526 continue;
527 }
528 for (int dx = -step_size; dx <= step_size; dx += step_size) {
529 int xx = x + dx;
530 if (xx < 0 || xx >= size.x) {
531 continue;
532 }
533 JFACoord val = input[size_t(yy) * size.x + xx];
534 if (val.x == JFA_INVALID) {
535 continue;
536 }
537
538 float squared_distance = math::distance_squared(float2(val.x, val.y), coord);
539 if (squared_distance < minimum_squared_distance) {
540 minimum_squared_distance = squared_distance;
541 closest_texel = val;
542 }
543 }
544 }
545
546 output[index + x] = closest_texel;
547 }
548 }
549 });
550}
551
552static void text_draw(const TextVarsRuntime *runtime, float color[4])
553{
554 const bool use_fallback = BLF_is_builtin(runtime->font);
555 if (!use_fallback) {
557 }
558
559 for (const LineInfo &line : runtime->lines) {
560 for (const CharInfo &character : line.characters) {
561 BLF_position(runtime->font, character.position.x, character.position.y, 0.0f);
562 BLF_buffer_col(runtime->font, color);
563 BLF_draw_buffer(runtime->font, character.str_ptr, character.byte_length);
564 }
565 }
566
567 if (!use_fallback) {
569 }
570}
571
572static rcti draw_text_outline(const RenderData *context,
573 const TextVars *data,
574 const TextVarsRuntime *runtime,
575 const ColorManagedDisplay *display,
576 ImBuf *out)
577{
578 /* Outline width of 1.0 maps to half of text line height. */
579 const int outline_width = int(runtime->line_height * 0.5f * data->outline_width);
580 if (outline_width < 1 || data->outline_color[3] <= 0.0f ||
581 ((data->flag & SEQ_TEXT_OUTLINE) == 0))
582 {
583 return runtime->text_boundbox;
584 }
585
586 const int2 size = int2(context->rectx, context->recty);
587
588 /* Draw white text into temporary buffer. */
589 const size_t pixel_count = size_t(size.x) * size.y;
590 Array<uchar4> tmp_buf(pixel_count, uchar4(0));
591 BLF_buffer(runtime->font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display);
592
593 text_draw(runtime, float4(1.0f));
594
595 rcti outline_rect = runtime->text_boundbox;
596 BLI_rcti_pad(&outline_rect, outline_width + 1, outline_width + 1);
597 outline_rect.xmin = clamp_i(outline_rect.xmin, 0, size.x - 1);
598 outline_rect.xmax = clamp_i(outline_rect.xmax, 0, size.x - 1);
599 outline_rect.ymin = clamp_i(outline_rect.ymin, 0, size.y - 1);
600 outline_rect.ymax = clamp_i(outline_rect.ymax, 0, size.y - 1);
601 const IndexRange rect_x_range(outline_rect.xmin, outline_rect.xmax - outline_rect.xmin + 1);
602 const IndexRange rect_y_range(outline_rect.ymin, outline_rect.ymax - outline_rect.ymin + 1);
603
604 /* Initialize JFA: invalid values for empty regions, pixel coordinates
605 * for opaque regions. */
606 Array<JFACoord> boundary(pixel_count, NoInitialization());
607 threading::parallel_for(IndexRange(size.y), 16, [&](const IndexRange y_range) {
608 for (const int y : y_range) {
609 size_t index = size_t(y) * size.x;
610 for (int x = 0; x < size.x; x++, index++) {
611 bool is_opaque = tmp_buf[index].w >= 128;
612 JFACoord coord;
613 coord.x = is_opaque ? x : JFA_INVALID;
614 coord.y = is_opaque ? y : JFA_INVALID;
615 boundary[index] = coord;
616 }
617 }
618 });
619
620 /* Do jump flooding calculations. */
621 JFACoord invalid_coord{JFA_INVALID, JFA_INVALID};
622 Array<JFACoord> initial_flooded_result(pixel_count, invalid_coord);
623 jump_flooding_pass(boundary, initial_flooded_result, size, rect_x_range, rect_y_range, 1);
624
625 Array<JFACoord> *result_to_flood = &initial_flooded_result;
626 Array<JFACoord> intermediate_result(pixel_count, invalid_coord);
627 Array<JFACoord> *result_after_flooding = &intermediate_result;
628
629 int step_size = power_of_2_max_i(outline_width) / 2;
630
631 while (step_size != 0) {
633 *result_to_flood, *result_after_flooding, size, rect_x_range, rect_y_range, step_size);
634 std::swap(result_to_flood, result_after_flooding);
635 step_size /= 2;
636 }
637
638 /* Premultiplied outline color. */
639 float4 color = data->outline_color;
640 color.x *= color.w;
641 color.y *= color.w;
642 color.z *= color.w;
643
644 const float text_color_alpha = data->color[3];
645
646 /* We have distances to the closest opaque parts of the image now. Composite the
647 * outline into the output image. */
648
649 threading::parallel_for(rect_y_range, 8, [&](const IndexRange y_range) {
650 for (const int y : y_range) {
651 size_t index = size_t(y) * size.x + rect_x_range.start();
652 uchar *dst = out->byte_buffer.data + index * 4;
653 for (int x = rect_x_range.start(); x < rect_x_range.one_after_last(); x++, index++, dst += 4)
654 {
655 JFACoord closest_texel = (*result_to_flood)[index];
656 if (closest_texel.x == JFA_INVALID) {
657 /* Outside of outline, leave output pixel as is. */
658 continue;
659 }
660
661 /* Fade out / anti-alias the outline over one pixel towards outline distance. */
662 float distance = math::distance(float2(x, y), float2(closest_texel.x, closest_texel.y));
663 float alpha = math::clamp(outline_width - distance + 1.0f, 0.0f, 1.0f);
664
665 /* Do not put outline inside the text shape:
666 * - When overall text color is fully opaque, we want to make
667 * outline fully transparent only where text is fully opaque.
668 * This ensures that combined anti-aliased pixels at text boundary
669 * are properly fully opaque.
670 * - However when text color is fully transparent, we want to
671 * Use opposite alpha of text, to anti-alias the inner edge of
672 * the outline.
673 * In between those two, interpolate the alpha modulation factor. */
674 float text_alpha = tmp_buf[index].w * (1.0f / 255.0f);
675 float mul_opaque_text = text_alpha >= 1.0f ? 0.0f : 1.0f;
676 float mul_transparent_text = 1.0f - text_alpha;
677 float mul = math::interpolate(mul_transparent_text, mul_opaque_text, text_color_alpha);
678 alpha *= mul;
679
680 float4 col1 = color;
681 col1 *= alpha;
682
683 /* Blend over the output. */
684 float mfac = 1.0f - col1.w;
685 float4 col2 = load_premul_pixel(dst);
686 float4 col = col1 + mfac * col2;
688 }
689 }
690 });
691 BLF_buffer(runtime->font, nullptr, out->byte_buffer.data, size.x, size.y, display);
692
693 return outline_rect;
694}
695
696/* Similar to #IMB_rectfill_area but blends the given color under the
697 * existing image. Also can do rounded corners. Only works on byte buffers. */
699 const ImBuf *ibuf, const float col[4], int x1, int y1, int x2, int y2, float corner_radius)
700{
701 const int width = ibuf->x;
702 const int height = ibuf->y;
703 x1 = math::clamp(x1, 0, width);
704 x2 = math::clamp(x2, 0, width);
705 y1 = math::clamp(y1, 0, height);
706 y2 = math::clamp(y2, 0, height);
707 if (x1 > x2) {
708 std::swap(x1, x2);
709 }
710 if (y1 > y2) {
711 std::swap(y1, y2);
712 }
713 if (x1 == x2 || y1 == y2) {
714 return;
715 }
716
717 corner_radius = math::clamp(corner_radius, 0.0f, math::min(x2 - x1, y2 - y1) / 2.0f);
718
719 float4 premul_col_base;
720 straight_to_premul_v4_v4(premul_col_base, col);
721
722 threading::parallel_for(IndexRange::from_begin_end(y1, y2), 16, [&](const IndexRange y_range) {
723 for (const int y : y_range) {
724 uchar *dst = ibuf->byte_buffer.data + (size_t(width) * y + x1) * 4;
725 float origin_x = 0.0f, origin_y = 0.0f;
726 for (int x = x1; x < x2; x++) {
727 float4 pix = load_premul_pixel(dst);
728 float fac = 1.0f - pix.w;
729
730 float4 premul_col = premul_col_base;
731 bool is_corner = false;
732 if (x < x1 + corner_radius && y < y1 + corner_radius) {
733 is_corner = true;
734 origin_x = x1 + corner_radius - 1;
735 origin_y = y1 + corner_radius - 1;
736 }
737 else if (x >= x2 - corner_radius && y < y1 + corner_radius) {
738 is_corner = true;
739 origin_x = x2 - corner_radius;
740 origin_y = y1 + corner_radius - 1;
741 }
742 else if (x < x1 + corner_radius && y >= y2 - corner_radius) {
743 is_corner = true;
744 origin_x = x1 + corner_radius - 1;
745 origin_y = y2 - corner_radius;
746 }
747 else if (x >= x2 - corner_radius && y >= y2 - corner_radius) {
748 is_corner = true;
749 origin_x = x2 - corner_radius;
750 origin_y = y2 - corner_radius;
751 }
752 if (is_corner) {
753 /* If we are inside rounded corner, evaluate a superellipse and
754 * modulate color with that. Superellipse instead of just a circle
755 * since the curvature between flat and rounded area looks a bit
756 * nicer. */
757 constexpr float curve_pow = 2.1f;
758 float r = powf(powf(abs(x - origin_x), curve_pow) + powf(abs(y - origin_y), curve_pow),
759 1.0f / curve_pow);
760 float alpha = math::clamp(corner_radius - r, 0.0f, 1.0f);
761 premul_col *= alpha;
762 }
763
764 float4 dst_fl = fac * premul_col + pix;
765 store_premul_pixel(dst_fl, dst);
766 dst += 4;
767 }
768 }
769 });
770}
771
772static int text_effect_line_size_get(const RenderData *context, const Strip *strip)
773{
774 TextVars *data = static_cast<TextVars *>(strip->effectdata);
775 /* Compensate text size for preview render size. */
776 double proxy_size_comp = context->scene->r.size / 100.0;
777 if (context->preview_render_size != SEQ_RENDER_SIZE_SCENE) {
778 proxy_size_comp = rendersize_to_scale_factor(context->preview_render_size);
779 }
780
781 return proxy_size_comp * data->text_size;
782}
783
784static int text_effect_font_init(const RenderData *context, const Strip *strip, int font_flags)
785{
786 TextVars *data = static_cast<TextVars *>(strip->effectdata);
787 int font = blf_mono_font_render;
788
789 /* In case font got unloaded behind our backs: mark it as needing a load. */
790 if (data->text_blf_id >= 0 && !BLF_is_loaded_id(data->text_blf_id)) {
791 data->text_blf_id = STRIP_FONT_NOT_LOADED;
792 }
793
794 if (data->text_blf_id == STRIP_FONT_NOT_LOADED) {
795 data->text_blf_id = -1;
796
798 }
799
800 if (data->text_blf_id >= 0) {
801 font = data->text_blf_id;
802 }
803
804 BLF_size(font, text_effect_line_size_get(context, strip));
805 BLF_enable(font, font_flags);
806 return font;
807}
808
810{
811 Vector<CharInfo> characters;
812 const size_t len_max = STRNLEN(data->text);
813 int byte_offset = 0;
814 int char_index = 0;
815
816 const bool use_fallback = BLF_is_builtin(font);
817 if (!use_fallback) {
819 }
820
821 while (byte_offset <= len_max) {
822 const char *str = data->text + byte_offset;
823 const int char_length = BLI_str_utf8_size_safe(str);
824
825 CharInfo char_info;
826 char_info.index = char_index;
827 char_info.str_ptr = str;
828 char_info.byte_length = char_length;
829 char_info.advance_x = BLF_glyph_advance(font, str);
830 characters.append(char_info);
831
832 byte_offset += char_length;
833 char_index++;
834 }
835
836 if (!use_fallback) {
838 }
839
840 return characters;
841}
842
843static int wrap_width_get(const TextVars *data, const int2 image_size)
844{
845 if (data->wrap_width == 0.0f) {
846 return std::numeric_limits<int>::max();
847 }
848 return data->wrap_width * image_size.x;
849}
850
851/* Lines must contain CharInfo for newlines and \0, as UI must know where they begin. */
853 TextVarsRuntime *runtime,
854 const int2 image_size,
855 Vector<CharInfo> &characters)
856{
857 const int wrap_width = wrap_width_get(data, image_size);
858
859 float2 char_position{0.0f, 0.0f};
860 CharInfo *last_space = nullptr;
861
862 /* First pass: Find characters where line has to be broken. */
863 for (CharInfo &character : characters) {
864 if (character.str_ptr[0] == ' ') {
865 character.position = char_position;
866 last_space = &character;
867 }
868 if (character.str_ptr[0] == '\n') {
869 char_position.x = 0;
870 last_space = nullptr;
871 }
872 if (character.str_ptr[0] != '\0' && char_position.x > wrap_width && last_space != nullptr) {
873 last_space->do_wrap = true;
874 char_position -= last_space->position + last_space->advance_x;
875 }
876 char_position.x += character.advance_x;
877 }
878
879 /* Second pass: Fill lines with characters. */
880 char_position = {0.0f, 0.0f};
881 runtime->lines.append(LineInfo());
882 for (CharInfo &character : characters) {
883 character.position = char_position;
884 runtime->lines.last().characters.append(character);
885 runtime->lines.last().width = char_position.x;
886
887 char_position.x += character.advance_x;
888
889 if (character.do_wrap || character.str_ptr[0] == '\n') {
890 runtime->lines.append(LineInfo());
891 char_position.x = 0;
892 char_position.y -= runtime->line_height;
893 }
894 }
895}
896
897static int text_box_width_get(const Vector<LineInfo> &lines)
898{
899 int width_max = 0;
900
901 for (const LineInfo &line : lines) {
902 width_max = std::max(width_max, line.width);
903 }
904 return width_max;
905}
906
908 float line_width,
909 int width_max)
910{
911 const float line_offset = (width_max - line_width);
912
913 if (data->align == SEQ_TEXT_ALIGN_X_RIGHT) {
914 return {line_offset, 0.0f};
915 }
916 if (data->align == SEQ_TEXT_ALIGN_X_CENTER) {
917 return {line_offset / 2.0f, 0.0f};
918 }
919
920 return {0.0f, 0.0f};
921}
922
923static float2 anchor_offset_get(const TextVars *data, int width_max, int text_height)
924{
925 float2 anchor_offset;
926
927 switch (data->anchor_x) {
929 anchor_offset.x = 0;
930 break;
932 anchor_offset.x = -width_max / 2.0f;
933 break;
935 anchor_offset.x = -width_max;
936 break;
937 }
938 switch (data->anchor_y) {
940 anchor_offset.y = 0;
941 break;
943 anchor_offset.y = text_height / 2.0f;
944 break;
946 anchor_offset.y = text_height;
947 break;
948 }
949
950 return anchor_offset;
951}
952
953static void calc_boundbox(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size)
954{
955 const int text_height = runtime->lines.size() * runtime->line_height;
956
957 int width_max = text_box_width_get(runtime->lines);
958
959 /* Add width to empty text, so there is something to draw or select. */
960 if (width_max == 0) {
961 width_max = text_height * 2;
962 }
963
964 const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
965 const float2 anchor = anchor_offset_get(data, width_max, text_height);
966
967 runtime->text_boundbox.xmin = anchor.x + image_center.x;
968 runtime->text_boundbox.xmax = anchor.x + image_center.x + width_max;
969 runtime->text_boundbox.ymin = anchor.y + image_center.y - text_height;
970 runtime->text_boundbox.ymax = runtime->text_boundbox.ymin + text_height;
971}
972
974 TextVarsRuntime *runtime,
975 const int2 image_size)
976{
977 const int width_max = text_box_width_get(runtime->lines);
978 const int text_height = runtime->lines.size() * runtime->line_height;
979
980 const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
981 const float2 line_height_offset{0.0f,
982 float(-runtime->line_height - BLF_descender(runtime->font))};
983 const float2 anchor = anchor_offset_get(data, width_max, text_height);
984
985 for (LineInfo &line : runtime->lines) {
986 const float2 alignment_x = horizontal_alignment_offset_get(data, line.width, width_max);
987 const float2 alignment = math::round(image_center + line_height_offset + alignment_x + anchor);
988
989 for (CharInfo &character : line.characters) {
990 character.position += alignment;
991 }
992 }
993}
994
995static void calc_text_runtime(const Strip *strip, int font, const int2 image_size)
996{
997 TextVars *data = static_cast<TextVars *>(strip->effectdata);
998
999 if (data->runtime != nullptr) {
1000 MEM_delete(data->runtime);
1001 }
1002
1003 data->runtime = MEM_new<TextVarsRuntime>(__func__);
1004 TextVarsRuntime *runtime = data->runtime;
1005 runtime->font = font;
1006 runtime->line_height = BLF_height_max(font);
1007 runtime->font_descender = BLF_descender(font);
1008 runtime->character_count = BLI_strlen_utf8(data->text);
1009
1010 Vector<CharInfo> characters_temp = build_character_info(data, font);
1011 apply_word_wrapping(data, runtime, image_size, characters_temp);
1012 apply_text_alignment(data, runtime, image_size);
1013 calc_boundbox(data, runtime, image_size);
1014}
1015
1016static ImBuf *do_text_effect(const RenderData *context,
1017 Strip *strip,
1018 float /*timeline_frame*/,
1019 float /*fac*/,
1020 ImBuf * /*ibuf1*/,
1021 ImBuf * /*ibuf2*/)
1022{
1023 /* NOTE: text rasterization only fills in part of output image,
1024 * need to clear it. */
1025 ImBuf *out = prepare_effect_imbufs(context, nullptr, nullptr, false);
1026 TextVars *data = static_cast<TextVars *>(strip->effectdata);
1027
1028 const char *display_device = context->scene->display_settings.display_device;
1029 const ColorManagedDisplay *display = IMB_colormanagement_display_get_named(display_device);
1030 const int font_flags = ((data->flag & SEQ_TEXT_BOLD) ? BLF_BOLD : 0) |
1031 ((data->flag & SEQ_TEXT_ITALIC) ? BLF_ITALIC : 0);
1032
1033 /* Guard against parallel accesses to the fonts map. */
1034 std::lock_guard lock(g_font_map.mutex);
1035
1036 const int font = text_effect_font_init(context, strip, font_flags);
1037
1038 calc_text_runtime(strip, font, {out->x, out->y});
1039 TextVarsRuntime *runtime = data->runtime;
1040
1041 rcti outline_rect = draw_text_outline(context, data, runtime, display, out);
1042 BLF_buffer(font, nullptr, out->byte_buffer.data, out->x, out->y, display);
1043 text_draw(runtime, data->color);
1044 BLF_buffer(font, nullptr, nullptr, 0, 0, nullptr);
1045 BLF_disable(font, font_flags);
1046
1047 /* Draw shadow. */
1048 if (data->flag & SEQ_TEXT_SHADOW) {
1049 draw_text_shadow(context, data, runtime->line_height, outline_rect, out);
1050 }
1051
1052 /* Draw box under text. */
1053 if (data->flag & SEQ_TEXT_BOX) {
1054 if (out->byte_buffer.data) {
1055 const int margin = data->box_margin * out->x;
1056 const int minx = runtime->text_boundbox.xmin - margin;
1057 const int maxx = runtime->text_boundbox.xmax + margin;
1058 const int miny = runtime->text_boundbox.ymin - margin;
1059 const int maxy = runtime->text_boundbox.ymax + margin;
1060 float corner_radius = data->box_roundness * (maxy - miny) / 2.0f;
1061 fill_rect_alpha_under(out, data->box_color, minx, miny, maxx, maxy, corner_radius);
1062 }
1063 }
1064
1065 return out;
1066}
1067
1069{
1071 rval.init = init_text_effect;
1072 rval.free = free_text_effect;
1073 rval.load = load_text_effect;
1074 rval.copy = copy_text_effect;
1076 rval.execute = do_text_effect;
1077}
1078
1080
1081} // namespace blender::seq
@ LIB_ID_CREATE_NO_USER_REFCOUNT
void BKE_id_full_name_get(char name[MAX_ID_FULL_NAME], const ID *id, char separator_char)
Definition lib_id.cc:2412
void id_us_plus(ID *id)
Definition lib_id.cc:353
void id_us_min(ID *id)
Definition lib_id.cc:361
#define MAX_ID_FULL_NAME
int BLF_load_mem_unique(const char *name, const unsigned char *mem, int mem_size) ATTR_NONNULL(1
void BLF_size(int fontid, float size)
Definition blf.cc:440
int BLF_descender(int fontid) ATTR_WARN_UNUSED_RESULT
Definition blf.cc:875
void BLF_addref_id(int fontid)
Definition blf.cc:311
void BLF_draw_buffer(int fontid, const char *str, size_t str_len, ResultBLF *r_info=nullptr) ATTR_NONNULL(2)
Definition blf.cc:1035
bool BLF_is_loaded_id(int fontid) ATTR_WARN_UNUSED_RESULT
Definition blf.cc:170
void BLF_buffer(int fontid, float *fbuf, unsigned char *cbuf, int w, int h, const ColorManagedDisplay *display)
Definition blf.cc:950
int blf_mono_font_render
Definition blf.cc:49
@ BLF_ITALIC
Definition BLF_api.hh:446
@ BLF_BOLD
Definition BLF_api.hh:445
@ BLF_NO_FALLBACK
Definition BLF_api.hh:465
void BLF_disable(int fontid, int option)
Definition blf.cc:329
void BLF_buffer_col(int fontid, const float rgba[4]) ATTR_NONNULL(2)
Definition blf.cc:1010
int BLF_glyph_advance(int fontid, const char *str)
Definition blf.cc:829
blender::ocio::Display ColorManagedDisplay
Definition BLF_api.hh:35
int BLF_load_unique(const char *filepath) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL(1)
Definition blf.cc:188
void BLF_enable(int fontid, int option)
Definition blf.cc:320
bool BLF_unload_id(int fontid)
Definition blf.cc:280
bool BLF_is_builtin(int fontid)
Definition blf.cc:338
int BLF_height_max(int fontid) ATTR_WARN_UNUSED_RESULT
Definition blf.cc:853
void BLF_position(int fontid, float x, float y, float z)
Definition blf.cc:385
MINLINE int power_of_2_max_i(int n)
MINLINE int clamp_i(int value, int min, int max)
MINLINE void straight_to_premul_v4_v4(float premul[4], const float straight[4])
#define DEG2RADF(_deg)
MINLINE void copy_v4_fl(float r[4], float f)
bool BLI_path_abs(char path[FILE_MAX], const char *basepath) ATTR_NONNULL(1
#define FILE_MAX
void BLI_rcti_pad(struct rcti *rect, int pad_x, int pad_y)
Definition rct.cc:629
void BLI_rcti_translate(struct rcti *rect, int x, int y)
Definition rct.cc:566
#define STRNLEN(str)
Definition BLI_string.h:608
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:688
int BLI_str_utf8_size_safe(const char *p) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL(1)
size_t BLI_strlen_utf8(const char *strc) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
unsigned char uchar
@ SEQ_TEXT_ITALIC
@ SEQ_TEXT_SHADOW
@ SEQ_TEXT_BOLD
@ SEQ_TEXT_OUTLINE
@ SEQ_TEXT_BOX
@ SEQ_TEXT_ALIGN_X_RIGHT
@ SEQ_TEXT_ALIGN_X_CENTER
@ SEQ_TEXT_ALIGN_X_LEFT
#define STRIP_FONT_NOT_LOADED
@ SEQ_TEXT_ALIGN_Y_BOTTOM
@ SEQ_TEXT_ALIGN_Y_TOP
@ SEQ_TEXT_ALIGN_Y_CENTER
@ SEQ_RENDER_SIZE_SCENE
const ColorManagedDisplay * IMB_colormanagement_display_get_named(const char *name)
volatile int lock
BMesh const char void * data
long long int int64_t
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
static void mul(btAlignedObjectArray< T > &items, const Q &value)
const T * data() const
Definition BLI_array.hh:301
constexpr int64_t first() const
constexpr int64_t size() const
static constexpr IndexRange from_begin_end(const int64_t begin, const int64_t end)
void append(const T &value)
#define sinf(x)
#define cosf(x)
#define powf(x, y)
#define str(s)
#define pf(_x, _i)
Prefetch 64.
Definition gim_memory.h:48
uint col
#define input
#define abs
#define out
#define output
float distance(VecOp< float, D >, VecOp< float, D >) RET
#define ID_BLEND_PATH_FROM_GLOBAL(_id)
void * MEM_callocN(size_t len, const char *str)
Definition mallocn.cc:118
void * MEM_dupallocN(const void *vmemh)
Definition mallocn.cc:143
void MEM_freeN(void *vmemh)
Definition mallocn.cc:113
T clamp(const T &a, const T &min, const T &max)
T distance(const T &a, const T &b)
T min(const T &a, const T &b)
T interpolate(const T &a, const T &b, const FactorT &t)
T distance_squared(const VecBase< T, Size > &a, const VecBase< T, Size > &b)
T max(const T &a, const T &b)
T round(const T &a)
static void text_draw(const TextVarsRuntime *runtime, float color[4])
static float2 horizontal_alignment_offset_get(const TextVars *data, float line_width, int width_max)
static int wrap_width_get(const TextVars *data, const int2 image_size)
static int text_effect_line_size_get(const RenderData *context, const Strip *strip)
static rcti draw_text_outline(const RenderData *context, const TextVars *data, const TextVarsRuntime *runtime, const ColorManagedDisplay *display, ImBuf *out)
static void calc_boundbox(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size)
static void initialize_shadow_alpha(int width, int height, int2 offset, const rcti &shadow_rect, const uchar *input, Array< uchar > &r_shadow_mask)
double rendersize_to_scale_factor(int render_size)
Definition proxy.cc:87
void text_effect_get_handle(EffectHandle &rval)
Array< float > make_gaussian_blur_kernel(float rad, int size)
Definition effects.cc:80
void store_premul_pixel(const blender::float4 &pix, uchar *dst)
Definition effects.hh:57
ImBuf * prepare_effect_imbufs(const RenderData *context, ImBuf *ibuf1, ImBuf *ibuf2, bool uninitialized_pixels)
Definition effects.cc:28
static Vector< CharInfo > build_character_info(const TextVars *data, int font)
static float2 anchor_offset_get(const TextVars *data, int width_max, int text_height)
blender::float4 load_premul_pixel(const uchar *ptr)
Definition effects.hh:45
static SeqFontMap g_font_map
static void init_text_effect(Strip *strip)
static StripEarlyOut early_out_text(const Strip *strip, float)
void effect_text_font_load(TextVars *data, const bool do_id_user)
bool effects_can_render_text(const Strip *strip)
static void jump_flooding_pass(Span< JFACoord > input, MutableSpan< JFACoord > output, int2 size, IndexRange x_range, IndexRange y_range, int step_size)
static void apply_word_wrapping(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size, Vector< CharInfo > &characters)
static void composite_shadow(int width, const rcti &shadow_rect, const float4 &shadow_color, const Array< uchar > &shadow_mask, uchar *output)
static void load_text_effect(Strip *strip)
static void apply_text_alignment(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size)
static int num_inputs_text()
static void text_gaussian_blur_y(const Span< float > gaussian, int half_size, int start_line, int width, int height, const uchar *rect, uchar *dst, const rcti &shadow_rect)
static int text_box_width_get(const Vector< LineInfo > &lines)
void effect_text_font_unload(TextVars *data, const bool do_id_user)
static int text_effect_font_init(const RenderData *context, const Strip *strip, int font_flags)
static void draw_text_shadow(const RenderData *context, const TextVars *data, int line_height, const rcti &rect, ImBuf *out)
static void text_gaussian_blur_x(const Span< float > gaussian, int half_size, int start_line, int width, int height, const uchar *rect, uchar *dst, const rcti &shadow_rect)
static void free_text_effect(Strip *strip, const bool do_id_user)
static int strip_load_font_mem(const std::string &name, const uchar *data, int data_size)
static void copy_text_effect(Strip *dst, const Strip *src, const int flag)
static void strip_unload_font(int fontid)
static ImBuf * do_text_effect(const RenderData *context, Strip *strip, float, float, ImBuf *, ImBuf *)
static void calc_text_runtime(const Strip *strip, int font, const int2 image_size)
static int strip_load_font_file(const std::string &path)
static void fill_rect_alpha_under(const ImBuf *ibuf, const float col[4], int x1, int y1, int x2, int y2, float corner_radius)
constexpr uint16_t JFA_INVALID
static void clamp_rect(int width, int height, rcti &r_rect)
void parallel_for(const IndexRange range, const int64_t grain_size, const Function &function, const TaskSizeHints &size_hints=detail::TaskSizeHints_Static(1))
Definition BLI_task.hh:93
blender::VecBase< uint8_t, 4 > uchar4
VecBase< float, 4 > float4
VecBase< int32_t, 2 > int2
VecBase< float, 2 > float2
ImBufByteBuffer byte_buffer
void * effectdata
char filepath[1024]
struct PackedFile * packedfile
void(* copy)(Strip *dst, const Strip *src, int flag)
ImBuf *(* execute)(const RenderData *context, Strip *strip, float timeline_frame, float fac, ImBuf *ibuf1, ImBuf *ibuf2)
void(* free)(Strip *strip, bool do_id_user)
StripEarlyOut(* early_out)(const Strip *strip, float fac)
void(* load)(Strip *seqconst)
void(* init)(Strip *strip)
Vector< CharInfo > characters
Map< std::string, int > path_to_file_font_id
std::recursive_mutex mutex
Map< std::string, int > name_to_mem_font_id
int ymin
int ymax
int xmin
int xmax
uint8_t flag
Definition wm_window.cc:139