Alternatively, write me a mail.
1 #!/usr/bin/env zsh 2 3 # fnt+png→@tf converts fonts drawn for the playdate using tools such as 4 # https://play.date/caps/ to otf, ttf, or any font file managed by fontforge 5 # using phosphor 6 7 # made in bebou 8 # september~november 2025 9 10 # Dependencies (tested on nixos, see shell.nix) 11 # phosphor https://gitlab.com/emilegreis/phosphor 12 # jq 13 # imagemagick 14 # ascii-image-converter 15 # fontforge 16 # python3 17 # fonttools 18 # fontforge 19 # svgwrite 20 # pyyaml 21 # python-frontmatter 22 23 # TODO 24 # decouple html from the rest 25 # read ascend line from glyph 26 # build composable glyphs 27 # -h/usage 29 30 disclaimer() { clear; printf "\ 31 COLLECTIVE CONDITIONS FOR REUSE (see LICENSE) 32 33 By typing YES, I confirm the gesture I'm about to perform does NOT go against 34 any conditions of use stated by initial authors of the font: "; read allowed; 35 36 case "$allowed" in # user's ethic 37 [Yy]*) echo;; 38 *) echo "Not today !"; exit 1;; 39 esac 40 } 41 42 # nix-shell 43 # ╲ 44 # zsh 45 # ╲ 46 # fnt+png→@tf.sh 47 # _______╱╲_____________________________ 48 # │ │ 49 # │ for each font.png with metrics.fnt │ 50 # │ ╱ ╱╲ │ 51 # │ ┌──────────┐ glyph width │ 52 # │ │0123456789│ % 15 │ 53 # │ │ !"#$%&'()│ < 9 │ 54 # │ │*+,-.╱:;<=│ = 8 │ 55 # │ │>?@ABCDEFG│ > 9 │ 56 # │ │HIJKLMNOPQ│ + ? 10 │ 57 # │ │RSTUVWXYZ[│ @ 13 │ 58 # │ │╲]^_`abcde│ A 13 │ 59 # │ │fghijklmno│ B 11 │ 60 # │ │pqrstuvwxy│ C 14 │ 61 # │ │z{│}~¥…™�v│ ⋮ ⋮ │ 62 # │ └──────────┘ │ 63 # │_________________ _______________│ 64 # ╲ ╱ 65 #_____________________╲ ╱___________________________________ python ________ 66 # ╲╱ │ 67 # magick ascii- for COLS │ phosphor.py 68 # -bg white image- for ROWS │ 69 # -rm alpha converter awk │ 70 # │ 71 # table.png > table.bmp > table.txt > glyph.zns → .otf .ttf 72 # │ 73 # ▄▀▄▀▄▀▄▀▄▀ ▇▇▇▇▇▇▇▇▇▇ .......... ....│..... ·········· 74 # ▄▀▄▀▄▀▄▀▄▀ ▇▇▇▇▇▇▇▇▇▇ .......... .......... ·········· 75 # ▄▀▄░░░░▀▄▀ ▇▇▇ ▇▇▇ ...@@@@... ...####... ···????··· 76 # ▄▀░░░░░░▄▀ ▇▇ ▇▇ ..@@@@@@.. ..######.. ··??????·· 77 # ▄▀▄▀▄▀░░▄▀ ▇▇▇▇▇▇ ▇▇ ......@@.. ......##.. ······??·· 78 # ▄▀▄░░░░░▄▀ → ▇▇▇ ▇▇ → ...@@@@@.. → ...#####.. → ···?????·· 79 # ▄▀░░▄▀░░▄▀ ▇▇ ▇▇ ▇▇ ..@@..@@.. ..##..##.. ··??··??·· 80 # ▄▀░░░░░░▄▀ ▇▇ ▇▇ ..@@@@@@.. ..######.. ··??????·· 81 # ▄▀▄░░░░░▄▀ ▇▇▇ ▇▇ ...@@@@@.. ...#####.. ···?????·· 82 # ▄▀▄▀▄▀▄▀▄▀ ▇▇▇▇▇▇▇▇▇▇ .......... .......... ·········· 83 # ▄▀▄▀▄▀▄▀▄▀ ▇▇▇▇▇▇▇▇▇▇ .......... .......... ·········· 84 # │ 85 #_____________________________________________________│________________________ 86 87 unicode_lookup () { # Caching unicode points from .fnt 88 find . -name "*.fnt" | while read fnt; do 89 awk 'length($1) == 1 || $1 == "space" {printf "%s\n", $1}' $fnt 90 done | 91 sort fnt | uniq | while read char; do 92 unicode -s "$char" --brief 93 done | tee /dev/stderr | sed "s!U+!u!g" > unicode.lookup 94 } 95 96 if [ ! -f unicode.lookup ]; then 97 echo "unicode.lookup not found, generating lookup…" 98 unicode_lookup 99 echo "done." 100 fi 101 102 ID=0 # counter for applying fonts to specimen (css) 103 extract_glyphs_from_txt() { 104 ROWS=$1; COLS=$2; DIR=$3; NAME=$4; W=$5; H=$6; BEARING=$7 105 ID=$((ID + 1)) 106 107 START=$(date +%s.%N) 108 cat <<- % >> $SPECIMEN 109 <style> 110 @font-face { 111 font-family: "$NAME"; 112 src: url("$DIR/$NAME/$NAME.ttf") format("truetype"); 113 src: url("$DIR/$NAME/$NAME.otf") format("opentype"); 114 } 115 section:nth-of-type($ID){font-family: "$NAME"; } 116 </style> 117 <section> 118 <h1 contenteditable="true">$NAME</h1> 119 <p>$COLS*$ROWS, $W*$H px, tracking=$TRACKING</p> 120 <p>$png</p> 121 $( 122 if [ -e "${fnt%.*}.fea" ]; then 123 echo "<details><summary>${fnt%.*}.fea</summary>" 124 echo "<pre>" 125 cat "${fnt%.*}.fea" 126 echo "</pre></details>" 127 fi 128 ) 129 <img src="$png" alt="$png"> 130 <table> 131 % 132 133 printf "%s" "$NAME" 134 printf "\n" 135 136 N=1 137 for y in $(seq 1 $ROWS); do 138 printf "<tr>" >> $SPECIMEN 139 h=$((y * H - H)) 140 h_h=$((h + H)) 141 for x in $(seq 1 $COLS); do 142 w=$((x * W - W + 1)) 143 chasse=$(sed -n "${N} p" $DIR/$NAME/widths) 144 if [[ $chasse -gt $W ]]; then chasse="$W"; fi 145 CHAR=$(sed -n "${N} p" $DIR/$NAME/labels) 146 AGL=$(awk -v C="$CHAR" '$1 == C {printf "%s\n", $2; exit}' unicode.lookup) 147 if [[ $CHAR = "u0020" ]]; then AGL="u0020"; CHAR=" " fi 148 printf " %s |" $CHAR 149 printf "<td>%s<span>%s %s → %s</span></td>" $CHAR $CHAR $AGL $chasse >> $SPECIMEN 150 cat <<- % > "$DIR/$NAME/glyphs/$AGL.zns" 151 --- 152 name: $AGL 153 bearing_right: $BEARING 154 --- 155 $(awk -v h="$h" -v H="$H" -v w="$w" -v W="$W" -v c="$chasse" -v t="$BEARING" \ 156 'NR > h && NR <= h+H { print substr($0, w, W) }' \ 157 "$DIR/$NAME/$NAME.txt") 158 % 159 N=$((N + 1)) 160 done 161 printf "\n" 162 printf "</tr>\n" >> $SPECIMEN 163 done 164 printf "</table></section>\n" >> $SPECIMEN 165 STOP=$(echo $(date +%s.%N) - $START | bc | cut -c -5) 166 printf "%ss\n" $STOP 167 printf "\n" 168 } 169 170 #DIST=playdateFonts$(date +%s 171 #IN 172 #rm -rf $DIST 173 174 TOTAL_START=$(date +%s.%N) 175 176 disclaimer 177 178 DIST=out 179 rm -rf Fonts/ 180 unzip Fonts.zip 181 SPECIMEN=specimen.html 182 cat << % > $SPECIMEN 183 <!DOCTYPE html> 184 <html> 185 <head> 186 <title>playdate fonts $(date)</title> 187 <meta charset="utf-8" /> 188 <style> 189 h1 {font-weight:normal} 190 img{ 191 image-rendering: pixelated; 192 border:1px solid gray 193 } 194 span, p {font-family:monospace;} 195 td span {display:none;position:absolute;pointer-events:none} 196 td:hover span {display:block;background:white;} 197 td:hover{color:fuchsia} 198 section{border-top:1px solid black} 199 </style> 200 </head> 201 <body> 202 <p>$(date)</p> 203 % 204 KERN_COEF=48 205 find . -name "*.png" -printf '%P\n' | # https://stackoverflow.com/a/2596736 206 grep -v Numerals | 207 grep -E "[0-9]+-[0-9]+" | 208 while read png; do 209 fnt=$(echo "$png" | sed -r "s!(.*)-table.*!\1.fnt!g") 210 211 NAME=$(basename "$png" | sed -r "s!(.*)-table.*!\1!g") 212 DIR=$(dirname "$png") 213 214 FAMILY=$(echo "$png" | rev | cut -d/ -f 2 | rev | sed 's/-[0-9].*//') 215 echo $FAMILY 216 DIMS=$(echo "$png" | grep -oE "[0-9]+-[0-9]+") 217 png_LENGTH=$(identify -format "%[fx:w]" "$png") 218 png_HEIGHT=$(identify -format "%[fx:h]" "$png") 219 W=$(echo $DIMS | cut -d- -f 1) 220 H=$(echo $DIMS | cut -d- -f 2) 221 COLS=$((png_LENGTH / W)) 222 ROWS=$((png_HEIGHT / H)) 223 FONT_NAME=$(echo "$png" | sed -r "s!^.*/(.*)-table.*!\1!g; s!^font-!!g") 224 225 if grep -q -e "--metrics" "$fnt"; then # ugly+++ :-) 226 grep -e "--metrics" "$fnt" | sed "s!--metrics=!!g" | 227 jq -rc '.pairs' | 228 sed -re 's!^\{!!g; s!\}$!!g;' | 229 sed -re 's!,"(..)"!\n"\1"!g;' | 230 sed -re 's@"(.)(.)":\[(-?.),.*@\\\1 \\\2 \3@g'| 231 sed ' 232 s/\\0/zero/g 233 s/\\1/one/g 234 s/\\2/two/g 235 s/\\3/three/g 236 s/\\4/four/g 237 s/\\5/five/g 238 s/\\6/six/g 239 s/\\7/seven/g 240 s/\\8/eight/g 241 s/\\9/nine/g 242 s/\\\./period/g 243 s/\\,/comma/g 244 s/\\'\''/quotesingle/g 245 s/\\;/semicolon/g 246 s/\\:/colon/g 247 s/\\-/hyphen/g 248 s/\\ /space/g 249 s/\\?/question/g 250 s/\\!/exclam/g 251 s/\\‼/exclamdbl/g 252 s/\xef\xb8\x8e//g 253 s/\\\//slash/g 254 s/\\//g 255 '| tee pairs | 256 awk -v kc="$KERN_COEF" '{printf "\t\tpos @%s @%s %s;\n", $1, $2, $3 * kc} 257 ' > kerning_pairs #"${fnt%.*}.fea" 258 259 sed -re 's@([[:alpha:]]*) ([[:alpha:]]*).*@\1\n\2@g' pairs | sort | uniq | 260 sed -re 's!([[:alpha:]]*)!\t\t@\1 = [\\\1 ];!g' > lookups 261 262 cat << % | sed "/@[ 8]/d" > "${fnt%.*}.fea" 263 lookup kern { 264 lookupflag 0; 265 $(cat lookups) 266 $(cat kerning_pairs) 267 } kern; 268 269 feature kern { 270 script DFLT; 271 language dflt ; 272 lookup kern; 273 script grek; 274 language dflt ; 275 lookup kern; 276 script latn; 277 language dflt ; 278 lookup kern; 279 } kern; 280 % 281 fi 282 283 TRACKING=$(grep tracking $fnt | cut -d= -f2) 284 if [ -z "$TRACKING" ]; then TRACKING="1"; fi 285 #BEARING=$(echo $TRACKING / 2 | bc) 286 287 mkdir -p "$DIR/$NAME/glyphs" 288 mkdir -p "$DIR/$NAME/svg" 289 290 awk 'NR > 10 && /^$/{exit}; length($1) == 1 || $1 == "space" {printf "%s\n", $1}' $fnt | sed "s/space/u0020/g" > "$DIR/$NAME/labels" 291 awk 'NR > 10 && /^$/{exit}; length($1) == 1 || $1 == "space" {printf "%s\n", $2}' $fnt | sed "s/space/u0020/g" > "$DIR/$NAME/widths" 292 293 # TODO ascenders and descenders guessing 294 # if [[ ]]; then DESCENT_CHAR_REF=u0058 #X 295 # elif [[ ]]; then DESCENT_CHAR_REF=u0078 #x 296 # elif [[ ]]; then DESCENT_CHAR_REF=u0030; fi #0 297 # DESCENT=$(tac "$OUT_FONT_PATH/$DESCENT_CHAR_REF.zns" | awk '{ #if $0 has @ print NR exit }') 298 # ASCENT= $(cat "$OUT_FONT_PATH/$DESCENT_CHAR_REF.zns" | awk '{ #if $0 has @ print NR exit }') 299 300 #if [ -e "${fnt%.*}.fea" ]; then fea = "--features ${fnt%.*}.fea"; fi 301 #python ./phosphor/phosphor.py --glyphs "$DIR/$NAME/glyphs/"*.zns \ 302 # --ascent $H --descent 3 --top $H \ 303 # $fea \ 304 # --familyname "$FAMILY" --fontname "$NAME" \ 305 # --destination "$PWD/$DIR/$NAME" 306 fea="${fnt%.*}.fea" 307 magick "$png" -background white -alpha remove "$DIR/$NAME/$NAME.bmp" 308 ascii-image-converter "$DIR/$NAME/$NAME.bmp" -d $png_LENGTH,$png_HEIGHT -m ".#" -n > "$DIR/$NAME/$NAME.txt" 309 extract_glyphs_from_txt $ROWS $COLS "$DIR" "$NAME" $W $H $TRACKING 310 if [ -e "${fnt%.*}.fea" ]; then 311 python ./phosphor/phosphor.py --glyphs "$DIR/$NAME/glyphs/"*.zns \ 312 --ascent $H --descent 3 --top $H \ 313 --features "$fea" \ 314 --familyname "$FAMILY" --fontname "$NAME" \ 315 --destination "$PWD/$DIR/$NAME" 316 else 317 python ./phosphor/phosphor.py --glyphs "$DIR/$NAME/glyphs/"*.zns \ 318 --ascent $H --descent 3 --top $H \ 319 --familyname "$FAMILY" --fontname "$NAME" \ 320 --destination "$PWD/$DIR/$NAME" 321 fi 322 #--features kerning.fea \ 323 324 mv "$fnt" "$png" "$DIR/$NAME" 325 done 326 327 cat << % >> $SPECIMEN 328 </body> 329 </html> 330 % 331 332 # INFO 333 TOTAL_STOP=$(echo $(date +%s.%N) - $TOTAL_START | bc | cut -c -5) 334 cat << % | tee report 335 took $TOTAL_STOP seconds 336 $(uname -a) 337 $(magick -version | head -n 1 | sed "s/^Version: //g") 338 ascii-image-converter $(ascii-image-converter -v) 339 $(awk -V | head -n 1) 340 $(grep -V | head -n 1) 341 $(sed --v | head -n 1) 342 %