From a9e25668fb9c62ccf69ed58abf9f207034ee8374 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 11 Dec 2024 11:56:37 -0500 Subject: [PATCH] Rought font extractor that probably has a memory error causing it to behave mysteriously, and the designer now uses a json file of the characters that will work. --- assets/config.json | 2 +- constants.hpp | 1 + meson.build | 8 ++ render.cpp | 2 +- status.txt | 7 +- tools/designer.cpp | 110 ++++++++++++++-------- tools/fontextract.cpp | 207 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 293 insertions(+), 44 deletions(-) create mode 100644 tools/fontextract.cpp diff --git a/assets/config.json b/assets/config.json index 3f52ab1..d4b2c97 100644 --- a/assets/config.json +++ b/assets/config.json @@ -3,7 +3,7 @@ "WALL_TILE": "\ua5b8", "FLOOR_TILE": "\u2849", "PLAYER_TILE": "\ua66b", - "ENEMY_TILE": "Ω", + "ENEMY_TILE": "\u1d5c", "BG_TILE": "█", "WATER_TILE": "\u26c6" }, diff --git a/constants.hpp b/constants.hpp index 3be61e5..c99dce1 100644 --- a/constants.hpp +++ b/constants.hpp @@ -17,3 +17,4 @@ const int MAX_FONT_SIZE = 140; const int MIN_FONT_SIZE = 20; const int SCREEN_WIDTH = 40; const int SCREEN_HEIGHT = 30; +#define FONT_FILE_NAME "./assets/text.otf" diff --git a/meson.build b/meson.build index d83df99..90eab10 100644 --- a/meson.build +++ b/meson.build @@ -92,6 +92,14 @@ designer = executable('designer', [ ], dependencies: dependencies) +fontextract = executable('fontextract', [ + 'dbc.cpp', + 'rand.cpp', + 'config.cpp', + 'tools/fontextract.cpp' + ], + dependencies: dependencies) + img2ansi = executable('img2ansi', [ 'dbc.cpp', 'panel.cpp', diff --git a/render.cpp b/render.cpp index 207b3f0..5aff842 100644 --- a/render.cpp +++ b/render.cpp @@ -25,7 +25,7 @@ SFMLRender::SFMLRender() : $ansi($default_fg, $default_bg) { // force true color, but maybe I want to support different color sets - $font.loadFromFile("./assets/text.otf"); + $font.loadFromFile(FONT_FILE_NAME); $font.setSmooth(false); $ui_text.setFont($font); $ui_text.setPosition(0,0); diff --git a/status.txt b/status.txt index 3049fb9..f8b36a5 100644 --- a/status.txt +++ b/status.txt @@ -1,10 +1,13 @@ TODAY'S GOAL: +==== +Font Extractor +=== + 1. Why do Sliders only have to be kept around forever and can't go in containers like everything else? 2. Why are sliders not selected when I click on them? Is it a hover? 3. Why do fonts render blank? Also when I scroll they slowly disappear until there's a column. -4. Use freetype to iterate chars. -5. Why are all wchar converted chars in from_unicode 3 bytes not 2? +* \u2738 is missing on the row when in grid but works when clicked. * A designer tool to help find characters for foreground, background, and figure out their colors. diff --git a/tools/designer.cpp b/tools/designer.cpp index 4c68cfc..d6d5fe7 100644 --- a/tools/designer.cpp +++ b/tools/designer.cpp @@ -1,3 +1,5 @@ +#include +#include #include // for operator""s, chrono_literals #include // for sleep_for #include "dinkyecs.hpp" @@ -13,9 +15,9 @@ #include #include #include -#include -#include FT_FREETYPE_H +#include +using namespace nlohmann; using namespace fmt; using namespace ftxui; using namespace std::chrono_literals; @@ -26,58 +28,80 @@ using std::string, std::wstring, std::vector; const Point GRID_SIZE={15,8}; const int DEFAULT_FONT_SIZE=200; +struct FontGridCell { + size_t cm_index; + string as_string; + wstring as_wstring; +}; + struct FontGrid { size_t width; size_t height; - vector> $chars; + vector> $grid; std::wstring_convert> $converter; + vector $wcharmap; + vector $charmap; FontGrid(size_t width, size_t height) : width(width), height(height), - $chars(height, vector(width, "")) + $grid(height, vector(width, {0, "", L""})) { + configure_font(); } - void render(wchar_t start_char, bool fill) { - wchar_t cur_char = start_char; + void configure_font() { + std::ifstream in_file("./fontlist.json"); + json input = json::parse(in_file); + + for(auto inchar : input) { + $charmap.push_back(inchar); + $wcharmap.push_back($converter.from_bytes(inchar)); + } + } + + size_t max_chars() { + return $wcharmap.size(); + } + + void render(size_t start_char, bool fill) { + size_t next_char = start_char; for(size_t y = 0; y < height; ++y) { for(size_t x = 0; x < width; ++x) { if(!fill) { - cur_char += (x+1) * (y+1); + next_char++; } - wstring out_w{cur_char}; - $chars[y][x] = from_unicode(out_w); + $grid[y][x] = { + .cm_index = next_char, + .as_string = $charmap[next_char], + .as_wstring = $wcharmap[next_char] + }; } } } - string from_unicode(wstring input) { - try { - return $converter.to_bytes(input); - } catch(...) { - return $converter.to_bytes(L"?"); - } + size_t charmap_index(size_t x, size_t y) { + FontGridCell &cell = $grid[y][x]; + return cell.cm_index; } - wchar_t to_unicode_char(size_t x, size_t y) { - try { - string input = $chars[y][x]; // BUG: bounds check this instead - return $converter.from_bytes(input)[0]; - } catch(...) { - return L'?'; - } + string& as_string(size_t x, size_t y) { + return $grid[y][x].as_string; + } + + wstring& as_wstring(size_t x, size_t y) { + return $grid[y][x].as_wstring; } - string at(size_t x, size_t y) { - return $chars[y][x]; + wchar_t as_wchar(size_t cm_index) { + return $wcharmap[cm_index][0]; } + unsigned int page_size() { return width * height; } - }; struct WhatTheColor { @@ -97,8 +121,8 @@ class GUI { Canvas $canvas; SFMLRender $renderer; FontGrid $font_grid; - wchar_t $start_char = L'\u28cc'; - wchar_t $fill_char = WCHAR_MIN; + size_t $start_char = 0; + size_t $fill_char = 0; WhatTheColor $fg_color; WhatTheColor $bg_color; Component $fg_settings; @@ -113,7 +137,11 @@ class GUI { $bg_color{.h=100, .s=100, .v=100} { resize_fonts(DEFAULT_FONT_SIZE); - $font_grid.render($start_char, false); + render_grid($start_char, false); + } + + void render_grid(size_t start_char, bool fill) { + $font_grid.render(start_char, fill); } void resize_fonts(int new_size) { @@ -124,11 +152,12 @@ class GUI { } void draw_font_grid() { + int flip_it = 0; for(size_t y = 0; y < $font_grid.height; y++) { - for(size_t x = 0; x < $font_grid.width; x++) { - $canvas.DrawText(x * 2, y * 4, $font_grid.at(x, y), [&](auto &pixel) { + for(size_t x = 0; x < $font_grid.width; x++, flip_it++) { + $canvas.DrawText(x * 2, y * 4, $font_grid.as_string(x, y), [&](auto &pixel) { pixel.foreground_color = Color::HSV($fg_color.h, $fg_color.s, $fg_color.v); - pixel.background_color = Color::HSV($bg_color.h, $bg_color.s, $bg_color.v); + pixel.background_color = Color::HSV($bg_color.h, $bg_color.s, $bg_color.v / (flip_it % 2 * 2 + 1)); }); } } @@ -150,10 +179,12 @@ class GUI { $bg_color.v_slider = Slider("BG V:", &$bg_color.v, 0, 255, 1); $status_ui.set_renderer(Renderer([&]{ + wchar_t fill_char = $font_grid.as_wchar($fill_char); + return hbox({ hflow( vbox( - text(format("\\u{:x} {} MIN: {}, MAX: {}", int($fill_char), int($fill_char), WCHAR_MIN, WCHAR_MAX)) | border, + text(format("\\u{:x} {} IDX: {}, MIN: {}, MAX: {}", int(fill_char), int(fill_char), $fill_char, WCHAR_MIN, WCHAR_MAX)) | border, separator(), text(format("FG H: {}, S: {}, V: {}", $fg_color.h, $fg_color.s, $fg_color.v)) | border, @@ -190,12 +221,12 @@ class GUI { } void select_cell(Point pos) { - $fill_char = $font_grid.to_unicode_char(pos.x, pos.y); - $font_grid.render($fill_char, true); + $fill_char = $font_grid.charmap_index(pos.x, pos.y); + render_grid($fill_char, true); } void deselect_cell() { - $font_grid.render($start_char, false); + render_grid($start_char, false); } bool handle_ui_events() { @@ -210,13 +241,13 @@ class GUI { return true; } else if(event.type == sf::Event::KeyPressed) { if(KB::isKeyPressed(KB::Up)) { - $start_char = std::max(WCHAR_MIN+1, $start_char - $font_grid.page_size()); - $font_grid.render($start_char, false); + $start_char = std::max(size_t(1), $start_char - $font_grid.page_size()); + render_grid($start_char, false); event_happened = true; $renderer.clear_cache(); } else if(KB::isKeyPressed(KB::Down)) { - $start_char = std::min(WCHAR_MAX, $start_char + $font_grid.page_size()); - $font_grid.render($start_char, false); + $start_char = std::min($font_grid.max_chars(), $start_char + $font_grid.page_size()); + render_grid($start_char, false); $renderer.clear_cache(); } else if(KB::isKeyPressed(KB::Equal)) { resize_fonts(font_size + 10); @@ -264,7 +295,6 @@ int main(int argc, char *argv[]) { gui.render_scene(); if(gui.handle_ui_events()) { - println("THERE WERE EVENTS"); } std::this_thread::sleep_for(10ms); diff --git a/tools/fontextract.cpp b/tools/fontextract.cpp new file mode 100644 index 0000000..614cebe --- /dev/null +++ b/tools/fontextract.cpp @@ -0,0 +1,207 @@ +#include +#include +#include // for operator""s, chrono_literals +#include // for sleep_for +#include "dbc.hpp" +#include +#include +#include "constants.hpp" +#include +#include +#include +#include +#include +#include +#include +#include FT_FREETYPE_H +#include FT_TRUETYPE_TABLES_H +#include FT_TRUETYPE_IDS_H + +using namespace nlohmann; +using namespace fmt; +namespace fs = std::filesystem; +using std::string, std::wstring, std::vector; + +const size_t CLEAR_CACHE_POINT=100; + +struct FontIndex { + size_t cm_index; // charmap index + string glyph; +}; + +struct FontExtractor { + size_t $clear_count = 1; + wchar_t ui_base_char = L'█'; + vector $index; + std::wstring_convert> $converter; + vector $charmap; + vector $wcharmap; + sf::Font $font; + sf::FloatRect $grid_bounds; + int $font_size; + fs::path $font_path; + json $results; + + FontExtractor(fs::path font_path) : + $font_path(font_path) + { + bool good = $font.loadFromFile($font_path.string()); + dbc::check(good, format("failed to load font {}", $font_path.string())); + $font.setSmooth(false); + + for(int i = 100; i < 200; i++) { + auto glyph = $font.getGlyph(ui_base_char, i, false); + if(glyph.bounds.width > 0 && glyph.bounds.height > 0) { + $grid_bounds = glyph.bounds; + $font_size = i; + break; + } + } + + dbc::check($grid_bounds.width > 0 && $grid_bounds.height > 0, "couldn't find a valid font size"); + + println("!!!!!!!!!!!!!!!!!!!!! FONT SIZE {}", $font_size); + } + + void configure_font() { + FT_ULong charcode; + FT_UInt gindex; + FT_String buf[32] = {0}; + FT_Library library; + FT_Face face; + + auto error = FT_Init_FreeType(&library); + dbc::check(!error, "Failed to initialize freetype library."); + + error = FT_New_Face(library, FONT_FILE_NAME, 0, &face); + + dbc::check(face->num_faces == 1, format("Font {} has {} but I only support 1.", FONT_FILE_NAME, face->num_faces)); + dbc::check(face->charmap, "Font doesn't have a charmap."); + + println("Font charmaps {}", face->num_charmaps); + auto active = FT_Get_Charmap_Index(face->charmap); + + for(int i = 0; i < face->num_charmaps; i++) { + auto format = FT_Get_CMap_Format(face->charmaps[i]); + auto lang_id = FT_Get_CMap_Language_ID(face->charmaps[i]); + + println("{}: {} {}, active: {}, platform: {}, encoding: {}, ms_unicode: {}, language: {}", + i, + format >= 0 ? "format" : "synthetic", + format, + i == active, + face->charmaps[i]->platform_id, + face->charmaps[i]->encoding_id, + TT_MS_ID_UNICODE_CS, + lang_id); + + // BUG: this is windows only + if(face->charmaps[i]->encoding_id == TT_MS_ID_UNICODE_CS) { + FT_Set_Charmap(face, face->charmaps[i]); + } + } + + bool has_names = FT_HAS_GLYPH_NAMES(face); + + // get the first char of the map + charcode = FT_Get_First_Char(face, &gindex); + + // go through every char and pre-convert + for(int i = 0; gindex; i++) { + if(has_names) FT_Get_Glyph_Name(face, gindex, buf, 32 ); + + // store it in my pre-convert charmap for later + wstring wcodestr{(wchar_t)charcode}; + string codestr = from_unicode(wcodestr); + $charmap.emplace_back(codestr); + $wcharmap.push_back(charcode); + + // keep going + charcode = FT_Get_Next_Char(face, charcode, &gindex ); + } + + FT_Done_Face(face); + FT_Done_FreeType(library); + + println("FreeType extracted {} glyphs.", $charmap.size()); + } + + size_t next_valid_char(size_t cur_char) { + for(size_t i = cur_char+1; i < $wcharmap.size(); i++) { + wchar_t test_char = $wcharmap[i]; + if($font.hasGlyph(test_char)) { + auto glyph = $font.getGlyph(test_char, $font_size, false); + auto bounds = glyph.bounds; + + // skip bad chars + if(bounds.width <= 0 || bounds.height <= 0) continue; + + if(bounds.width <= $grid_bounds.width && + bounds.height <= $grid_bounds.height) { + return i; + } + } + + clear_font_cache(); + } + + return 0; + } + + void extract_valid() { + size_t cur_char = next_valid_char(1); + + while(cur_char != 0) { + string out = from_unicode(wstring_at(cur_char)); + $results.push_back(out); + cur_char = next_valid_char(cur_char + 1); + } + } + + string from_unicode(wstring input) { + try { + return $converter.to_bytes(input); + } catch(...) { + return $converter.to_bytes(L"?"); + } + } + + string at(size_t x) { + return $index[x].glyph; + } + + wstring wstring_at(size_t cm_index) { + return wstring{$wcharmap[cm_index]}; + } + + void clear_font_cache() { + if($clear_count % CLEAR_CACHE_POINT == 0) { + bool good = $font.loadFromFile($font_path.string()); + dbc::check(good, format("failed to load font {}", $font_path.string())); + $font.setSmooth(false); + + $clear_count++; + } + } +}; + +int main(int argc, char *argv[]) { + if(argc != 3) { + println("USAGE: fontextractor "); + return 1; + } + + fs::path font_path = argv[1]; + fs::path out_path = argv[2]; + + dbc::check(fs::exists(font_path), "ERROR: input file doesn't exist"); + + FontExtractor fex(font_path); + fex.configure_font(); + fex.extract_valid(); + + std::ofstream json_out(out_path, std::ios::binary); + json_out << fex.$results.dump(2) << std::endl; + + println("Wrote {} chars to {}.", fex.$results.size(), out_path.string()); +}