-
-
Save brimston3/998330f32594bb00af50fe9cc37b74c6 to your computer and use it in GitHub Desktop.
How to render color emoji font with FreeType 2.12
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, | |
// = with freetype >=2.10, libbrotli | |
// = How to compile: | |
// % export CXXFLAGS=`pkg-config --cflags freetype2 libpng` | |
// % export LDFLAGS=`pkg-config --libs freetype2 libpng libbrotlidec libbrotlicommon` | |
// % clang++ -o clfontpng -static $(CXXFLAGS) clfontpng.cc $(LDFLAGS) \ | |
// -licuuc -lz -lbz2 | |
#include <cassert> | |
#include <cctype> | |
#include <iostream> | |
#include <memory> | |
#include <vector> | |
#include <string> | |
#include <cstring> | |
#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; | |
FT_Library gFtLibrary; | |
// Only support horizontal direction. | |
class DrawContext { | |
public: | |
DrawContext() | |
: pos_(0), width_(0), height_(0), descent_(0) {} | |
uint8_t* Bitmap() { return &bitmap_[0]; } | |
const uint32_t Width() const { return width_; } | |
const uint32_t Height() const { return height_; } //!< max rise above baseline | |
const uint32_t Descent() const { return descent_; } //!< max descent below baseline | |
const uint32_t TotalHeight() const { return height_ + descent_; } //!< This is the cumulative height of the draw buffer, max height + max descent | |
void SetSize(int width, int height, int descent) { | |
width_ = width; | |
height_ = height; | |
descent_ = descent; | |
int size = width * (height + descent) * 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_; | |
uint32_t descent_; | |
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, uint32_t& descent) { | |
if (!RenderGlyph(codepoint)) | |
return false; | |
width += (face_->glyph->advance.x >> 6); | |
// The original doesn't capture glyphs that go below the baseline, see: | |
// https://stackoverflow.com/a/62375274 | |
height = std::max( | |
height, static_cast<uint32_t>(face_->glyph->metrics.height >> 6)); | |
descent = std::max( | |
descent, static_cast<uint32_t>((face_->glyph->metrics.height >> 6) - face_->glyph->bitmap_top)); | |
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; | |
int yoffset = context.Height() - slot->bitmap_top; | |
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; | |
int yoffset = context.Height() - slot->bitmap_top; | |
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, uint32_t& descent) { | |
for (auto& face : face_list_) { | |
if (face->CalculateBox(codepoint, width, height, descent)) | |
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: | |
App() : | |
png_file_(kDefaultOutputFile) {} | |
void SetOutput(const std::string& png_file) { png_file_ = png_file; } | |
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, descent = 0; | |
for (auto c : codepoints_) | |
font_list_.CalculateBox(c, width, height, descent); | |
printf("width: %u, height: %u, descent: %u\n", width, height, descent); | |
draw_context_.SetSize(width, height, descent); | |
} | |
void Draw() { | |
for (auto c : codepoints_) | |
font_list_.DrawCodepoint(draw_context_, c); | |
} | |
bool Output() { | |
PngWriter writer(png_file_); | |
return writer.Write(draw_context_.Bitmap(), | |
draw_context_.Width(), | |
draw_context_.TotalHeight()); | |
} | |
std::vector<uint32_t> codepoints_; | |
FontList font_list_; | |
DrawContext draw_context_; | |
std::string png_file_; | |
}; | |
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 [-o output.png] 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) { | |
if (!std::strcmp("-o", argv[i])) { | |
if (++i >= argc - 1) | |
return false; | |
else | |
app.SetOutput(argv[i]); | |
} else { | |
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