1# 2# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> 3# 4# This program is free software; you can redistribute it and/or 5# modify it under the terms of the GNU General Public License as 6# published by the Free Software Foundation; either version 2 of 7# the License, or (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14 15import re 16 17import fnutil 18import fncli 19import fnio 20import bmpf 21 22 23class Params(fncli.Params): 24 def __init__(self): 25 fncli.Params.__init__(self) 26 self.version = -1 27 self.exchange = -1 28 self.output = None 29 30 31HELP = ('' + 32 'usage: bdftopsf [-1|-2|-r] [-g|-G] [-o OUTPUT] [INPUT.bdf] [TABLE...]\n' + 33 'Convert a BDF font to PC Screen Font or raw font\n' + 34 '\n' + 35 ' -1, -2 write a PSF version 1 or 2 font (default = 1 if possible)\n' + 36 ' -r, --raw write a RAW font\n' + 37 ' -g, --vga exchange the characters at positions 0...31 with these at\n' + 38 ' 192...223 (default for VGA text mode compliant PSF fonts\n' + 39 ' with 224 to 512 characters starting with unicode 00A3)\n' + 40 ' -G do not exchange characters 0...31 and 192...223\n' + 41 ' -o OUTPUT output file (default = stdout, must not be a terminal)\n' + 42 ' --help display this help and exit\n' + 43 ' --version display the program version and license, and exit\n' + 44 ' --excstk display the exception stack on error\n' + 45 '\n' + 46 'The input must be a monospaced unicode-encoded BDF 2.1 font.\n' + 47 '\n' + 48 'The tables are text files with two or more hexadecimal unicodes per line:\n' + 49 'a character code from the BDF, and extra code(s) for it. All extra codes\n' + 50 'are stored sequentially in the PSF unicode table for their character.\n' + 51 '<ss> is always specified as FFFE, although it is stored as FE in PSF2.\n') 52 53VERSION = 'bdftopsf 1.50, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE 54 55class Options(fncli.Options): 56 def __init__(self): 57 fncli.Options.__init__(self, ['-o'], HELP, VERSION) 58 59 60 def parse(self, name, value, params): 61 if name in ['-1', '-2']: 62 params.version = int(name[1]) 63 elif name in ['-r', '--raw']: 64 params.version = 0 65 elif name in ['-g', '--vga']: 66 params.exchange = True 67 elif name == '-G': 68 params.exchange = False 69 elif name == '-o': 70 params.output = value 71 else: 72 self.fallback(name, params) 73 74 75def main_program(nonopt, parsed): 76 version = parsed.version 77 exchange = parsed.exchange 78 bdfile = len(nonopt) > 0 and nonopt[0].lower().endswith('.bdf') 79 ver1_unicodes = True 80 81 # READ INPUT 82 ifs = fnio.InputStream(nonopt[0] if bdfile else None) 83 84 try: 85 font = bmpf.Font.read(ifs) 86 ifs.close() 87 88 for char in font.chars: 89 prefix = 'char %d: ' % char.code 90 91 if char.width != font.bbx.width: 92 raise Exception(prefix + 'output width not equal to maximum output width') 93 94 if char.code == 65534: 95 raise Exception(prefix + 'not a character, use 65535 for empty position') 96 97 if char.code >= 65536: 98 if version == 1: 99 raise Exception(prefix + '-1 requires unicodes <= 65535') 100 ver1_unicodes = False 101 102 # VERSION 103 ver1_num_chars = len(font.chars) == 256 or len(font.chars) == 512 104 105 if version == 1: 106 if not ver1_num_chars: 107 raise Exception('-1 requires a font with 256 or 512 characters') 108 109 if font.bbx.width != 8: 110 raise Exception('-1 requires a font with width 8') 111 112 # EXCHANGE 113 vga_num_chars = len(font.chars) >= 224 and len(font.chars) <= 512 114 vga_text_size = font.bbx.width == 8 and font.bbx.height in [8, 14, 16] 115 116 if exchange is True: 117 if not vga_num_chars: 118 raise Exception('-g/--vga requires a font with 224...512 characters') 119 120 if not vga_text_size: 121 raise Exception('-g/--vga requires an 8x8, 8x14 or 8x16 font') 122 123 except Exception as ex: 124 raise Exception(ifs.location() + str(ex)) 125 126 # READ TABLES 127 tables = dict() 128 129 def load_extra(line): 130 nonlocal ver1_unicodes 131 132 words = re.split(br'\s+', line) 133 134 if len(words) < 2: 135 raise Exception('invalid format') 136 137 uni = fnutil.parse_hex('unicode', words[0]) 138 139 if uni == 0xFFFE: 140 raise Exception('FFFE is not a character') 141 142 if next((char for char in font.chars if char.code == uni), None): 143 if uni >= 0x10000: 144 ver1_unicodes = False 145 146 if uni not in tables: 147 tables[uni] = [] 148 149 table = tables[uni] 150 151 for word in words[1:]: 152 dup = fnutil.parse_hex('extra code', word) 153 154 if dup == 0xFFFF: 155 raise Exception('FFFF is not a character') 156 157 if dup >= 0x10000: 158 ver1_unicodes = False 159 160 if not dup in table or 0xFFFE in table: 161 tables[uni].append(dup) 162 163 if version == 1 and not ver1_unicodes: 164 raise Exception('-1 requires unicodes <= FFFF') 165 166 for name in nonopt[int(bdfile):]: 167 ifs = fnio.InputStream(name) 168 169 try: 170 ifs.read_lines(load_extra) 171 ifs.close() 172 except Exception as ex: 173 raise Exception(ifs.location() + str(ex)) 174 175 # VERSION 176 if version == -1: 177 version = 1 if ver1_num_chars and ver1_unicodes and font.bbx.width == 8 else 2 178 179 # EXCHANGE 180 if exchange == -1: 181 exchange = vga_text_size and version >= 1 and vga_num_chars and font.chars[0].code == 0x00A3 182 183 if exchange: 184 font.chars = font.chars[192:224] + font.chars[32:192] + font.chars[0:32] + font.chars[224:] 185 186 # WRITE 187 ofs = fnio.OutputStream(parsed.output) 188 189 if ofs.file.isatty(): 190 raise Exception('binary output may not be send to a terminal, use -o or redirect/pipe it') 191 192 try: 193 # HEADER 194 if version == 1: 195 ofs.write8(0x36) 196 ofs.write8(0x04) 197 ofs.write8((len(font.chars) >> 8) + 1) 198 ofs.write8(font.bbx.height) 199 elif version == 2: 200 ofs.write32(0x864AB572) 201 ofs.write32(0x00000000) 202 ofs.write32(0x00000020) 203 ofs.write32(0x00000001) 204 ofs.write32(len(font.chars)) 205 ofs.write32(len(font.chars[0].data)) 206 ofs.write32(font.bbx.height) 207 ofs.write32(font.bbx.width) 208 209 # GLYPHS 210 for char in font.chars: 211 ofs.write(char.data) 212 213 # UNICODES 214 if version > 0: 215 def write_unicode(code): 216 if version == 1: 217 ofs.write16(code) 218 elif code <= 0x7F: 219 ofs.write8(code) 220 elif code in [0xFFFE, 0xFFFF]: 221 ofs.write8(code & 0xFF) 222 else: 223 if code <= 0x7FF: 224 ofs.write8(0xC0 + (code >> 6)) 225 else: 226 if code <= 0xFFFF: 227 ofs.write8(0xE0 + (code >> 12)) 228 else: 229 ofs.write8(0xF0 + (code >> 18)) 230 ofs.write8(0x80 + ((code >> 12) & 0x3F)) 231 232 ofs.write8(0x80 + ((code >> 6) & 0x3F)) 233 234 ofs.write8(0x80 + (code & 0x3F)) 235 236 for char in font.chars: 237 if char.code != 0xFFFF: 238 write_unicode(char.code) 239 240 if char.code in tables: 241 for extra in tables[char.code]: 242 write_unicode(extra) 243 244 write_unicode(0xFFFF) 245 246 # FINISH 247 ofs.close() 248 249 except Exception as ex: 250 raise Exception(ofs.location() + str(ex) + ofs.destroy()) 251 252 253if __name__ == '__main__': 254 fncli.start('bdftopsf.py', Options(), Params(), main_program) 255