Skip to content

Instantly share code, notes, and snippets.

@jokertarot
Created November 21, 2013 15:43
Show Gist options
  • Save jokertarot/7583938 to your computer and use it in GitHub Desktop.
Save jokertarot/7583938 to your computer and use it in GitHub Desktop.
How to render color emoji font with FreeType 2.5
// = Requirements: freetype 2.5, libpng, libicu, libz, libzip2
// = How to compile:
// % export CXXFLAGS=`pkg-config --cflags freetype2 libpng`
// % export LDFLAGS=`pkg-config --libs freetype2 libpng`
// % clang++ -o clfontpng -static $(CXXFLAGS) clfontpng.cc $(LDFLAGS) \
// -licuuc -lz -lbz2
#include <cassert>
#include <cctype>
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <stdio.h>
#include <unicode/umachine.h>
#include <unicode/utf.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H
#define PNG_SKIP_SETJMP_CHECK
#include <png.h>
namespace {
const char* kDefaultOutputFile = "out.png";
const int kBytesPerPixel = 4; // RGBA
const int kDefaultPixelSize = 128;
const int kSpaceWidth = kDefaultPixelSize / 2;
FT_Library gFtLibrary;
// Only support horizontal direction.
class DrawContext {
public:
DrawContext()
: pos_(0), width_(0), height_(0) {}
uint8_t* Bitmap() { return &bitmap_[0]; }
const uint32_t Width() const { return width_; }
const uint32_t Height() const { return height_; }
void SetSize(int width, int height) {
width_ = width;
height_ = height;
int size = width * height * kBytesPerPixel;
bitmap_.resize(size);
bitmap_.assign(size, 0x00);
}
void Advance(int dx) { pos_ += dx; }
uint8_t* GetDrawPosition(int row) {
uint32_t index =(row * width_ + pos_) * kBytesPerPixel;
assert(index < bitmap_.size());
return &bitmap_[index];
}
private:
DrawContext(const DrawContext&) = delete;
DrawContext& operator=(const DrawContext&) = delete;
uint32_t pos_;
uint32_t width_;
uint32_t height_;
std::vector<uint8_t> bitmap_;
};
struct FaceOptions {
int pixel_size;
int load_flags;
FT_Render_Mode render_mode;
FaceOptions()
: pixel_size(kDefaultPixelSize)
, load_flags(0), render_mode(FT_RENDER_MODE_NORMAL) {}
};
class FreeTypeFace {
public:
FreeTypeFace(const std::string& font_file)
: font_file_(font_file)
, options_()
, face_(nullptr)
{
error_ = FT_New_Face(gFtLibrary, font_file_.c_str(), 0, &face_);
if (error_) {
face_ = nullptr;
return;
}
if (IsColorEmojiFont())
SetupColorFont();
else
SetupNormalFont();
}
~FreeTypeFace() {
if (face_)
FT_Done_Face(face_);
}
FreeTypeFace(FreeTypeFace&& rhs)
: font_file_(rhs.font_file_)
, options_(rhs.options_)
, face_(rhs.face_)
, error_(rhs.error_)
{
rhs.face_ = nullptr;
}
bool CalculateBox(uint32_t codepoint, uint32_t& width, uint32_t& height) {
if (!RenderGlyph(codepoint))
return false;
width += (face_->glyph->advance.x >> 6);
height = std::max(
height, static_cast<uint32_t>(face_->glyph->metrics.height >> 6));
return true;
}
bool DrawCodepoint(DrawContext& context, uint32_t codepoint) {
if (!RenderGlyph(codepoint))
return false;
printf("U+%08X -> %s\n", codepoint, font_file_.c_str());
return DrawBitmap(context, face_->glyph);
}
int Error() const { return error_; }
private:
FreeTypeFace(const FreeTypeFace&) = delete;
FreeTypeFace& operator=(const FreeTypeFace&) = delete;
bool RenderGlyph(uint32_t codepoint) {
if (!face_)
return false;
uint32_t glyph_index = FT_Get_Char_Index(face_, codepoint);
if (glyph_index == 0)
return false;
error_ = FT_Load_Glyph(face_, glyph_index, options_.load_flags);
if (error_)
return false;
error_ = FT_Render_Glyph(face_->glyph, options_.render_mode);
if (error_)
return false;
return true;
}
bool IsColorEmojiFont() {
static const uint32_t tag = FT_MAKE_TAG('C', 'B', 'D', 'T');
unsigned long length = 0;
FT_Load_Sfnt_Table(face_, tag, 0, nullptr, &length);
if (length) {
std::cout << font_file_ << " is color font" << std::endl;
return true;
}
return false;
}
void SetupNormalFont() {
error_ = FT_Set_Pixel_Sizes(face_, 0, options_.pixel_size);
}
void SetupColorFont() {
options_.load_flags |= FT_LOAD_COLOR;
if (face_->num_fixed_sizes == 0)
return;
int best_match = 0;
int diff = std::abs(options_.pixel_size - face_->available_sizes[0].width);
for (int i = 1; i < face_->num_fixed_sizes; ++i) {
int ndiff =
std::abs(options_.pixel_size - face_->available_sizes[i].width);
if (ndiff < diff) {
best_match = i;
diff = ndiff;
}
}
error_ = FT_Select_Size(face_, best_match);
}
bool DrawBitmap(DrawContext& context, FT_GlyphSlot slot) {
int pixel_mode = slot->bitmap.pixel_mode;
if (pixel_mode == FT_PIXEL_MODE_BGRA)
DrawColorBitmap(context, slot);
else
DrawNormalBitmap(context, slot);
context.Advance(slot->advance.x >> 6);
return true;
}
void DrawColorBitmap(DrawContext& context, FT_GlyphSlot slot) {
uint8_t* src = slot->bitmap.buffer;
// FIXME: Should use metrics for drawing. (e.g. calculate baseline)
int yoffset = context.Height() - slot->bitmap.rows;
for (int y = 0; y < slot->bitmap.rows; ++y) {
uint8_t* dest = context.GetDrawPosition(y + yoffset);
for (int x = 0; x < slot->bitmap.width; ++x) {
uint8_t b = *src++, g = *src++, r = *src++, a = *src++;
*dest++ = r; *dest++ = g; *dest++ = b; *dest++ = a;
}
}
}
void DrawNormalBitmap(DrawContext& context, FT_GlyphSlot slot) {
uint8_t* src = slot->bitmap.buffer;
// FIXME: Same as DrawColorBitmap()
int yoffset = context.Height() - slot->bitmap.rows;
for (int y = 0; y < slot->bitmap.rows; ++y) {
uint8_t* dest = context.GetDrawPosition(y + yoffset);
for (int x = 0; x < slot->bitmap.width; ++x) {
*dest++ = 255 - *src;
*dest++ = 255 - *src;
*dest++ = 255 - *src;
*dest++ = *src; // Alpha
++src;
}
}
}
std::string font_file_;
FaceOptions options_;
FT_Face face_;
int error_;
};
class FontList {
typedef std::vector<std::unique_ptr<FreeTypeFace>> FaceList;
public:
FontList() {}
void AddFont(const std::string& font_file) {
auto face = std::unique_ptr<FreeTypeFace>(new FreeTypeFace(font_file));
face_list_.push_back(std::move(face));
}
void CalculateBox(uint32_t codepoint, uint32_t& width, uint32_t& height) {
static const uint32_t kSpace = 0x20;
if (codepoint == kSpace) {
width += kSpaceWidth;
} else {
for (auto& face : face_list_) {
if (face->CalculateBox(codepoint, width, height))
return;
}
}
}
void DrawCodepoint(DrawContext& context, uint32_t codepoint) {
for (auto& face : face_list_) {
if (face->DrawCodepoint(context, codepoint))
return;
}
std::cerr << "Missing glyph for codepoint: " << codepoint << std::endl;
}
private:
FontList(const FontList&) = delete;
FontList& operator=(const FontList&) = delete;
FaceList face_list_;
};
class PngWriter {
public:
PngWriter(const std::string& outfile)
: outfile_(outfile), png_(nullptr), info_(nullptr)
{
fp_ = fopen(outfile_.c_str(), "wb");
if (!fp_) {
std::cerr << "Failed to open: " << outfile_ << std::endl;
Cleanup();
return;
}
png_ = png_create_write_struct(
PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_) {
std::cerr << "Failed to create PNG file" << std::endl;
Cleanup();
return;
}
info_ = png_create_info_struct(png_);
if (!info_) {
std::cerr << "Failed to create PNG file" << std::endl;
Cleanup();
return;
}
}
~PngWriter() { Cleanup(); }
bool Write(uint8_t* rgba, int width, int height) {
static const int kDepth = 8;
if (!png_) {
std::cerr << "Writer is not initialized" << std::endl;
return false;
}
if (setjmp(png_jmpbuf(png_))) {
std::cerr << "Failed to write PNG" << std::endl;
Cleanup();
return false;
}
png_set_IHDR(png_, info_, width, height, kDepth,
PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_init_io(png_, fp_);
png_byte** row_pointers =
static_cast<png_byte**>(png_malloc(png_, height * sizeof(png_byte*)));
uint8_t* src = rgba;
for (int y = 0; y < height; ++y) {
png_byte* row =
static_cast<png_byte*>(png_malloc(png_, width * kBytesPerPixel));
row_pointers[y] = row;
for (int x = 0; x < width; ++x) {
*row++ = *src++;
*row++ = *src++;
*row++ = *src++;
*row++ = *src++;
}
assert(row - row_pointers[y] == width * kBytesPerPixel);
}
assert(src - rgba == width * height * kBytesPerPixel);
png_set_rows(png_, info_, row_pointers);
png_write_png(png_, info_, PNG_TRANSFORM_IDENTITY, 0);
for (int y = 0; y < height; y++)
png_free(png_, row_pointers[y]);
png_free(png_, row_pointers);
Cleanup();
return true;
}
private:
PngWriter(const PngWriter&) = delete;
PngWriter operator=(const PngWriter&) = delete;
void Cleanup() {
if (fp_) { fclose(fp_); }
if (png_) png_destroy_write_struct(&png_, &info_);
fp_ = nullptr; png_ = nullptr; info_ = nullptr;
}
std::string outfile_;
FILE* fp_;
png_structp png_;
png_infop info_;
char* rgba_;
uint32_t width_;
uint32_t height_;
};
class App {
public:
void AddFont(const std::string& font_file) { font_list_.AddFont(font_file); }
bool SetText(const char* text) { return UTF8ToCodepoint(text); }
bool Execute() {
CalculateImageSize();
Draw();
return Output();
}
private:
bool UTF8ToCodepoint(const char* text) {
int32_t i = 0, length = strlen(text), c;
while (i < length) {
U8_NEXT(text, i, length, c);
if (c < 0) {
std::cerr << "Invalid input text" << std::endl;
return false;
}
codepoints_.push_back(c);
}
return true;
}
void CalculateImageSize() {
uint32_t width = 0, height = 0;
for (auto c : codepoints_)
font_list_.CalculateBox(c, width, height);
printf("width: %u, height: %u\n", width, height);
draw_context_.SetSize(width, height);
}
void Draw() {
for (auto c : codepoints_)
font_list_.DrawCodepoint(draw_context_, c);
}
bool Output() {
PngWriter writer(kDefaultOutputFile);
return writer.Write(draw_context_.Bitmap(),
draw_context_.Width(),
draw_context_.Height());
}
std::vector<uint32_t> codepoints_;
FontList font_list_;
DrawContext draw_context_;
};
bool Init() {
int error = FT_Init_FreeType(&gFtLibrary);
if (error) {
std::cerr << "Failed to initialize freetype" << std::endl;
return error;
}
return error == 0;
}
void Finish() {
FT_Done_FreeType(gFtLibrary);
}
void Usage() {
std::cout
<< "Usage: clfontpng font1.ttf [font2.ttf ...] text"
<< std::endl;
std::exit(1);
}
bool ParseArgs(App& app, int argc, char** argv) {
if (argc < 2)
return false;
for (int i = 1; i < argc - 1; ++i)
app.AddFont(argv[i]);
return app.SetText(argv[argc - 1]);
}
bool Start(int argc, char** argv) {
App app;
if (!ParseArgs(app, argc, argv))
Usage();
return app.Execute();
}
} // namespace
int main(int argc, char** argv) {
if (!Init())
std::exit(1);
bool success = Start(argc, argv);
Finish();
return success ? 0 : 1;
}
@Rickodesea
Copy link

Thanks for your example. It helped me out a lot.

With your example and some research, I have developed a more simplier (and generic) way to load color emoji with freetype and OpenGL. You can just add this (in appropriate location of course) to your current code base that you already use to load normal fonts.

//Check for the correct way to size your fonts
if (FT_HAS_FIXED_SIZES(face)){
	FT_Select_Size(face, 0); //just use the first one
} else {
	//The usual way to set the size
}

//Assign the appropriate load flags
int loadFlag;
if( FT_HAS_COLOR(face)){
	loadFlag = FT_LOAD_COLOR | FT_LOAD_DEFAULT;
} else {
	loadFlag = FT_LOAD_DEFAULT;
}

//Loading and generating character font uses the same usual code
//...

//Generate the appropriate texture in OpenGL
if((loadFlag & FT_LOAD_COLOR) == 1) {
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RGBA,
		width,
		height,
		0,
		GL_BGRA,
		GL_UNSIGNED_BYTE,
		face->glyph->bitmap.buffer
	);
} else {
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		GL_RED,
		width,
		height,
		0,
		GL_RED,
		GL_UNSIGNED_BYTE,
		face->glyph->bitmap.buffer
	);
}

That's it in a nutshell.

@brimston3
Copy link

I fixed the trailing EOL spacing and adjusted the glyph placement to use a calculated baseline from font metrics.

https://gist.github.com/brimston3/998330f32594bb00af50fe9cc37b74c6

However, by fixing the baseline, the glyphs take up a lot more space if you want to use it for say, generating a glyph atlas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment