/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ #if __has_include("msdfgen.h") #include "FontAtlasGenerator.hpp" #include "Base/Logger.hpp" #include "Scene/AtlasMetadata.hpp" #include #include #include #define STBI_MSC_SECURE_CRT #define STB_IMAGE_WRITE_IMPLEMENTATION #include #include #include FT_FREETYPE_H #include #include namespace OpenVulkano::Scene { using namespace msdfgen; using namespace msdf_atlas; FontAtlasGeneratorConfig FontAtlasGeneratorConfig::sdfDefaultConfig = { 42, 1.0, 5 }; FontAtlasGeneratorConfig FontAtlasGeneratorConfig::msdfDefaultConfig = { 32, 1.0, 3 }; template Charset FontAtlasGenerator::LoadAllGlyphs(const std::variant>& data) { FT_Library library; auto error = FT_Init_FreeType(&library); if (error) { throw std::runtime_error("Could not initalize freetype library\n"); } FT_Face face; if (std::holds_alternative(data)) { error = FT_New_Face(library, std::get<0>(data).c_str(), 0, &face); } else { auto& arr = std::get<1>(data); error = FT_New_Memory_Face(library, (const FT_Byte*)(arr.Data()), arr.Size(), 0, &face); } if (error == FT_Err_Unknown_File_Format) { throw std::runtime_error("Unknown font file format\n"); } else if (error) { throw std::runtime_error("Font file could not be opened or read or it's corrupted\n"); } // some fancy font without unicode charmap if (face->charmap == nullptr) { throw std::runtime_error("Selected font doesn't contain unicode charmap"); } Charset s; FT_UInt glyphIndex; FT_ULong unicode = FT_Get_First_Char(face, &glyphIndex); while (glyphIndex != 0) { s.add(unicode); unicode = FT_Get_Next_Char(face, unicode, &glyphIndex); } FT_Done_Face(face); FT_Done_FreeType(library); return s; } template void FontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const std::set& charset, const std::optional& pngOutput) { FreetypeHandle* ft; FontHandle* font; InitFreetypeFromFile(ft, font, fontFile); Charset s; std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); }); Generate(ft, font, s, pngOutput); } template FontAtlasGenerator::FontAtlasGenerator() { if constexpr (Channels == 1) m_config = FontAtlasGeneratorConfig::sdfDefaultConfig; else m_config = FontAtlasGeneratorConfig::msdfDefaultConfig; } template void FontAtlasGenerator::GenerateAtlas(const Array& fontData, int length, const std::set& charset, const std::optional& pngOutput) { FreetypeHandle* ft; FontHandle* font; InitFreetypeFromBuffer(ft, font, (const msdfgen::byte*)(fontData.Data()), length); Charset s; std::for_each(s.begin(), s.end(), [&](uint32_t unicode) { s.add(unicode); }); Generate(ft, font, s, pngOutput); } template void FontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const Charset& charset, const std::optional& pngOutput) { // TODO: dynamic atlas and add only those symbols which are not present yet in current atlas FreetypeHandle* ft; FontHandle* font; InitFreetypeFromFile(ft, font, fontFile); Generate(ft, font, charset, pngOutput); } template void FontAtlasGenerator::GenerateAtlas(const msdfgen::byte* fontData, int length, const Charset& charset, const std::optional& pngOutput) { FreetypeHandle* ft; FontHandle* font; InitFreetypeFromBuffer(ft, font, fontData, length); Generate(ft, font, charset, pngOutput); } template void FontAtlasGenerator::SaveAtlasMetadataInfo(const std::string& outputFile, bool packIntoSingleFile) const { if (m_symbols.empty()) { Logger::DATA->info("No glyphs loaded. Nothing to save."); return; } std::string fileName = outputFile; uint32_t packedFlag = packIntoSingleFile; if (packIntoSingleFile) { bool isPng = std::filesystem::path(fileName).extension() == ".png"; if (!isPng) { fileName += "_packed.png"; } else { fileName.insert(fileName.size() - 4, "_packed"); } SavePng(fileName); } std::fstream fs(fileName.c_str(), std::ios_base::out | std::ios_base::binary | (packedFlag ? std::ios_base::app : std::ios_base::trunc)); fs.write(reinterpret_cast(&m_meta), sizeof(AtlasMetadata)); uint64_t metadataBytes = sizeof(AtlasMetadata); for (const auto& [key, val] : m_symbols) { fs.write(reinterpret_cast(&key), sizeof(uint32_t)); fs.write(reinterpret_cast(&val), sizeof(GlyphInfo)); metadataBytes += sizeof(uint32_t); metadataBytes += sizeof(GlyphInfo); } fs.write(reinterpret_cast(&metadataBytes), sizeof(uint64_t)); fs.write(reinterpret_cast(&packedFlag), sizeof(uint32_t)); } template void FontAtlasGenerator::InitFreetypeFromFile(FreetypeHandle*& ft, FontHandle*& font, const std::string& fontFile) { ft = initializeFreetype(); if (!ft) { throw std::runtime_error("Failed to initialize freetype"); } font = loadFont(ft, fontFile.data()); if (!font) { deinitializeFreetype(ft); ft = nullptr; throw std::runtime_error(fmt::format("Failed to load font from file {0}", fontFile.data())); } } template void FontAtlasGenerator::InitFreetypeFromBuffer(FreetypeHandle*& ft, FontHandle*& font, const msdfgen::byte* fontData, int length) { ft = initializeFreetype(); if (!ft) { throw std::runtime_error("Failed to initialize freetype"); } font = loadFontData(ft, fontData, length); if (!font) { deinitializeFreetype(ft); ft = nullptr; throw std::runtime_error("Failed to load font data from given buffer"); } } template void FontAtlasGenerator::Generate(FreetypeHandle* ft, FontHandle* font, const Charset& chset, const std::optional& pngOutput) { m_symbols.clear(); std::vector glyphsGeometry; // FontGeometry is a helper class that loads a set of glyphs from a single font. FontGeometry fontGeometry(&glyphsGeometry); fontGeometry.loadCharset(font, 1, chset); if constexpr (Channels == 3) { const double maxCornerAngle = 3.0; for (GlyphGeometry& glyph: glyphsGeometry) glyph.edgeColoring(&msdfgen::edgeColoringByDistance, maxCornerAngle, 0); } TightAtlasPacker packer; packer.setDimensionsConstraint(DimensionsConstraint::SQUARE); int width, height; const int glyphsPerRow = std::sqrt(glyphsGeometry.size()); const int glyphSize = m_config.glyphSize; const int rowWidth = glyphSize * glyphsPerRow; packer.setDimensions(rowWidth, rowWidth); // something to play with. should not be too high. // more value - more sdf impact packer.setPixelRange(m_config.pixelRange); packer.setMiterLimit(m_config.miterLimit); packer.pack(glyphsGeometry.data(), glyphsGeometry.size()); packer.getDimensions(width, height); Generator generator; generator.resize(width, height); GeneratorAttributes attributes; generator.setAttributes(attributes); generator.setThreadCount(4); generator.generate(glyphsGeometry.data(), glyphsGeometry.size()); //const auto& storage = generator.atlasStorage(); int idx = 0; if constexpr (Channels == 3) { // store RGB as RGBA const BitmapConstRef storage = generator.atlasStorage(); msdfgen::Bitmap bitmap(width, height); msdfgen::byte* data = static_cast(bitmap); for (size_t srcPos = 0, dstPos = 0; srcPos < width * height * 3; srcPos += 3, dstPos += 4) { data[dstPos] = storage.pixels[srcPos]; data[dstPos + 1] = storage.pixels[srcPos + 1]; data[dstPos + 2] = storage.pixels[srcPos + 2]; data[dstPos + 3] = 255; } m_atlasStorage = std::move(bitmap); } else { m_atlasStorage = generator.atlasStorage(); } m_atlasTex.resolution = Math::Vector3ui(m_atlasStorage.width(), m_atlasStorage.height(), 1); m_atlasTex.textureBuffer = (msdfgen::byte*) m_atlasStorage; m_atlasTex.format = (channelsCount == 1 ? OpenVulkano::DataFormat::R8_UNORM : OpenVulkano::DataFormat::R8G8B8A8_UNORM); m_atlasTex.size = m_atlasStorage.width() * m_atlasStorage.height() * channelsCount; m_meta.lineHeight = fontGeometry.getMetrics().lineHeight; struct Bbox { double l = 0, r = 0, t = 0, b = 0; }; for (const auto& glyph: glyphsGeometry) { GlyphInfo& info = m_symbols[glyph.getCodepoint()]; const GlyphBox& glyphBox = generator.getLayout()[idx++]; Bbox glyphBaselineBbox, glyphAtlasBbox; glyph.getQuadPlaneBounds(glyphBaselineBbox.l, glyphBaselineBbox.b, glyphBaselineBbox.r, glyphBaselineBbox.t); glyph.getQuadAtlasBounds(glyphAtlasBbox.l, glyphAtlasBbox.b, glyphAtlasBbox.r, glyphAtlasBbox.t); double bearingX = glyphBox.bounds.l; double bearingY = glyphBox.bounds.t; double w = glyphBaselineBbox.r - glyphBaselineBbox.l; double h = glyphBaselineBbox.t - glyphBaselineBbox.b; double l = glyphAtlasBbox.l; double r = glyphAtlasBbox.r; double t = glyphAtlasBbox.t; double b = glyphAtlasBbox.b; info.xyz[0].x = bearingX; info.xyz[0].y = h - bearingY; info.xyz[0].z = 1; info.uv[0].x = l / m_atlasTex.resolution.x; info.uv[0].y = b / m_atlasTex.resolution.y; info.xyz[1].x = bearingX + w; info.xyz[1].y = h - bearingY; info.xyz[1].z = 1; info.uv[1].x = r / m_atlasTex.resolution.x; info.uv[1].y = b / m_atlasTex.resolution.y; info.xyz[2].x = bearingX + w; info.xyz[2].y = bearingY; //h - bearingY + h; info.xyz[2].z = 1; info.uv[2].x = r / m_atlasTex.resolution.x; info.uv[2].y = t / m_atlasTex.resolution.y; info.xyz[3].x = bearingX; info.xyz[3].y = bearingY; info.xyz[3].z = 1; info.uv[3].x = l / m_atlasTex.resolution.x; info.uv[3].y = t / m_atlasTex.resolution.y; info.advance = glyphBox.advance; } if (pngOutput && !pngOutput->empty()) { SavePng(pngOutput.value()); } destroyFont(font); deinitializeFreetype(ft); } template void FontAtlasGenerator::SavePng(const std::string& output) const { // rework here. do not pass storage . use m_atlasStorage stbi_flip_vertically_on_write(1); if (std::filesystem::path(output).extension() == ".png") { stbi_write_png(output.c_str(), m_atlasStorage.width(), m_atlasStorage.height(), channelsCount, m_atlasStorage.operator const msdfgen::byte*(), channelsCount * m_atlasStorage.width()); } else { stbi_write_png((output + ".png").c_str(), m_atlasStorage.width(), m_atlasStorage.height(), channelsCount, m_atlasStorage.operator const msdfgen::byte*(), channelsCount * m_atlasStorage.width()); } } template class FontAtlasGenerator<1>; template class FontAtlasGenerator<3>; } #endif