From 024120bc427fb9d784dca360be1d9b411132189b Mon Sep 17 00:00:00 2001 From: Isaiah Odhner Date: Sat, 13 May 2023 00:05:51 -0400 Subject: [PATCH] Generate FIGlet fonts of NanoTiny --- NanoTiny_v14_2x2.flf | 309 +++++++++++++++++++ NanoTiny_v14_4x4.flf | 513 +++++++++++++++++++++++++++++++ cspell.json | 1 + src/textual_paint/repack_font.py | 239 +++++++++++++- 4 files changed, 1049 insertions(+), 13 deletions(-) create mode 100644 NanoTiny_v14_2x2.flf create mode 100644 NanoTiny_v14_4x4.flf diff --git a/NanoTiny_v14_2x2.flf b/NanoTiny_v14_2x2.flf new file mode 100644 index 0000000..1dc5969 --- /dev/null +++ b/NanoTiny_v14_2x2.flf @@ -0,0 +1,309 @@ +flf2a$ 3 2 4 -1 2 0 0 0 +NanoTiny 2x2 +by Isaiah Odhner + @ + @ + @@ +▜▛@ +▗▖@ + @@ +▌▌@ + @ + @@ +▙▙@ +▙▙@ + @@ +▘▝@ +▞▚@ + @@ +▘▞@ +▞▗@ + @@ +▗ @ +▜▘@ + @@ +▌ @ + @ + @@ +▞ @ +▚ @ + @@ +▚ @ +▞ @ + @@ +▚▘@ +▘▘@ + @@ +▟▖@ +▝ @ + @@ + @ +▟ @ + @@ +▄▖@ + @ + @@ + @ +▖ @ + @@ + ▞@ +▞ @ + @@ +▞▖@ +▚▘@ + @@ +▜ @ +▟▖@ + @@ +▀▖@ +▟▖@ + @@ +▜▌@ +▄▌@ + @@ +▌▌@ +▀▛@ + @@ +█▘@ +▄▘@ + @@ +▟▘@ +▚▘@ + @@ +▀▌@ +▐ @ + @@ +▐▚@ +▚▌@ + @@ +▞▖@ +▝▌@ + @@ +▘ @ +▖ @ + @@ +▝ @ +▟ @ + @@ +▞ @ +▝ @ + @@ +▀▘@ +▀▘@ + @@ +▚ @ +▘ @ + @@ +▜▌@ +▗ @ + @@ +▞▚@ +▘█@ + @@ +▞▚@ +▛▜@ + @@ +▛▖@ +█▞@ + @@ +▞▘@ +▚▖@ + @@ +▛▚@ +▙▞@ + @@ +█▘@ +▙▖@ + @@ +▛▘@ +▛▘@ + @@ +▞▘@ +▚▛@ + @@ +▌▐@ +▛▜@ + @@ +▜▘@ +▟▖@ + @@ +▝▛@ +▚▘@ + @@ +▌▞@ +▛▚@ + @@ +▌ @ +▙▖@ + @@ +█▟@ +▌▐@ + @@ +▙▐@ +▌▜@ + @@ +▞▚@ +▚▞@ + @@ +▛▚@ +▛▘@ + @@ +▞▚@ +▝▚@ + @@ +▛▚@ +▛▚@ + @@ +▟▘@ +▗▛@ + @@ +▜▘@ +▐ @ + @@ +▌▐@ +▚▞@ + @@ +▌▐@ +▙▘@ + @@ +▌▐@ +█▜@ + @@ +▌▌@ +▞▖@ + @@ +▌▌@ +▐ @ + @@ +▀▛@ +▟▄@ + @@ +▛ @ +▙ @ + @@ +▚ @ + ▚@ + @@ +▜ @ +▟ @ + @@ +▞▖@ + @ + @@ + @ +▄▄@ + @@ +▚ @ + @ + @@ +▗▖@ +▚▌@ + @@ +▙ @ +▙▘@ + @@ +▗▖@ +▚▖@ + @@ +▗▌@ +▚▌@ + @@ +▞▖@ +▜▖@ + @@ +▞ @ +▛ @ + @@ +▞▖@ +▟▘@ + @@ +▙ @ +▌▌@ + @@ +▘ @ +▌ @ + @@ +▐ @ +▞ @ + @@ +▌▖@ +▛▖@ + @@ +▌ @ +▚ @ + @@ +▄▖@ +▛▐@ + @@ +▗ @ +▌▌@ + @@ +▗ @ +▚▘@ + @@ +▞▖@ +▛ @ + @@ +▞▖@ +▝▌@ + @@ +▗▖@ +▌ @ + @@ +▞ @ +▞ @ + @@ +▟▖@ +▐▖@ + @@ +▖▗@ +▚▞@ + @@ +▖▖@ +▚▘@ + @@ +▖▗@ +▚█@ + @@ +▖▖@ +▞▖@ + @@ +▚▌@ +▄▘@ + @@ +▀▌@ +▟▖@ + @@ +▗▘@ +▜▖@ + @@ +▌ @ +▌ @ + @@ +▚ @ +▟▘@ + @@ +▞▞@ + @ + @@ +▗▄@ +▐▛@ + @@ +██@ +██@ + @@ +▞▙@ +▌▟@ + @@ +▗▘@ +▙▌@ + @@ +▖ @ + @ + @@ + @ + @ + @@ +▟▞@ +▚▘@ + @@ diff --git a/NanoTiny_v14_4x4.flf b/NanoTiny_v14_4x4.flf new file mode 100644 index 0000000..0db9d4a --- /dev/null +++ b/NanoTiny_v14_4x4.flf @@ -0,0 +1,513 @@ +flf2a$ 5 4 6 -1 2 0 0 0 +NanoTiny 4x4 +by Isaiah Odhner + @ + @ + @ + @ + @@ +████@ + ██ @ + @ + ██ @ + @@ +█ █ @ +█ █ @ + @ + @ + @@ +█ █ @ +████@ +█ █ @ +████@ + @@ +█ █@ + @ + ██ @ +█ █@ + @@ +█ █@ + █ @ + █ @ +█ █@ + @@ + @ + █ @ +███ @ + █ @ + @@ +█ @ +█ @ + @ + @ + @@ + █ @ +█ @ +█ @ + █ @ + @@ +█ @ + █ @ + █ @ +█ @ + @@ +█ █ @ + █ @ +█ █ @ + @ + @@ + █ @ +███ @ + █ @ + @ + @@ + @ + @ + █ @ +██ @ + @@ + @ +███ @ + @ + @ + @@ + @ + @ + @ +█ @ + @@ + █@ + █ @ + █ @ +█ @ + @@ + █ @ +█ █ @ +█ █ @ + █ @ + @@ +██ @ + █ @ + █ @ +███ @ + @@ +██ @ + █ @ + █ @ +███ @ + @@ +███ @ + ██ @ + █ @ +███ @ + @@ +█ █ @ +█ █ @ +████@ + █ @ + @@ +███ @ +██ @ + █ @ +██ @ + @@ + ██ @ +██ @ +█ █ @ + █ @ + @@ +███ @ + █ @ + █ @ + █ @ + @@ + ██ @ + █ █@ +█ █ @ + ██ @ + @@ + █ @ +█ █ @ + ██ @ + █ @ + @@ +█ @ + @ + @ +█ @ + @@ + █ @ + @ + █ @ +██ @ + @@ + █ @ +█ @ + █ @ + @ + @@ +███ @ + @ +███ @ + @ + @@ +█ @ + █ @ +█ @ + @ + @@ +███ @ + ██ @ + @ + █ @ + @@ + ██ @ +█ █@ +█ ██@ + ██@ + @@ + ██ @ +█ █@ +████@ +█ █@ + @@ +██ @ +█ █ @ +██ █@ +███ @ + @@ + ██ @ +█ @ +█ @ + ██ @ + @@ +███ @ +█ █@ +█ █@ +███ @ + @@ +███ @ +██ @ +█ @ +███ @ + @@ +███ @ +█ @ +███ @ +█ @ + @@ + ██ @ +█ @ +█ ██@ + ██ @ + @@ +█ █@ +█ █@ +████@ +█ █@ + @@ +███ @ + █ @ + █ @ +███ @ + @@ + ███@ + █ @ +█ █ @ + █ @ + @@ +█ █@ +█ █ @ +███ @ +█ █@ + @@ +█ @ +█ @ +█ @ +███ @ + @@ +██ █@ +████@ +█ █@ +█ █@ + @@ +█ █@ +██ █@ +█ ██@ +█ █@ + @@ + ██ @ +█ █@ +█ █@ + ██ @ + @@ +███ @ +█ █@ +███ @ +█ @ + @@ + ██ @ +█ █@ + ██ @ + █@ + @@ +███ @ +█ █@ +███ @ +█ █@ + @@ + ██ @ +██ @ + ██@ + ██ @ + @@ +███ @ + █ @ + █ @ + █ @ + @@ +█ █@ +█ █@ +█ █@ + ██ @ + @@ +█ █@ +█ █@ +█ █ @ +██ @ + @@ +█ █@ +█ █@ +████@ +██ █@ + @@ +█ █ @ +█ █ @ + █ @ +█ █ @ + @@ +█ █ @ +█ █ @ + █ @ + █ @ + @@ +████@ + █ @ + █ @ +████@ + @@ +██ @ +█ @ +█ @ +██ @ + @@ +█ @ + █ @ + █ @ + █@ + @@ +██ @ + █ @ + █ @ +██ @ + @@ + █ @ +█ █ @ + @ + @ + @@ + @ + @ + @ +████@ + @@ +█ @ + █ @ + @ + @ + @@ + @ + ██ @ +█ █ @ + ██ @ + @@ +█ @ +██ @ +█ █ @ +██ @ + @@ + @ + ██ @ +█ @ + ██ @ + @@ + █ @ + ██ @ +█ █ @ + ██ @ + @@ + █ @ +█ █ @ +██ @ + ██ @ + @@ + █ @ +█ @ +██ @ +█ @ + @@ + █ @ +█ █ @ + ██ @ +██ @ + @@ +█ @ +██ @ +█ █ @ +█ █ @ + @@ +█ @ + @ +█ @ +█ @ + @@ + █ @ + █ @ + █ @ +█ @ + @@ +█ @ +█ █ @ +██ @ +█ █ @ + @@ +█ @ +█ @ +█ @ + █ @ + @@ + @ +███ @ +██ █@ +█ █@ + @@ + @ + █ @ +█ █ @ +█ █ @ + @@ + @ + █ @ +█ █ @ + █ @ + @@ + █ @ +█ █ @ +██ @ +█ @ + @@ + █ @ +█ █ @ + ██ @ + █ @ + @@ + @ + ██ @ +█ @ +█ @ + @@ + █ @ +█ @ + █ @ +█ @ + @@ + █ @ +███ @ + █ @ + ██ @ + @@ + @ +█ █@ +█ █@ + ██ @ + @@ + @ +█ █ @ +█ █ @ + █ @ + @@ + @ +█ █@ +█ ██@ + ███@ + @@ + @ +█ █ @ + █ @ +█ █ @ + @@ +█ █ @ + ██ @ + █ @ +██ @ + @@ +███ @ + █ @ + █ @ +███ @ + @@ + █ @ + █ @ +██ @ + ██ @ + @@ +█ @ +█ @ +█ @ +█ @ + @@ +█ @ + █ @ + ██ @ +██ @ + @@ + █ █@ +█ █ @ + @ + @ + @@ + @ + ███@ + ███@ + ██ @ + @@ +████@ +████@ +████@ +████@ + @@ + ██ @ +█ ██@ +█ █@ +█ ██@ + @@ + █ @ + █ @ +█ █ @ +███ @ + @@ + @ +█ @ + @ + @ + @@ + @ + @ + @ + @ + @@ + █ █@ +███ @ +█ █ @ + █ @ + @@ diff --git a/cspell.json b/cspell.json index 82efc4d..35659fa 100644 --- a/cspell.json +++ b/cspell.json @@ -61,6 +61,7 @@ "setuptools", "Shft", "smam", + "Smushing", "stransi", "STRINGTABLE", "textconv", diff --git a/src/textual_paint/repack_font.py b/src/textual_paint/repack_font.py index 7c356cc..149bd19 100755 --- a/src/textual_paint/repack_font.py +++ b/src/textual_paint/repack_font.py @@ -22,6 +22,181 @@ block_char_lookup = { 0xF: '█', } +def spacePad(num: int) -> str: + return ' ' * num + +def blankLines(num: int, width: int) -> str: + lines = [spacePad(width) for _ in range(num)] + return '\n'.join(lines) + +class FIGletFontWriter: + + charOrder: list[int] = [ii for ii in range(32, 127)] + [196, 214, 220, 228, 246, 252, 223] + + def __init__( + self, + fontName: str, + figChars: dict[int, str], + height: int, + baseline: int, + maxLength: int, + commentLines: list[str], + rightToLeft: bool = False, + horizontalLayout: str = 'Universal Smushing', + verticalLayout: str = 'Universal Smushing', + codeTagCount: int = 0, + hardBlank: str = "$", + endMark: str = "@", + ): + self.fontName = fontName + self.figChars: dict[int, str] = figChars + self.height = height + self.baseline = baseline + self.maxLength = maxLength + self.commentLines: list[str] = commentLines + self.rightToLeft = rightToLeft + self.codeTagCount = codeTagCount + self.hardBlank = hardBlank + self.endMark = endMark + self.horizontalLayout = horizontalLayout + self.verticalLayout = verticalLayout + self.hRule = [False] * 7 + self.vRule = [False] * 7 + self.caseInsensitive = False + + def getOldLayoutValue(self) -> int: + val = 0 + if self.horizontalLayout == 'Full': + return -1 + elif self.horizontalLayout == 'Fitted': + return 0 + elif self.horizontalLayout == 'Universal Smushing': + return 0 + else: + val += 1 if self.hRule[1] else 0 + val += 2 if self.hRule[2] else 0 + val += 4 if self.hRule[3] else 0 + val += 8 if self.hRule[4] else 0 + val += 16 if self.hRule[5] else 0 + val += 32 if self.hRule[6] else 0 + return val + + def getFullLayoutValue(self) -> int: + val = 0 + + # horizontal rules + if self.horizontalLayout == 'Full': + val += 0 + elif self.horizontalLayout == 'Fitted': + val += 64 + elif self.horizontalLayout == 'Universal Smushing': + val += 128 + else: + val += 128 + val += 1 if self.hRule[1] else 0 + val += 2 if self.hRule[2] else 0 + val += 4 if self.hRule[3] else 0 + val += 8 if self.hRule[4] else 0 + val += 16 if self.hRule[5] else 0 + val += 32 if self.hRule[6] else 0 + + # vertical rules + if self.verticalLayout == 'Full': + val += 0 + elif self.verticalLayout == 'Fitted': + val += 8192 + elif self.verticalLayout == 'Universal Smushing': + val += 16384 + else: + val += 16384 + val += 256 if self.vRule[1] else 0 + val += 512 if self.vRule[2] else 0 + val += 1024 if self.vRule[3] else 0 + val += 2048 if self.vRule[4] else 0 + val += 4096 if self.vRule[5] else 0 + + return val + + def generateFigFontHeader(self) -> str: + header: list[str] = [] + baseline = self.baseline + + if not baseline: + baseline = self.height + baseline = int(baseline) + if baseline <= 0 or baseline > self.height: + baseline = self.height + + header.append('flf2a' + self.hardBlank) + header.append(str(self.height)) + header.append(str(baseline)) + header.append(str(self.maxLength)) + header.append(str(self.getOldLayoutValue())) + header.append(str(len(self.commentLines))) + header.append("1" if self.rightToLeft else "0") + header.append(str(self.getFullLayoutValue())) + header.append(str(self.codeTagCount)) + + return ' '.join(header) + + def fixFigChars(self): + height = 0 + charWidth: dict[int, int] = {} + maxWidth = 0 + + # Fix case insensitivity + if self.caseInsensitive is True: + for ii in range(97, 123): + self.figChars[ii] = self.figChars[ii - 32] + + # Calculate max height and ensure consistent width for each character + for idx in self.figChars: + if idx == -1: + continue # ignore comment header + + figChar = self.figChars[idx].replace('\r\n', '\n').split('\n') + height = max(height, len(figChar)) + charWidth[idx] = 0 + + for line in figChar: + charWidth[idx] = max(charWidth[idx], len(line)) + + for i in range(len(figChar)): + if len(figChar[i]) < charWidth[idx]: + figChar[i] += spacePad(charWidth[idx] - len(figChar[i])) + + maxWidth = max(maxWidth, charWidth[idx]) + + self.figChars[idx] = '\n'.join(figChar) + + # Fix any height issues + for idx in self.figChars: + if idx == -1: + continue # ignore comment header + + figChar = self.figChars[idx].replace('\r\n', '\n').split('\n') + if len(figChar) < height: + self.figChars[idx] = '\n'.join(figChar) + '\n' + blankLines(height - len(figChar), charWidth[idx]) + + self.height = height + self.maxLength = maxWidth + 2 + + def createFigFileData(self) -> str: + output = '' + self.fixFigChars() + + output = self.generateFigFontHeader() + '\n' + output += "\n".join(self.commentLines) + '\n' + + for char in self.charOrder: + figChar = self.figChars.get(char) + if figChar is None: + raise Exception(f"Character {char} missing from figChars") + output += (self.endMark + '\n').join(figChar.split('\n')) + output += self.endMark + self.endMark + '\n' + + return output + def extract_textures(image_path: str): # Open the image image = Image.open(image_path) @@ -38,8 +213,9 @@ def extract_textures(image_path: str): # Create a new image to store the extracted textures extracted_image = Image.new('RGB', (num_textures_x * texture_width, num_textures_y * texture_height)) - extracted_text_half = "" - extracted_text_full = "" + + half_size_meta_glyphs = {} + full_size_meta_glyphs = {} # Extract textures for row in range(num_textures_y): @@ -60,7 +236,12 @@ def extract_textures(image_path: str): # Paste the texture onto the extracted image extracted_image.paste(texture, (paste_x, paste_y)) - # Extract as text + # Calculate the ordinal of the character + ordinal = row * num_textures_x + col + ordinal -= 2 + + # Extract as half-size FIGlet font + extracted_text_half = "" for y in range(0, texture_height, 2): for x in range(0, texture_width, 2): # Get the four pixels that make up a character @@ -79,9 +260,11 @@ def extract_textures(image_path: str): # Add a newline after each row extracted_text_half += '\n' - # extracted_text += '(end of character ' + str(row * num_textures_x + col) + ')\n' - - # Extract as text 2 + + half_size_meta_glyphs[ordinal] = extracted_text_half + + # Extract as full-size FIGlet font + extracted_text_full = "" for y in range(texture_height): for x in range(texture_width): # Get the pixel @@ -96,22 +279,52 @@ def extract_textures(image_path: str): # Add a newline after each row extracted_text_full += '\n' + + full_size_meta_glyphs[ordinal] = extracted_text_full + + + half_size_font = FIGletFontWriter( + fontName="NanoTiny 2x2", + figChars=half_size_meta_glyphs, + height=2, + baseline=2, + maxLength=2, + commentLines=[ + "NanoTiny 2x2", + "by Isaiah Odhner", + ], + horizontalLayout="Full", + verticalLayout="Full", + ) + full_size_font = FIGletFontWriter( + fontName="NanoTiny 4x4", + figChars=full_size_meta_glyphs, + height=4, + baseline=4, + maxLength=4, + commentLines=[ + "NanoTiny 4x4", + "by Isaiah Odhner", + ], + horizontalLayout="Full", + verticalLayout="Full", + ) - return extracted_image, extracted_text_half, extracted_text_full + return extracted_image, half_size_font.createFigFileData(), full_size_font.createFigFileData() base_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')) samples_folder = os.path.join(base_folder, 'samples') image_path = os.path.join(samples_folder, 'NanoTiny_v14.png') output_path = os.path.join(samples_folder, 'NanoTiny_v14_no_border.png') -half_size_text_output_path = os.path.join(base_folder, 'NanoTiny_v14_2x2.txt') -full_size_text_output_path = os.path.join(base_folder, 'NanoTiny_v14_4x4.txt') +half_size_flf_output_path = os.path.join(base_folder, 'NanoTiny_v14_2x2.flf') +full_size_flf_output_path = os.path.join(base_folder, 'NanoTiny_v14_4x4.flf') extracted_image, extracted_text_half, extracted_text_full = extract_textures(image_path) extracted_image.save(output_path) print(f'Wrote extracted textures to {output_path}') -with open(full_size_text_output_path, 'w') as f: +with open(full_size_flf_output_path, 'w') as f: f.write(extracted_text_full) -print(f'Wrote extracted textures to {full_size_text_output_path}') -with open(half_size_text_output_path, 'w') as f: +print(f'Wrote FIGlet font {full_size_flf_output_path}') +with open(half_size_flf_output_path, 'w') as f: f.write(extracted_text_half) -print(f'Wrote extracted textures to {half_size_text_output_path}') +print(f'Wrote FIGlet font {half_size_flf_output_path}')