omxplayer/SubtitleRenderer.cpp

593 lines
17 KiB
C++

// 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 <bcm_host.h>
#include <VG/vgu.h>
#include <cassert>
#include <algorithm>
#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<int>(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<VGfloat>(padding - bit_glyph->left);
glyph_origin[1] = static_cast<VGfloat>(padding + static_cast<int>(bitmap.rows) - bit_glyph->top - 1);
}
escapement[0] = static_cast<VGfloat>((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<InternalChar>& text) {
int width = 0;
for (auto c = text.begin(); c != text.end(); ++c) {
width += glyphs_.at(*c).advance;
}
return width;
}
std::vector<SubtitleRenderer::InternalChar> SubtitleRenderer::
get_internal_chars(const std::string& str, TagTracker& tag_tracker) {
std::vector<InternalChar> 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<InternalChar>& 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<SubtitleRenderer::InternalChar>& 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<VGfloat>(x), static_cast<VGfloat>(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<int>(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<int>(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<std::string>& 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);
}