Introduce fonts v2 & add freetype font implementation
This commit is contained in:
parent
a4d666096e
commit
61626b9179
3 changed files with 448 additions and 2 deletions
|
|
@ -1,8 +1,13 @@
|
|||
find_package(Freetype REQUIRED)
|
||||
|
||||
file(GLOB_RECURSE PSEMEK_FONTS_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp")
|
||||
file(GLOB_RECURSE PSEMEK_FONTS_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp")
|
||||
|
||||
psemek_add_library(psemek-fonts ${PSEMEK_FONTS_HEADERS} ${PSEMEK_FONTS_SOURCES})
|
||||
target_include_directories(psemek-fonts PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_link_libraries(psemek-fonts PUBLIC psemek-util psemek-geom psemek-gfx rapidjson)
|
||||
target_include_directories(psemek-fonts PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ${FREETYPE_INCLUDE_DIRS})
|
||||
target_link_libraries(psemek-fonts PUBLIC psemek-util psemek-geom psemek-gfx rapidjson ${FREETYPE_LIBRARY})
|
||||
if(PSEMEK_GRAPHICS_API STREQUAL WEBGPU)
|
||||
target_link_libraries(psemek-fonts PUBLIC psemek-wgpu)
|
||||
endif()
|
||||
|
||||
psemek_glob_resources(psemek-fonts resources psemek/fonts/resources)
|
||||
|
|
|
|||
95
libs/fonts/include/psemek/fonts/font_v2.hpp
Normal file
95
libs/fonts/include/psemek/fonts/font_v2.hpp
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/geom/vector.hpp>
|
||||
#include <psemek/geom/box.hpp>
|
||||
#include <psemek/util/span.hpp>
|
||||
|
||||
#if defined(PSEMEK_GRAPHICS_API_OPENGL)
|
||||
#error "Fonts v2 don't support OpenGL yet"
|
||||
#elif defined(PSEMEK_GRAPHICS_API_WEBGPU)
|
||||
#include <psemek/wgpu/device.hpp>
|
||||
#include <psemek/wgpu/texture_view.hpp>
|
||||
#endif
|
||||
|
||||
#include <psemek/io/stream.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <filesystem>
|
||||
|
||||
namespace psemek::fonts
|
||||
{
|
||||
|
||||
enum class font_type
|
||||
{
|
||||
bitmap,
|
||||
sdf,
|
||||
msdf,
|
||||
};
|
||||
|
||||
#if defined(PSEMEK_GRAPHICS_API_OPENGL)
|
||||
// TODO
|
||||
#elif defined(PSEMEK_GRAPHICS_API_WEBGPU)
|
||||
using texture_type = wgpu::texture_view;
|
||||
#endif
|
||||
|
||||
struct shaped_glyph
|
||||
{
|
||||
texture_type const * texture;
|
||||
geom::box<float, 2> position;
|
||||
geom::box<float, 2> texcoords;
|
||||
};
|
||||
|
||||
struct shape_options
|
||||
{
|
||||
enum direction_t
|
||||
{
|
||||
left_to_right,
|
||||
right_to_left,
|
||||
top_to_bottom,
|
||||
bottom_to_top,
|
||||
} direction = left_to_right;
|
||||
|
||||
float scale = 1.f;
|
||||
};
|
||||
|
||||
struct font
|
||||
{
|
||||
virtual font_type type() const = 0;
|
||||
|
||||
virtual std::string_view name() const = 0;
|
||||
|
||||
virtual geom::vector<int, 2> size() const = 0;
|
||||
virtual int xheight() const = 0;
|
||||
|
||||
virtual std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options, geom::point<float, 2> & pen) = 0;
|
||||
virtual std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options, geom::point<float, 2> & pen) = 0;
|
||||
|
||||
virtual std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options)
|
||||
{
|
||||
geom::point<float, 2> pen{0.f, 0.f};
|
||||
return shape(str, options, pen);
|
||||
}
|
||||
|
||||
virtual std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options)
|
||||
{
|
||||
geom::point<float, 2> pen{0.f, 0.f};
|
||||
return shape(str, options, pen);
|
||||
}
|
||||
|
||||
virtual ~font() {}
|
||||
};
|
||||
|
||||
struct font_builder
|
||||
{
|
||||
virtual std::string_view name() const = 0;
|
||||
|
||||
virtual std::unique_ptr<font> create(font_type type, int size) = 0;
|
||||
|
||||
virtual ~font_builder() {}
|
||||
};
|
||||
|
||||
std::unique_ptr<font_builder> load_freetype_font(wgpu::device device, std::filesystem::path const & path);
|
||||
|
||||
}
|
||||
346
libs/fonts/source/freetype.cpp
Normal file
346
libs/fonts/source/freetype.cpp
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
#include <psemek/fonts/font_v2.hpp>
|
||||
|
||||
#include <psemek/gfx/pixmap.hpp>
|
||||
#include <psemek/util/exception.hpp>
|
||||
#include <psemek/util/to_string.hpp>
|
||||
#include <psemek/util/unicode.hpp>
|
||||
#include <psemek/util/hash_table.hpp>
|
||||
#include <psemek/log/log.hpp>
|
||||
|
||||
#include <psemek/io/file_stream.hpp>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
#include <iomanip>
|
||||
|
||||
namespace psemek::fonts
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void ft_check_result(FT_Error error, std::string const & message)
|
||||
{
|
||||
if (error != 0)
|
||||
{
|
||||
if (auto error_str = FT_Error_String(error))
|
||||
throw util::exception(message + error_str);
|
||||
else
|
||||
throw util::exception(message + util::to_string("0x", std::hex, error));
|
||||
}
|
||||
}
|
||||
|
||||
struct ft_initializer
|
||||
{
|
||||
ft_initializer()
|
||||
{
|
||||
ft_check_result(FT_Init_FreeType(&library_), "Failed to initialize Freetype library: ");
|
||||
|
||||
int major, minor, patch;
|
||||
FT_Library_Version(library_, &major, &minor, &patch);
|
||||
log::info() << "Initialized Freetype " << major << '.' << minor << '.' << patch;
|
||||
}
|
||||
|
||||
~ft_initializer()
|
||||
{
|
||||
if (auto error = FT_Done_FreeType(library_); error != 0)
|
||||
{
|
||||
if (auto error_str = FT_Error_String(error))
|
||||
log::warning() << "Failed to deinitialize Freetype library: " << error_str;
|
||||
else
|
||||
log::warning() << "Failed to deinitialize Freetype library: 0x" << std::hex << error;
|
||||
}
|
||||
}
|
||||
|
||||
FT_Library library() const
|
||||
{
|
||||
return library_;
|
||||
}
|
||||
|
||||
private:
|
||||
FT_Library library_;
|
||||
};
|
||||
|
||||
FT_Library ft_library()
|
||||
{
|
||||
static ft_initializer initializer;
|
||||
|
||||
return initializer.library();
|
||||
}
|
||||
|
||||
struct face_shared
|
||||
{
|
||||
wgpu::device device;
|
||||
FT_Face face;
|
||||
|
||||
~face_shared()
|
||||
{
|
||||
FT_Done_Face(face);
|
||||
}
|
||||
};
|
||||
|
||||
struct freetype_font
|
||||
: font
|
||||
{
|
||||
freetype_font(std::shared_ptr<face_shared> state, font_type type, int size)
|
||||
: state_(state)
|
||||
, type_(type)
|
||||
, size_(size)
|
||||
{
|
||||
font::shape("x", {});
|
||||
xheight_ = -shaped_text_[0].position[1].min;
|
||||
shaped_text_.clear();
|
||||
}
|
||||
|
||||
font_type type() const override
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return FT_Get_Postscript_Name(state_->face);
|
||||
}
|
||||
|
||||
geom::vector<int, 2> size() const override
|
||||
{
|
||||
return {size_, size_};
|
||||
}
|
||||
|
||||
int xheight() const override
|
||||
{
|
||||
return xheight_;
|
||||
}
|
||||
|
||||
std::vector<shaped_glyph> const & shape(std::string_view str, shape_options const & options, geom::point<float, 2> & pen) override
|
||||
{
|
||||
return shape_impl(util::utf8_range(str), options, pen);
|
||||
}
|
||||
|
||||
std::vector<shaped_glyph> const & shape(std::u32string_view str, shape_options const & options, geom::point<float, 2> & pen) override
|
||||
{
|
||||
return shape_impl(str, options, pen);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<face_shared> state_;
|
||||
font_type type_;
|
||||
int size_;
|
||||
int xheight_;
|
||||
|
||||
static constexpr int page_size = 256;
|
||||
static constexpr int padding = 1;
|
||||
|
||||
struct page
|
||||
{
|
||||
util::array<std::uint8_t, 2> pixmap;
|
||||
|
||||
int current_row_start = 0;
|
||||
int current_row_height = 0;
|
||||
int current_row_x = 0;
|
||||
|
||||
wgpu::texture texture;
|
||||
std::unique_ptr<wgpu::texture_view> texture_view;
|
||||
bool needs_update = true;
|
||||
};
|
||||
|
||||
std::vector<page> pages_;
|
||||
|
||||
struct glyph_data
|
||||
{
|
||||
int page;
|
||||
geom::box<int, 2> part;
|
||||
geom::vector<int, 2> offset;
|
||||
geom::vector<int, 2> advance;
|
||||
};
|
||||
|
||||
util::hash_map<char32_t, std::uint32_t> glyph_mapping_;
|
||||
util::hash_map<std::uint32_t, glyph_data> glyphs_;
|
||||
|
||||
std::vector<shaped_glyph> shaped_text_;
|
||||
|
||||
template <typename String>
|
||||
std::vector<shaped_glyph> const & shape_impl(String const & string, shape_options const & options, geom::point<float, 2> & pen)
|
||||
{
|
||||
(void)options; // TODO: support options
|
||||
|
||||
shaped_text_.clear();
|
||||
|
||||
auto face = state_->face;
|
||||
|
||||
ft_check_result(FT_Set_Char_Size(face, 0, size_ << 6, 0, 0), "Failed to select freetype face size: ");
|
||||
|
||||
bool need_update_pages = false;
|
||||
|
||||
for (char32_t ch : string)
|
||||
{
|
||||
std::uint32_t glyph_id;
|
||||
|
||||
if (auto it = glyph_mapping_.find(ch); it != glyph_mapping_.end())
|
||||
{
|
||||
glyph_id = it->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
glyph_id = FT_Get_Char_Index(face, ch);
|
||||
glyph_mapping_[ch] = glyph_id;
|
||||
}
|
||||
|
||||
glyph_data * data = nullptr;
|
||||
|
||||
if (auto it = glyphs_.find(glyph_id); it != glyphs_.end())
|
||||
{
|
||||
data = &(it->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = &glyphs_[glyph_id];
|
||||
|
||||
FT_Load_Glyph(face, glyph_id, type_ == font_type::bitmap ? FT_LOAD_TARGET_LIGHT : FT_LOAD_DEFAULT);
|
||||
FT_Render_Glyph(face->glyph, type_ == font_type::bitmap ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_SDF);
|
||||
|
||||
if (face->glyph->bitmap.width + 2 * padding > page_size || face->glyph->bitmap.rows + 2 * padding > page_size)
|
||||
{
|
||||
data->page = 0;
|
||||
data->part = {{{0, 0}, {0, 0}}};
|
||||
data->offset = {0, 0};
|
||||
data->advance = {size_, 0};
|
||||
log::warning() << "Glyph with ID " << glyph_id << " (U+" << std::hex << (int)ch << ") is larger than font atlas page: " << face->glyph->bitmap.width << "x" << face->glyph->bitmap.rows;
|
||||
}
|
||||
else
|
||||
{
|
||||
data->offset[0] = face->glyph->bitmap_left;
|
||||
data->offset[1] = - face->glyph->bitmap_top;
|
||||
data->advance[0] = face->glyph->advance.x >> 6;
|
||||
data->advance[1] = face->glyph->advance.y >> 6;
|
||||
|
||||
bool need_new_page = false;
|
||||
|
||||
if (pages_.empty())
|
||||
{
|
||||
need_new_page = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pages_.back().current_row_x + face->glyph->bitmap.width + 2 * padding > page_size)
|
||||
{
|
||||
pages_.back().current_row_start += pages_.back().current_row_height;
|
||||
pages_.back().current_row_height = 0;
|
||||
pages_.back().current_row_x = 0;
|
||||
}
|
||||
|
||||
if (pages_.back().current_row_start + face->glyph->bitmap.rows + 2 * padding > page_size)
|
||||
{
|
||||
need_new_page = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (need_new_page)
|
||||
{
|
||||
pages_.emplace_back();
|
||||
pages_.back().pixmap.resize({page_size, page_size}, 0);
|
||||
|
||||
pages_.back().texture = state_->device.create_texture({
|
||||
.usage = wgpu::texture::usage::copy_dst | wgpu::texture::usage::texture_binding,
|
||||
.dimension = wgpu::texture::dimension::_2d,
|
||||
.size = {page_size, page_size, 1},
|
||||
.format = wgpu::texture::format::r8unorm,
|
||||
});
|
||||
|
||||
pages_.back().texture_view = std::make_unique<wgpu::texture_view>(pages_.back().texture.create_view(wgpu::texture_view::descriptor{
|
||||
.format = wgpu::texture::format::r8unorm,
|
||||
.dimension = wgpu::texture_view::dimension::_2d,
|
||||
}));
|
||||
}
|
||||
|
||||
data->page = pages_.size() - 1;
|
||||
data->part[0].min = pages_.back().current_row_x + padding;
|
||||
data->part[0].max = data->part[0].min + face->glyph->bitmap.width;
|
||||
data->part[1].min = pages_.back().current_row_start + padding;
|
||||
data->part[1].max = data->part[1].min + face->glyph->bitmap.rows;
|
||||
|
||||
for (int y = 0; y < face->glyph->bitmap.rows; ++y)
|
||||
{
|
||||
auto src_begin = face->glyph->bitmap.buffer + y * face->glyph->bitmap.pitch;
|
||||
auto src_end = src_begin + face->glyph->bitmap.width;
|
||||
auto dst_begin = pages_.back().pixmap.data() + (pages_.back().current_row_start + y + padding) * page_size + pages_.back().current_row_x + padding;
|
||||
|
||||
std::copy(src_begin, src_end, dst_begin);
|
||||
}
|
||||
|
||||
pages_.back().current_row_x += face->glyph->bitmap.width + 2 * padding;
|
||||
geom::make_max<int>(pages_.back().current_row_height, face->glyph->bitmap.rows + 2 * padding);
|
||||
|
||||
pages_.back().needs_update = true;
|
||||
need_update_pages = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto & result = shaped_text_.emplace_back();
|
||||
result.texture = pages_[data->page].texture_view.get();
|
||||
result.position[0].min = pen[0] + data->offset[0];
|
||||
result.position[0].max = result.position[0].min + data->part[0].length();
|
||||
result.position[1].min = pen[1] + data->offset[1];
|
||||
result.position[1].max = result.position[1].min + data->part[1].length();
|
||||
result.texcoords[0].min = static_cast<float>(data->part[0].min) / page_size;
|
||||
result.texcoords[0].max = static_cast<float>(data->part[0].max) / page_size;
|
||||
result.texcoords[1].min = static_cast<float>(data->part[1].min) / page_size;
|
||||
result.texcoords[1].max = static_cast<float>(data->part[1].max) / page_size;
|
||||
|
||||
pen += geom::cast<float>(data->advance);
|
||||
}
|
||||
|
||||
if (need_update_pages)
|
||||
{
|
||||
for (auto & page : pages_)
|
||||
{
|
||||
if (!page.needs_update) continue;
|
||||
page.needs_update = false;
|
||||
|
||||
state_->device.get_queue().write_texture({.texture = page.texture}, {(char *)page.pixmap.data(), page.pixmap.size()}, {.bytes_per_row = page_size, .rows_per_image = page_size}, {page_size, page_size, 1});
|
||||
}
|
||||
}
|
||||
|
||||
return shaped_text_;
|
||||
}
|
||||
};
|
||||
|
||||
struct freetype_font_builder
|
||||
: font_builder
|
||||
{
|
||||
freetype_font_builder(std::shared_ptr<face_shared> state)
|
||||
: state_(state)
|
||||
{}
|
||||
|
||||
std::string_view name() const override
|
||||
{
|
||||
return FT_Get_Postscript_Name(state_->face);
|
||||
}
|
||||
|
||||
std::unique_ptr<font> create(font_type type, int size) override
|
||||
{
|
||||
if (type == font_type::msdf)
|
||||
throw util::exception("MSDF fonts are not supported by Freetype");
|
||||
return std::make_unique<freetype_font>(state_, type, size);
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<face_shared> state_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
std::unique_ptr<font_builder> load_freetype_font(wgpu::device device, std::filesystem::path const & path)
|
||||
{
|
||||
FT_Face face;
|
||||
ft_check_result(FT_New_Face(ft_library(), path.c_str(), 0, &face), "Failed to load font " + path.string() + ": ");
|
||||
|
||||
auto result = std::make_unique<freetype_font_builder>(std::make_shared<face_shared>(device, face));
|
||||
|
||||
log::debug() << "Loaded font " << result->name() << " (" << path << ")";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue