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.90"
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(u'.', 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(u'.', 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(u'.', 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(u'.', 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 (%s) imported from another module" % (MY_MODULE_NAME, MY_MODULE_VERSION))
pass
|