2023-05-13 01:18:17 +03:00
#!/usr/bin/env python3
import os
from PIL import Image
2023-05-13 01:49:33 +03:00
block_char_lookup = {
0x0 : ' ' ,
0x1 : ' ▘ ' ,
0x2 : ' ▝ ' ,
0x3 : ' ▀ ' ,
0x4 : ' ▖ ' ,
0x5 : ' ▌ ' ,
0x6 : ' ▞ ' ,
0x7 : ' ▛ ' ,
0x8 : ' ▗ ' ,
0x9 : ' ▚ ' ,
0xA : ' ▐ ' ,
0xB : ' ▜ ' ,
0xC : ' ▄ ' ,
0xD : ' ▙ ' ,
0xE : ' ▟ ' ,
0xF : ' █ ' ,
}
2023-05-13 07:05:51 +03:00
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 :
2023-05-13 10:18:20 +03:00
""" Used to write FIGlet fonts.
createFigFileData ( ) returns a string that can be written to a . flf file .
It can automatically fix some common problems with FIGlet fonts , such as
incorrect character widths / heights , and missing lowercase characters .
"""
2023-05-13 07:05:51 +03:00
charOrder : list [ int ] = [ ii for ii in range ( 32 , 127 ) ] + [ 196 , 214 , 220 , 228 , 246 , 252 , 223 ]
2023-05-13 10:18:20 +03:00
R """ Character codes that are required to be in any FIGlet font.
Printable portion of the ASCII character set :
32 ( blank / space ) 64 @ 96 `
33 ! 65 A 97 a
34 " 66 B 98 b
35 # 67 C 99 c
36 $ 68 D 100 d
37 % 69 E 101 e
38 & 70 F 102 f
39 ' 71 G 103 g
40 ( 72 H 104 h
41 ) 73 I 105 i
42 * 74 J 106 j
43 + 75 K 107 k
44 , 76 L 108 l
45 - 77 M 109 m
46 . 78 N 110 n
47 / 79 O 111 o
48 0 80 P 112 p
49 1 81 Q 113 q
50 2 82 R 114 r
51 3 83 S 115 s
52 4 84 T 116 t
53 5 85 U 117 u
54 6 86 V 118 v
55 7 87 W 119 w
56 8 88 X 120 x
57 9 89 Y 121 y
58 : 90 Z 122 z
59 ; 91 [ 123 {
60 < 92 \ 124 |
61 = 93 ] 125 }
62 > 94 ^ 126 ~
63 ? 95 _
Additional required Deutsch FIGcharacters , in order :
196 Ä ( umlauted " A " - - two dots over letter " A " )
214 Ö ( umlauted " O " - - two dots over letter " O " )
220 Ü ( umlauted " U " - - two dots over letter " U " )
228 ä ( umlauted " a " - - two dots over letter " a " )
246 ö ( umlauted " o " - - two dots over letter " o " )
252 ü ( umlauted " u " - - two dots over letter " u " )
223 ß ( " ess-zed " - - see FIGcharacter illustration below )
___
/ _ \
| | / /
Ess - zed >> - - - > | | \ \
| | | _ /
| _ |
Additional characters must use code tagged characters , which are not yet supported .
"""
2023-05-13 07:05:51 +03:00
def __init__ (
self ,
2023-05-13 22:00:41 +03:00
figChars : dict [ int , str ] = { } ,
height : int | None = None ,
baseline : int | None = None ,
maxLength : int | None = None ,
commentLines : list [ str ] = [ ] ,
2023-05-13 07:05:51 +03:00
rightToLeft : bool = False ,
horizontalLayout : str = ' Universal Smushing ' ,
verticalLayout : str = ' Universal Smushing ' ,
codeTagCount : int = 0 ,
hardBlank : str = " $ " ,
endMark : str = " @ " ,
2023-05-13 22:00:41 +03:00
caseInsensitive : bool = False ,
2023-05-13 07:05:51 +03:00
) :
self . figChars : dict [ int , str ] = figChars
2023-05-13 10:18:20 +03:00
""" Dictionary that maps character codes to FIGcharacter strings. """
2023-05-13 07:05:51 +03:00
self . height = height
2023-05-13 10:18:20 +03:00
""" Height of a FIGcharacter, in characters. """
2023-05-13 07:05:51 +03:00
self . baseline = baseline
2023-05-13 10:18:20 +03:00
""" Distance from the top of the character to the baseline. If not specified, defaults to height. """
2023-05-13 07:05:51 +03:00
self . maxLength = maxLength
2023-05-13 10:18:20 +03:00
""" Maximum length of a line INCLUDING two endMark characters. """
2023-05-13 07:05:51 +03:00
self . commentLines : list [ str ] = commentLines
2023-05-13 10:18:20 +03:00
""" List of comment lines to be included in the header. It ' s recommended to include at least the name of the font and the name of the author. """
2023-05-13 07:05:51 +03:00
self . rightToLeft = rightToLeft
2023-05-13 10:18:20 +03:00
""" Indicates RTL writing direction (or LTR if False). """
2023-05-13 07:05:51 +03:00
self . codeTagCount = codeTagCount
2023-05-13 10:18:20 +03:00
""" Number of extra characters included in the font (in addition to the required 102 untagged characters). Outputting tagged characters is not yet supported. """
2023-05-13 07:05:51 +03:00
self . hardBlank = hardBlank
2023-05-13 10:18:20 +03:00
""" Invisible character used to prevent smushing. """
2023-05-13 07:05:51 +03:00
self . endMark = endMark
2023-05-13 10:18:20 +03:00
""" Denotes the end of a line. Two of these characters in a row denotes the end of a FIGcharacter. """
2023-05-13 07:05:51 +03:00
self . horizontalLayout = horizontalLayout
2023-05-13 10:18:20 +03:00
""" One of ' Full ' , ' Fitted ' , ' Universal Smushing ' , or ' Controlled Smushing ' """
2023-05-13 07:05:51 +03:00
self . verticalLayout = verticalLayout
2023-05-13 10:18:20 +03:00
""" One of ' Full ' , ' Fitted ' , ' Universal Smushing ' , or ' Controlled Smushing ' """
2023-05-13 07:05:51 +03:00
self . hRule = [ False ] * 7
2023-05-13 10:18:20 +03:00
""" Horizontal Smushing Rules, 1-6 (0 is not used, so that indices correspond with the names of the parameters). horizontalLayout must be ' Controlled Smushing ' for these to take effect. """
2023-05-13 10:17:50 +03:00
self . vRule = [ False ] * 6
2023-05-13 10:18:20 +03:00
""" Vertical Smushing Rules, 1-5 (0 is not used, so that indices correspond with the names of the parameters). verticalLayout must be ' Controlled Smushing ' for these to take effect. """
2023-05-13 09:55:15 +03:00
self . caseInsensitive = caseInsensitive
2023-05-13 10:18:20 +03:00
""" Makes lowercase same as uppercase. Note that this is one-way overwrite. It doesn ' t check if a character already exists, and it won ' t fill in uppercase using lowercase. """
2023-05-13 07:05:51 +03:00
2023-05-13 10:28:08 +03:00
def _getOldLayoutValue ( self ) - > int :
2023-05-13 07:05:51 +03:00
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
2023-05-13 10:28:08 +03:00
def _getFullLayoutValue ( self ) - > int :
2023-05-13 07:05:51 +03:00
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
2023-05-13 22:00:41 +03:00
if self . height is None :
raise ValueError ( " Height must be specified, or should be automatically determined. " )
if baseline is None :
2023-05-13 07:05:51 +03:00
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 ) )
2023-05-13 10:28:08 +03:00
header . append ( str ( self . _getOldLayoutValue ( ) ) )
2023-05-13 07:05:51 +03:00
header . append ( str ( len ( self . commentLines ) ) )
header . append ( " 1 " if self . rightToLeft else " 0 " )
2023-05-13 10:28:08 +03:00
header . append ( str ( self . _getFullLayoutValue ( ) ) )
2023-05-13 07:05:51 +03:00
header . append ( str ( self . codeTagCount ) )
return ' ' . join ( header )
2023-05-13 10:28:08 +03:00
def _fixFigChars ( self ) :
2023-05-13 07:05:51 +03:00
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 :
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 :
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 :
2023-05-13 10:18:20 +03:00
""" Generates the FIGlet file data for the current font. """
2023-05-13 07:05:51 +03:00
output = ' '
2023-05-13 10:28:08 +03:00
self . _fixFigChars ( )
2023-05-13 07:05:51 +03:00
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
2023-05-13 01:18:17 +03:00
def extract_textures ( image_path : str ) :
2023-05-14 03:41:05 +03:00
""" Removes the border around glyphs in an image, creates a new image without the border, and converts the image into FIGlet format font files. """
2023-05-13 10:18:20 +03:00
2023-05-13 01:18:17 +03:00
# Open the image
image = Image . open ( image_path )
# Calculate the texture size and border width
width , height = image . size
texture_width = 4
texture_height = 4
border_width = 1
# Calculate the number of textures in each dimension
num_textures_x = ( width - border_width ) / / ( texture_width + border_width )
num_textures_y = ( height - border_width ) / / ( texture_height + border_width )
# Create a new image to store the extracted textures
extracted_image = Image . new ( ' RGB ' , ( num_textures_x * texture_width , num_textures_y * texture_height ) )
2023-05-13 07:05:51 +03:00
2023-05-13 21:34:39 +03:00
half_size_meta_glyphs : dict [ int , str ] = { }
full_size_meta_glyphs : dict [ int , str ] = { }
2023-05-13 01:18:17 +03:00
# Extract textures
for row in range ( num_textures_y ) :
for col in range ( num_textures_x ) :
# Calculate the coordinates for the current texture
left = col * ( texture_width + border_width ) + border_width
upper = row * ( texture_height + border_width ) + border_width
right = left + texture_width
lower = upper + texture_height
# Crop the texture from the original image
texture = image . crop ( ( left , upper , right , lower ) )
# Calculate the paste coordinates on the extracted image
paste_x = col * texture_width
paste_y = row * texture_height
# Paste the texture onto the extracted image
extracted_image . paste ( texture , ( paste_x , paste_y ) )
2023-05-13 07:05:51 +03:00
# Calculate the ordinal of the character
ordinal = row * num_textures_x + col
ordinal - = 2
# Extract as half-size FIGlet font
extracted_text_half = " "
2023-05-13 01:49:33 +03:00
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
fg_palette_index = 1
aa = texture . getpixel ( ( x , y ) ) == fg_palette_index
ab = texture . getpixel ( ( x , y + 1 ) ) == fg_palette_index
ba = texture . getpixel ( ( x + 1 , y ) ) == fg_palette_index
bb = texture . getpixel ( ( x + 1 , y + 1 ) ) == fg_palette_index
# Convert the pixel to a character
# char = block_char_lookup[(aa << 3) | (ab << 2) | (ba << 1) | bb]
char = block_char_lookup [ ( bb << 3 ) | ( ab << 2 ) | ( ba << 1 ) | aa ]
# Add the character to the extracted text
2023-05-13 05:35:38 +03:00
extracted_text_half + = char
2023-05-13 01:49:33 +03:00
# Add a newline after each row
2023-05-13 05:35:38 +03:00
extracted_text_half + = ' \n '
2023-05-13 07:05:51 +03:00
half_size_meta_glyphs [ ordinal ] = extracted_text_half
# Extract as full-size FIGlet font
extracted_text_full = " "
2023-05-13 05:35:38 +03:00
for y in range ( texture_height ) :
for x in range ( texture_width ) :
# Get the pixel
fg_palette_index = 1
pixel = texture . getpixel ( ( x , y ) ) == fg_palette_index
# Convert the pixel to a character
char = ' █ ' if pixel else ' '
# Add the character to the extracted text
extracted_text_full + = char
# Add a newline after each row
extracted_text_full + = ' \n '
2023-05-13 07:05:51 +03:00
full_size_meta_glyphs [ ordinal ] = extracted_text_full
2023-05-13 21:34:39 +03:00
for figChars in [ half_size_meta_glyphs , full_size_meta_glyphs ] :
# Fill in the space characters with hard blanks
# figChars[32] = figChars[32].replace(' ', '$')
# Or just half of the max width of the FIGcharacters
figChars [ 32 ] = ' \n ' . join ( [ ' $ ' * ( len ( row ) / / 2 ) for row in figChars [ 32 ] . split ( ' \n ' ) ] )
# Add hard blanks to the end of non-whitespace of each row of each FIGcharacter
# With the "Full" layout, this will ensure a space between rendered FIGcharacters.
# The fixup code (_fixFigChars) will handle the ragged right edge.
for ordinal in figChars :
figChars [ ordinal ] = ' \n ' . join ( [ row . rstrip ( ) + ' $ ' for row in figChars [ ordinal ] . split ( ' \n ' ) ] )
2023-05-13 07:05:51 +03:00
half_size_font = FIGletFontWriter (
figChars = half_size_meta_glyphs ,
baseline = 2 ,
commentLines = [
2023-05-13 21:54:58 +03:00
" NanoTiny 2x2 (version 14) " ,
2023-05-13 07:05:51 +03:00
" by Isaiah Odhner " ,
] ,
horizontalLayout = " Full " ,
verticalLayout = " Full " ,
)
full_size_font = FIGletFontWriter (
figChars = full_size_meta_glyphs ,
baseline = 4 ,
commentLines = [
2023-05-13 21:54:58 +03:00
" NanoTiny 4x4 (version 14) " ,
2023-05-13 07:05:51 +03:00
" by Isaiah Odhner " ,
] ,
horizontalLayout = " Full " ,
verticalLayout = " Full " ,
)
2023-05-13 05:35:38 +03:00
2023-05-13 07:05:51 +03:00
return extracted_image , half_size_font . createFigFileData ( ) , full_size_font . createFigFileData ( )
2023-05-13 01:18:17 +03:00
2023-05-14 03:13:31 +03:00
repo_folder = os . path . abspath ( os . path . join ( os . path . dirname ( __file__ ) , ' ../.. ' ) )
font_folder = os . path . join ( repo_folder , ' fonts/NanoTiny ' )
image_input_path = os . path . join ( font_folder , ' NanoTiny_v14.png ' )
image_output_path = os . path . join ( font_folder , ' NanoTiny_v14_no_border.png ' )
half_size_flf_output_path = os . path . join ( font_folder , ' NanoTiny_v14_2x2.flf ' )
full_size_flf_output_path = os . path . join ( font_folder , ' NanoTiny_v14_4x4.flf ' )
extracted_image , extracted_text_half , extracted_text_full = extract_textures ( image_input_path )
extracted_image . save ( image_output_path )
print ( f ' Wrote extracted textures to { image_output_path } ' )
2023-05-13 07:05:51 +03:00
with open ( full_size_flf_output_path , ' w ' ) as f :
2023-05-13 05:35:38 +03:00
f . write ( extracted_text_full )
2023-05-13 07:05:51 +03:00
print ( f ' Wrote FIGlet font { full_size_flf_output_path } ' )
with open ( half_size_flf_output_path , ' w ' ) as f :
2023-05-13 05:35:38 +03:00
f . write ( extracted_text_half )
2023-05-13 07:05:51 +03:00
print ( f ' Wrote FIGlet font { half_size_flf_output_path } ' )