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