-
-
Save jokertarot/7583938 to your computer and use it in GitHub Desktop.
// = 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; | |
} |
Make sure that your FreeType is compiled with #define FT_CONFIG_OPTION_USE_PNG
. This is located in ftopen.h
and without it you will be unable to follow the details above.
How to generate a emoji png ? Could you please offer an example cmd line?
got it, need to copy the emoji to the terminal as the cmd line params like this: ./clfontpng NotoColorEmoji.ttf [copyed emoji]
Hello @jokertarot
I'm trying to compile your code, but I ran into many problems following is the link to my issue on freetype git:
metafloor/freetype-js#4
any rcommendations?
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.
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.
great!!