Files
test2/source/blender/sequencer/intern/effects/vse_effect_text.cc
Aras Pranckevicius 11bf3dd71e VSE: Text Strip supports text longer than 512 bytes
Text strip had a fixed size buffer of 512 bytes to hold the displayed
text (this can be much fewer actual characters with non-English
languages). Switch to dynamically allocated buffer instead, which can
hold longer text.

In order to support forward/backward compatibility, TextVars continues
to hold the 512 byte buffer in memory. When writing out the .blend file,
dynamic text buffer is copied into the fixed one. If it is longer, the
text is truncated, so opening the .blend file in an older version
will contain the first 512 bytes of the longer text. When reading
existing files without the dynamic text buffer, it is created from the
static buffer. Conceptually this approach is similar to constraints
name length increase PR !137310.

The text strip editing code was switched to operate on the dynamic
buffer, resizing it as needed. seq::CharInfo internal struct was
switched to be more independent of the actual buffer address; now
each char entry just stores an index into the buffer instead of direct
pointer (side effect: makes the struct smaller as well).

Pull Request: https://projects.blender.org/blender/blender/pulls/140733
2025-06-20 21:27:12 +02:00

1092 lines
36 KiB
C++

/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <cmath>
#include <mutex>
#include "BKE_lib_id.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BLI_path_utils.hh"
#include "BLI_rect.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "BLF_api.hh"
#include "DNA_packedFile_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"
#include "DNA_vfont_types.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf_types.hh"
#include "SEQ_effects.hh"
#include "SEQ_proxy.hh"
#include "SEQ_render.hh"
#include "SEQ_utils.hh"
#include "effects.hh"
namespace blender::seq {
/* -------------------------------------------------------------------- */
/* Sequencer font access.
*
* Text strips can access and use fonts from a background thread
* (when depsgraph evaluation copies the scene, or when prefetch renders
* frames with text strips in a background thread).
*
* To not interfere with what might be happening on the main thread, all
* fonts used by the sequencer are made unique via #BLF_load_unique
* #BLF_load_mem_unique, and there's a mutex to guard against
* sequencer itself possibly using the fonts from several threads.
*/
struct SeqFontMap {
/* File path -> font ID mapping for file-based fonts. */
Map<std::string, int> path_to_file_font_id;
/* Datablock name -> font ID mapping for memory (datablock) fonts. */
Map<std::string, int> name_to_mem_font_id;
/* Font access mutex. Recursive since it is locked from
* text strip rendering, which can call into loading from within. */
std::recursive_mutex mutex;
};
static SeqFontMap g_font_map;
void fontmap_clear()
{
for (const auto &item : g_font_map.path_to_file_font_id.items()) {
BLF_unload_id(item.value);
}
g_font_map.path_to_file_font_id.clear();
for (const auto &item : g_font_map.name_to_mem_font_id.items()) {
BLF_unload_id(item.value);
}
g_font_map.name_to_mem_font_id.clear();
}
static int strip_load_font_file(const std::string &path)
{
std::lock_guard lock(g_font_map.mutex);
int fontid = g_font_map.path_to_file_font_id.add_or_modify(
path,
[&](int *fontid) {
/* New path: load font. */
*fontid = BLF_load_unique(path.c_str());
return *fontid;
},
[&](int *fontid) {
/* Path already in cache: add reference to already loaded font,
* or load a new one in case that
* font id was unloaded behind our backs. */
if (*fontid >= 0) {
if (BLF_is_loaded_id(*fontid)) {
BLF_addref_id(*fontid);
}
else {
*fontid = BLF_load_unique(path.c_str());
}
}
return *fontid;
});
return fontid;
}
static int strip_load_font_mem(const std::string &name, const uchar *data, int data_size)
{
std::lock_guard lock(g_font_map.mutex);
int fontid = g_font_map.name_to_mem_font_id.add_or_modify(
name,
[&](int *fontid) {
/* New name: load font. */
*fontid = BLF_load_mem_unique(name.c_str(), data, data_size);
return *fontid;
},
[&](int *fontid) {
/* Name already in cache: add reference to already loaded font,
* or (if we're on the main thread) load a new one in case that
* font id was unloaded behind our backs. */
if (*fontid >= 0) {
if (BLF_is_loaded_id(*fontid)) {
BLF_addref_id(*fontid);
}
else {
*fontid = BLF_load_mem_unique(name.c_str(), data, data_size);
}
}
return *fontid;
});
return fontid;
}
static void strip_unload_font(int fontid)
{
std::lock_guard lock(g_font_map.mutex);
bool unloaded = BLF_unload_id(fontid);
/* If that was the last usage of the font and it got unloaded: remove
* it from our maps. */
if (unloaded) {
g_font_map.path_to_file_font_id.remove_if([&](auto item) { return item.value == fontid; });
g_font_map.name_to_mem_font_id.remove_if([&](auto item) { return item.value == fontid; });
}
}
/* -------------------------------------------------------------------- */
/** \name Text Effect
* \{ */
bool effects_can_render_text(const Strip *strip)
{
/* `data->text[0] == 0` is ignored on purpose in order to make it possible to edit. */
TextVars *data = static_cast<TextVars *>(strip->effectdata);
if (data->text_size < 1.0f ||
((data->color[3] == 0.0f) &&
(data->shadow_color[3] == 0.0f || (data->flag & SEQ_TEXT_SHADOW) == 0) &&
(data->outline_color[3] == 0.0f || data->outline_width <= 0.0f ||
(data->flag & SEQ_TEXT_OUTLINE) == 0)))
{
return false;
}
return true;
}
static void init_text_effect(Strip *strip)
{
if (strip->effectdata) {
MEM_freeN(strip->effectdata);
}
TextVars *data = MEM_callocN<TextVars>("textvars");
strip->effectdata = data;
data->text_font = nullptr;
data->text_blf_id = -1;
data->text_size = 60.0f;
copy_v4_fl(data->color, 1.0f);
data->shadow_color[3] = 0.7f;
data->shadow_angle = DEG2RADF(65.0f);
data->shadow_offset = 0.04f;
data->shadow_blur = 0.0f;
data->box_color[0] = 0.2f;
data->box_color[1] = 0.2f;
data->box_color[2] = 0.2f;
data->box_color[3] = 0.7f;
data->box_margin = 0.01f;
data->box_roundness = 0.0f;
data->outline_color[3] = 0.7f;
data->outline_width = 0.05f;
data->text_ptr = BLI_strdup("Text");
data->text_len_bytes = strlen(data->text_ptr);
data->loc[0] = 0.5f;
data->loc[1] = 0.5f;
data->anchor_x = SEQ_TEXT_ALIGN_X_CENTER;
data->anchor_y = SEQ_TEXT_ALIGN_Y_CENTER;
data->align = SEQ_TEXT_ALIGN_X_CENTER;
data->wrap_width = 1.0f;
}
void effect_text_font_unload(TextVars *data, const bool do_id_user)
{
if (data == nullptr) {
return;
}
/* Unlink the VFont */
if (do_id_user && data->text_font != nullptr) {
id_us_min(&data->text_font->id);
data->text_font = nullptr;
}
/* Unload the font. */
if (data->text_blf_id >= 0) {
strip_unload_font(data->text_blf_id);
data->text_blf_id = -1;
}
}
void effect_text_font_load(TextVars *data, const bool do_id_user)
{
VFont *vfont = data->text_font;
if (vfont == nullptr) {
return;
}
if (do_id_user) {
id_us_plus(&vfont->id);
}
if (vfont->packedfile != nullptr) {
PackedFile *pf = vfont->packedfile;
/* Create a name that's unique between library data-blocks to avoid loading
* a font per strip which will load fonts many times.
*
* WARNING: this isn't fool proof!
* The #VFont may be renamed which will cause this to load multiple times,
* in practice this isn't so likely though. */
char name[MAX_ID_FULL_NAME];
BKE_id_full_name_get(name, &vfont->id, 0);
data->text_blf_id = strip_load_font_mem(name, static_cast<const uchar *>(pf->data), pf->size);
}
else {
char filepath[FILE_MAX];
STRNCPY(filepath, vfont->filepath);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&vfont->id));
data->text_blf_id = strip_load_font_file(filepath);
}
}
static void free_text_effect(Strip *strip, const bool do_id_user)
{
TextVars *data = static_cast<TextVars *>(strip->effectdata);
effect_text_font_unload(data, do_id_user);
if (data) {
MEM_SAFE_FREE(data->text_ptr);
MEM_delete(data->runtime);
MEM_freeN(data);
strip->effectdata = nullptr;
}
}
static void load_text_effect(Strip *strip)
{
TextVars *data = static_cast<TextVars *>(strip->effectdata);
effect_text_font_load(data, false);
}
static void copy_text_effect(Strip *dst, const Strip *src, const int flag)
{
dst->effectdata = MEM_dupallocN(src->effectdata);
TextVars *data = static_cast<TextVars *>(dst->effectdata);
data->text_ptr = BLI_strdup_null(data->text_ptr);
data->runtime = nullptr;
data->text_blf_id = -1;
effect_text_font_load(data, (flag & LIB_ID_CREATE_NO_USER_REFCOUNT) == 0);
}
static int num_inputs_text()
{
return 0;
}
static StripEarlyOut early_out_text(const Strip *strip, float /*fac*/)
{
if (!effects_can_render_text(strip)) {
return StripEarlyOut::UseInput1;
}
return StripEarlyOut::NoInput;
}
/* Simplified version of gaussian blur specifically for text shadow blurring:
* - Data is only the alpha channel,
* - Skips blur outside of shadow rectangle. */
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)
{
dst += int64_t(start_line) * width;
for (int y = start_line; y < start_line + height; y++) {
for (int x = 0; x < width; x++) {
float accum(0.0f);
if (x >= shadow_rect.xmin && x <= shadow_rect.xmax) {
float accum_weight = 0.0f;
int xmin = math::max(x - half_size, shadow_rect.xmin);
int xmax = math::min(x + half_size, shadow_rect.xmax);
for (int nx = xmin, index = (xmin - x) + half_size; nx <= xmax; nx++, index++) {
float weight = gaussian[index];
int offset = y * width + nx;
accum += rect[offset] * weight;
accum_weight += weight;
}
accum *= (1.0f / accum_weight);
}
*dst = accum;
dst++;
}
}
}
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)
{
dst += int64_t(start_line) * width;
for (int y = start_line; y < start_line + height; y++) {
for (int x = 0; x < width; x++) {
float accum(0.0f);
if (x >= shadow_rect.xmin && x <= shadow_rect.xmax) {
float accum_weight = 0.0f;
int ymin = math::max(y - half_size, shadow_rect.ymin);
int ymax = math::min(y + half_size, shadow_rect.ymax);
for (int ny = ymin, index = (ymin - y) + half_size; ny <= ymax; ny++, index++) {
float weight = gaussian[index];
int offset = ny * width + x;
accum += rect[offset] * weight;
accum_weight += weight;
}
accum *= (1.0f / accum_weight);
}
*dst = accum;
dst++;
}
}
}
static void clamp_rect(int width, int height, rcti &r_rect)
{
r_rect.xmin = math::clamp(r_rect.xmin, 0, width - 1);
r_rect.xmax = math::clamp(r_rect.xmax, 0, width - 1);
r_rect.ymin = math::clamp(r_rect.ymin, 0, height - 1);
r_rect.ymax = math::clamp(r_rect.ymax, 0, height - 1);
}
static void initialize_shadow_alpha(int width,
int height,
int2 offset,
const rcti &shadow_rect,
const uchar *input,
Array<uchar> &r_shadow_mask)
{
const IndexRange shadow_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
threading::parallel_for(shadow_y_range, 8, [&](const IndexRange y_range) {
for (const int64_t y : y_range) {
const int64_t src_y = math::clamp<int64_t>(y + offset.y, 0, height - 1);
for (int x = shadow_rect.xmin; x <= shadow_rect.xmax; x++) {
int src_x = math::clamp(x - offset.x, 0, width - 1);
size_t src_offset = width * src_y + src_x;
size_t dst_offset = width * y + x;
r_shadow_mask[dst_offset] = input[src_offset * 4 + 3];
}
}
});
}
static void composite_shadow(int width,
const rcti &shadow_rect,
const float4 &shadow_color,
const Array<uchar> &shadow_mask,
uchar *output)
{
const IndexRange shadow_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
threading::parallel_for(shadow_y_range, 8, [&](const IndexRange y_range) {
for (const int64_t y : y_range) {
size_t offset = y * width + shadow_rect.xmin;
uchar *dst = output + offset * 4;
for (int x = shadow_rect.xmin; x <= shadow_rect.xmax; x++, offset++, dst += 4) {
uchar a = shadow_mask[offset];
if (a == 0) {
/* Fully transparent, leave output pixel as is. */
continue;
}
float4 col1 = load_premul_pixel(dst);
float4 col2 = shadow_color * (a * (1.0f / 255.0f));
/* Blend under the output. */
float fac = 1.0f - col1.w;
float4 col = col1 + fac * col2;
store_premul_pixel(col, dst);
}
}
});
}
static void draw_text_shadow(
const RenderData *context, const TextVars *data, int line_height, const rcti &rect, ImBuf *out)
{
const int width = context->rectx;
const int height = context->recty;
/* Blur value of 1.0 applies blur kernel that is half of text line height. */
const float blur_amount = line_height * 0.5f * data->shadow_blur;
bool do_blur = blur_amount >= 1.0f;
Array<uchar> shadow_mask(size_t(width) * height, 0);
const int2 offset = int2(cosf(data->shadow_angle) * line_height * data->shadow_offset,
sinf(data->shadow_angle) * line_height * data->shadow_offset);
rcti shadow_rect = rect;
BLI_rcti_translate(&shadow_rect, offset.x, -offset.y);
BLI_rcti_pad(&shadow_rect, 1, 1);
clamp_rect(width, height, shadow_rect);
/* Initialize shadow by copying existing text/outline alpha. */
initialize_shadow_alpha(width, height, offset, shadow_rect, out->byte_buffer.data, shadow_mask);
if (do_blur) {
/* Create blur kernel weights. */
const int half_size = int(blur_amount + 0.5f);
Array<float> gaussian = make_gaussian_blur_kernel(blur_amount, half_size);
BLI_rcti_pad(&shadow_rect, half_size + 1, half_size + 1);
clamp_rect(width, height, shadow_rect);
/* Horizontal blur: blur shadow_mask into blur_buffer. */
Array<uchar> blur_buffer(size_t(width) * height, NoInitialization());
IndexRange blur_y_range(shadow_rect.ymin, shadow_rect.ymax - shadow_rect.ymin + 1);
threading::parallel_for(blur_y_range, 8, [&](const IndexRange y_range) {
const int y_first = y_range.first();
const int y_size = y_range.size();
text_gaussian_blur_x(gaussian,
half_size,
y_first,
width,
y_size,
shadow_mask.data(),
blur_buffer.data(),
shadow_rect);
});
/* Vertical blur: blur blur_buffer into shadow_mask. */
threading::parallel_for(blur_y_range, 8, [&](const IndexRange y_range) {
const int y_first = y_range.first();
const int y_size = y_range.size();
text_gaussian_blur_y(gaussian,
half_size,
y_first,
width,
y_size,
blur_buffer.data(),
shadow_mask.data(),
shadow_rect);
});
}
/* Composite shadow under regular output. */
float4 color = data->shadow_color;
color.x *= color.w;
color.y *= color.w;
color.z *= color.w;
composite_shadow(width, shadow_rect, color, shadow_mask, out->byte_buffer.data);
}
/* Text outline calculation is done by Jump Flooding Algorithm (JFA).
* This is similar to inpaint/jump_flooding in Compositor, also to
* "The Quest for Very Wide Outlines", Ben Golus 2020
* https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9 */
constexpr uint16_t JFA_INVALID = 0xFFFF;
struct JFACoord {
uint16_t x;
uint16_t y;
};
static void jump_flooding_pass(Span<JFACoord> input,
MutableSpan<JFACoord> output,
int2 size,
IndexRange x_range,
IndexRange y_range,
int step_size)
{
threading::parallel_for(y_range, 8, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
size_t index = y * size.x;
for (const int64_t x : x_range) {
float2 coord = float2(x, y);
/* For each pixel, sample 9 pixels at +/- step size pattern,
* and output coordinate of closest to the boundary. */
JFACoord closest_texel{JFA_INVALID, JFA_INVALID};
float minimum_squared_distance = std::numeric_limits<float>::max();
for (int dy = -step_size; dy <= step_size; dy += step_size) {
int yy = y + dy;
if (yy < 0 || yy >= size.y) {
continue;
}
for (int dx = -step_size; dx <= step_size; dx += step_size) {
int xx = x + dx;
if (xx < 0 || xx >= size.x) {
continue;
}
JFACoord val = input[size_t(yy) * size.x + xx];
if (val.x == JFA_INVALID) {
continue;
}
float squared_distance = math::distance_squared(float2(val.x, val.y), coord);
if (squared_distance < minimum_squared_distance) {
minimum_squared_distance = squared_distance;
closest_texel = val;
}
}
}
output[index + x] = closest_texel;
}
}
});
}
static void text_draw(const char *text_ptr, const TextVarsRuntime *runtime, float color[4])
{
const bool use_fallback = BLF_is_builtin(runtime->font);
if (!use_fallback) {
BLF_enable(runtime->font, BLF_NO_FALLBACK);
}
for (const LineInfo &line : runtime->lines) {
for (const CharInfo &character : line.characters) {
BLF_position(runtime->font, character.position.x, character.position.y, 0.0f);
BLF_buffer_col(runtime->font, color);
BLF_draw_buffer(runtime->font, text_ptr + character.offset, character.byte_length);
}
}
if (!use_fallback) {
BLF_disable(runtime->font, BLF_NO_FALLBACK);
}
}
static rcti draw_text_outline(const RenderData *context,
const TextVars *data,
const TextVarsRuntime *runtime,
const ColorManagedDisplay *display,
ImBuf *out)
{
/* Outline width of 1.0 maps to half of text line height. */
const int outline_width = int(runtime->line_height * 0.5f * data->outline_width);
if (outline_width < 1 || data->outline_color[3] <= 0.0f ||
((data->flag & SEQ_TEXT_OUTLINE) == 0))
{
return runtime->text_boundbox;
}
const int2 size = int2(context->rectx, context->recty);
/* Draw white text into temporary buffer. */
const size_t pixel_count = size_t(size.x) * size.y;
Array<uchar4> tmp_buf(pixel_count, uchar4(0));
BLF_buffer(runtime->font, nullptr, (uchar *)tmp_buf.data(), size.x, size.y, display);
text_draw(data->text_ptr, runtime, float4(1.0f));
rcti outline_rect = runtime->text_boundbox;
BLI_rcti_pad(&outline_rect, outline_width + 1, outline_width + 1);
outline_rect.xmin = clamp_i(outline_rect.xmin, 0, size.x - 1);
outline_rect.xmax = clamp_i(outline_rect.xmax, 0, size.x - 1);
outline_rect.ymin = clamp_i(outline_rect.ymin, 0, size.y - 1);
outline_rect.ymax = clamp_i(outline_rect.ymax, 0, size.y - 1);
const IndexRange rect_x_range(outline_rect.xmin, outline_rect.xmax - outline_rect.xmin + 1);
const IndexRange rect_y_range(outline_rect.ymin, outline_rect.ymax - outline_rect.ymin + 1);
/* Initialize JFA: invalid values for empty regions, pixel coordinates
* for opaque regions. */
Array<JFACoord> boundary(pixel_count, NoInitialization());
threading::parallel_for(IndexRange(size.y), 16, [&](const IndexRange y_range) {
for (const int y : y_range) {
size_t index = size_t(y) * size.x;
for (int x = 0; x < size.x; x++, index++) {
bool is_opaque = tmp_buf[index].w >= 128;
JFACoord coord;
coord.x = is_opaque ? x : JFA_INVALID;
coord.y = is_opaque ? y : JFA_INVALID;
boundary[index] = coord;
}
}
});
/* Do jump flooding calculations. */
JFACoord invalid_coord{JFA_INVALID, JFA_INVALID};
Array<JFACoord> initial_flooded_result(pixel_count, invalid_coord);
jump_flooding_pass(boundary, initial_flooded_result, size, rect_x_range, rect_y_range, 1);
Array<JFACoord> *result_to_flood = &initial_flooded_result;
Array<JFACoord> intermediate_result(pixel_count, invalid_coord);
Array<JFACoord> *result_after_flooding = &intermediate_result;
int step_size = power_of_2_max_i(outline_width) / 2;
while (step_size != 0) {
jump_flooding_pass(
*result_to_flood, *result_after_flooding, size, rect_x_range, rect_y_range, step_size);
std::swap(result_to_flood, result_after_flooding);
step_size /= 2;
}
/* Premultiplied outline color. */
float4 color = data->outline_color;
color.x *= color.w;
color.y *= color.w;
color.z *= color.w;
const float text_color_alpha = data->color[3];
/* We have distances to the closest opaque parts of the image now. Composite the
* outline into the output image. */
threading::parallel_for(rect_y_range, 8, [&](const IndexRange y_range) {
for (const int y : y_range) {
size_t index = size_t(y) * size.x + rect_x_range.start();
uchar *dst = out->byte_buffer.data + index * 4;
for (int x = rect_x_range.start(); x < rect_x_range.one_after_last(); x++, index++, dst += 4)
{
JFACoord closest_texel = (*result_to_flood)[index];
if (closest_texel.x == JFA_INVALID) {
/* Outside of outline, leave output pixel as is. */
continue;
}
/* Fade out / anti-alias the outline over one pixel towards outline distance. */
float distance = math::distance(float2(x, y), float2(closest_texel.x, closest_texel.y));
float alpha = math::clamp(outline_width - distance + 1.0f, 0.0f, 1.0f);
/* Do not put outline inside the text shape:
* - When overall text color is fully opaque, we want to make
* outline fully transparent only where text is fully opaque.
* This ensures that combined anti-aliased pixels at text boundary
* are properly fully opaque.
* - However when text color is fully transparent, we want to
* Use opposite alpha of text, to anti-alias the inner edge of
* the outline.
* In between those two, interpolate the alpha modulation factor. */
float text_alpha = tmp_buf[index].w * (1.0f / 255.0f);
float mul_opaque_text = text_alpha >= 1.0f ? 0.0f : 1.0f;
float mul_transparent_text = 1.0f - text_alpha;
float mul = math::interpolate(mul_transparent_text, mul_opaque_text, text_color_alpha);
alpha *= mul;
float4 col1 = color;
col1 *= alpha;
/* Blend over the output. */
float mfac = 1.0f - col1.w;
float4 col2 = load_premul_pixel(dst);
float4 col = col1 + mfac * col2;
store_premul_pixel(col, dst);
}
}
});
BLF_buffer(runtime->font, nullptr, out->byte_buffer.data, size.x, size.y, display);
return outline_rect;
}
/* Similar to #IMB_rectfill_area but blends the given color under the
* existing image. Also can do rounded corners. Only works on byte buffers. */
static void fill_rect_alpha_under(
const ImBuf *ibuf, const float col[4], int x1, int y1, int x2, int y2, float corner_radius)
{
const int width = ibuf->x;
const int height = ibuf->y;
x1 = math::clamp(x1, 0, width);
x2 = math::clamp(x2, 0, width);
y1 = math::clamp(y1, 0, height);
y2 = math::clamp(y2, 0, height);
if (x1 > x2) {
std::swap(x1, x2);
}
if (y1 > y2) {
std::swap(y1, y2);
}
if (x1 == x2 || y1 == y2) {
return;
}
corner_radius = math::clamp(corner_radius, 0.0f, math::min(x2 - x1, y2 - y1) / 2.0f);
float4 premul_col_base;
straight_to_premul_v4_v4(premul_col_base, col);
threading::parallel_for(IndexRange::from_begin_end(y1, y2), 16, [&](const IndexRange y_range) {
for (const int y : y_range) {
uchar *dst = ibuf->byte_buffer.data + (size_t(width) * y + x1) * 4;
float origin_x = 0.0f, origin_y = 0.0f;
for (int x = x1; x < x2; x++) {
float4 pix = load_premul_pixel(dst);
float fac = 1.0f - pix.w;
float4 premul_col = premul_col_base;
bool is_corner = false;
if (x < x1 + corner_radius && y < y1 + corner_radius) {
is_corner = true;
origin_x = x1 + corner_radius - 1;
origin_y = y1 + corner_radius - 1;
}
else if (x >= x2 - corner_radius && y < y1 + corner_radius) {
is_corner = true;
origin_x = x2 - corner_radius;
origin_y = y1 + corner_radius - 1;
}
else if (x < x1 + corner_radius && y >= y2 - corner_radius) {
is_corner = true;
origin_x = x1 + corner_radius - 1;
origin_y = y2 - corner_radius;
}
else if (x >= x2 - corner_radius && y >= y2 - corner_radius) {
is_corner = true;
origin_x = x2 - corner_radius;
origin_y = y2 - corner_radius;
}
if (is_corner) {
/* If we are inside rounded corner, evaluate a superellipse and
* modulate color with that. Superellipse instead of just a circle
* since the curvature between flat and rounded area looks a bit
* nicer. */
constexpr float curve_pow = 2.1f;
float r = powf(powf(abs(x - origin_x), curve_pow) + powf(abs(y - origin_y), curve_pow),
1.0f / curve_pow);
float alpha = math::clamp(corner_radius - r, 0.0f, 1.0f);
premul_col *= alpha;
}
float4 dst_fl = fac * premul_col + pix;
store_premul_pixel(dst_fl, dst);
dst += 4;
}
}
});
}
static int text_effect_line_size_get(const RenderData *context, const Strip *strip)
{
TextVars *data = static_cast<TextVars *>(strip->effectdata);
/* Used to calculate boundbox. Proxy size compensation is not needed there. */
if (context == nullptr) {
return data->text_size;
}
/* Compensate text size for preview render size. */
double proxy_size_comp = context->scene->r.size / 100.0;
if (context->preview_render_size != SEQ_RENDER_SIZE_SCENE) {
proxy_size_comp = rendersize_to_scale_factor(context->preview_render_size);
}
return proxy_size_comp * data->text_size;
}
int text_effect_font_init(const RenderData *context, const Strip *strip, int font_flags)
{
TextVars *data = static_cast<TextVars *>(strip->effectdata);
int font = blf_mono_font_render;
/* In case font got unloaded behind our backs: mark it as needing a load. */
if (data->text_blf_id >= 0 && !BLF_is_loaded_id(data->text_blf_id)) {
data->text_blf_id = STRIP_FONT_NOT_LOADED;
}
if (data->text_blf_id == STRIP_FONT_NOT_LOADED) {
data->text_blf_id = -1;
effect_text_font_load(data, false);
}
if (data->text_blf_id >= 0) {
font = data->text_blf_id;
}
BLF_size(font, text_effect_line_size_get(context, strip));
BLF_enable(font, font_flags);
return font;
}
static Vector<CharInfo> build_character_info(const TextVars *data, int font)
{
Vector<CharInfo> characters;
const int len_max = data->text_len_bytes;
int byte_offset = 0;
int char_index = 0;
const bool use_fallback = BLF_is_builtin(font);
if (!use_fallback) {
BLF_enable(font, BLF_NO_FALLBACK);
}
while (byte_offset <= len_max) {
const char *str = data->text_ptr + byte_offset;
const int char_length = BLI_str_utf8_size_safe(str);
CharInfo char_info;
char_info.index = char_index;
char_info.offset = byte_offset;
char_info.byte_length = char_length;
char_info.advance_x = BLF_glyph_advance(font, str);
characters.append(char_info);
byte_offset += char_length;
char_index++;
}
if (!use_fallback) {
BLF_disable(font, BLF_NO_FALLBACK);
}
return characters;
}
static int wrap_width_get(const TextVars *data, const int2 image_size)
{
if (data->wrap_width == 0.0f) {
return std::numeric_limits<int>::max();
}
return data->wrap_width * image_size.x;
}
/* Lines must contain CharInfo for newlines and \0, as UI must know where they begin. */
static void apply_word_wrapping(const TextVars *data,
TextVarsRuntime *runtime,
const int2 image_size,
Vector<CharInfo> &characters)
{
const int wrap_width = wrap_width_get(data, image_size);
float2 char_position{0.0f, 0.0f};
CharInfo *last_space = nullptr;
/* First pass: Find characters where line has to be broken. */
for (CharInfo &character : characters) {
char ch = data->text_ptr[character.offset];
if (ch == ' ') {
character.position = char_position;
last_space = &character;
}
if (ch == '\n') {
char_position.x = 0;
last_space = nullptr;
}
if (ch != '\0' && char_position.x > wrap_width && last_space != nullptr) {
last_space->do_wrap = true;
char_position -= last_space->position + last_space->advance_x;
}
char_position.x += character.advance_x;
}
/* Second pass: Fill lines with characters. */
char_position = {0.0f, 0.0f};
runtime->lines.append(LineInfo());
for (CharInfo &character : characters) {
character.position = char_position;
runtime->lines.last().characters.append(character);
runtime->lines.last().width = char_position.x;
char_position.x += character.advance_x;
if (character.do_wrap || data->text_ptr[character.offset] == '\n') {
runtime->lines.append(LineInfo());
char_position.x = 0;
char_position.y -= runtime->line_height;
}
}
}
static int text_box_width_get(const Vector<LineInfo> &lines)
{
int width_max = 0;
for (const LineInfo &line : lines) {
width_max = std::max(width_max, line.width);
}
return width_max;
}
static float2 horizontal_alignment_offset_get(const TextVars *data,
float line_width,
int width_max)
{
const float line_offset = (width_max - line_width);
if (data->align == SEQ_TEXT_ALIGN_X_RIGHT) {
return {line_offset, 0.0f};
}
if (data->align == SEQ_TEXT_ALIGN_X_CENTER) {
return {line_offset / 2.0f, 0.0f};
}
return {0.0f, 0.0f};
}
static float2 anchor_offset_get(const TextVars *data, int width_max, int text_height)
{
float2 anchor_offset;
switch (data->anchor_x) {
case SEQ_TEXT_ALIGN_X_LEFT:
anchor_offset.x = 0;
break;
case SEQ_TEXT_ALIGN_X_CENTER:
anchor_offset.x = -width_max / 2.0f;
break;
case SEQ_TEXT_ALIGN_X_RIGHT:
anchor_offset.x = -width_max;
break;
}
switch (data->anchor_y) {
case SEQ_TEXT_ALIGN_Y_TOP:
anchor_offset.y = 0;
break;
case SEQ_TEXT_ALIGN_Y_CENTER:
anchor_offset.y = text_height / 2.0f;
break;
case SEQ_TEXT_ALIGN_Y_BOTTOM:
anchor_offset.y = text_height;
break;
}
return anchor_offset;
}
static void calc_boundbox(const TextVars *data, TextVarsRuntime *runtime, const int2 image_size)
{
const int text_height = runtime->lines.size() * runtime->line_height;
int width_max = text_box_width_get(runtime->lines);
/* Add width to empty text, so there is something to draw or select. */
if (width_max == 0) {
width_max = text_height * 2;
}
const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
const float2 anchor = anchor_offset_get(data, width_max, text_height);
runtime->text_boundbox.xmin = anchor.x + image_center.x;
runtime->text_boundbox.xmax = anchor.x + image_center.x + width_max;
runtime->text_boundbox.ymin = anchor.y + image_center.y - text_height;
runtime->text_boundbox.ymax = runtime->text_boundbox.ymin + text_height;
}
static void apply_text_alignment(const TextVars *data,
TextVarsRuntime *runtime,
const int2 image_size)
{
const int width_max = text_box_width_get(runtime->lines);
const int text_height = runtime->lines.size() * runtime->line_height;
const float2 image_center{data->loc[0] * image_size.x, data->loc[1] * image_size.y};
const float2 line_height_offset{0.0f,
float(-runtime->line_height - BLF_descender(runtime->font))};
const float2 anchor = anchor_offset_get(data, width_max, text_height);
for (LineInfo &line : runtime->lines) {
const float2 alignment_x = horizontal_alignment_offset_get(data, line.width, width_max);
const float2 alignment = math::round(image_center + line_height_offset + alignment_x + anchor);
for (CharInfo &character : line.characters) {
character.position += alignment;
}
}
}
TextVarsRuntime *text_effect_calc_runtime(const Strip *strip, int font, const int2 image_size)
{
TextVars *data = static_cast<TextVars *>(strip->effectdata);
TextVarsRuntime *runtime = MEM_new<TextVarsRuntime>(__func__);
runtime->font = font;
runtime->line_height = BLF_height_max(font);
runtime->font_descender = BLF_descender(font);
runtime->character_count = BLI_strlen_utf8(data->text_ptr);
Vector<CharInfo> characters_temp = build_character_info(data, font);
apply_word_wrapping(data, runtime, image_size, characters_temp);
apply_text_alignment(data, runtime, image_size);
calc_boundbox(data, runtime, image_size);
return runtime;
}
static ImBuf *do_text_effect(const RenderData *context,
Strip *strip,
float /*timeline_frame*/,
float /*fac*/,
ImBuf * /*ibuf1*/,
ImBuf * /*ibuf2*/)
{
/* NOTE: text rasterization only fills in part of output image,
* need to clear it. */
ImBuf *out = prepare_effect_imbufs(context, nullptr, nullptr, false);
TextVars *data = static_cast<TextVars *>(strip->effectdata);
const char *display_device = context->scene->display_settings.display_device;
const ColorManagedDisplay *display = IMB_colormanagement_display_get_named(display_device);
const int font_flags = ((data->flag & SEQ_TEXT_BOLD) ? BLF_BOLD : 0) |
((data->flag & SEQ_TEXT_ITALIC) ? BLF_ITALIC : 0);
/* Guard against parallel accesses to the fonts map. */
std::lock_guard lock(g_font_map.mutex);
const int font = text_effect_font_init(context, strip, font_flags);
if (data->runtime != nullptr) {
MEM_delete(data->runtime);
}
TextVarsRuntime *runtime = text_effect_calc_runtime(strip, font, {out->x, out->y});
data->runtime = runtime;
rcti outline_rect = draw_text_outline(context, data, runtime, display, out);
BLF_buffer(font, nullptr, out->byte_buffer.data, out->x, out->y, display);
text_draw(data->text_ptr, runtime, data->color);
BLF_buffer(font, nullptr, nullptr, 0, 0, nullptr);
BLF_disable(font, font_flags);
/* Draw shadow. */
if (data->flag & SEQ_TEXT_SHADOW) {
draw_text_shadow(context, data, runtime->line_height, outline_rect, out);
}
/* Draw box under text. */
if (data->flag & SEQ_TEXT_BOX) {
if (out->byte_buffer.data) {
const int margin = data->box_margin * out->x;
const int minx = runtime->text_boundbox.xmin - margin;
const int maxx = runtime->text_boundbox.xmax + margin;
const int miny = runtime->text_boundbox.ymin - margin;
const int maxy = runtime->text_boundbox.ymax + margin;
float corner_radius = data->box_roundness * (maxy - miny) / 2.0f;
fill_rect_alpha_under(out, data->box_color, minx, miny, maxx, maxy, corner_radius);
}
}
return out;
}
void text_effect_get_handle(EffectHandle &rval)
{
rval.num_inputs = num_inputs_text;
rval.init = init_text_effect;
rval.free = free_text_effect;
rval.load = load_text_effect;
rval.copy = copy_text_effect;
rval.early_out = early_out_text;
rval.execute = do_text_effect;
}
/** \} */
} // namespace blender::seq