Created
November 21, 2013 15:43
-
-
Save jokertarot/7583938 to your computer and use it in GitHub Desktop.
How to render color emoji font with FreeType 2.5
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// = 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.