1# Convert Font Awesome, Fork Awesome, Google Material Design, Material Design Icons, Kenney Game and Ionicons
2# icon font parameters to C89, C++11 and C# compatible formats.
3#
4#------------------------------------------------------------------------------
5# 1 - Source material
6#
7#   1.1 - Font Awesome
8#		1.1.1 - version 4
9#			https://raw.githubusercontent.com/FortAwesome/Font-Awesome/fa-4/src/icons.yml
10#			https://github.com/FortAwesome/Font-Awesome/blob/fa-4/fonts/fontawesome-webfont.ttf
11#		1.1.2 - version 5
12#			https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/advanced-options/metadata/icons.yml
13#			https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-brands-400.ttf
14#			https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-regular-400.ttf
15#			https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-solid-900.ttf
16#	1.2 - Fork Awesome
17# 			https://raw.githubusercontent.com/ForkAwesome/Fork-Awesome/master/src/icons/icons.yml
18# 			https://github.com/ForkAwesome/Fork-Awesome/blob/master/fonts/forkawesome-webfont.ttf
19#   1.3 - Google Material Design
20# 			https://raw.githubusercontent.com/google/material-design-icons/master/iconfont/codepoints
21#			https://github.com/google/material-design-icons/blob/master/iconfont/MaterialIcons-Regular.ttf
22#   1.4 - Material Design Icons
23#			https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/css/materialdesignicons.css
24#			https://github.com/Templarian/MaterialDesign-Webfont/blob/master/fonts/materialdesignicons-webfont.ttf
25#   1.5 - Kenney Game icons
26#           https://raw.githubusercontent.com/nicodinh/kenney-icon-font/master/css/kenney-icons.css
27#			https://github.com/nicodinh/kenney-icon-font/blob/master/fonts/kenney-icon-font.ttf
28#   1.6 - Ionicons
29#			https://raw.githubusercontent.com/ionic-team/ionicons/master/src/docs/archived/v2/css/ionicons.css
30#			https://github.com/ionic-team/ionicons/blob/master/src/docs/archived/v2/fonts/ionicons.ttf
31#
32#------------------------------------------------------------------------------
33# 2 - Data sample
34#
35#   Font Awesome example:
36#           - input:            music:
37#                                 changes:
38#                                   - '1'
39#                                   - 5.0.0
40#                                 label: Music
41#                                 search:
42#                                   terms:
43#                                     - note
44#                                     - sound
45#                                 styles:
46#                                   - solid
47#                                 unicode: f001
48#           - output C++11:     #define ICON_FA_MUSIC u8"\uf001"
49#           - output C89:       #define ICON_FA_MUSIC "\xEF\x80\x81"
50#			- output C#:		public const string Music = "\uf001";
51#
52#   All fonts have computed min and max unicode fonts ICON_MIN and ICON_MAX
53#           - output C89, C++11:	#define ICON_MIN_FA 0xf000
54#                               	#define ICON_MAX_FA 0xf2e0
55#			- output C#:			public const int IconMin = 0xf000;
56#									public const int IconMax = 0xf2e0;
57#
58#------------------------------------------------------------------------------
59# 3 - Script dependencies
60#
61#   3.1 - Fonts source material online
62#   3.2 - Python 2.7 - https://www.python.org/download/releases/2.7/
63#   3.3 - Requests - http://docs.python-requests.org/
64#   3.4 - PyYAML - http://pyyaml.org/
65#
66#------------------------------------------------------------------------------
67# 4 - References
68#
69# 	GitHub repository: https://github.com/juliettef/IconFontCppHeaders/
70#
71#------------------------------------------------------------------------------
72
73
74import requests
75import yaml
76
77
78# Fonts
79
80class Font:
81	font_name = '[ ERROR - missing font name ]'
82	font_abbr = '[ ERROR - missing font abbreviation ]'
83	font_url_data = '[ ERROR - missing font data url ]'
84	font_url_ttf = '[ ERROR - missing ttf file url ]'
85	font_file_name_ttf = '[ ERROR - missing ttf file name ]'
86
87	@classmethod
88	def get_icons( cls, input ):
89		# intermediate representation of the fonts data, identify the min and max
90		print( '[ ERROR - missing implementation of class method get_icons for {!s} ]'.format( cls.font_name ))
91		icons_data = {}
92		icons_data.update({ 'font_min' : '[ ERROR - missing font min ]',
93							'font_max' : '[ ERROR - missing font max ]',
94							'icons' : '[ ERROR - missing list of pairs [ font icon name, code ]]' })
95		return icons_data
96
97	@classmethod
98	def download( cls ):
99		input_raw = ''
100		response = requests.get( cls.font_url_data, timeout = 2 )
101		if response.status_code == 200:
102			input_raw = response.content
103			print( 'Downloaded - ' + cls.font_name )
104		else:
105			raise Exception( 'Download failed - ' + cls.font_name )
106		return input_raw
107
108	@classmethod
109	def get_intermediate_representation( cls ):
110		font_ir = {}
111		input_raw = cls.download()
112		if input_raw:
113			icons_data = cls.get_icons( input_raw )
114			font_ir.update( icons_data )
115			font_ir.update({ 'font_url_ttf' : cls.font_url_ttf,
116							 'font_url_data' : cls.font_url_data,
117							 'font_file_name_ttf' : cls.font_file_name_ttf,
118							 'font_name' : cls.font_name,
119							 'font_abbr' : cls.font_abbr })
120			print( 'Generated intermediate data - ' + cls.font_name )
121		return font_ir
122
123
124class FontFA4( Font ):	# legacy Font Awesome version 4
125	font_name = 'Font Awesome 4'
126	font_abbr = 'FA'
127	font_url_data = 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/fa-4/src/icons.yml'
128	font_url_ttf = 'https://github.com/FortAwesome/Font-Awesome/blob/fa-4/fonts/fontawesome-webfont.ttf'
129	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/')+1: ]]]
130
131	@classmethod
132	def get_icons( self, input ):
133		icons_data = { }
134		data = yaml.safe_load(input)
135		font_min = 'ffff'
136		font_max = '0'
137		icons = []
138		for item in data[ 'icons' ]:
139			if item[ 'unicode' ] < font_min:
140				font_min = item[ 'unicode' ]
141			if item[ 'unicode' ] >= font_max:
142				font_max = item[ 'unicode' ]
143			icons.append([ item[ 'id' ], item[ 'unicode' ]])
144		icons_data.update({ 'font_min' : font_min,
145						'font_max' : font_max,
146						'icons' : icons })
147		return icons_data
148
149
150class FontFK( FontFA4 ):	# Fork Awesome, based on Font Awesome 4
151	font_name = 'Fork Awesome'
152	font_abbr = 'FK'
153	font_url_data = 'https://raw.githubusercontent.com/ForkAwesome/Fork-Awesome/master/src/icons/icons.yml'
154	font_url_ttf = 'https://github.com/ForkAwesome/Fork-Awesome/blob/master/fonts/forkawesome-webfont.ttf'
155
156
157class FontFA5( Font ):	# Font Awesome version 5. Solid and Regular styles (Regular is a subset of Solid).
158	font_name = 'Font Awesome 5'
159	font_abbr = 'FA'
160	font_url_data = 'https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/advanced-options/metadata/icons.yml'
161	font_url_ttf = 'https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-solid-900.ttf, ' +\
162		'https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-regular-400.ttf, '
163	font_file_name_ttf = [[ 'FAS', 'fa-solid-900.ttf' ], [ 'FAR', 'fa-regular-400.ttf' ]]
164	font_fa_style = [ 'solid', 'regular' ]
165
166	@classmethod
167	def get_icons( self, input ):
168		icons_data = { }
169		data = yaml.safe_load(input)
170		if data:
171			font_min = 'ffff'
172			font_max = '0'
173			icons = []
174			for key in data:
175				item = data[ key ]
176				for style in item[ 'styles' ]:
177					if style in self.font_fa_style:
178						if [ key, item[ 'unicode' ]] not in icons:
179							if item[ 'unicode' ] < font_min:
180								font_min = item[ 'unicode' ]
181							if item[ 'unicode' ] >= font_max:
182								font_max = item[ 'unicode' ]
183							icons.append([ key, item[ 'unicode' ] ])
184			icons_data.update({ 'font_min':font_min, 'font_max':font_max, 'icons':icons })
185		return icons_data
186
187
188class FontFA5Brands( FontFA5 ):	# Font Awesome version 5, Brand styles.
189	font_name = 'Font Awesome 5 Brands'
190	font_abbr = 'FAB'
191	font_url_ttf = 'https://github.com/FortAwesome/Font-Awesome/blob/master/web-fonts-with-css/webfonts/fa-brands-400.ttf'
192	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/') + 1: ]]]
193	font_fa_style = [ 'brands' ]
194
195	@classmethod
196	def get_icons( self, input ):
197		icons_data = { }
198		data = yaml.safe_load(input)
199		if data:
200			font_min = 'ffff'
201			font_max = '0'
202			icons = [ ]
203			for key in data:
204				item = data[ key ]
205				for style in item[ 'styles' ]:
206					if style in self.font_fa_style:
207						if item[ 'unicode' ] < font_min:
208							font_min = item[ 'unicode' ]
209						if item[ 'unicode' ] >= font_max:
210							font_max = item[ 'unicode' ]
211						icons.append([ key, item[ 'unicode' ]])
212			icons_data.update({ 'font_min':font_min, 'font_max':font_max, 'icons':icons })
213		return icons_data
214
215
216class FontMD( Font ):	# Material Design
217	font_name = 'Material Design'
218	font_abbr = 'MD'
219	font_url_data = 'https://raw.githubusercontent.com/google/material-design-icons/master/iconfont/codepoints'
220	font_url_ttf = 'https://github.com/google/material-design-icons/blob/master/iconfont/MaterialIcons-Regular.ttf'
221	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/')+1: ]]]
222
223	@classmethod
224	def get_icons( self, input ):
225		icons_data = {}
226		lines = str.split( input, '\n' )
227		if lines:
228			font_min = 'ffff'
229			font_max = '0'
230			icons = []
231			for line in lines :
232				words = str.split(line)
233				if words and len( words ) >= 2:
234					if words[ 1 ] < font_min:
235						font_min = words[ 1 ]
236					if words[ 1 ] >= font_max:
237						font_max = words[ 1 ]
238					icons.append( words )
239			icons_data.update({ 'font_min' : font_min,
240								'font_max' : font_max,
241								'icons' : icons })
242		return icons_data
243
244
245class FontMDI( Font ):	# Material Design Icons
246	font_name = 'Material Design Icons'
247	font_abbr = 'MDI'
248	font_url_data = 'https://raw.githubusercontent.com/Templarian/MaterialDesign-Webfont/master/css/materialdesignicons.css'
249	font_url_ttf = 'https://github.com/Templarian/MaterialDesign-Webfont/blob/master/fonts/materialdesignicons-webfont.ttf'
250	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/')+1: ]]]
251
252	@classmethod
253	def get_icons( self, input ):
254		icons_data = {}
255		input_trimmed = input[ input.find( '-moz-osx-font-smoothing: grayscale;\n}\n\n' ) + len( '-moz-osx-font-smoothing: grayscale;\n}\n\n' ) : input.find( '.mdi-18px.mdi-set,' )]
256		lines = str.split( input_trimmed, '}\n\n' )
257		if lines:
258			font_min = 'ffff'
259			font_max = '0'
260			icons = []
261			for line in lines :
262				if '.mdi-' in line:
263					words = str.split(line)
264					if words and '.mdi-' in words[ 0 ]:
265						font_id = words[ 0 ].partition( '.mdi-' )[2].partition( ':before' )[0]
266						font_code = words[ 3 ].partition( '"\\' )[2].partition( '";' )[0]
267						if font_code < font_min:
268							font_min = font_code
269						if font_code >= font_max:
270							font_max = font_code
271						icons.append([ font_id, font_code ])
272			icons_data.update({ 'font_min' : font_min,
273								'font_max' : font_max,
274								'icons' : icons  })
275		return icons_data
276
277
278class FontKI( Font ):	# Kenney Game icons
279	font_name = 'Kenney'
280	font_abbr = 'KI'
281	font_url_data = 'https://raw.githubusercontent.com/nicodinh/kenney-icon-font/master/css/kenney-icons.css'
282	font_url_ttf = 'https://github.com/nicodinh/kenney-icon-font/blob/master/fonts/kenney-icon-font.ttf'
283	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/')+1: ]]]
284
285	@classmethod
286	def get_icons( self, input ):
287		icons_data = {}
288		lines = str.split( input, '\n' )
289		if lines:
290			font_min = 'ffff'
291			font_max = '0'
292			icons = []
293			for line in lines :
294				if '.ki-' in line:
295					words = str.split(line)
296					if words and '.ki-' in words[ 0 ]:
297						font_id = words[ 0 ].partition( '.ki-' )[2].partition( ':before' )[0]
298						font_code = words[ 2 ].partition( '"\\' )[2].partition( '";' )[0]
299						if font_code < font_min:
300							font_min = font_code
301						if font_code >= font_max:
302							font_max = font_code
303						icons.append([ font_id, font_code ])
304			icons_data.update({ 'font_min' : font_min,
305								'font_max' : font_max,
306								'icons' : icons  })
307		return icons_data
308
309
310class FontII( Font ):	# Ionicons
311	font_name = 'Ionicons'
312	font_abbr = 'II'
313	font_url_data = 'https://raw.githubusercontent.com/ionic-team/ionicons/master/src/docs/archived/v2/css/ionicons.css'
314	font_url_ttf = 'https://github.com/ionic-team/ionicons/blob/master/src/docs/archived/v2/fonts/ionicons.ttf'
315	font_file_name_ttf = [[ font_abbr, font_url_ttf[ font_url_ttf.rfind('/') + 1: ]]]
316
317	@classmethod
318	def get_icons( self, input ):
319		icons_data = {}
320		lines = str.split( input, '\n' )
321		if lines:
322			font_min = 'ffff'
323			font_max = '0'
324			icons = []
325			for line in lines :
326				if ( '.ion-' and 'content:' ) in line:
327					words = str.split(line)
328					if words and '.ion-' in words[ 0 ]:
329						font_id = words[ 0 ].partition( '.ion-' )[2].partition( ':before' )[0]
330						font_code = words[ 3 ].partition( '"\\' )[2].partition( '";' )[0]
331						if font_code < font_min:
332							font_min = font_code
333						if font_code >= font_max:
334							font_max = font_code
335						icons.append([ font_id, font_code ])
336			icons_data.update({ 'font_min' : font_min,
337								'font_max' : font_max,
338								'icons' : icons  })
339		return icons_data
340
341
342# Languages
343
344
345class Language:
346	language_name = '[ ERROR - missing language name ]'
347	file_name = '[ ERROR - missing file name ]'
348	intermediate = {}
349
350	def __init__( self, intermediate ):
351		self.intermediate = intermediate
352
353	@classmethod
354	def prelude( cls ):
355		print('[ ERROR - missing implementation of class method prelude for {!s} ]'.format( cls.language_name ))
356		result = '[ ERROR - missing prelude ]'
357		return result
358
359	@classmethod
360	def lines_minmax( cls ):
361		print('[ ERROR - missing implementation of class method lines_minmax for {!s} ]'.format( cls.language_name ))
362		result = '[ ERROR - missing min and max ]'
363		return result
364
365	@classmethod
366	def line_icon( cls, icon ):
367		print('[ ERROR - missing implementation of class method line_icon for {!s} ]'.format( cls.language_name ))
368		result = '[ ERROR - missing icon line ]'
369		return result
370
371	@classmethod
372	def epilogue( cls ):
373		return ''
374
375	@classmethod
376	def convert( cls ):
377		result = cls.prelude() + cls.lines_minmax()
378		for icon in cls.intermediate.get( 'icons' ):
379			line_icon = cls.line_icon( icon )
380			result += line_icon
381		result += cls.epilogue()
382		print ( 'Converted - {!s} for {!s}' ).format( cls.intermediate.get( 'font_name' ), cls.language_name )
383		return result
384
385	@classmethod
386	def save_to_file( cls ):
387		filename = cls.file_name.format( name = str.lower(cls.intermediate.get( 'font_name' )).replace( ' ', '_' ))
388		converted = cls.convert()
389		with open( filename, 'w' ) as f:
390			f.write( converted )
391		print( 'Saved - {!s}' ).format( filename )
392
393
394class LanguageC89( Language ):
395	language_name = 'C89'
396	file_name = 'icons_{name}.h'
397
398	@classmethod
399	def prelude( cls ):
400		tmpl_prelude = '// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py for language {lang}\n' + \
401			'// from {url_data}\n' + \
402			'// for use with {url_ttf}\n' + \
403			'#pragma once\n\n'
404		result = tmpl_prelude.format(lang = cls.language_name,
405									 url_data = cls.intermediate.get( 'font_url_data' ),
406									 url_ttf = cls.intermediate.get( 'font_url_ttf' ))
407		tmpl_prelude_define_file_name = '#define FONT_ICON_FILE_NAME_{font_abbr} "{file_name_ttf}"\n'
408		file_names_ttf = cls.intermediate.get( 'font_file_name_ttf' )
409		for file_name_ttf in file_names_ttf:
410			result += tmpl_prelude_define_file_name.format( font_abbr = file_name_ttf[ 0 ], file_name_ttf = file_name_ttf[ 1 ])
411		return result + '\n'
412
413	@classmethod
414	def lines_minmax( cls ):
415		tmpl_line_minmax = '#define ICON_{minmax}_{abbr} 0x{val}\n'
416		result = tmpl_line_minmax.format(minmax = 'MIN',
417										 abbr = cls.intermediate.get( 'font_abbr' ),
418										 val = cls.intermediate.get( 'font_min' )) + \
419				 tmpl_line_minmax.format(minmax = 'MAX',
420										 abbr = cls.intermediate.get( 'font_abbr' ),
421										 val = cls.intermediate.get( 'font_max' ))
422		return result
423
424	@classmethod
425	def line_icon( cls, icon ):
426		tmpl_line_icon = '#define ICON_{abbr}_{icon} "{code}"\n'
427		icon_name = str.upper( icon[ 0 ]).replace( '-', '_' )
428		code_base = ''.join([ '{0:x}'.format( ord( x )) for x in unichr( int( icon[ 1 ], 16 )).encode( 'utf-8' )]).upper()
429		icon_code = '\\x' + code_base[ :2 ] + '\\x' + code_base[ 2:4 ] + '\\x' + code_base[ 4: ]
430		result = tmpl_line_icon.format( abbr = cls.intermediate.get( 'font_abbr' ),
431										icon = icon_name,
432										code = icon_code )
433		return result
434
435
436class LanguageCpp11( LanguageC89 ):
437	language_name = 'C++11'
438	file_name = 'Icons{name}.h'
439
440	@classmethod
441	def line_icon( cls, icon ):
442		tmpl_line_icon = '#define ICON_{abbr}_{icon} u8"\u{code}"\n'
443		icon_name = str.upper( icon[ 0 ]).replace( '-', '_' )
444		icon_code = icon[ 1 ]
445		result = tmpl_line_icon.format( abbr = cls.intermediate.get( 'font_abbr' ),
446										icon = icon_name,
447										code = icon_code)
448		return result
449
450
451class LanguageCSharp( Language ):
452	language_name = "C#"
453	file_name = 'Icons{name}.cs'
454
455	@classmethod
456	def prelude( cls ):
457		tmpl_prelude = '// Generated by https://github.com/juliettef/IconFontCppHeaders script GenerateIconFontCppHeaders.py for language {lang}\n' + \
458			'// from {url_data}\n' + \
459			'// for use with {url_ttf}\n' + \
460			'namespace IconFonts\n' + \
461			'{{\n' + \
462			'    public class {font_name}\n' + \
463			'    {{\n'
464
465		result = tmpl_prelude.format(lang = cls.language_name,
466									 url_data = cls.intermediate.get( 'font_url_data' ),
467									 url_ttf = cls.intermediate.get( 'font_url_ttf' ),
468									 font_name = cls.intermediate.get( 'font_name' ).replace( ' ', '' )
469									 )
470		tmpl_prelude_define_file_name = '        public const string FontIconFileName = "{file_name_ttf}";\n'
471		file_names_ttf = cls.intermediate.get( 'font_file_name_ttf' )
472		for file_name_ttf in file_names_ttf:
473			result += tmpl_prelude_define_file_name.format( file_name_ttf = file_name_ttf[ 1 ])
474		return result + '\n'
475
476	@classmethod
477	def epilogue( cls ):
478		return '    }\n' + \
479			'}\n'
480
481	@classmethod
482	def lines_minmax( cls ):
483		tmpl_line_minmax = '        public const int Icon{minmax} = 0x{val};\n'
484		result = tmpl_line_minmax.format(minmax = 'Min',
485										 val = cls.intermediate.get( 'font_min' )) + \
486				 tmpl_line_minmax.format(minmax = 'Max',
487										 val = cls.intermediate.get( 'font_max' ))
488		return result
489
490	@classmethod
491	def line_icon( cls, icon ):
492
493		tmpl_line_icon = '        public const string {icon} = "\u{code}";\n'
494		icon_name = cls.to_camelcase(icon[ 0 ])
495		icon_code = icon[ 1 ]
496
497		if icon_name[ 0 ].isdigit():
498			# Variable may not start with a digit
499			icon_name = 'The' + icon_name
500
501		if icon_name == cls.intermediate.get( 'font_name' ).replace( ' ', '' ):
502			# Member may not have same name as enclosing class
503			icon_name += 'Icon'
504
505		result = tmpl_line_icon.format( icon = icon_name,
506										code = icon_code)
507		return result
508
509	@classmethod
510	def to_camelcase( cls, text ):
511		parts = text.split( '-' )
512		for i in range( len( parts ) ):
513			p = parts[i]
514			parts[ i ] = p[ 0 ].upper() + p[ 1: ].lower()
515		return ''.join( parts )
516
517
518# Main
519fonts = [ FontFA4, FontFA5, FontFA5Brands, FontFK, FontMD, FontMDI, FontKI, FontII ]
520languages = [ LanguageC89 ]
521
522intermediates = []
523for font in fonts:
524	try:
525		font_intermediate = font.get_intermediate_representation()
526		intermediates.append( font_intermediate )
527	except Exception as e:
528		print( '[ ERROR: {!s} ]'.format( e ))
529for interm in intermediates:
530	Language.intermediate = interm
531	for lang in languages:
532		lang.save_to_file()
533