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
|
#!/usr/bin/python
# -*- coding: UTF-8 -*-
#
import os, sys, shutil
import struct
from struct import *
import Image
my_module_version = "0.50"
my_module_name = "fonFileLib"
class FonHeader:
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:
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
def __init__(self):
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
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
print "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
print "FON Header Info: "
print "Num of entries: %d\tGlyph max-Width: %d\tGlyph max-Height: %d\tGraphic Segment size %d" % (self.header().maxEntriesInTableOfDetails, self.header().maxGlyphWidth, self.header().maxGlyphHeight, self.header().graphicSegmentByteSize)
#print "Num of entries: %d\tGlyph max-Width: %d\tGlyph max-Height: %d\tGraphic Segment size %d" % (self.realNumOfCharactersInImageSegment, 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
print "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:
print "Index: %d\t UNUSED *****************************************************************" % (idx)
else:
self.nonEmptyCharacters += 1
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
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 "Loading failure!"
raise
return False
def outputFonToPNG(self):
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. Fon file load 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 "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
imTargetGameFont.save(os.path.join('.', self.simpleFontFileName + ".PNG"), "PNG")
def header(self):
return self.m_header
#
#
#
if __name__ == '__main__':
# main()
print "Running %s as main module" % (my_module_name)
# assumes a file of name TAHOMA24.FON in same directory
inFONFile = None
#inFONFileName = 'TAHOMA24.FON' # USED IN CREDIT END-TITLES and SCORERS BOARD AT POLICE STATION
#inFONFileName = 'TAHOMA18.FON' # USED IN CREDIT END-TITLES
#inFONFileName = '10PT.FON' # BLADE RUNNER UNUSED FONT?
#inFONFileName = 'KIA6PT.FON' # BLADE RUNNER MAIN FONT
inFONFileName = 'SUBTLS_E.FON' # Subtitles font custom
errorFound = False
try:
print "Opening %s" % (inFONFileName)
inFONFile = open(os.path.join('.',inFONFileName), 'rb')
except:
errorFound = True
print "Unexpected error:", sys.exc_info()[0]
raise
if not errorFound:
allOfFonFileInBuffer = inFONFile.read()
fonFileInstance = fonFile()
if (fonFileInstance.loadFonFile(allOfFonFileInBuffer, len(allOfFonFileInBuffer), inFONFileName)):
print "FON file loaded successfully!"
fonFileInstance.outputFonToPNG()
else:
print "Error while loading FON file!"
inFONFile.close()
else:
#debug
#print "Running %s imported from another module" % (my_module_name)
pass
|