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.
This commit is contained in:
		
							parent
							
								
									9ab064126f
								
							
						
					
					
						commit
						a9e25668fb
					
				
					 7 changed files with 293 additions and 44 deletions
				
			
		|  | @ -3,7 +3,7 @@ | |||
|     "WALL_TILE": "\ua5b8", | ||||
|     "FLOOR_TILE": "\u2849", | ||||
|     "PLAYER_TILE": "\ua66b", | ||||
|     "ENEMY_TILE": "Ω", | ||||
|     "ENEMY_TILE": "\u1d5c", | ||||
|     "BG_TILE": "█", | ||||
|     "WATER_TILE": "\u26c6" | ||||
|   }, | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| #include <iostream> | ||||
| #include <fstream> | ||||
| #include <chrono>                   // for operator""s, chrono_literals
 | ||||
| #include <thread>                   // for sleep_for
 | ||||
| #include "dinkyecs.hpp" | ||||
|  | @ -13,9 +15,9 @@ | |||
| #include <locale> | ||||
| #include <codecvt> | ||||
| #include <vector> | ||||
| #include <ft2build.h> | ||||
| #include FT_FREETYPE_H | ||||
| #include <nlohmann/json.hpp> | ||||
| 
 | ||||
| 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<vector<string>> $chars; | ||||
|   vector<vector<FontGridCell>> $grid; | ||||
|   std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter; | ||||
|   vector<wstring> $wcharmap; | ||||
|   vector<string> $charmap; | ||||
| 
 | ||||
|   FontGrid(size_t width, size_t height) : | ||||
|     width(width), height(height), | ||||
|     $chars(height, vector<string>(width, "")) | ||||
|     $grid(height, vector<FontGridCell>(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; | ||||
|   } | ||||
| 
 | ||||
|   string at(size_t x, size_t y) { | ||||
|     return $chars[y][x]; | ||||
|   wstring& as_wstring(size_t x, size_t y) { | ||||
|     return $grid[y][x].as_wstring; | ||||
|   } | ||||
| 
 | ||||
|   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); | ||||
|  |  | |||
							
								
								
									
										207
									
								
								tools/fontextract.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								tools/fontextract.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,207 @@ | |||
| #include <fstream> | ||||
| #include <iostream> | ||||
| #include <chrono>                   // for operator""s, chrono_literals
 | ||||
| #include <thread>                   // for sleep_for
 | ||||
| #include "dbc.hpp" | ||||
| #include <filesystem> | ||||
| #include <fcntl.h> | ||||
| #include "constants.hpp" | ||||
| #include <fmt/core.h> | ||||
| #include <locale> | ||||
| #include <codecvt> | ||||
| #include <vector> | ||||
| #include <nlohmann/json.hpp> | ||||
| #include <ft2build.h> | ||||
| #include <SFML/Graphics/Font.hpp> | ||||
| #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<FontIndex> $index; | ||||
|   std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> $converter; | ||||
|   vector<string> $charmap; | ||||
|   vector<wchar_t> $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 <input> <output>"); | ||||
|     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()); | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Zed A. Shaw
						Zed A. Shaw