aboutsummaryrefslogtreecommitdiff
path: root/devtools/create_bladerunner/subtitles/fontCreator/fonFileLib.py
blob: 9e3c7472a4f99dc1dfaebc928e579d488fb81275 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
osLibFound = False
sysLibFound = False
shutilLibFound = False
structLibFound = False
imagePilLibFound = False

try:
	import os
except ImportError:
	print "[Error] os python library is required to be installed!"
else:
	osLibFound = True
	
try:
	import sys
except ImportError:
	print "[Error] sys python library is required to be installed!"
else:
	sysLibFound = True

try:
	import struct
except ImportError:
	print "[Error] struct python library is required to be installed!"
else:
	structLibFound = True
	
try:
	from PIL import Image
except ImportError:
	print "[Error] Image python library (PIL) is required to be installed!"
else:
	imagePilLibFound = True

if 	(not osLibFound) \
	or (not sysLibFound) \
	or (not structLibFound) \
	or (not imagePilLibFound):
	sys.stdout.write("[Error] Errors were found when trying to import required python libraries\n")
	sys.exit(1)

from struct import *

MY_MODULE_VERSION = "0.80"
MY_MODULE_NAME    = "fonFileLib"

class FonHeader(object):
	maxEntriesInTableOfDetails = -1 # this is probably always the number of entries in table of details, but it won't work for the corrupted TAHOMA18.FON file
	maxGlyphWidth = -1              # in pixels
	maxGlyphHeight = -1             # in pixels
	graphicSegmentByteSize = -1     # Graphic segment byte size
	
	def __init__(self):
		return


class fonFile(object):
	m_header = FonHeader()
	
	simpleFontFileName = 'GENERIC.FON'
	realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
	nonEmptyCharacters = 0
	
	glyphDetailEntriesLst = [] # list of 5-value tuples. Tuple values are (X-offset, Y-offset, Width, Height, Offset in Graphics segment)
	glyphPixelData = None      # buffer of pixel data for glyphs
	
	m_traceModeEnabled = False
	
	# traceModeEnabled is bool to enable more printed debug messages
	def __init__(self, traceModeEnabled = True):
		del self.glyphDetailEntriesLst[:]
		self.glyphPixelData = None # buffer of pixel data for glyphs
		self.simpleFontFileName = 'GENERIC.FON'
		self.realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
		self.nonEmptyCharacters = 0
		self.m_traceModeEnabled = traceModeEnabled
		
		return
		
	def loadFonFile(self, fonBytesBuff, maxLength, fonFileName):
		self.simpleFontFileName =  fonFileName
		
		offsInFonFile = 0
		localLstOfDataOffsets = []
		del localLstOfDataOffsets[:]
		#
		# parse FON file fields for header
		#
		try:
			tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
			self.header().maxEntriesInTableOfDetails = tmpTuple[0]
			offsInFonFile += 4
			
			if self.simpleFontFileName == 'TAHOMA18.FON': # deal with corrupted original 'TAHOMA18.FON' file
				self.realNumOfCharactersInImageSegment = 176
				if self.m_traceModeEnabled:
					print "[Debug] SPECIAL CASE. WORKAROUND FOR CORRUPTED %s FILE. Only %d characters supported!" % (self.simpleFontFileName, self.realNumOfCharactersInImageSegment)
			else:
				self.realNumOfCharactersInImageSegment = self.header().maxEntriesInTableOfDetails
				
			tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
			self.header().maxGlyphWidth = tmpTuple[0]
			offsInFonFile += 4
			
			tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
			self.header().maxGlyphHeight = tmpTuple[0]
			offsInFonFile += 4
			
			tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
			self.header().graphicSegmentByteSize = tmpTuple[0]
			offsInFonFile += 4
			
			if self.m_traceModeEnabled:
				print "[Debug] Font file (FON) Header Info: "
				print "[Debug] Number of entries: %d, Glyph max-Width: %d, Glyph max-Height: %d, Graphic Segment size: %d" % (self.header().maxEntriesInTableOfDetails, self.header().maxGlyphWidth, self.header().maxGlyphHeight, self.header().graphicSegmentByteSize)
			#
			# Glyph details table (each entry is 5 unsigned integers == 5*4 = 20 bytes)
			# For most characters, their ASCII value + 1 is the index of their glyph's entry in the details table. The 0 entry of this table is reserved
			#
			#tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset
			if self.m_traceModeEnabled:
				print "[Debug] Font file (FON) glyph details table: "
			for idx in range(0, self.realNumOfCharactersInImageSegment):
				tmpTuple = struct.unpack_from('i', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
				tmpXOffset = tmpTuple[0]
				offsInFonFile += 4
				
				tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
				tmpYOffset = tmpTuple[0]
				offsInFonFile += 4
				
				tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
				tmpWidth = tmpTuple[0]
				offsInFonFile += 4
				
				tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
				tmpHeight = tmpTuple[0]
				offsInFonFile += 4
				
				tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile)  # unsigned integer 4 bytes
				tmpDataOffset = tmpTuple[0]
				offsInFonFile += 4
				
				if tmpWidth == 0 or tmpHeight == 0:
					if self.m_traceModeEnabled:
						print "Index: %d\t UNUSED *****************************************************************" % (idx)
				else:
					self.nonEmptyCharacters += 1
					if self.m_traceModeEnabled:
						print "Index: %d\txOffs: %d\tyOffs: %d\twidth: %d\theight: %d\tdataOffs: %d" % (idx, tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset)
					if tmpDataOffset not in localLstOfDataOffsets:
						localLstOfDataOffsets.append(tmpDataOffset)
					else:
						# This never happens in the original files. Offsets are "re-used" but not really because it happens only for empty (height = 0) characters which all seem to point to the next non-empty character
						if self.m_traceModeEnabled:
							print "Index: %d\t RE-USING ANOTHER GLYPH *****************************************************************" % (idx)
							
				self.glyphDetailEntriesLst.append( ( tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset) )
				
			offsInFonFile = (4 * 4) + (self.header().maxEntriesInTableOfDetails * 5 * 4) # we need the total self.header().maxEntriesInTableOfDetails here and not self.realNumOfCharactersInImageSegment
			self.glyphPixelData = fonBytesBuff[offsInFonFile:]
			return True
		except:
			print "[Error] Loading Font file (FON) %s failed!" % (self.simpleFontFileName)
			raise
		return False
		
	def outputFonToPNG(self):
		print "[Info] Exporting font file (FON) to PNG: %s" % (self.simpleFontFileName + ".PNG")
		
		targWidth = 0
		targHeight = 0
		paddingFromTopY = 2
		paddingBetweenGlyphsX = 10
		
		if len(self.glyphDetailEntriesLst) == 0 or (len(self.glyphDetailEntriesLst) != self.realNumOfCharactersInImageSegment and len(self.glyphDetailEntriesLst) != self.header().maxEntriesInTableOfDetails) :
			print "[Error] Font file (FON) loading process did not complete correctly. Missing important data in structures. Cannot output image!"
			return
			
		# TODO asdf refine this code here. the dimensions calculation is very crude for now
		if self.header().maxGlyphWidth > 0 :
			targWidth = (self.header().maxGlyphWidth + paddingBetweenGlyphsX) * (self.realNumOfCharactersInImageSegment + 1)
		else:
			targWidth = 1080
			
		# TODO asdf refine this code here. the dimensions calculation is very crude for now
		if self.header().maxGlyphHeight > 0 :
			targHeight = self.header().maxGlyphHeight * 2
		else:
			targHeight = 480
			
		imTargetGameFont = Image.new("RGBA",(targWidth, targHeight), (0,0,0,0))
		#print imTargetGameFont.getbands()
		#
		# Now fill in the image segment
		# Fonts in image segment are stored in pixel colors from TOP to Bottom, Left to Right per GLYPH.
		# Each pixel is 16 bit (2 bytes). Highest bit seems to determine transparency (on/off flag).
		# There seem to be 5 bits per RGB channel and the value is the corresponding 8bit value (from the 24 bit pixel color) shifting out (right) the 3 LSBs
		# First font image is the special character (border of top row and left column) - color of font pixels should be "0x7FFF" for filled and "0x8000" for transparent
		drawIdx = 0
		drawIdxDeductAmount = 0
		for idx in range(0, self.realNumOfCharactersInImageSegment):
			# TODO check for size > 0 for self.glyphPixelData
			# TODO mark glyph OUTLINES? (optional by switch)
			(glyphXoffs, glyphYoffs, glyphWidth, glyphHeight, glyphDataOffs) = self.glyphDetailEntriesLst[idx]
			glyphDataOffs = glyphDataOffs * 2
			#print idx, glyphDataOffs
			currX = 0
			currY = 0
			if (glyphWidth == 0 or glyphHeight == 0):
				drawIdxDeductAmount += 1
			drawIdx = idx - drawIdxDeductAmount
			
			for colorIdx in range(0, glyphWidth*glyphHeight):
				tmpTuple = struct.unpack_from('H', self.glyphPixelData, glyphDataOffs)	# unsigned short 2 bytes
				pixelColor = tmpTuple[0]
				glyphDataOffs += 2
				
#				 if pixelColor > 0x8000:
#					 print "[Debug] WEIRD CASE" # NEVER HAPPENS - TRANSPARENCY IS ON/OFF. There's no grades of transparency
				rgbacolour = (0,0,0,0)
				if pixelColor == 0x8000:
					rgbacolour = (0,0,0,0) # alpha: 0.0 fully transparent
				else:
					tmp8bitR1 =	 ( (pixelColor >> 10) ) << 3
					tmp8bitG1 =	 ( (pixelColor & 0x3ff) >> 5 ) << 3
					tmp8bitB1 =	 ( (pixelColor & 0x1f) ) << 3
					rgbacolour = (tmp8bitR1,tmp8bitG1,tmp8bitB1, 255) # alpha: 1.0 fully opaque
					#rgbacolour = (255,255,255, 255)   # alpha: 1.0 fully opaque
					
				if currX == glyphWidth:
					currX = 0
					currY += 1
					
				imTargetGameFont.putpixel(( (drawIdx + 1) * (self.header().maxGlyphWidth + paddingBetweenGlyphsX ) + currX, paddingFromTopY + glyphYoffs + currY), rgbacolour)
				currX += 1
		try:
			imTargetGameFont.save(os.path.join('.', self.simpleFontFileName + ".PNG"), "PNG")
		except Exception as e:
			print '[Error] Unable to write to output PNG file. ' + str(e)
			
	def header(self):
		return self.m_header
#
#
#
if __name__ == '__main__':
	# main()
	errorFound = False
	# By default assumes a file of name SUBTLS_E.FON in same directory
	# otherwise tries to use the first command line argument as input file
	# 'TAHOMA24.FON'   # USED IN CREDIT END-TITLES and SCORERS BOARD AT POLICE STATION
	# 'TAHOMA18.FON'   # USED IN CREDIT END-TITLES
	# '10PT.FON'       # BLADE RUNNER UNUSED FONT -  Probably font for reporting system errors
	# 'KIA6PT.FON'     # BLADE RUNNER MAIN FONT
	# 'SUBTLS_E.FON'   # OUR EXTRA FONT USED FOR SUBTITLES
	inFONFile = None
	inFONFileName =  'SUBTLS_E.FON'    # Subtitles font custom
	
	if len(sys.argv[1:])  > 0 \
		and os.path.isfile(os.path.join('.', sys.argv[1])) \
		and len(sys.argv[1]) >= 5 \
		and sys.argv[1][-3:].upper() == 'FON':
		inFONFileName = sys.argv[1]
		print "[Info] Attempting to use %s as input FON file..." % (inFONFileName)
	elif os.path.isfile(os.path.join('.', inFONFileName)):
		print "[Info] Using default %s as input FON file..." % (inFONFileName)
	else:
		print "[Error] No valid input file argument was specified and default input file %s is missing." % (inFONFileName)
		errorFound = True
	
	if not errorFound:
		try:
			print "[Info] Opening %s" % (inFONFileName)
			inFONFile = open(os.path.join('.',inFONFileName), 'rb')
		except:
			errorFound = True
			print "[Error] Unexpected event:", sys.exc_info()[0]
			raise
		if not errorFound:
			allOfFonFileInBuffer = inFONFile.read()
			fonFileInstance = fonFile(True)
			if fonFileInstance.m_traceModeEnabled:
				print "[Debug] Running %s (%s) as main module" % (MY_MODULE_NAME, MY_MODULE_VERSION)
			if (fonFileInstance.loadFonFile(allOfFonFileInBuffer, len(allOfFonFileInBuffer), inFONFileName)):
				print "[Info] Font file (FON) was loaded successfully!"
				fonFileInstance.outputFonToPNG()
			else:
				print "[Error] Error while loading Font file (FON)!"
			inFONFile.close()
else:
	#debug
	#print "[Debug] Running	 %s imported from another module" % (MY_MODULE_NAME)
	pass