#!/usr/bin/python

# dewinfont is copyright 2001,2017 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
import struct

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

bytestruct = struct.Struct("B")
wordstruct = struct.Struct("<H")
dwordstruct = struct.Struct("<L")

def frombyte(s):
    return bytestruct.unpack_from(s, 0)[0]
def fromword(s):
    return wordstruct.unpack_from(s, 0)[0]
def fromdword(s):
    return dwordstruct.unpack_from(s, 0)[0]

def asciz(s):
    for length in range(len(s)):
        if frombyte(s[length:length+1]) == 0:
            break
    return s[:length].decode("ASCII")

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 = 1 << (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:])
    #sys.stdout.write("Face name: {}\n".format(f.facename))
    f.copyright = asciz(fnt[6:66])
    #sys.stdout.write("Copyright: {}\n".format(f.copyright))
    f.pointsize = fromword(fnt[0x44:])
    #sys.stdout.write("Point size: {}\n".format(f.pointsize))
    f.ascent = fromword(fnt[0x4A:])
    #sys.stdout.write("Ascent: {}\n".format(f.ascent))
    f.height = fromword(fnt[0x58:])
    #sys.stdout.write("Height: {}\n".format(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:])
    #sys.stdout.write("Attrs: italic={} underline={} strikeout={} weight={}\n".format(f.italic, f.underline, f.strikeout, f.weight))
    #sys.stdout.write("Charset: {}\n".format(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 = [0] * 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:])
        #sys.stdout.write("Char {}: width={} offset={} filelen={}".format(i, w, off, len(fnt)))
        widthbytes = (w + 7) // 8
        for j in range(f.height):
            for k in range(widthbytes):
                bytepos = off + k * f.height + j
                #sys.stdout.write("{} -> {:x}\n".format(bytepos, 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
                #sys.stdout.write("Font at {} size {}\n".format(start, 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:
    sys.stdout.write("usage: dewinfont [-o outfile | -p prefix] file\n")
    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()
