#!/usr/bin/python

# dewinfont is copyright 2001 Simon Tatham. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys
import string

# Extract bitmap font data from a Windows .FON or .FNT file.

def frombyte(s):
    return ord(s[0])
def fromword(s):
    return frombyte(s[0:1]) + 256 * frombyte(s[1:2])
def fromdword(s):
    return fromword(s[0:2]) | (fromword(s[2:4]) << 16)

def asciz(s):
    i = string.find(s, "\0")
    if i != -1:
	s = s[:i]
    return s

def bool(n):
    if n:
	return "yes"
    else:
	return "no"

class font:
    pass

class char:
    pass

def savefont(f, file):
    "Write out a .fd form of an internal font description."
    file.write("# .fd font description generated by dewinfont.\n\n")
    file.write("facename " + f.facename + "\n")
    file.write("copyright " + f.copyright + "\n\n")
    file.write("height " + "%d"%f.height + "\n")
    file.write("ascent " + "%d"%f.ascent + "\n")
    if f.height == f.pointsize: file.write("# ")
    file.write("pointsize " + "%d"%f.pointsize + "\n\n")
    if not f.italic: file.write("# ")
    file.write("italic " + bool(f.italic) + "\n")
    if not f.underline: file.write("# ")
    file.write("underline " + bool(f.underline) + "\n")
    if not f.strikeout: file.write("# ")
    file.write("strikeout " + bool(f.strikeout) + "\n")
    if f.weight == 400: file.write("# ")
    file.write("weight " + "%d"%f.weight + "\n\n")
    if f.charset == 0: file.write("# ")
    file.write("charset " + "%d"%f.charset + "\n\n")
    for i in range(256):
	file.write("char " + "%d"%i + "\nwidth " + "%d"%f.chars[i].width+"\n")
	if f.chars[i].width != 0:
	    for j in range(f.height):
		v = f.chars[i].data[j]
		m = 1L << (f.chars[i].width-1)
		for k in range(f.chars[i].width):
		    if v & m:
			file.write("1")
		    else:
			file.write("0")
		    v = v << 1
		file.write("\n")
	file.write("\n")

def dofnt(fnt):
    "Create an internal font description from a .FNT-shaped string."
    f = font()
    f.chars = [None] * 256
    version = fromword(fnt[0:])
    ftype = fromword(fnt[0x42:])
    if ftype & 1:
	sys.stderr.write("This font is a vector font\n")
	return None
    off_facename = fromdword(fnt[0x69:])
    if off_facename < 0 or off_facename > len(fnt):
	sys.stderr.write("Face name not contained within font data")
	return None
    f.facename = asciz(fnt[off_facename:])
    #print "Face name", f.facename
    f.copyright = asciz(fnt[6:66] + "\0")
    #print "Copyright", f.copyright
    f.pointsize = fromword(fnt[0x44:])
    #print "Point size", f.pointsize
    f.ascent = fromword(fnt[0x4A:])
    #print "Ascent", f.ascent
    f.height = fromword(fnt[0x58:])
    #print "Height", f.height
    f.italic = frombyte(fnt[0x50:]) != 0
    f.underline = frombyte(fnt[0x51:]) != 0
    f.strikeout = frombyte(fnt[0x52:]) != 0
    f.weight = fromword(fnt[0x53:])
    f.charset = frombyte(fnt[0x55:])
    #print "Attrs", f.italic, f.underline, f.strikeout, f.weight
    #print "Charset", f.charset
    # Read the char table.
    if version == 0x200:
	ctstart = 0x76
	ctsize = 4
    else:
	ctstart = 0x94
	ctsize = 6
    maxwidth = 0
    for i in range(256):
	f.chars[i] = char()
	f.chars[i].width = 0
	f.chars[i].data = [0L] * f.height
    firstchar = frombyte(fnt[0x5F:])
    lastchar = frombyte(fnt[0x60:])
    for i in range(firstchar,lastchar+1):
	entry = ctstart + ctsize * (i-firstchar)
	w = fromword(fnt[entry:])
	f.chars[i].width = w
	if ctsize == 4:
	    off = fromword(fnt[entry+2:])
	else:
	    off = fromdword(fnt[entry+2:])
	#print "Char", i, "width", w, "offset", off, "filelen", len(fnt)
	widthbytes = (w + 7) / 8
	for j in range(f.height):
	    for k in range(widthbytes):
		bytepos = off + k * f.height + j
		#print bytepos, "->", hex(frombyte(fnt[bytepos:]))
		f.chars[i].data[j] = f.chars[i].data[j] << 8
		f.chars[i].data[j] = f.chars[i].data[j] | frombyte(fnt[bytepos:])
	    f.chars[i].data[j] = f.chars[i].data[j] >> (8*widthbytes - w)
    return f

def nefon(fon, neoff):
    "Finish splitting up a NE-format FON file."
    ret = []
    # Find the resource table.
    rtable = fromword(fon[neoff + 0x24:])
    rtable = rtable + neoff
    # Read the shift count out of the resource table.
    shift = fromword(fon[rtable:])
    # Now loop over the rest of the resource table.
    p = rtable+2
    while 1:
	rtype = fromword(fon[p:])
	if rtype == 0:
	    break  # end of resource table
	count = fromword(fon[p+2:])
	p = p + 8  # type, count, 4 bytes reserved
	for i in range(count):
	    start = fromword(fon[p:]) << shift
	    size = fromword(fon[p+2:]) << shift
	    if start < 0 or size < 0 or start+size > len(fon):
		sys.stderr.write("Resource overruns file boundaries\n")
		return None
	    if rtype == 0x8008: # this is an actual font
		#print "Font at", start, "size", size
		font = dofnt(fon[start:start+size])
		if font == None:
		    sys.stderr.write("Failed to read font resource at %x" \
		    % start)
		    return None
		ret = ret + [font]
	    p = p + 12 # start, size, flags, name/id, 4 bytes reserved
    return ret

def pefon(fon, peoff):
    "Finish splitting up a PE-format FON file."
    dirtables=[]
    dataentries=[]
    def gotoffset(off,dirtables=dirtables,dataentries=dataentries):
	if off & 0x80000000:
	    off = off &~ 0x80000000
	    dirtables.append(off)
	else:
	    dataentries.append(off)
    def dodirtable(rsrc, off, rtype, gotoffset=gotoffset):
	number = fromword(rsrc[off+12:]) + fromword(rsrc[off+14:])
	for i in range(number):
	    entry = off + 16 + 8*i
	    thetype = fromdword(rsrc[entry:])
	    theoff = fromdword(rsrc[entry+4:])
	    if rtype == -1 or rtype == thetype:
		gotoffset(theoff)

    # We could try finding the Resource Table entry in the Optional
    # Header, but it talks about RVAs instead of file offsets, so
    # it's probably easiest just to go straight to the section table.
    # So let's find the size of the Optional Header, which we can
    # then skip over to find the section table.
    secentries = fromword(fon[peoff+0x06:])
    sectable = peoff + 0x18 + fromword(fon[peoff+0x14:])
    for i in range(secentries):
	secentry = sectable + i * 0x28
	secname = asciz(fon[secentry:secentry+8])
	secrva = fromdword(fon[secentry+0x0C:])
	secsize = fromdword(fon[secentry+0x10:])
	secptr = fromdword(fon[secentry+0x14:])
	if secname == ".rsrc":
	    break
    if secname != ".rsrc":
	sys.stderr.write("Unable to locate resource section\n")
	return None
    # Now we've found the resource section, let's throw away the rest.
    rsrc = fon[secptr:secptr+secsize]

    # Now the fun begins. To start with, we must find the initial
    # Resource Directory Table and look up type 0x08 (font) in it.
    # If it yields another Resource Directory Table, we stick the
    # address of that on a list. If it gives a Data Entry, we put
    # that in another list.
    dodirtable(rsrc, 0, 0x08)
    # Now process Resource Directory Tables until no more remain
    # in the list. For each of these tables, we accept _all_ entries
    # in it, and if they point to subtables we stick the subtables in
    # the list, and if they point to Data Entries we put those in
    # the other list.
    while len(dirtables) > 0:
	table = dirtables[0]
	del dirtables[0]
	dodirtable(rsrc, table, -1) # accept all entries
    # Now we should be left with Resource Data Entries. Each of these
    # describes a font.
    ret = []
    for off in dataentries:
	rva = fromdword(rsrc[off:])
	start = rva - secrva
	size = fromdword(rsrc[off+4:])
	font = dofnt(rsrc[start:start+size])
	if font == None:
	    sys.stderr.write("Failed to read font resource at %x" % start)
	    return None
	ret = ret + [font]
    return ret

def dofon(fon):
    "Split a .FON up into .FNTs and pass each to dofnt."
    # Check the MZ header.
    if fon[0:2] != "MZ":
	sys.stderr.write("MZ signature not found\n")
	return None
    # Find the NE header.
    neoff = fromdword(fon[0x3C:])
    if fon[neoff:neoff+2] == "NE":
	return nefon(fon, neoff)
    elif fon[neoff:neoff+4] == "PE\0\0":
	return pefon(fon, neoff)
    else:
	sys.stderr.write("NE or PE signature not found\n")
	return None

def isfon(data):
    "Determine if a file is a .FON or a .FNT format font."
    if data[0:2] == "MZ":
	return 1 # FON
    else:
	return 0 # FNT

a = sys.argv[1:]
options = 1
outfile = None
prefix = None
infile = None
if len(a) == 0:
    print "usage: dewinfont [-o outfile | -p prefix] file"
    sys.exit(0)
while len(a) > 0:
    if a[0] == "--":
	options = 0
	a = a[1:]
    elif options and a[0][0:1] == "-":
	if a[0] == "-o":
	    try:
		outfile = a[1]
		a = a[2:]
	    except IndexError:
		sys.stderr.write("option -o requires an argument\n")
		sys.exit(1)
	elif a[0] == "-p":
	    try:
		prefix = a[1]
		a = a[2:]
	    except IndexError:
		sys.stderr.write("option -p requires an argument\n")
		sys.exit(1)
	else:
	    sys.stderr.write("ignoring unrecognised option "+a[0]+"\n")
	    a = a[1:]
    else:
	if infile != None:
	    sys.stderr.write("one input file at once, please\n")
	    sys.exit(1)
	infile = a[0]
	a = a[1:]

fp = open(infile, "rb")
data = fp.read()
fp.close()

if isfon(data):
    fonts = dofon(data)
else:
    fonts = [dofnt(data)]

if len(fonts) > 1 and prefix == None:
    sys.stderr.write("more than one font in file; use -p prefix\n")
    sys.exit(1)
if outfile == None and prefix == None:
    sys.stderr.write("please specify -o outfile or -p prefix\n")
    sys.exit(1)

for i in range(len(fonts)):
    if len(fonts) == 1 and outfile != None:
	fname = outfile
    else:
	fname = prefix + "%02d"%i + ".fd"
    fp = open(fname, "w")
    savefont(fonts[i], fp)
    fp.close()
