#include "msdf_font_builder.h" #include #include #include FT_FREETYPE_H #include FT_OUTLINE_H #define STB_IMAGE_WRITE_IMPLEMENTATION #include #include #include #include #include #include namespace extra2d { namespace tools { using namespace msdfgen; /** * @brief Freetype 轮廓转换上下文 */ struct OutlineContext { Shape* shape; Contour* currentContour; Point2 lastPoint; bool hasLastPoint; std::map advanceMap; }; /** * @brief MSDF 字体构建器实现 */ struct MSDFFontBuilder::Impl { FT_Library ftLibrary = nullptr; FT_Face ftFace = nullptr; OutlineContext outlineCtx; struct GlyphBitmap { char32_t codepoint; Bitmap bitmap; double advance; double l, b, r, t; double pl, pb, pr, pt; }; std::vector glyphBitmaps; }; /** * @brief Freetype 移动到点回调函数 */ static int ftMoveTo(const FT_Vector* to, void* user) { OutlineContext* ctx = static_cast(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(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(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(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(static_cast(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(std::ceil(width)); int h = static_cast(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(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 positions(impl_->glyphBitmaps.size() * 2, 0); std::vector> sizes; for (const auto& gb : impl_->glyphBitmaps) { sizes.emplace_back(gb.bitmap.width(), gb.bitmap.height()); } std::vector indices(sizes.size()); for (size_t i = 0; i < indices.size(); ++i) { indices[i] = static_cast(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 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(gb.advance); glyph.left = static_cast(px); glyph.top = static_cast(atlasHeight_ - py - gb.bitmap.height()); glyph.right = static_cast(px + gb.bitmap.width()); glyph.bottom = static_cast(atlasHeight_ - py); glyph.uvMin.x = static_cast(glyph.left / atlasWidth_); glyph.uvMin.y = static_cast(glyph.top / atlasHeight_); glyph.uvMax.x = static_cast(glyph.right / atlasWidth_); glyph.uvMax.y = static_cast(glyph.bottom / atlasHeight_); glyph.size.x = static_cast(gb.pr - gb.pl); glyph.size.y = static_cast(gb.pt - gb.pb); glyph.bearing.x = static_cast(gb.pl); glyph.bearing.y = static_cast(gb.pt); metadata_.glyphs.push_back(glyph); } metadata_.fontSize = fontSize_; metadata_.pxRange = pxRange_; metadata_.atlasWidth = atlasWidth_; metadata_.atlasHeight = atlasHeight_; metadata_.lineHeight = static_cast(fontSize_ * 1.25); metadata_.baseline = static_cast(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 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(std::clamp(pixel[0] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); rgbaData[idx + 1] = static_cast(std::clamp(pixel[1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); rgbaData[idx + 2] = static_cast(std::clamp(pixel[2] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f); rgbaData[idx + 3] = 255; } } std::string jsonStr = generateMetadataJson(); std::vector 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(textChunk.size()); std::vector lengthBytes(4); lengthBytes[0] = (length >> 24) & 0xFF; lengthBytes[1] = (length >> 16) & 0xFF; lengthBytes[2] = (length >> 8) & 0xFF; lengthBytes[3] = length & 0xFF; std::vector 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 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 pngData((std::istreambuf_iterator(inputFile)), std::istreambuf_iterator()); inputFile.close(); std::vector 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(pngData[pos]) << 24) | (static_cast(pngData[pos + 1]) << 16) | (static_cast(pngData[pos + 2]) << 8) | static_cast(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(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(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