// Author: Torarin Hals Bakke (2012) // Boost Software License - Version 1.0 - August 17th, 2003 // Permission is hereby granted, free of charge, to any person or organization // obtaining a copy of the software and accompanying documentation covered by // this license (the "Software") to use, reproduce, display, distribute, // execute, and transmit the Software, and to prepare derivative works of the // Software, and to permit third-parties to whom the Software is furnished to // do so, all subject to the following: // The copyright notices in the Software and this entire statement, including // the above license grant, this restriction and the following disclaimer, // must be included in all copies of the Software, in whole or in part, and // all derivative works of the Software, unless such copies or derivative // works are solely in the form of machine-executable object code generated by // a source language processor. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT // SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE // FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #include "SubtitleRenderer.h" #include "Unicode.h" #include "utils/ScopeExit.h" #include "utils/Enforce.h" #include "utils/Clamp.h" #include #include #include #include #include "bcm_host.h" class BoxRenderer { VGPath path_; VGPaint paint_; public: BoxRenderer(unsigned int opacity) { path_ = vgCreatePath(VG_PATH_FORMAT_STANDARD, VG_PATH_DATATYPE_F, 1.0, 0.0, 0, 0, VG_PATH_CAPABILITY_ALL); assert(path_); paint_ = vgCreatePaint(); assert(paint_); vgSetColor(paint_, opacity); assert(!vgGetError()); } ~BoxRenderer() { vgDestroyPath(path_); assert(!vgGetError()); vgDestroyPaint(paint_); assert(!vgGetError()); } BoxRenderer(const BoxRenderer&) = delete; BoxRenderer& operator=(const BoxRenderer&) = delete; void clear() { vgClearPath(path_, VG_PATH_CAPABILITY_ALL); assert(!vgGetError()); } void push(int x, int y, int width, int height) { assert(width >= 0); assert(height >= 0); vguRect(path_, x, y, width, height); assert(!vgGetError()); }; void render() { vgSetPaint(paint_, VG_FILL_PATH); assert(!vgGetError()); vgDrawPath(path_, VG_FILL_PATH); assert(!vgGetError()); } }; void SubtitleRenderer::load_glyph(InternalChar ch) { VGfloat escapement[2]{}; auto load_glyph_internal = [&](FT_Face ft_face, VGFont vg_font, bool border) { try { auto glyph_index = FT_Get_Char_Index(ft_face, ch.codepoint()); ENFORCE(!FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_NO_HINTING)); FT_Glyph glyph; ENFORCE(!FT_Get_Glyph(ft_face->glyph, &glyph)); SCOPE_EXIT {FT_Done_Glyph(glyph);}; if (border) ENFORCE(!FT_Glyph_StrokeBorder(&glyph, ft_stroker_, 0, 1)); ENFORCE(!FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, NULL, 1)); FT_BitmapGlyph bit_glyph = (FT_BitmapGlyph) glyph; FT_Bitmap& bitmap = bit_glyph->bitmap; VGImage image{}; VGfloat glyph_origin[2]{}; if (bitmap.width > 0 && bitmap.rows > 0) { constexpr VGfloat blur_stddev = 0.52; const int padding = static_cast(3*blur_stddev + 0.5); const int image_width = bitmap.width + padding*2; const int image_height = bitmap.rows + padding*2; image = vgCreateImage(VG_A_8, image_width, image_height, VG_IMAGE_QUALITY_NONANTIALIASED); assert(image); if (bitmap.pitch > 0) { vgImageSubData(image, bitmap.buffer + bitmap.pitch*(bitmap.rows-1), -bitmap.pitch, VG_A_8, padding, padding, bitmap.width, bitmap.rows); assert(!vgGetError()); } else { vgImageSubData(image, bitmap.buffer, bitmap.pitch, VG_A_8, padding, padding, bitmap.width, bitmap.rows); assert(!vgGetError()); } auto softened_image = vgCreateImage(VG_A_8, image_width, image_height, VG_IMAGE_QUALITY_NONANTIALIASED); assert(softened_image); // Even out hard and soft edges vgGaussianBlur(softened_image, image, blur_stddev, blur_stddev, VG_TILE_FILL); assert(!vgGetError()); vgDestroyImage(image); assert(!vgGetError()); image = softened_image; glyph_origin[0] = static_cast(padding - bit_glyph->left); glyph_origin[1] = static_cast(padding + static_cast(bitmap.rows) - bit_glyph->top - 1); } escapement[0] = static_cast((ft_face->glyph->advance.x + 32) / 64); escapement[1] = 0; vgSetGlyphToImage(vg_font, ch.val, image, glyph_origin, escapement); assert(!vgGetError()); if (image) { vgDestroyImage(image); assert(!vgGetError()); } } catch(...) { escapement[0] = 0; escapement[1] = 0; vgSetGlyphToImage(vg_font, ch.val, VG_INVALID_HANDLE, escapement, escapement); assert(!vgGetError()); } }; if (!ch.italic()) { load_glyph_internal(ft_face_, vg_font_, false); glyphs_[ch].advance = escapement[0]; load_glyph_internal(ft_face_, vg_font_border_, true); } else { load_glyph_internal(ft_face_italic_, vg_font_, false); glyphs_[ch].advance = escapement[0]; load_glyph_internal(ft_face_italic_, vg_font_border_, true); } } int SubtitleRenderer::get_text_width(const std::vector& text) { int width = 0; for (auto c = text.begin(); c != text.end(); ++c) { width += glyphs_.at(*c).advance; } return width; } std::vector SubtitleRenderer:: get_internal_chars(const std::string& str, TagTracker& tag_tracker) { std::vector internal_chars; auto c_str = str.c_str(); for (size_t i = 0, len = str.length(); i < len;) { try { auto cp = decodeUtf8(c_str, len, i); tag_tracker.put(cp); if (!tag_tracker.in_tag()) internal_chars.push_back(InternalChar(cp, tag_tracker.italic())); } catch (...) { ++i; // Keep going } } return internal_chars; } void SubtitleRenderer:: prepare_glyphs(const std::vector& text) { for (auto c = text.begin(); c != text.end(); ++c) { if (glyphs_.find(*c) == glyphs_.end()) load_glyph(*c); } } void SubtitleRenderer:: draw_text(VGFont font, const std::vector& text, int x, int y, unsigned int lightness) { VGPaint paint = vgCreatePaint(); assert(paint); vgSetColor(paint, (lightness<<8) | (lightness<<16) | (lightness<<24) | 0xFF); assert(!vgGetError()); vgSetPaint(paint, VG_FILL_PATH); assert(!vgGetError()); vgDestroyPaint(paint); assert(!vgGetError()); vgSeti(VG_IMAGE_MODE, VG_DRAW_IMAGE_MULTIPLY); assert(!vgGetError()); VGfloat pos[] = {static_cast(x), static_cast(y)}; vgSetfv(VG_GLYPH_ORIGIN, 2, pos); assert(!vgGetError()); for (auto c = text.begin(); c != text.end(); ++c) { vgDrawGlyph(font, c->val, VG_FILL_PATH, VG_FALSE); assert(!vgGetError()); } } SubtitleRenderer::~SubtitleRenderer() BOOST_NOEXCEPT { destroy(); } SubtitleRenderer:: SubtitleRenderer(int display, int layer, const std::string& font_path, const std::string& italic_font_path, float font_size, float margin_left, float margin_bottom, bool centered, unsigned int white_level, unsigned int box_opacity, unsigned int lines) : prepared_(), dispman_element_(), dispman_display_(), display_(), context_(), surface_(), vg_font_(), vg_font_border_(), ft_library_(), ft_face_(), ft_face_italic_(), ft_stroker_(), line_height_(), box_offset_(), box_h_padding_(), margin_left_(), margin_bottom_(), buffer_width_(), buffer_height_(), centered_(centered), white_level_(white_level), box_opacity_(box_opacity) { try { uint32_t screen_width, screen_height; ENFORCE(graphics_get_display_size(display, &screen_width, &screen_height) >= 0); initialize_fonts(font_path, italic_font_path, font_size*screen_height); int abs_margin_bottom = static_cast(margin_bottom * screen_height + 0.5f) - box_offset_; int buffer_padding = (line_height_+2)/4; int buffer_bottom = clamp(abs_margin_bottom + box_offset_ - buffer_padding, 0, (int) screen_height-1); int buffer_top = clamp(buffer_bottom + line_height_ * (int) lines + buffer_padding*2, 0, (int) screen_height-1); buffer_width_ = screen_width; buffer_height_ = buffer_top - buffer_bottom + 1; margin_left_ = (screen_width - screen_height) / 2 + static_cast(margin_left * screen_height + 0.5f); margin_bottom_ = abs_margin_bottom - buffer_bottom; initialize_window(display, layer, 0, screen_height - buffer_top - 1, screen_width, buffer_height_); initialize_vg(); } catch (...) { destroy(); throw; } } void SubtitleRenderer::destroy() { destroy_vg(); destroy_window(); destroy_fonts(); } void SubtitleRenderer:: initialize_fonts(const std::string& font_path, const std::string& italic_font_path, unsigned int font_size) { ENFORCE(!FT_Init_FreeType(&ft_library_)); ENFORCE2(!FT_New_Face(ft_library_, font_path.c_str(), 0, &ft_face_), "Unable to open font"); ENFORCE2(!FT_New_Face(ft_library_, italic_font_path.c_str(), 0, &ft_face_italic_), "Unable to open italic font"); ENFORCE(!FT_Set_Pixel_Sizes(ft_face_, 0, font_size)); ENFORCE(!FT_Set_Pixel_Sizes(ft_face_italic_, 0, font_size)); auto get_bbox = [this](char32_t cp) { auto glyph_index = FT_Get_Char_Index(ft_face_, cp); ENFORCE(!FT_Load_Glyph(ft_face_, glyph_index, FT_LOAD_NO_HINTING)); FT_Glyph glyph; ENFORCE(!FT_Get_Glyph(ft_face_->glyph, &glyph)); SCOPE_EXIT {FT_Done_Glyph(glyph);}; FT_BBox bbox; FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); return bbox; }; constexpr float padding_factor = 0.05f; int y_min = get_bbox('g').yMin; int y_max = get_bbox('M').yMax; y_max += -y_min*0.7f; line_height_ = y_max - y_min; const int v_padding = line_height_*padding_factor + 0.5f; line_height_ += v_padding*2; box_offset_ = y_min-v_padding; box_h_padding_ = line_height_/5.0f + 0.5f; constexpr float border_thickness = 0.044f; ENFORCE(!FT_Stroker_New(ft_library_, &ft_stroker_)); FT_Stroker_Set(ft_stroker_, line_height_*border_thickness*64.0f, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0); } void SubtitleRenderer::destroy_fonts() { if (ft_library_) { auto error = FT_Done_FreeType(ft_library_); assert(!error); ft_library_ = {}; ft_face_ = {}; ft_face_italic_ = {}; ft_stroker_ = {}; } } void SubtitleRenderer::initialize_window(int display, int layer, unsigned int x, unsigned int y, unsigned int width, unsigned int height) { VC_RECT_T dst_rect; dst_rect.x = x; dst_rect.y = y; dst_rect.width = width; dst_rect.height = height; VC_RECT_T src_rect; src_rect.x = 0; src_rect.y = 0; src_rect.width = dst_rect.width << 16; src_rect.height = dst_rect.height << 16; dispman_display_ = vc_dispmanx_display_open(display); ENFORCE(dispman_display_); { auto dispman_update = vc_dispmanx_update_start(0); ENFORCE(dispman_update); SCOPE_EXIT { ENFORCE(!vc_dispmanx_update_submit_sync(dispman_update)); }; dispman_element_ = vc_dispmanx_element_add(dispman_update, dispman_display_, layer, &dst_rect, 0 /*src*/, &src_rect, DISPMANX_PROTECTION_NONE, 0 /*alpha*/, 0 /*clamp*/, (DISPMANX_TRANSFORM_T) 0 /*transform*/); ENFORCE(dispman_element_); } } void SubtitleRenderer::destroy_window() { if (dispman_element_) { auto dispman_update = vc_dispmanx_update_start(0); assert(dispman_update); if (dispman_update) { auto error = vc_dispmanx_element_remove(dispman_update, dispman_element_); assert(!error); error = vc_dispmanx_update_submit_sync(dispman_update); assert(!error); } dispman_element_ = {}; } if (dispman_display_) { auto error = vc_dispmanx_display_close(dispman_display_); assert(!error); dispman_display_ = {}; } } void SubtitleRenderer::initialize_vg() { // get an EGL display connection display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY); ENFORCE(display_); // initialize the EGL display connection ENFORCE(eglInitialize(display_, NULL, NULL)); // get an appropriate EGL frame buffer configuration static const EGLint attribute_list[] = { EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE }; EGLConfig config{}; EGLint num_config{}; ENFORCE(eglChooseConfig(display_, attribute_list, &config, 1, &num_config)); ENFORCE(num_config); ENFORCE(eglBindAPI(EGL_OPENVG_API)); static EGL_DISPMANX_WINDOW_T nativewindow; nativewindow.element = dispman_element_; nativewindow.width = buffer_width_; nativewindow.height = buffer_height_; surface_ = eglCreateWindowSurface(display_, config, &nativewindow, NULL); ENFORCE(surface_); // create an EGL rendering context context_ = eglCreateContext(display_, config, EGL_NO_CONTEXT, NULL); ENFORCE(context_); auto result = eglMakeCurrent(display_, surface_, surface_, context_); assert(result); vgSeti(VG_FILTER_FORMAT_LINEAR, VG_TRUE); assert(!vgGetError()); vgSeti(VG_IMAGE_QUALITY, VG_IMAGE_QUALITY_NONANTIALIASED); assert(!vgGetError()); auto create_vg_font = [](VGFont& font) { font = vgCreateFont(64); ENFORCE(font); }; create_vg_font(vg_font_); create_vg_font(vg_font_border_); // VGfloat color[4] = { 1.0f, 0.0f, 0.0f, 1.0f }; // vgSetfv(VG_CLEAR_COLOR, 4, color); } void SubtitleRenderer::destroy_vg() { if (display_) { auto result = eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); assert(result); result = eglTerminate(display_); assert(result); context_ = {}; surface_ = {}; display_ = {}; } } void SubtitleRenderer:: prepare(const std::vector& text_lines) BOOST_NOEXCEPT { const int n_lines = text_lines.size(); TagTracker tag_tracker; internal_lines_.resize(n_lines); line_widths_.resize(n_lines); line_positions_.resize(n_lines); for (int i = 0; i < n_lines; ++i) { internal_lines_[i] = get_internal_chars(text_lines[i], tag_tracker); prepare_glyphs(internal_lines_[i]); line_widths_[i] = get_text_width(internal_lines_[i]); line_positions_[i].second = margin_bottom_ + (n_lines-i-1)*line_height_; if (centered_) line_positions_[i].first = buffer_width_/2 - line_widths_[i]/2; else line_positions_[i].first = margin_left_; } prepared_ = true; } void SubtitleRenderer::clear() BOOST_NOEXCEPT { vgClear(0, 0, buffer_width_, buffer_height_); assert(!vgGetError()); } void SubtitleRenderer::draw() BOOST_NOEXCEPT { clear(); const auto n_lines = internal_lines_.size(); { BoxRenderer box_renderer(box_opacity_); for (size_t i = 0; i < n_lines; ++i) { box_renderer.push(line_positions_[i].first - box_h_padding_, line_positions_[i].second + box_offset_, line_widths_[i] + box_h_padding_*2, line_height_); } box_renderer.render(); } for (size_t i = 0; i < n_lines; ++i) { draw_text(vg_font_border_, internal_lines_[i], line_positions_[i].first, line_positions_[i].second, 0); } for (size_t i = 0; i < n_lines; ++i) { draw_text(vg_font_, internal_lines_[i], line_positions_[i].first, line_positions_[i].second, white_level_); } prepared_ = false; } void SubtitleRenderer::swap_buffers() BOOST_NOEXCEPT { EGLBoolean result = eglSwapBuffers(display_, surface_); assert(result); }