Extra2D/tools/msdf_font_builder/msdf_font_builder.cpp

589 lines
16 KiB
C++
Raw Normal View History

#include "msdf_font_builder.h"
#include <msdfgen/msdfgen.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cmath>
#include <map>
namespace extra2d {
namespace tools {
using namespace msdfgen;
/**
* @brief Freetype
*/
struct OutlineContext {
Shape* shape;
Contour* currentContour;
Point2 lastPoint;
bool hasLastPoint;
std::map<char32_t, double> advanceMap;
};
/**
* @brief MSDF
*/
struct MSDFFontBuilder::Impl {
FT_Library ftLibrary = nullptr;
FT_Face ftFace = nullptr;
OutlineContext outlineCtx;
struct GlyphBitmap {
char32_t codepoint;
Bitmap<float, 3> bitmap;
double advance;
double l, b, r, t;
double pl, pb, pr, pt;
};
std::vector<GlyphBitmap> glyphBitmaps;
};
/**
* @brief Freetype
*/
static int ftMoveTo(const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
ctx->currentContour = &ctx->shape->addContour();
ctx->lastPoint = Point2(to->x / 64.0, to->y / 64.0);
ctx->hasLastPoint = true;
return 0;
}
/**
* @brief Freetype 线
*/
static int ftLineTo(const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
/**
* @brief Freetype 线
*/
static int ftConicTo(const FT_Vector* control, const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 controlPoint(control->x / 64.0, control->y / 64.0);
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, controlPoint, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
/**
* @brief Freetype 线
*/
static int ftCubicTo(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 cp1(control1->x / 64.0, control1->y / 64.0);
Point2 cp2(control2->x / 64.0, control2->y / 64.0);
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, cp1, cp2, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
MSDFFontBuilder::MSDFFontBuilder() : impl_(new Impl()) {
}
MSDFFontBuilder::~MSDFFontBuilder() {
if (impl_) {
if (impl_->ftFace) {
FT_Done_Face(impl_->ftFace);
}
if (impl_->ftLibrary) {
FT_Done_FreeType(impl_->ftLibrary);
}
delete impl_;
}
}
void MSDFFontBuilder::setInputFont(const std::string& path) {
inputFont_ = path;
}
void MSDFFontBuilder::setOutputPath(const std::string& path) {
outputPath_ = path;
}
void MSDFFontBuilder::setCharset(const std::string& charset) {
charset_ = charset;
}
void MSDFFontBuilder::setCharsetFile(const std::string& path) {
charsetFile_ = path;
}
void MSDFFontBuilder::setFontSize(int size) {
fontSize_ = size;
}
void MSDFFontBuilder::setPxRange(float range) {
pxRange_ = range;
}
void MSDFFontBuilder::setAtlasSize(int width, int height) {
atlasWidth_ = width;
atlasHeight_ = height;
}
bool MSDFFontBuilder::build() {
if (inputFont_.empty()) {
error_ = "Input font path not set";
return false;
}
if (outputPath_.empty()) {
error_ = "Output path not set";
return false;
}
if (!loadCharset()) {
return false;
}
FT_Error error = FT_Init_FreeType(&impl_->ftLibrary);
if (error) {
error_ = "Failed to initialize FreeType";
return false;
}
error = FT_New_Face(impl_->ftLibrary, inputFont_.c_str(), 0, &impl_->ftFace);
if (error) {
error_ = "Failed to load font: " + inputFont_;
return false;
}
error = FT_Set_Char_Size(impl_->ftFace, fontSize_ * 64, fontSize_ * 64, 72, 72);
if (error) {
error_ = "Failed to set font size";
return false;
}
if (!generateAllGlyphs()) {
return false;
}
if (!packGlyphs()) {
return false;
}
if (!savePngWithMetadata()) {
return false;
}
return true;
}
bool MSDFFontBuilder::loadCharset() {
if (!charsetFile_.empty()) {
std::ifstream file(charsetFile_);
if (!file) {
error_ = "Failed to open charset file: " + charsetFile_;
return false;
}
std::stringstream buffer;
buffer << file.rdbuf();
charset_ = buffer.str();
}
if (charset_.empty()) {
charset_ = " !\"#$%&'()*+,-./0123456789:;<=>?@"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
"abcdefghijklmnopqrstuvwxyz{|}~";
}
return true;
}
/**
* @brief Freetype msdfgen Shape
*/
static bool loadGlyphShape(FT_Face face, char32_t codepoint, Shape& shape, double& advance) {
FT_UInt glyphIndex = FT_Get_Char_Index(face, codepoint);
if (glyphIndex == 0) {
return false;
}
FT_Error error = FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_SCALE);
if (error) {
return false;
}
advance = face->glyph->metrics.horiAdvance / 64.0;
FT_Outline* outline = &face->glyph->outline;
if (outline->n_contours == 0) {
return false;
}
OutlineContext ctx;
ctx.shape = &shape;
ctx.currentContour = nullptr;
ctx.hasLastPoint = false;
FT_Outline_Funcs funcs;
funcs.move_to = ftMoveTo;
funcs.line_to = ftLineTo;
funcs.conic_to = ftConicTo;
funcs.cubic_to = ftCubicTo;
funcs.shift = 0;
funcs.delta = 0;
error = FT_Outline_Decompose(outline, &funcs, &ctx);
if (error) {
return false;
}
return true;
}
bool MSDFFontBuilder::generateAllGlyphs() {
impl_->glyphBitmaps.clear();
double unitsPerEM = impl_->ftFace->units_per_EM;
if (unitsPerEM == 0) {
unitsPerEM = 1000.0;
}
double scale = fontSize_ / unitsPerEM;
for (char c : charset_) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
Shape shape;
double glyphAdvance = 0.0;
if (!loadGlyphShape(impl_->ftFace, codepoint, shape, glyphAdvance)) {
continue;
}
shape.normalize();
edgeColoringSimple(shape, 3.0);
Shape::Bounds bounds = shape.getBounds();
double l = bounds.l;
double b = bounds.b;
double r = bounds.r;
double t = bounds.t;
if (l >= r || b >= t) {
continue;
}
double pl = l * scale;
double pb = b * scale;
double pr = r * scale;
double pt = t * scale;
double range = pxRange_ / scale;
l -= range;
b -= range;
r += range;
t += range;
double width = (r - l) * scale;
double height = (t - b) * scale;
int w = static_cast<int>(std::ceil(width));
int h = static_cast<int>(std::ceil(height));
w = std::max(w, 1);
h = std::max(h, 1);
Impl::GlyphBitmap gb;
gb.codepoint = codepoint;
gb.advance = glyphAdvance * scale;
gb.l = l;
gb.b = b;
gb.r = r;
gb.t = t;
gb.pl = pl;
gb.pb = pb;
gb.pr = pr;
gb.pt = pt;
gb.bitmap = Bitmap<float, 3>(w, h);
MSDFGeneratorConfig config;
config.overlapSupport = true;
Projection projection(Vector2(scale, scale), Vector2(-l * scale, -b * scale));
Range distanceRange(range * scale);
generateMSDF(gb.bitmap, shape, projection, distanceRange, config);
impl_->glyphBitmaps.push_back(std::move(gb));
}
return !impl_->glyphBitmaps.empty();
}
bool MSDFFontBuilder::packGlyphs() {
std::vector<int> positions(impl_->glyphBitmaps.size() * 2, 0);
std::vector<std::pair<int, int>> sizes;
for (const auto& gb : impl_->glyphBitmaps) {
sizes.emplace_back(gb.bitmap.width(), gb.bitmap.height());
}
std::vector<int> indices(sizes.size());
for (size_t i = 0; i < indices.size(); ++i) {
indices[i] = static_cast<int>(i);
}
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
return sizes[a].second > sizes[b].second;
});
int x = 0, y = 0;
int rowHeight = 0;
for (int idx : indices) {
int w = sizes[idx].first;
int h = sizes[idx].second;
if (x + w > atlasWidth_) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
if (y + h > atlasHeight_) {
error_ = "Atlas size too small for all glyphs";
return false;
}
positions[idx * 2] = x;
positions[idx * 2 + 1] = y;
x += w;
rowHeight = std::max(rowHeight, h);
}
Bitmap<float, 3> atlas(atlasWidth_, atlasHeight_);
for (int py = 0; py < atlasHeight_; ++py) {
for (int px = 0; px < atlasWidth_; ++px) {
float* pixel = atlas(px, py);
pixel[0] = 0.0f;
pixel[1] = 0.0f;
pixel[2] = 0.0f;
}
}
for (size_t i = 0; i < impl_->glyphBitmaps.size(); ++i) {
const auto& gb = impl_->glyphBitmaps[i];
int px = positions[i * 2];
int py = positions[i * 2 + 1];
for (int gy = 0; gy < gb.bitmap.height(); ++gy) {
for (int gx = 0; gx < gb.bitmap.width(); ++gx) {
int targetY = atlasHeight_ - 1 - (py + gy);
float* pixel = atlas(px + gx, targetY);
const float* srcPixel = gb.bitmap(gx, gy);
pixel[0] = srcPixel[0];
pixel[1] = srcPixel[1];
pixel[2] = srcPixel[2];
}
}
GlyphData glyph;
glyph.codepoint = gb.codepoint;
glyph.advance = static_cast<float>(gb.advance);
glyph.left = static_cast<double>(px);
glyph.top = static_cast<double>(atlasHeight_ - py - gb.bitmap.height());
glyph.right = static_cast<double>(px + gb.bitmap.width());
glyph.bottom = static_cast<double>(atlasHeight_ - py);
glyph.uvMin.x = static_cast<float>(glyph.left / atlasWidth_);
glyph.uvMin.y = static_cast<float>(glyph.top / atlasHeight_);
glyph.uvMax.x = static_cast<float>(glyph.right / atlasWidth_);
glyph.uvMax.y = static_cast<float>(glyph.bottom / atlasHeight_);
glyph.size.x = static_cast<float>(gb.pr - gb.pl);
glyph.size.y = static_cast<float>(gb.pt - gb.pb);
glyph.bearing.x = static_cast<float>(gb.pl);
glyph.bearing.y = static_cast<float>(gb.pt);
metadata_.glyphs.push_back(glyph);
}
metadata_.fontSize = fontSize_;
metadata_.pxRange = pxRange_;
metadata_.atlasWidth = atlasWidth_;
metadata_.atlasHeight = atlasHeight_;
metadata_.lineHeight = static_cast<int>(fontSize_ * 1.25);
metadata_.baseline = static_cast<int>(fontSize_ * 0.25);
impl_->glyphBitmaps.clear();
impl_->glyphBitmaps.push_back({0, std::move(atlas), 0, 0, 0, 0, 0, 0, 0, 0, 0});
return true;
}
bool MSDFFontBuilder::savePngWithMetadata() {
if (impl_->glyphBitmaps.empty()) {
error_ = "No atlas generated";
return false;
}
const auto& atlas = impl_->glyphBitmaps[0].bitmap;
std::vector<uint8_t> rgbaData(atlasWidth_ * atlasHeight_ * 4);
for (int y = 0; y < atlasHeight_; ++y) {
for (int x = 0; x < atlasWidth_; ++x) {
const float* pixel = atlas(x, y);
int idx = (y * atlasWidth_ + x) * 4;
rgbaData[idx + 0] = static_cast<uint8_t>(std::clamp(pixel[0] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 1] = static_cast<uint8_t>(std::clamp(pixel[1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 2] = static_cast<uint8_t>(std::clamp(pixel[2] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 3] = 255;
}
}
std::string jsonStr = generateMetadataJson();
std::vector<uint8_t> textChunk;
std::string keyword = "msdf";
textChunk.insert(textChunk.end(), keyword.begin(), keyword.end());
textChunk.push_back(0);
textChunk.insert(textChunk.end(), jsonStr.begin(), jsonStr.end());
uint32_t length = static_cast<uint32_t>(textChunk.size());
std::vector<uint8_t> lengthBytes(4);
lengthBytes[0] = (length >> 24) & 0xFF;
lengthBytes[1] = (length >> 16) & 0xFF;
lengthBytes[2] = (length >> 8) & 0xFF;
lengthBytes[3] = length & 0xFF;
std::vector<uint8_t> crcData;
crcData.insert(crcData.end(), {'t', 'E', 'X', 't'});
crcData.insert(crcData.end(), textChunk.begin(), textChunk.end());
uint32_t crc = 0xFFFFFFFF;
for (uint8_t byte : crcData) {
crc ^= byte;
for (int i = 0; i < 8; ++i) {
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
}
}
crc ^= 0xFFFFFFFF;
std::vector<uint8_t> crcBytes(4);
crcBytes[0] = (crc >> 24) & 0xFF;
crcBytes[1] = (crc >> 16) & 0xFF;
crcBytes[2] = (crc >> 8) & 0xFF;
crcBytes[3] = crc & 0xFF;
std::string tempPng = outputPath_ + ".tmp.png";
if (!stbi_write_png(tempPng.c_str(), atlasWidth_, atlasHeight_, 4, rgbaData.data(), atlasWidth_ * 4)) {
error_ = "Failed to save PNG: " + tempPng;
return false;
}
std::ifstream inputFile(tempPng, std::ios::binary);
if (!inputFile) {
error_ = "Failed to open temporary PNG file";
return false;
}
std::vector<uint8_t> pngData((std::istreambuf_iterator<char>(inputFile)),
std::istreambuf_iterator<char>());
inputFile.close();
std::vector<uint8_t> newPng;
newPng.insert(newPng.end(), pngData.begin(), pngData.begin() + 8);
size_t pos = 8;
bool inserted = false;
while (pos < pngData.size()) {
if (pos + 8 > pngData.size()) break;
std::string chunkType(pngData.begin() + pos + 4, pngData.begin() + pos + 8);
if (!inserted && chunkType != "IHDR") {
newPng.insert(newPng.end(), lengthBytes.begin(), lengthBytes.end());
newPng.insert(newPng.end(), {'t', 'E', 'X', 't'});
newPng.insert(newPng.end(), textChunk.begin(), textChunk.end());
newPng.insert(newPng.end(), crcBytes.begin(), crcBytes.end());
inserted = true;
}
uint32_t chunkLen = (static_cast<uint32_t>(pngData[pos]) << 24) |
(static_cast<uint32_t>(pngData[pos + 1]) << 16) |
(static_cast<uint32_t>(pngData[pos + 2]) << 8) |
static_cast<uint32_t>(pngData[pos + 3]);
size_t chunkEnd = pos + 12 + chunkLen;
newPng.insert(newPng.end(), pngData.begin() + pos, pngData.begin() + chunkEnd);
pos = chunkEnd;
}
std::ofstream outputFile(outputPath_, std::ios::binary);
if (!outputFile) {
error_ = "Failed to write PNG file: " + outputPath_;
return false;
}
outputFile.write(reinterpret_cast<const char*>(newPng.data()), newPng.size());
std::remove(tempPng.c_str());
return true;
}
std::string MSDFFontBuilder::generateMetadataJson() {
nlohmann::json j;
j["size"] = metadata_.fontSize;
j["pxRange"] = metadata_.pxRange;
j["lineHeight"] = metadata_.lineHeight;
j["baseline"] = metadata_.baseline;
j["atlas"]["width"] = metadata_.atlasWidth;
j["atlas"]["height"] = metadata_.atlasHeight;
for (const auto& glyph : metadata_.glyphs) {
nlohmann::json glyphJson;
glyphJson["unicode"] = static_cast<uint32_t>(glyph.codepoint);
glyphJson["advance"] = glyph.advance;
glyphJson["atlasBounds"]["left"] = glyph.left;
glyphJson["atlasBounds"]["bottom"] = glyph.bottom;
glyphJson["atlasBounds"]["right"] = glyph.right;
glyphJson["atlasBounds"]["top"] = glyph.top;
glyphJson["planeBounds"]["left"] = glyph.bearing.x;
glyphJson["planeBounds"]["bottom"] = glyph.bearing.y - glyph.size.y;
glyphJson["planeBounds"]["right"] = glyph.bearing.x + glyph.size.x;
glyphJson["planeBounds"]["top"] = glyph.bearing.y;
j["glyphs"].push_back(glyphJson);
}
return j.dump();
}
} // namespace tools
} // namespace extra2d