diff --git a/cmake/Modules/FindFreetype.cmake b/cmake/Modules/FindFreetype.cmake new file mode 100644 index 0000000000000000000000000000000000000000..71e59310666ded63cc0b7fde1e822ed53df7f68c --- /dev/null +++ b/cmake/Modules/FindFreetype.cmake @@ -0,0 +1,56 @@ +# Once done these will be defined: +# +# LIBFREETYPE_FOUND +# LIBFREETYPE_INCLUDE_DIRS +# LIBFREETYPE_LIBRARIES +# +# For use in OBS: +# +# FREETYPE_INCLUDE_DIR +# + +if(LIBFREETYPE_INCLUDE_DIRS AND LIBFREETYPE_LIBRARIES) + set(LIBFREETYPE_FOUND TRUE) +else() + find_package(PkgConfig QUIET) + if (PKG_CONFIG_FOUND) + pkg_check_modules(_FREETYPE QUIET freetype2) + endif() + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) + else() + set(_lib_suffix 32) + endif() + + set(FREETYPE_PATH_ARCH FreetypePath${_lib_suffix}) + + find_path(FREETYPE_INCLUDE_DIR + NAMES ft2build.h + HINTS + ${_FREETYPE_INCLUDE_DIRS} + "${CMAKE_SOURCE_DIR}/additional_install_files/include" + "$ENV{obsAdditionalInstallFiles}/include" + ENV FreetypePath + ENV ${FREETYPE_PATH_ARCH} + PATHS + /usr/include /usr/local/include /opt/local/include /sw/include) + + find_library(FREETYPE_LIB + NAMES ${_FREETYPE_LIBRARIES} freetype libfreetype + HINTS + ${_FREETYPE_LIBRARY_DIRS} + "${FREETYPE_INCLUDE_DIR}/../lib" + "${FREETYPE_INCLUDE_DIR}/../lib${_lib_suffix}" + "${FREETYPE_INCLUDE_DIR}/../libs${_lib_suffix}" + "${FREETYPE_INCLUDE_DIR}/lib" + "${FREETYPE_INCLUDE_DIR}/lib${_lib_suffix}" + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib) + + set(LIBFREETYPE_INCLUDE_DIRS ${FREETYPE_INCLUDE_DIR} CACHE PATH "freetype include dir") + set(LIBFREETYPE_LIBRARIES ${FREETYPE_LIB} CACHE STRING "freetype libraries") + + find_package_handle_standard_args(Libfreetype DEFAULT_MSG FREETYPE_LIB FREETYPE_INCLUDE_DIR) + mark_as_advanced(FREETYPE_INCLUDE_DIR FREETYPE_LIB) +endif() diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 6aa6fcee367690e7886ca67aaff50a6ad7b0e705..f2d6ae0aecfe013eecaf7d452a583138d85a4cd7 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -19,3 +19,4 @@ add_subdirectory(obs-libfdk) add_subdirectory(obs-ffmpeg) add_subdirectory(obs-outputs) add_subdirectory(rtmp-services) +add_subdirectory(text-freetype2) diff --git a/plugins/text-freetype2/CMakeLists.txt b/plugins/text-freetype2/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..4770e17cbddcd31c66c0568be45db9d0fb47ab26 --- /dev/null +++ b/plugins/text-freetype2/CMakeLists.txt @@ -0,0 +1,23 @@ +project(text-freetype2) + +find_package(Freetype REQUIRED) + +include_directories( + SYSTEM "${CMAKE_SOURCE_DIR}/libobs" + ${LIBFREETYPE_INCLUDE_DIRS}) + +set(text-freetype2_SOURCES + obs-convenience.c + text-functionality.c + text-freetype2.c + obs-convenience.h + text-freetype2.h) + +add_library(text-freetype2 MODULE + ${text-freetype2_SOURCES}) +target_link_libraries(text-freetype2 + libobs + ${LIBFREETYPE_LIBRARIES}) + +install_obs_plugin(text-freetype2) +install_obs_plugin_data(text-freetype2 data) diff --git a/plugins/text-freetype2/data/locale/en-US.ini b/plugins/text-freetype2/data/locale/en-US.ini new file mode 100644 index 0000000000000000000000000000000000000000..b7e445cb583965d275406e89e4f2752fb068f0af --- /dev/null +++ b/plugins/text-freetype2/data/locale/en-US.ini @@ -0,0 +1,12 @@ +FontFile="Font File" +Text="Text" +TextFile="Text File (UTF-8 or UTF-16)" +ChatLogMode="Chat log mode (last 6 lines)" +FontSize="Font Size (Pixels)" +Color1="Color 1" +Color2="Color 2" +Outline="Outline" +DropShadow="Drop Shadow" +ReadFromFile="Read from file" +CustomWidth="Custom text width" +WordWrap="Word Wrap" \ No newline at end of file diff --git a/plugins/text-freetype2/data/text_default.effect b/plugins/text-freetype2/data/text_default.effect new file mode 100644 index 0000000000000000000000000000000000000000..96804161086e0275aa2afffe662e94f79de1c8bd --- /dev/null +++ b/plugins/text-freetype2/data/text_default.effect @@ -0,0 +1,56 @@ +uniform float4x4 ViewProj; +uniform float4x4 color_matrix; +uniform float3 color_range_min = {0.0, 0.0, 0.0}; +uniform float3 color_range_max = {1.0, 1.0, 1.0}; +uniform texture2d image; + +sampler_state def_sampler { + Filter = Linear; + AddressU = Clamp; + AddressV = Clamp; +}; + +struct VertInOut { + float4 pos : POSITION; + float2 uv : TEXCOORD0; + float4 col : COLOR; +}; + +VertInOut VSDefault(VertInOut vert_in) +{ + VertInOut vert_out; + vert_out.pos = mul(float4(vert_in.pos.xyz, 1.0), ViewProj); + vert_out.uv = vert_in.uv; + vert_out.col = vert_in.col; + return vert_out; +} + +float4 PSDrawBare(VertInOut vert_in) : TARGET +{ + return image.Sample(def_sampler, vert_in.uv) * vert_in.col; +} + +float4 PSDrawMatrix(VertInOut vert_in) : TARGET +{ + float4 yuv = image.Sample(def_sampler, vert_in.uv); + yuv.xyz = clamp(yuv.xyz, color_range_min, color_range_max); + return saturate(mul(float4(yuv.xyz, 1.0), color_matrix)); +} + +technique Draw +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSDrawBare(vert_in); + } +} + +technique DrawMatrix +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSDrawMatrix(vert_in); + } +} diff --git a/plugins/text-freetype2/obs-convenience.c b/plugins/text-freetype2/obs-convenience.c new file mode 100644 index 0000000000000000000000000000000000000000..0bbd7a936367dcc88318ef616fd78d94f0794921 --- /dev/null +++ b/plugins/text-freetype2/obs-convenience.c @@ -0,0 +1,84 @@ +/****************************************************************************** +Copyright (C) 2014 by Nibbles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include +#include "obs-convenience.h" + +gs_vertbuffer_t create_uv_vbuffer(uint32_t num_verts, bool add_color) { + obs_enter_graphics(); + + gs_vertbuffer_t tmp = NULL; + struct gs_vb_data *vrect = NULL; + + vrect = gs_vbdata_create(); + vrect->num = num_verts; + vrect->points = (struct vec3 *)bmalloc(sizeof(struct vec3) * num_verts); + vrect->num_tex = 1; + vrect->tvarray = + (struct gs_tvertarray *)bmalloc(sizeof(struct gs_tvertarray)); + vrect->tvarray[0].width = 2; + vrect->tvarray[0].array = bmalloc(sizeof(struct vec2) * num_verts); + if (add_color) + vrect->colors = (uint32_t *)bmalloc + (sizeof(uint32_t)* num_verts); + + memset(vrect->points, 0, sizeof(struct vec3) * num_verts); + memset(vrect->tvarray[0].array, 0, sizeof(struct vec2) * num_verts); + if (add_color) + memset(vrect->colors, 0, sizeof(uint32_t)* num_verts); + + tmp = gs_vertexbuffer_create(vrect, GS_DYNAMIC); + + if (tmp == NULL) { + blog(LOG_WARNING, "Couldn't create UV vertex buffer."); + } + + obs_leave_graphics(); + + return tmp; +} + +void draw_uv_vbuffer(gs_vertbuffer_t vbuf, gs_texture_t tex, gs_effect_t effect, + uint32_t num_verts) { + gs_texture_t texture = tex; + gs_technique_t tech = gs_effect_get_technique(effect, "Draw"); + gs_eparam_t image = gs_effect_get_param_by_name(effect, "image"); + size_t passes; + + if (vbuf == NULL || tex == NULL) return; + + gs_vertexbuffer_flush(vbuf); + gs_load_vertexbuffer(vbuf); + gs_load_indexbuffer(NULL); + + passes = gs_technique_begin(tech); + + for (size_t i = 0; i < passes; i++) { + if (gs_technique_begin_pass(tech, i)) { + gs_effect_set_texture(image, texture); + + gs_draw(GS_TRIS, 0, num_verts); + + gs_technique_end_pass(tech); + } + } + + gs_technique_end(tech); +} diff --git a/plugins/text-freetype2/obs-convenience.h b/plugins/text-freetype2/obs-convenience.h new file mode 100644 index 0000000000000000000000000000000000000000..0a701ad9ce5f6b03a28429ff7c57e260ed1d38e0 --- /dev/null +++ b/plugins/text-freetype2/obs-convenience.h @@ -0,0 +1,43 @@ +/****************************************************************************** +Copyright (C) 2014 by Nibbles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +******************************************************************************/ + +#include + +gs_vertbuffer_t create_uv_vbuffer(uint32_t num_verts, bool add_color); +void draw_uv_vbuffer(gs_vertbuffer_t vbuf, gs_texture_t tex, gs_effect_t effect, + uint32_t num_verts); + +#define set_v3_rect(a, x, y, w, h) \ + vec3_set(a, x, y, 0.0f); \ + vec3_set(a + 1, x + w, y, 0.0f); \ + vec3_set(a + 2, x, y + h, 0.0f); \ + vec3_set(a + 3, x, y + h, 0.0f); \ + vec3_set(a + 4, x + w, y, 0.0f); \ + vec3_set(a + 5, x + w, y + h, 0.0f); + +#define set_v2_uv(a, u, v, u2, v2) \ + vec2_set(a, u, v); \ + vec2_set(a + 1, u2, v); \ + vec2_set(a + 2, u, v2); \ + vec2_set(a + 3, u, v2); \ + vec2_set(a + 4, u2, v); \ + vec2_set(a + 5, u2, v2); + +#define set_rect_colors2(a, c1, c2) \ + uint32_t *b = a; \ + b[0] = b[1] = b[4] = c1; \ + b[2] = b[3] = b[5] = c2; diff --git a/plugins/text-freetype2/text-freetype2.c b/plugins/text-freetype2/text-freetype2.c new file mode 100644 index 0000000000000000000000000000000000000000..66deba405cc3a770ac1f8eab0c36aa2cb83843b9 --- /dev/null +++ b/plugins/text-freetype2/text-freetype2.c @@ -0,0 +1,399 @@ +/****************************************************************************** +Copyright (C) 2014 by Nibbles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include FT_FREETYPE_H +#include +#include "text-freetype2.h" +#include "obs-convenience.h" + +FT_Library ft2_lib; + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("text-freetype2", "en-US") + +uint32_t texbuf_w = 2048, texbuf_h = 2048; + +static struct obs_source_info freetype2_source_info = { + .id = "text_ft2_source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = ft2_source_get_name, + .create = ft2_source_create, + .destroy = ft2_source_destroy, + .update = ft2_source_update, + .get_width = ft2_source_get_width, + .get_height = ft2_source_get_height, + .video_render = ft2_source_render, + .video_tick = ft2_video_tick, + .get_properties = ft2_source_properties, +}; + +bool obs_module_load() +{ + FT_Init_FreeType(&ft2_lib); + + if (ft2_lib == NULL) { + blog(LOG_WARNING, "FT2-text: Failed to initialize FT2."); + return false; + } + + obs_register_source(&freetype2_source_info); + + return true; +} + +void obs_module_unload(void) +{ + FT_Done_FreeType(ft2_lib); +} + +static const char *ft2_source_get_name(void) +{ + return obs_module_text("Text (FreeType 2)"); +} + +static uint32_t ft2_source_get_width(void *data) +{ + struct ft2_source *srcdata = data; + + return srcdata->cx; +} + +static uint32_t ft2_source_get_height(void *data) +{ + struct ft2_source *srcdata = data; + + return srcdata->cy; +} + +static obs_properties_t ft2_source_properties(void) +{ + obs_properties_t props = obs_properties_create(); + //obs_property_t prop; + + // TODO: + // Get system fonts, no idea how. + // Scrolling. Can't think of a way to do it with the render + // targets currently being broken. (0.4.2) + // Better/pixel shader outline/drop shadow + // Some way to pull text files from network, I dunno + + obs_properties_add_path(props, + "font_file", obs_module_text("FontFile"), + OBS_PATH_FILE, "All font formats (*.ttf *.otf);;", NULL); + + obs_properties_add_text(props, "text", + obs_module_text("Text"), OBS_TEXT_MULTILINE); + + obs_properties_add_bool(props, "from_file", + obs_module_text("ReadFromFile")); + + obs_properties_add_bool(props, "log_mode", + obs_module_text("ChatLogMode")); + + obs_properties_add_path(props, + "text_file", obs_module_text("TextFile"), + OBS_PATH_FILE, "All font formats (*.txt);;", NULL); + + obs_properties_add_int(props, "font_size", + obs_module_text("FontSize"), 0, 72, 1); + + obs_properties_add_color(props, "color1", + obs_module_text("Color1")); + + obs_properties_add_color(props, "color2", + obs_module_text("Color2")); + + obs_properties_add_bool(props, "outline", + obs_module_text("Outline")); + + obs_properties_add_bool(props, "drop_shadow", + obs_module_text("DropShadow")); + + obs_properties_add_int(props, "custom_width", + obs_module_text("CustomWidth"), 0, 4096, 1); + + obs_properties_add_bool(props, "word_wrap", + obs_module_text("WordWrap")); + + return props; +} + +static void ft2_source_destroy(void *data) +{ + struct ft2_source *srcdata = data; + + if (srcdata->font_face != NULL) { + FT_Done_Face(srcdata->font_face); + srcdata->font_face = NULL; + } + + for (uint32_t i = 0; i < num_cache_slots; i++) { + if (srcdata->cacheglyphs[i] != NULL) { + bfree(srcdata->cacheglyphs[i]); + srcdata->cacheglyphs[i] = NULL; + } + } + + if (srcdata->font_name != NULL) + bfree(srcdata->font_name); + if (srcdata->text != NULL) + bfree(srcdata->text); + if (srcdata->texbuf != NULL) + bfree(srcdata->texbuf); + if (srcdata->colorbuf != NULL) + bfree(srcdata->colorbuf); + if (srcdata->text_file != NULL) + bfree(srcdata->text_file); + + obs_enter_graphics(); + + if (srcdata->tex != NULL) { + gs_texture_destroy(srcdata->tex); + srcdata->tex = NULL; + } + if (srcdata->vbuf != NULL) { + gs_vertexbuffer_destroy(srcdata->vbuf); + srcdata->vbuf = NULL; + } + if (srcdata->draw_effect != NULL) { + gs_effect_destroy(srcdata->draw_effect); + srcdata->draw_effect = NULL; + } + + obs_leave_graphics(); + + bfree(srcdata); +} + +static void ft2_source_render(void *data, gs_effect_t effect) +{ + struct ft2_source *srcdata = data; + if (srcdata == NULL) return; + + if (srcdata->tex == NULL || srcdata->vbuf == NULL) return; + + gs_reset_blend_state(); + if (srcdata->outline_text) draw_outlines(srcdata); + if (srcdata->drop_shadow) draw_drop_shadow(srcdata); + + draw_uv_vbuffer(srcdata->vbuf, srcdata->tex, + srcdata->draw_effect, (uint32_t)wcslen(srcdata->text) * 6); + + UNUSED_PARAMETER(effect); +} + +static void ft2_video_tick(void *data, float seconds) +{ + struct ft2_source *srcdata = data; + if (srcdata == NULL) return; + if (srcdata->text_file == NULL) return; + + if (os_gettime_ns() - srcdata->last_checked >= 1000000000) { + srcdata->last_checked = os_gettime_ns(); + if (srcdata->m_timestamp != + get_modified_timestamp(srcdata->text_file)) { + if (srcdata->log_mode) + read_from_end(srcdata, srcdata->text_file); + else + load_text_from_file(srcdata, + srcdata->text_file); + set_up_vertex_buffer(srcdata); + } + } + + UNUSED_PARAMETER(seconds); +} + +static void ft2_source_update(void *data, obs_data_t settings) +{ + struct ft2_source *srcdata = data; + const char *tmp = obs_data_get_string(settings, "font_file"); + bool vbuf_needs_update = false; + bool word_wrap = false; + uint32_t color[2]; + uint32_t custom_width = 0; + + srcdata->drop_shadow = obs_data_get_bool(settings, "drop_shadow"); + srcdata->outline_text = obs_data_get_bool(settings, "outline"); + word_wrap = obs_data_get_bool(settings, "word_wrap"); + + color[0] = (uint32_t)obs_data_get_int(settings, "color1"); + color[1] = (uint32_t)obs_data_get_int(settings, "color2"); + + custom_width = (uint32_t)obs_data_get_int(settings, "custom_width"); + if (custom_width >= 100) { + if (custom_width != srcdata->custom_width) { + srcdata->custom_width = custom_width; + vbuf_needs_update = true; + } + } + else { + if (srcdata->custom_width >= 100) + vbuf_needs_update = true; + srcdata->custom_width = 0; + } + + if (word_wrap != srcdata->word_wrap) { + srcdata->word_wrap = word_wrap; + vbuf_needs_update = true; + } + + if (color[0] != srcdata->color[0] || color[1] != srcdata->color[1]) { + srcdata->color[0] = color[0]; + srcdata->color[1] = color[1]; + vbuf_needs_update = true; + } + + bool from_file = obs_data_get_bool(settings, "from_file"); + bool chat_log_mode = obs_data_get_bool(settings, "log_mode"); + + srcdata->log_mode = chat_log_mode; + + if (ft2_lib == NULL) return; + + if (!from_file) { + if (srcdata->text_file != NULL) { + bfree(srcdata->text_file); + srcdata->text_file = NULL; + } + } + + if (srcdata->draw_effect == NULL) { + char *effect_file = NULL; + char *error_string = NULL; + + effect_file = + obs_module_file("text_default.effect"); + + if (effect_file) { + obs_enter_graphics(); + srcdata->draw_effect = gs_effect_create_from_file( + effect_file, &error_string); + obs_leave_graphics(); + + bfree(effect_file); + if (error_string != NULL) + bfree(error_string); + } + } + + if (srcdata->font_size != obs_data_get_int(settings, "font_size")) + vbuf_needs_update = true; + + if (srcdata->font_name != NULL) { + if (strcmp(tmp, srcdata->font_name) == 0 && + srcdata->font_size == + obs_data_get_int(settings, "font_size")) + goto skip_font_load; + bfree(srcdata->font_name); + srcdata->font_name = NULL; + srcdata->max_h = 0; + } + + srcdata->font_name = bzalloc(strlen(tmp) + 1); + strcpy(srcdata->font_name, tmp); + + if (srcdata->font_face != NULL) { + FT_Done_Face(srcdata->font_face); + srcdata->font_face = NULL; + } + + FT_New_Face(ft2_lib, srcdata->font_name, 0, + &srcdata->font_face); + + if (srcdata->font_face == NULL) { + blog(LOG_WARNING, "FT2-text: Failed to load font %s", + srcdata->font_name); + return; + } + else { + srcdata->font_size = (uint16_t)obs_data_get_int(settings, + "font_size"); + FT_Set_Pixel_Sizes(srcdata->font_face, 0, srcdata->font_size); + FT_Select_Charmap(srcdata->font_face, FT_ENCODING_UNICODE); + } + + if (srcdata->texbuf != NULL) { + bfree(srcdata->texbuf); + srcdata->texbuf = NULL; + } + srcdata->texbuf = bzalloc(texbuf_w * texbuf_h * 4); + + cache_standard_glyphs(srcdata); +skip_font_load:; + if (from_file) { + tmp = obs_data_get_string(settings, "text_file"); + if (strlen(tmp) == 0) + return; + if (srcdata->text_file != NULL) { + if (strcmp(srcdata->text_file, tmp) == 0 + && !vbuf_needs_update) + return; + bfree(srcdata->text_file); + srcdata->text_file = NULL; + } + else + blog(LOG_WARNING, + "FT2-text: Failed to open %s for reading", tmp); + srcdata->text_file = bzalloc(strlen(tmp) + 1); + strcpy(srcdata->text_file, tmp); + + if (chat_log_mode) + read_from_end(srcdata, tmp); + else + load_text_from_file(srcdata, tmp); + srcdata->last_checked = os_gettime_ns(); + } + else { + tmp = obs_data_get_string(settings, "text"); + if (strlen(tmp) == 0) return; + + if (srcdata->text != NULL) { + bfree(srcdata->text); + srcdata->text = NULL; + } + srcdata->text = bzalloc((strlen(tmp) + 1)*sizeof(wchar_t)); + os_utf8_to_wcs(tmp, strlen(tmp), srcdata->text, + (strlen(tmp) + 1)); + } + + cache_glyphs(srcdata, srcdata->text); + + set_up_vertex_buffer(srcdata); +} + +static void *ft2_source_create(obs_data_t settings, obs_source_t source) +{ + struct ft2_source *srcdata = bzalloc(sizeof(struct ft2_source)); + srcdata->src = source; + + srcdata->font_size = 32; + + obs_data_set_default_int(settings, "font_size", 32); + obs_data_set_default_int(settings, "color1", 0xFFFFFFFF); + obs_data_set_default_int(settings, "color2", 0xFFFFFFFF); + obs_data_set_default_string(settings, "text", + "The lazy snake jumps over the happy MASKEN."); + + ft2_source_update(srcdata, settings); + + return srcdata; +} diff --git a/plugins/text-freetype2/text-freetype2.h b/plugins/text-freetype2/text-freetype2.h new file mode 100644 index 0000000000000000000000000000000000000000..e7ce846eaa94524092a78c6d3ec9b14d01b6d9ca --- /dev/null +++ b/plugins/text-freetype2/text-freetype2.h @@ -0,0 +1,90 @@ +/****************************************************************************** +Copyright (C) 2014 by Nibbles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +******************************************************************************/ + +#include +#include + +#define num_cache_slots 65535 +#define src_glyph srcdata->cacheglyphs[glyph_index] + +struct glyph_info { + float u, v, u2, v2; + int32_t w, h, xoff, yoff; + int32_t xadv; +}; + +struct ft2_source { + uint8_t font_type; + char *font_name; + char *text_file; + wchar_t *text; + uint16_t font_size; + time_t m_timestamp; + uint64_t last_checked; + + uint32_t cx, cy, max_h, custom_width; + uint32_t texbuf_x, texbuf_y; + uint32_t color[2]; + uint32_t *colorbuf; + + int32_t cur_scroll, scroll_speed; + + gs_texture_t tex; + + struct glyph_info *cacheglyphs[num_cache_slots]; + + FT_Face font_face; + + uint32_t *texbuf; + gs_vertbuffer_t vbuf; + + gs_effect_t draw_effect; + bool outline_text, drop_shadow; + bool log_mode, word_wrap; + + obs_source_t src; +}; + +extern FT_Library ft2_lib; + +static void *ft2_source_create(obs_data_t settings, obs_source_t source); +static void ft2_source_destroy(void *data); +static void ft2_source_update(void *data, obs_data_t settings); +static void ft2_source_render(void *data, gs_effect_t effect); +static void ft2_video_tick(void *data, float seconds); + +void draw_outlines(struct ft2_source *srcdata); +void draw_drop_shadow(struct ft2_source *srcdata); + +static uint32_t ft2_source_get_width(void *data); +static uint32_t ft2_source_get_height(void *data); + +static obs_properties_t ft2_source_properties(void); + +static const char *ft2_source_get_name(void); + +uint32_t get_ft2_text_width(wchar_t *text, struct ft2_source *srcdata); + +time_t get_modified_timestamp(char *filename); +void load_text_from_file(struct ft2_source *srcdata, const char *filename); +void read_from_end(struct ft2_source *srcdata, const char *filename); + +void cache_standard_glyphs(struct ft2_source *srcdata); +void cache_glyphs(struct ft2_source *srcdata, wchar_t *cache_glyphs); + +void set_up_vertex_buffer(struct ft2_source *srcdata); +void fill_vertex_buffer(struct ft2_source *srcdata); diff --git a/plugins/text-freetype2/text-functionality.c b/plugins/text-freetype2/text-functionality.c new file mode 100644 index 0000000000000000000000000000000000000000..852ae500466d0bb1988fa28277a3a9350a25cd14 --- /dev/null +++ b/plugins/text-freetype2/text-functionality.c @@ -0,0 +1,457 @@ +/****************************************************************************** +Copyright (C) 2014 by Nibbles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include FT_FREETYPE_H +#include +#include "text-freetype2.h" +#include "obs-convenience.h" + +float offsets[16] = { -2.0f, 0.0f, 0.0f, -2.0f, 2.0f, 0.0f, 2.0f, 0.0f, + 0.0f, 2.0f, 0.0f, 2.0f, -2.0f, 0.0f, -2.0f, 0.0f }; + +extern uint32_t texbuf_w, texbuf_h; + +void draw_outlines(struct ft2_source *srcdata) +{ + // Horrible (hopefully temporary) solution for outlines. + uint32_t *tmp; + + struct gs_vb_data *vdata = gs_vertexbuffer_get_data(srcdata->vbuf); + tmp = vdata->colors; + vdata->colors = srcdata->colorbuf; + + gs_matrix_push(); + for (int32_t i = 0; i < 8; i++) { + gs_matrix_translate3f(offsets[i * 2], offsets[(i * 2) + 1], + 0.0f); + draw_uv_vbuffer(srcdata->vbuf, srcdata->tex, + srcdata->draw_effect, + (uint32_t)wcslen(srcdata->text) * 6); + } + gs_matrix_identity(); + gs_matrix_pop(); + + vdata->colors = tmp; +} + +void draw_drop_shadow(struct ft2_source *srcdata) +{ + // Horrible (hopefully temporary) solution for drop shadow. + uint32_t *tmp; + + struct gs_vb_data *vdata = gs_vertexbuffer_get_data(srcdata->vbuf); + tmp = vdata->colors; + vdata->colors = srcdata->colorbuf; + + gs_matrix_push(); + gs_matrix_translate3f(4.0f, 4.0f, 0.0f); + draw_uv_vbuffer(srcdata->vbuf, srcdata->tex, + srcdata->draw_effect, (uint32_t)wcslen(srcdata->text) * 6); + gs_matrix_identity(); + gs_matrix_pop(); + + vdata->colors = tmp; +} + +void set_up_vertex_buffer(struct ft2_source *srcdata) +{ + FT_UInt glyph_index = 0; + uint32_t x = 0, space_pos = 0, word_width = 0; + + if (srcdata->custom_width >= 100) + srcdata->cx = srcdata->custom_width; + else + srcdata->cx = get_ft2_text_width(srcdata->text, srcdata); + srcdata->cy = srcdata->max_h; + + obs_enter_graphics(); + if (srcdata->vbuf != NULL) { + gs_vertbuffer_t tmpvbuf = srcdata->vbuf; + srcdata->vbuf = NULL; + gs_vertexbuffer_destroy(tmpvbuf); + } + srcdata->vbuf = create_uv_vbuffer((uint32_t)wcslen(srcdata->text) * 6, + true); + + if (srcdata->custom_width <= 100) goto skip_word_wrap; + if (!srcdata->word_wrap) goto skip_word_wrap; + + for (uint32_t i = 0; i <= wcslen(srcdata->text); i++) { + if (i == wcslen(srcdata->text)) goto eos_check; + + if (srcdata->text[i] != L' ' && srcdata->text[i] != L'\n') + goto next_char; + + eos_check:; + if (x + word_width > srcdata->custom_width) { + if (space_pos != 0) + srcdata->text[space_pos] = L'\n'; + x = 0; + } + if (i == wcslen(srcdata->text)) goto eos_skip; + + x += word_width; + word_width = 0; + if (srcdata->text[i] == L'\n') + x = 0; + if (srcdata->text[i] == L' ') + space_pos = i; + next_char:; + glyph_index = FT_Get_Char_Index(srcdata->font_face, + srcdata->text[i]); + word_width += src_glyph->xadv; + eos_skip:; + } + +skip_word_wrap:; + fill_vertex_buffer(srcdata); + obs_leave_graphics(); +} + +void fill_vertex_buffer(struct ft2_source *srcdata) +{ + struct gs_vb_data *vdata = gs_vertexbuffer_get_data(srcdata->vbuf); + if (vdata == NULL) return; + struct vec2 *tvarray = (struct vec2 *)vdata->tvarray[0].array; + uint32_t *col = (uint32_t *)vdata->colors; + + FT_UInt glyph_index = 0; + + uint32_t dx = 0, dy = srcdata->max_h, max_y = dy; + uint32_t cur_glyph = 0; + + if (srcdata->colorbuf != NULL) { + bfree(srcdata->colorbuf); + srcdata->colorbuf = NULL; + } + srcdata->colorbuf = bzalloc(sizeof(uint32_t)*wcslen(srcdata->text) * 6); + for (uint32_t i = 0; i < wcslen(srcdata->text) * 6; i++) { + srcdata->colorbuf[i] = 0xFF000000; + } + + for (uint32_t i = 0; i < wcslen(srcdata->text); i++) { + add_linebreak:; + if (srcdata->text[i] != L'\n') goto draw_glyph; + dx = 0; i++; + dy += srcdata->max_h + 4; + if (i == wcslen(srcdata->text)) goto skip_glyph; + if (srcdata->text[i] == L'\n') goto add_linebreak; + draw_glyph:; + // Skip filthy dual byte Windows line breaks + if (srcdata->text[i] == L'\r') goto skip_glyph; + + glyph_index = FT_Get_Char_Index(srcdata->font_face, + srcdata->text[i]); + if (src_glyph == NULL) + goto skip_glyph; + + if (srcdata->custom_width < 100) goto skip_custom_width; + + if (dx + src_glyph->xadv > srcdata->custom_width) { + dx = 0; + dy += srcdata->max_h + 4; + } + + skip_custom_width:; + + set_v3_rect(vdata->points + (cur_glyph * 6), + (float)dx + (float)src_glyph->xoff, + (float)dy - (float)src_glyph->yoff, + (float)src_glyph->w, + (float)src_glyph->h); + set_v2_uv(tvarray + (cur_glyph * 6), + src_glyph->u, + src_glyph->v, + src_glyph->u2, + src_glyph->v2); + set_rect_colors2(col + (cur_glyph * 6), + srcdata->color[0], + srcdata->color[1]); + dx += src_glyph->xadv; + if (dy - (float)src_glyph->yoff + src_glyph->h > max_y) + max_y = dy - src_glyph->yoff + src_glyph->h; + cur_glyph++; + skip_glyph:; + } + + srcdata->cy = max_y; +} + +void cache_standard_glyphs(struct ft2_source *srcdata) +{ + for (uint32_t i = 0; i < num_cache_slots; i++) { + if (srcdata->cacheglyphs[i] != NULL) { + bfree(srcdata->cacheglyphs[i]); + srcdata->cacheglyphs[i] = NULL; + } + } + + srcdata->texbuf_x = 0; + srcdata->texbuf_y = 0; + + cache_glyphs(srcdata, L"abcdefghijklmnopqrstuvwxyz" \ + L"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" \ + L"!@#$%^&*()-_=+,<.>/?\\|[]{}`~ \'\"\0"); +} + +#define glyph_pos x + (y*slot->bitmap.pitch) +#define buf_pos (dx + x) + ((dy + y) * texbuf_w) + +void cache_glyphs(struct ft2_source *srcdata, wchar_t *cache_glyphs) +{ + FT_GlyphSlot slot = srcdata->font_face->glyph; + FT_UInt glyph_index = 0; + + uint32_t dx = srcdata->texbuf_x, dy = srcdata->texbuf_y; + uint8_t alpha; + + int32_t g_pitch = 0; + int32_t cached_glyphs = 0; + + for (uint32_t i = 0; i < wcslen(cache_glyphs); i++) { + glyph_index = FT_Get_Char_Index(srcdata->font_face, + cache_glyphs[i]); + + if (src_glyph != NULL) + goto skip_glyph; + + FT_Load_Glyph(srcdata->font_face, glyph_index, FT_LOAD_DEFAULT); + FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL); + + uint32_t g_w = slot->bitmap.width; + uint32_t g_h = slot->bitmap.rows; + g_pitch = slot->bitmap.pitch; + + if (srcdata->max_h < g_h) srcdata->max_h = g_h; + + if (dx + g_w >= texbuf_w) { + dx = 0; + dy += srcdata->max_h + 1; + } + + src_glyph = bzalloc(sizeof(struct glyph_info)); + src_glyph->u = (float)dx / (float)texbuf_w; + src_glyph->u2 = (float)(dx + g_w) / (float)texbuf_w; + src_glyph->v = (float)dy / (float)texbuf_h; + src_glyph->v2 = (float)(dy + g_h) / (float)texbuf_h; + src_glyph->w = g_w; + src_glyph->h = g_h; + src_glyph->yoff = slot->bitmap_top; + src_glyph->xoff = slot->bitmap_left; + src_glyph->xadv = slot->advance.x >> 6; + + for (uint32_t y = 0; y < g_h; y++) { + for (uint32_t x = 0; x < g_w; x++) { + alpha = slot->bitmap.buffer[glyph_pos]; + srcdata->texbuf[buf_pos] = + 0x00FFFFFF ^ (alpha << 24); + } + } + + dx += (g_w + 1); + if (dx >= texbuf_w) { + dx = 0; + dy += srcdata->max_h; + } + + cached_glyphs++; + skip_glyph:; + } + + srcdata->texbuf_x = dx; + srcdata->texbuf_y = dy; + + if (cached_glyphs > 0) { + + obs_enter_graphics(); + + if (srcdata->tex != NULL) { + gs_texture_t tmp_texture = NULL; + tmp_texture = srcdata->tex; + srcdata->tex = NULL; + gs_texture_destroy(tmp_texture); + } + + srcdata->tex = gs_texture_create(texbuf_w, texbuf_h, + GS_RGBA, 1, (const uint8_t **)&srcdata->texbuf, 0); + + obs_leave_graphics(); + } +} + +time_t get_modified_timestamp(char *filename) +{ + struct stat stats; + + // stat is apparently terrifying and horrible, but we only call it once + // every second at most. + stat(filename, &stats); + + return stats.st_mtime; +} + +void load_text_from_file(struct ft2_source *srcdata, const char *filename) +{ + FILE *tmp_file = NULL; + uint32_t filesize = 0; + char *tmp_read = NULL; + uint16_t header = 0; + + tmp_file = fopen(filename, "rb"); + if (tmp_file == NULL) { + blog(LOG_WARNING, "Failed to open file %s", filename); + return; + } + fseek(tmp_file, 0, SEEK_END); + filesize = (uint32_t)ftell(tmp_file); + fseek(tmp_file, 0, SEEK_SET); + fread(&header, 2, 1, tmp_file); + + if (header == 0xFEFF) { + // File is already in UTF-16 format + if (srcdata->text != NULL) { + bfree(srcdata->text); + srcdata->text = NULL; + } + srcdata->text = bzalloc(filesize); + fread(srcdata->text, filesize - 2, 1, tmp_file); + + srcdata->m_timestamp = + get_modified_timestamp(srcdata->text_file); + bfree(tmp_read); + fclose(tmp_file); + + return; + } + + fseek(tmp_file, 0, SEEK_SET); + srcdata->m_timestamp = get_modified_timestamp(srcdata->text_file); + + tmp_read = bzalloc(filesize + 1); + fread(tmp_read, filesize, 1, tmp_file); + fclose(tmp_file); + + if (srcdata->text != NULL) { + bfree(srcdata->text); + srcdata->text = NULL; + } + srcdata->text = bzalloc((strlen(tmp_read) + 1)*sizeof(wchar_t)); + os_utf8_to_wcs(tmp_read, strlen(tmp_read), + srcdata->text, (strlen(tmp_read) + 1)); + bfree(tmp_read); +} + +void read_from_end(struct ft2_source *srcdata, const char *filename) +{ + FILE *tmp_file = NULL; + uint32_t filesize = 0, cur_pos = 0; + char *tmp_read = NULL; + uint16_t value = 0, line_breaks = 0; + char bvalue; + + bool utf16 = false; + + tmp_file = fopen(filename, "rb"); + if (tmp_file == NULL) { + blog(LOG_WARNING, "Failed to open file %s", filename); + return; + } + fread(&value, 2, 1, tmp_file); + + if (value == 0xFEFF) + utf16 = true; + + fseek(tmp_file, 0, SEEK_END); + filesize = (uint32_t)ftell(tmp_file); + cur_pos = filesize; + + while (line_breaks <= 6 && cur_pos != 0) { + if (!utf16) cur_pos--; + else cur_pos -= 2; + fseek(tmp_file, cur_pos, SEEK_SET); + + if (!utf16) { + fread(&bvalue, 1, 1, tmp_file); + if (bvalue == '\n') + line_breaks++; + } + else { + fread(&value, 2, 1, tmp_file); + if (value == L'\n') + line_breaks++; + } + } + + if (cur_pos != 0) + cur_pos += (utf16) ? 2 : 1; + + fseek(tmp_file, cur_pos, SEEK_SET); + + if (utf16) { + if (srcdata->text != NULL) { + bfree(srcdata->text); + srcdata->text = NULL; + } + srcdata->text = bzalloc(filesize - cur_pos); + fread(srcdata->text, (filesize - cur_pos), 1, tmp_file); + + srcdata->m_timestamp = + get_modified_timestamp(srcdata->text_file); + bfree(tmp_read); + fclose(tmp_file); + + return; + } + + tmp_read = bzalloc((filesize - cur_pos) + 1); + fread(tmp_read, filesize - cur_pos, 1, tmp_file); + fclose(tmp_file); + + if (srcdata->text != NULL) { + bfree(srcdata->text); + srcdata->text = NULL; + } + srcdata->text = bzalloc((strlen(tmp_read) + 1)*sizeof(wchar_t)); + os_utf8_to_wcs(tmp_read, strlen(tmp_read), + srcdata->text, (strlen(tmp_read) + 1)); + + srcdata->m_timestamp = get_modified_timestamp(srcdata->text_file); + bfree(tmp_read); +} + +uint32_t get_ft2_text_width(wchar_t *text, struct ft2_source *srcdata) +{ + FT_GlyphSlot slot = srcdata->font_face->glyph; + FT_UInt glyph_index = 0; + uint32_t w = 0, max_w = 0; + + for (uint32_t i = 0; i < (uint32_t)wcslen(text); i++) { + glyph_index = FT_Get_Char_Index(srcdata->font_face, text[i]); + FT_Load_Glyph(srcdata->font_face, glyph_index, FT_LOAD_DEFAULT); + + if (text[i] == L'\n') w = 0; + else { + w += slot->advance.x >> 6; + if (w > max_w) max_w = w; + } + } + + return max_w; +}