diff --git a/CMakeLists.txt b/CMakeLists.txt index 77d12a5..ba60876 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,5 +3,7 @@ cmake_minimum_required(VERSION 3.20) project(all) enable_testing() +add_subdirectory(applications) add_subdirectory(bmrshared) -add_subdirectory(bmrshared-magic) \ No newline at end of file +add_subdirectory(bmrshared-freetype) +add_subdirectory(bmrshared-magic) diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt new file mode 100644 index 0000000..c0bfeb5 --- /dev/null +++ b/applications/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(text2image) \ No newline at end of file diff --git a/applications/text2image/CMakeLists.txt b/applications/text2image/CMakeLists.txt new file mode 100644 index 0000000..b02592d --- /dev/null +++ b/applications/text2image/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.20) +find_package(Boost 1.84.0 REQUIRED COMPONENTS program_options headers CONFIG) +find_package(JPEG REQUIRED) + +project(text2image) + +add_executable( + ${PROJECT_NAME} + ./src/main.cpp +) + +set_property( + TARGET ${PROJECT_NAME} + PROPERTY CXX_STANDARD 20 +) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + bmrshared-freetype + Boost::program_options + Boost::headers + JPEG::JPEG +) + +install( + TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION bin +) diff --git a/applications/text2image/src/main.cpp b/applications/text2image/src/main.cpp new file mode 100644 index 0000000..51126d5 --- /dev/null +++ b/applications/text2image/src/main.cpp @@ -0,0 +1,102 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bpo = boost::program_options; + +int main(int argc, char** argv) +{ + std::string font_file_str; + std::string output_file_str; + std::string text; + int font_size; + + bpo::options_description desc("Options"); + + // clang-format off + desc.add_options() + ("text,t", bpo::value(&text), "The text to render.") + ("font,f", bpo::value(&font_file_str), "TTF font file to use for rendering.") + ("size,s", bpo::value(&font_size)->default_value(16), "Font size in pixels.") + ("output,o", bpo::value(&output_file_str), "Output file path.") + ("help,h", "Show help information."); + // clang-format on + + std::filesystem::path font_file; + std::filesystem::path output_file; + try + { + bpo::variables_map vm; + bpo::store(bpo::parse_command_line(argc, argv, desc), vm); + bpo::notify(vm); + + if (vm.count("help")) + { + std::cout << desc << std::endl; + return 1; + } + + font_file = font_file_str; + if (!std::filesystem::is_regular_file(font_file)) + { + throw std::runtime_error("TTF font file path is invalid."); + } + + output_file = output_file_str; + if (!std::filesystem::is_directory(output_file.parent_path())) + { + throw std::runtime_error("Output directory is invalid."); + } + } + catch (const std::exception& e) + { + std::cout << "Error while processing commandline arguments.\n" << e.what() << "\n" << desc << std::endl; + return 1; + } + std::cout << "text=" << text << "\nfont=" << font_file << "\nsize=" << font_size << "\noutput=" << output_file; + + + auto ftLib = std::make_shared(); + auto ftFace = std::make_shared(ftLib, font_file, 0, font_size, true); + + const auto dim = bmrshared::freetype_calculate_dimensions(*ftFace, text); + const int pixel_width = dim.width / 64; + const int pixel_height = dim.height / 64; + const int bearingY = dim.vertOriginOffsetY / 64; + + boost::gil::gray8_image_t image(pixel_width, pixel_height); + auto view = boost::gil::view(image); + boost::gil::fill_pixels(view, boost::gil::gray8_pixel_t(0)); + + auto painter = [&](int x, int y, const uint8_t level) -> void + { + const auto& _x = x; + const auto _y = y + bearingY; + + if (_x >= 0 && _y >= 0 && _x < pixel_width && _y < pixel_height) + { + const auto iter = view.at({_x, _y}); + boost::gil::color_convert(boost::gil::gray8_pixel_t(level), *iter); + } + }; + + bmrshared::freetype_paint(*ftFace, text, painter); + boost::gil::write_view(output_file, + boost::gil::view(image), + boost::gil::image_write_info(95)); + return 0; +} \ No newline at end of file diff --git a/bmrshared-freetype/CMakeLists.txt b/bmrshared-freetype/CMakeLists.txt new file mode 100644 index 0000000..5bac2b0 --- /dev/null +++ b/bmrshared-freetype/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.20) + +find_package(freetype REQUIRED) +find_package(Boost 1.84.0 REQUIRED COMPONENTS headers CONFIG) + +project(bmrshared-freetype) + +add_library( + ${PROJECT_NAME} + ./lib/freetype_lib.cpp + ./lib/freetype_face.cpp + ./lib/freetype_utils.cpp +) + +set_property( + TARGET ${PROJECT_NAME} + PROPERTY CXX_STANDARD 20 +) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_link_libraries( + ${PROJECT_NAME} + PUBLIC + Freetype::Freetype + Boost::headers +) diff --git a/bmrshared-freetype/include/bmrshared/freetype_face.hpp b/bmrshared-freetype/include/bmrshared/freetype_face.hpp new file mode 100644 index 0000000..71c40f2 --- /dev/null +++ b/bmrshared-freetype/include/bmrshared/freetype_face.hpp @@ -0,0 +1,57 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#pragma once + +#include +#include +#include + +#include +#include +#include FT_FREETYPE_H +#include FT_BITMAP_H + +namespace bmrshared +{ + class freetype_lib; + + class freetype_face final : public std::enable_shared_from_this + { + using character_index = FT_ULong; + using pixel_size = FT_UInt; + + public: + freetype_face(std::shared_ptr lib, + const std::string& font_path, + pixel_size pixel_width, + pixel_size pixel_height, + bool render_monochrome + ); + + ~freetype_face(); + + FT_BBox get_boundbox() const; + FT_Glyph_Metrics get_dimensions(character_index character_index) const; + + void render( + character_index character_index, + std::function painter); + +private: + FT_Int32 get_load_flags() const; + FT_Render_Mode get_render_mode() const; + + private: + std::shared_ptr m_lib; + FT_Face m_face; + bool m_renderMonochrome; + }; +} diff --git a/bmrshared-freetype/include/bmrshared/freetype_lib.hpp b/bmrshared-freetype/include/bmrshared/freetype_lib.hpp new file mode 100644 index 0000000..1612274 --- /dev/null +++ b/bmrshared-freetype/include/bmrshared/freetype_lib.hpp @@ -0,0 +1,26 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#pragma once +#include +#include +#include FT_FREETYPE_H + +namespace bmrshared +{ + class freetype_lib final : public std::enable_shared_from_this + { + public: + freetype_lib(); + ~freetype_lib(); + + FT_Library get_ft_library(); + + private: + FT_Library m_library; + }; +} diff --git a/bmrshared-freetype/include/bmrshared/freetype_types.hpp b/bmrshared-freetype/include/bmrshared/freetype_types.hpp new file mode 100644 index 0000000..f92f86c --- /dev/null +++ b/bmrshared-freetype/include/bmrshared/freetype_types.hpp @@ -0,0 +1,14 @@ + +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#pragma once + +namespace bmrshared +{ + +} diff --git a/bmrshared-freetype/include/bmrshared/freetype_utils.hpp b/bmrshared-freetype/include/bmrshared/freetype_utils.hpp new file mode 100644 index 0000000..ed481ae --- /dev/null +++ b/bmrshared-freetype/include/bmrshared/freetype_utils.hpp @@ -0,0 +1,23 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#pragma once +#include +#include + +namespace bmrshared +{ + struct dimensions final + { + FT_Pos width; + FT_Pos height; + FT_Pos vertOriginOffsetY; + }; + + dimensions freetype_calculate_dimensions(freetype_face& face, std::string_view text); + void freetype_paint(freetype_face& face, std::string_view text, const std::function& paintfn); +} \ No newline at end of file diff --git a/bmrshared-freetype/lib/freetype_face.cpp b/bmrshared-freetype/lib/freetype_face.cpp new file mode 100644 index 0000000..0bf10d4 --- /dev/null +++ b/bmrshared-freetype/lib/freetype_face.cpp @@ -0,0 +1,102 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#include +#include +#include + +using namespace bmrshared; + +freetype_face::freetype_face( + std::shared_ptr lib, + const std::string& font_path, + pixel_size pixel_width, + pixel_size pixel_height, + bool render_monochrome) + : m_lib(lib) + , m_renderMonochrome(render_monochrome) +{ + FT_New_Face( + m_lib->get_ft_library(), + font_path.c_str(), + 0, + &m_face); + + FT_Set_Pixel_Sizes( + m_face, + pixel_width, + pixel_height); +} + +freetype_face::~freetype_face() +{ + FT_Done_Face(m_face); +} + +FT_BBox freetype_face::get_boundbox() const +{ + return m_face->bbox; +} + +FT_Glyph_Metrics freetype_face::get_dimensions( + character_index character_index) const +{ + // Load glyph and retrieve metrics. + const auto glyph_index = FT_Get_Char_Index(m_face, character_index); + FT_Load_Glyph(m_face, glyph_index, get_load_flags()); + + FT_GlyphSlot& glyphSlot = m_face->glyph; + return glyphSlot->metrics; +} + + +void freetype_face::render( + FT_ULong character_index, + std::function painter) +{ + // Load glyph and retrieve metrics. + const auto glyph_index = FT_Get_Char_Index(m_face, character_index); + FT_Load_Glyph(m_face, glyph_index, get_load_flags()); + const FT_GlyphSlot& glyphSlot = m_face->glyph; + + // render glyph + FT_Render_Glyph(glyphSlot, get_render_mode()); + const FT_Bitmap& bitmap = glyphSlot->bitmap; + + FT_Bitmap target; + FT_Bitmap_Init(&target); + FT_Bitmap_Convert(m_lib->get_ft_library(), &bitmap, &target, 1); + + for(unsigned int x = 0; x < target.width; ++x) + { + for(unsigned int y = 0; y < target.rows; ++y) + { + const uint8_t& value = 0xff * target.buffer[x + target.width * y]; + painter(x,y,value); + } + } + + FT_Bitmap_Done(m_lib->get_ft_library(), &target); +} + +FT_Int32 freetype_face::get_load_flags() const +{ + FT_Int32 loadFlags = FT_LOAD_TARGET_NORMAL; + if (m_renderMonochrome) + { + loadFlags = FT_LOAD_TARGET_MONO; + } + return loadFlags; +} + +FT_Render_Mode freetype_face::get_render_mode() const +{ + return FT_RENDER_MODE_MONO; +} diff --git a/bmrshared-freetype/lib/freetype_lib.cpp b/bmrshared-freetype/lib/freetype_lib.cpp new file mode 100644 index 0000000..6076ead --- /dev/null +++ b/bmrshared-freetype/lib/freetype_lib.cpp @@ -0,0 +1,25 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#include + +using bmrshared::freetype_lib; + +freetype_lib::freetype_lib() +{ + FT_Init_FreeType(&m_library); +} + +freetype_lib::~freetype_lib() +{ + FT_Done_FreeType(m_library); +} + +FT_Library freetype_lib::get_ft_library() +{ + return m_library; +} diff --git a/bmrshared-freetype/lib/freetype_utils.cpp b/bmrshared-freetype/lib/freetype_utils.cpp new file mode 100644 index 0000000..de8bf6f --- /dev/null +++ b/bmrshared-freetype/lib/freetype_utils.cpp @@ -0,0 +1,56 @@ +// GNU Lesser General Public License v3.0 +// Copyright (c) 2025 Bart Beumer +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License v3.0 as published by +// the Free Software Foundation. +// +#include +#include +#include + +namespace bmrshared +{ + +dimensions freetype_calculate_dimensions(freetype_face& face, std::string_view text) +{ + FT_Pos line_width = 0; + FT_Pos line_above_origin = 0; + FT_Pos line_below_origin = 0; + + for (const char& c : text) + { + const FT_Glyph_Metrics& dim = face.get_dimensions(c); + + line_width += dim.horiAdvance; + const auto char_above_origin = dim.horiBearingY; + const auto char_below_origin = dim.height - dim.horiBearingY; + + line_above_origin = std::max(line_above_origin, char_above_origin); + line_below_origin = std::max(line_below_origin, char_below_origin); + } + return {.width = line_width, .height = (line_above_origin + line_below_origin), .vertOriginOffsetY = line_above_origin}; +} + +void freetype_paint(freetype_face& face, std::string_view text, const std::function& paintfn) +{ + FT_Pos line_x = 0; + FT_Pos char_offset_x = 0; + FT_Pos char_offset_y = 0; + auto painter_char = [&](int x, int y, uint8_t level) + { + paintfn((char_offset_x + x), (y - char_offset_y), level); + }; + + for( const char& c : text) + { + const FT_Glyph_Metrics& dim = face.get_dimensions(c); + char_offset_x = (line_x + (dim.horiAdvance - dim.width)/2)/64; + char_offset_y = dim.horiBearingY/64; + + face.render(c, painter_char); + line_x += dim.horiAdvance; + } +} + +} \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 1029895..7d9d643 100644 --- a/conanfile.py +++ b/conanfile.py @@ -5,7 +5,7 @@ from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps class HelloConan(ConanFile): settings = "os", "compiler", "build_type", "arch" - requires = "boost/1.84.0", "gtest/1.14.0", "libmagic/5.45" + requires = "boost/1.84.0", "gtest/1.14.0", "libmagic/5.45", "freetype/2.13.3", "libjpeg/9f" generators = "CMakeDeps" build_policy = "*"