199 lines
5.4 KiB
C++
199 lines
5.4 KiB
C++
#include "Font.h"
|
|
#include "GLTexture.h"
|
|
#include <stdint.h>
|
|
#include <ft2build.h>
|
|
#include FT_FREETYPE_H
|
|
#include <unordered_map>
|
|
|
|
#define round_up_26_6(val) (((val) + 63) >> 6u)
|
|
|
|
class Glyph
|
|
{
|
|
public:
|
|
Glyph();
|
|
bool load(FT_Face face, FT_ULong char_code);
|
|
int get_advance() { return m_advance; }
|
|
void render()
|
|
{
|
|
m_texture->bind();
|
|
glEnableVertexAttribArray(0);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id);
|
|
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
}
|
|
protected:
|
|
bool m_loaded;
|
|
int m_advance;
|
|
GLTextureRef m_texture;
|
|
GLuint m_vbo_id;
|
|
};
|
|
typedef Ref<Glyph> GlyphRef;
|
|
|
|
typedef struct
|
|
{
|
|
FT_Face face;
|
|
std::unordered_map<FT_ULong, GlyphRef> glyphs;
|
|
} Font;
|
|
|
|
static FT_Library ft;
|
|
static VALUE ruby_class;
|
|
|
|
static void Init_FreeType()
|
|
{
|
|
static bool initialized = false;
|
|
|
|
if (initialized)
|
|
return;
|
|
|
|
if (FT_Init_FreeType(&ft) != 0)
|
|
{
|
|
rb_raise(rb_eRuntimeError, "Error initializing FreeType");
|
|
}
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
void Font_free(void * ptr)
|
|
{
|
|
Font * font = (Font *)ptr;
|
|
|
|
FT_Done_Face(font->face);
|
|
|
|
delete font;
|
|
}
|
|
|
|
static GlyphRef get_glyph(Font * font, FT_ULong char_code)
|
|
{
|
|
auto it = font->glyphs.find(char_code);
|
|
if (it != font->glyphs.end())
|
|
return it->second;
|
|
GlyphRef glyph = new Glyph();
|
|
glyph = glyph->load(font->face, char_code) ? glyph : NULL;
|
|
font->glyphs[char_code] = glyph;
|
|
return glyph;
|
|
}
|
|
|
|
static VALUE Font_new(VALUE klass, VALUE fname, VALUE size)
|
|
{
|
|
Init_FreeType();
|
|
|
|
Font * font = new Font();
|
|
|
|
fname = rb_funcall(fname, rb_intern("to_s"), 0);
|
|
const char * fname_cstr = StringValueCStr(fname);
|
|
|
|
if (FT_New_Face(ft, fname_cstr, 0, &font->face) != 0)
|
|
{
|
|
delete font;
|
|
rb_raise(rb_eRuntimeError,
|
|
"Could not load font %s",
|
|
fname_cstr);
|
|
}
|
|
|
|
FT_Set_Pixel_Sizes(font->face, 0, NUM2INT(size));
|
|
|
|
static const char preload_glyphs[] = "HMgjqxy_|^";
|
|
int max_top = -9999;
|
|
int min_bottom = 9999;
|
|
|
|
for (unsigned int i = 0; i < sizeof(preload_glyphs) - 1u; i++)
|
|
{
|
|
GlyphRef g = get_glyph(font, preload_glyphs[i]);
|
|
if (g == NULL)
|
|
{
|
|
delete font;
|
|
rb_raise(rb_eRuntimeError,
|
|
"Error loading glyph '%c' from font '%s'",
|
|
preload_glyphs[i],
|
|
fname_cstr);
|
|
}
|
|
if (font->face->glyph->bitmap_top > max_top)
|
|
max_top = font->face->glyph->bitmap_top;
|
|
int bitmap_bottom = font->face->glyph->bitmap_top - font->face->glyph->bitmap.rows;
|
|
if (bitmap_bottom < min_bottom)
|
|
min_bottom = bitmap_bottom;
|
|
}
|
|
|
|
int line_height = round_up_26_6(font->face->size->metrics.height);
|
|
int baseline_offset = (line_height - (max_top - min_bottom)) / 2 - min_bottom;
|
|
|
|
VALUE rv = Data_Wrap_Struct(klass, NULL, Font_free, font);
|
|
rb_iv_set(rv, "@advance", INT2FIX(get_glyph(font, 'x')->get_advance()));
|
|
rb_iv_set(rv, "@line_height", INT2FIX(line_height));
|
|
rb_iv_set(rv, "@baseline_offset", INT2FIX(baseline_offset));
|
|
return rv;
|
|
}
|
|
|
|
void Font_Init()
|
|
{
|
|
ruby_class = rb_define_class("Font", rb_cObject);
|
|
rb_define_singleton_method(ruby_class, "new", (VALUE(*)(...))Font_new, 2);
|
|
rb_define_attr(ruby_class, "advance", 1, 0);
|
|
rb_define_attr(ruby_class, "line_height", 1, 0);
|
|
rb_define_attr(ruby_class, "baseline_offset", 1, 0);
|
|
}
|
|
|
|
Glyph::Glyph()
|
|
{
|
|
m_loaded = false;
|
|
}
|
|
|
|
bool Glyph::load(FT_Face face, FT_ULong char_code)
|
|
{
|
|
if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0)
|
|
return false;
|
|
|
|
int width = face->glyph->bitmap.width;
|
|
int height = face->glyph->bitmap.rows;
|
|
int left = face->glyph->bitmap_left;
|
|
int top = face->glyph->bitmap_top;
|
|
m_advance = round_up_26_6(face->glyph->advance.x);
|
|
uint8_t * texture = new uint8_t[width * height];
|
|
for (int i = 0; i < height; i++)
|
|
{
|
|
memcpy(&texture[width * i],
|
|
&face->glyph->bitmap.buffer[width * (height - i - 1)],
|
|
width);
|
|
}
|
|
|
|
m_texture = new GLTexture();
|
|
m_texture->create(width, height);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_ALPHA, GL_UNSIGNED_BYTE, texture);
|
|
delete[] texture;
|
|
|
|
float s_max = width / (float)m_texture->get_width();
|
|
float t_max = height / (float)m_texture->get_height();
|
|
GLfloat box[4][4] = {
|
|
{(GLfloat)left, (GLfloat)(top - height),
|
|
0.0, 0.0},
|
|
{(GLfloat)(left + width), (GLfloat)(top - height),
|
|
s_max, 0.0},
|
|
{(GLfloat)left, (GLfloat)top,
|
|
0.0, t_max},
|
|
{(GLfloat)(left + width), (GLfloat)top,
|
|
s_max, t_max},
|
|
};
|
|
glGenBuffers(1, &m_vbo_id);
|
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id);
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof(box), box, GL_STATIC_DRAW);
|
|
|
|
m_loaded = true;
|
|
return true;
|
|
}
|
|
|
|
void Font_RenderGlyph(VALUE font_obj, unsigned long char_code)
|
|
{
|
|
Font * font;
|
|
Data_Get_Struct(font_obj, Font, font);
|
|
GlyphRef g = get_glyph(font, char_code);
|
|
if (g != NULL)
|
|
{
|
|
g->render();
|
|
}
|
|
}
|