2018-12-29 00:29:14 +02:00
#!/usr/bin/env python
2018-12-24 14:59:38 +02:00
# -*- coding: UTF-8 -*-
#
2019-01-05 17:47:27 +02:00
osLibFound = False
sysLibFound = False
2018-12-28 19:07:12 +02:00
shutilLibFound = False
structLibFound = False
imagePilLibFound = False
2019-01-05 17:47:27 +02:00
try :
2019-07-23 12:30:39 +03:00
import os
2019-01-05 17:47:27 +02:00
except ImportError :
2019-07-23 12:30:39 +03:00
print " [Error] os python library is required to be installed! "
2019-01-05 17:47:27 +02:00
else :
osLibFound = True
2021-05-04 11:45:03 +03:00
2019-01-05 17:47:27 +02:00
try :
2019-07-23 12:30:39 +03:00
import sys
2019-01-05 17:47:27 +02:00
except ImportError :
2019-07-23 12:30:39 +03:00
print " [Error] sys python library is required to be installed! "
2019-01-05 17:47:27 +02:00
else :
sysLibFound = True
2018-12-28 19:07:12 +02:00
try :
2019-07-23 12:30:39 +03:00
import struct
2018-12-28 19:07:12 +02:00
except ImportError :
2019-07-23 12:30:39 +03:00
print " [Error] struct python library is required to be installed! "
2018-12-28 19:07:12 +02:00
else :
structLibFound = True
2021-05-04 11:45:03 +03:00
2018-12-28 19:07:12 +02:00
try :
2019-07-23 12:30:39 +03:00
from PIL import Image
2018-12-28 19:07:12 +02:00
except ImportError :
2019-07-23 12:30:39 +03:00
print " [Error] Image python library (PIL) is required to be installed! "
2018-12-28 19:07:12 +02:00
else :
imagePilLibFound = True
2019-01-05 17:47:27 +02:00
if ( not osLibFound ) \
or ( not sysLibFound ) \
or ( not structLibFound ) \
or ( not imagePilLibFound ) :
2019-01-02 14:00:45 +02:00
sys . stdout . write ( " [Error] Errors were found when trying to import required python libraries \n " )
2018-12-28 19:07:12 +02:00
sys . exit ( 1 )
2018-12-24 14:59:38 +02:00
from struct import *
2019-07-10 13:58:05 +03:00
MY_MODULE_VERSION = " 0.80 "
2019-07-23 17:46:15 +03:00
MY_MODULE_NAME = " fonFileLib "
2018-12-24 14:59:38 +02:00
2019-07-23 12:30:39 +03:00
class FonHeader ( object ) :
2019-07-23 17:46:15 +03:00
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
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
def __init__ ( self ) :
return
2019-07-23 12:30:39 +03:00
class fonFile ( object ) :
2018-12-24 14:59:38 +02:00
m_header = FonHeader ( )
2021-05-04 11:45:03 +03:00
2019-01-02 14:00:45 +02:00
simpleFontFileName = ' GENERIC.FON '
2019-07-23 12:30:39 +03:00
realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
2018-12-24 14:59:38 +02:00
nonEmptyCharacters = 0
2021-05-04 11:45:03 +03:00
2019-07-23 12:30:39 +03:00
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
2021-05-04 11:45:03 +03:00
2019-01-02 14:00:45 +02:00
m_traceModeEnabled = False
2021-05-04 11:45:03 +03:00
2019-07-23 12:30:39 +03:00
# traceModeEnabled is bool to enable more printed debug messages
2019-01-02 14:00:45 +02:00
def __init__ ( self , traceModeEnabled = True ) :
2018-12-24 14:59:38 +02:00
del self . glyphDetailEntriesLst [ : ]
2019-07-23 17:46:15 +03:00
self . glyphPixelData = None # buffer of pixel data for glyphs
2019-01-02 14:00:45 +02:00
self . simpleFontFileName = ' GENERIC.FON '
2019-07-23 17:46:15 +03:00
self . realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
2018-12-24 14:59:38 +02:00
self . nonEmptyCharacters = 0
2019-01-02 14:00:45 +02:00
self . m_traceModeEnabled = traceModeEnabled
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
return
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
def loadFonFile ( self , fonBytesBuff , maxLength , fonFileName ) :
self . simpleFontFileName = fonFileName
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
offsInFonFile = 0
localLstOfDataOffsets = [ ]
del localLstOfDataOffsets [ : ]
#
# parse FON file fields for header
#
try :
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
self . header ( ) . maxEntriesInTableOfDetails = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
if self . simpleFontFileName == ' TAHOMA18.FON ' : # deal with corrupted original 'TAHOMA18.FON' file
self . realNumOfCharactersInImageSegment = 176
2019-01-02 14:00:45 +02:00
if self . m_traceModeEnabled :
print " [Debug] SPECIAL CASE. WORKAROUND FOR CORRUPTED %s FILE. Only %d characters supported! " % ( self . simpleFontFileName , self . realNumOfCharactersInImageSegment )
2018-12-24 14:59:38 +02:00
else :
self . realNumOfCharactersInImageSegment = self . header ( ) . maxEntriesInTableOfDetails
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
self . header ( ) . maxGlyphWidth = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
self . header ( ) . maxGlyphHeight = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
self . header ( ) . graphicSegmentByteSize = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-01-02 14:00:45 +02:00
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 )
2018-12-24 14:59:38 +02:00
#
# 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
2019-01-02 14:00:45 +02:00
if self . m_traceModeEnabled :
print " [Debug] Font file (FON) glyph details table: "
2018-12-24 14:59:38 +02:00
for idx in range ( 0 , self . realNumOfCharactersInImageSegment ) :
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' i ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
tmpXOffset = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
tmpYOffset = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
tmpWidth = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
tmpHeight = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2019-07-23 17:46:15 +03:00
tmpTuple = struct . unpack_from ( ' I ' , fonBytesBuff , offsInFonFile ) # unsigned integer 4 bytes
2018-12-24 14:59:38 +02:00
tmpDataOffset = tmpTuple [ 0 ]
offsInFonFile + = 4
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
if tmpWidth == 0 or tmpHeight == 0 :
2019-01-02 14:00:45 +02:00
if self . m_traceModeEnabled :
print " Index: %d \t UNUSED ***************************************************************** " % ( idx )
2018-12-24 14:59:38 +02:00
else :
self . nonEmptyCharacters + = 1
2019-01-02 14:00:45 +02:00
if self . m_traceModeEnabled :
print " Index: %d \t xOffs: %d \t yOffs: %d \t width: %d \t height: %d \t dataOffs: %d " % ( idx , tmpXOffset , tmpYOffset , tmpWidth , tmpHeight , tmpDataOffset )
2018-12-24 14:59:38 +02:00
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
2019-01-02 14:00:45 +02:00
if self . m_traceModeEnabled :
print " Index: %d \t RE-USING ANOTHER GLYPH ***************************************************************** " % ( idx )
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
self . glyphDetailEntriesLst . append ( ( tmpXOffset , tmpYOffset , tmpWidth , tmpHeight , tmpDataOffset ) )
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
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 :
2019-01-02 14:00:45 +02:00
print " [Error] Loading Font file (FON) %s failed! " % ( self . simpleFontFileName )
2018-12-24 14:59:38 +02:00
raise
2019-07-23 12:30:39 +03:00
return False
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
def outputFonToPNG ( self ) :
2019-07-23 12:30:39 +03:00
print " [Info] Exporting font file (FON) to PNG: %s " % ( self . simpleFontFileName + " .PNG " )
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
targWidth = 0
targHeight = 0
paddingFromTopY = 2
paddingBetweenGlyphsX = 10
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
if len ( self . glyphDetailEntriesLst ) == 0 or ( len ( self . glyphDetailEntriesLst ) != self . realNumOfCharactersInImageSegment and len ( self . glyphDetailEntriesLst ) != self . header ( ) . maxEntriesInTableOfDetails ) :
2019-01-02 14:00:45 +02:00
print " [Error] Font file (FON) loading process did not complete correctly. Missing important data in structures. Cannot output image! "
2018-12-24 14:59:38 +02:00
return
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
# 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
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
# 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
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
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
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
for colorIdx in range ( 0 , glyphWidth * glyphHeight ) :
tmpTuple = struct . unpack_from ( ' H ' , self . glyphPixelData , glyphDataOffs ) # unsigned short 2 bytes
pixelColor = tmpTuple [ 0 ]
glyphDataOffs + = 2
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
# if pixelColor > 0x8000:
2019-01-02 14:00:45 +02:00
# print "[Debug] WEIRD CASE" # NEVER HAPPENS - TRANSPARENCY IS ON/OFF. There's no grades of transparency
2018-12-24 14:59:38 +02:00
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
2019-07-23 17:46:15 +03:00
rgbacolour = ( tmp8bitR1 , tmp8bitG1 , tmp8bitB1 , 255 ) # alpha: 1.0 fully opaque
2018-12-24 14:59:38 +02:00
#rgbacolour = (255,255,255, 255) # alpha: 1.0 fully opaque
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
if currX == glyphWidth :
currX = 0
currY + = 1
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
imTargetGameFont . putpixel ( ( ( drawIdx + 1 ) * ( self . header ( ) . maxGlyphWidth + paddingBetweenGlyphsX ) + currX , paddingFromTopY + glyphYoffs + currY ) , rgbacolour )
currX + = 1
2019-01-02 14:00:45 +02:00
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 )
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
def header ( self ) :
return self . m_header
#
#
#
if __name__ == ' __main__ ' :
2019-07-23 12:30:39 +03:00
# main()
2018-12-24 14:59:38 +02:00
errorFound = False
2019-07-10 13:58:05 +03:00
# 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
2021-05-04 11:45:03 +03:00
2019-07-10 13:58:05 +03:00
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 )
2018-12-24 14:59:38 +02:00
errorFound = True
2021-05-04 11:45:03 +03:00
2018-12-24 14:59:38 +02:00
if not errorFound :
2019-07-10 13:58:05 +03:00
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 ( )
2018-12-24 14:59:38 +02:00
else :
#debug
2019-07-10 13:58:05 +03:00
#print "[Debug] Running %s imported from another module" % (MY_MODULE_NAME)
2019-07-23 12:30:39 +03:00
pass