#!/usr/bin/python

# Copyright 2005 Ben Hutchings <ben@decadentplace.org.uk>
# See "copyright" file for licence terms.

def usage():
    print """\
make-menu-vob generates VOB files suitable for use as DVD menus.

The following command line arguments are required:

-w frame-width -h frame-height
    Dimensions of the video frame
-m menu-specification
    File containing specification of the menu (described below)
-b background
    MPEG-2 file containing background video and optionally audio
-o output-file
    Name of the file to generate
-n normal-colour -i highlight-colour -s select-colour
    Colours to use for text in various states
-f font-file
    TrueType font file containing font to render text with

The specification contains lines in the format:
    t x y size text...
Where: t is a type code B, C or T
       x, y are the coordinates of the bottom left of the text
       size is the x-height of the text (?)
       text is the text to be rendered
Type T creates static text.
Type B creates a button (selectable text with an associated action).
Type C specifies text to be combined into the next button.
Multi-line buttons are defined by one or more C lines followed by
a B line.

The specification may also include blank lines and comments prefixed
by a '#'.
"""

def prepare_image(width, height, fg_colour_text):
    '''Create a GD image with black (but transparent) background and a
    colour index allocated to the specified foreground colour.
    Return the image and foreground colour index.'''
    import gd
    image = gd.image((width, height))
    black = image.colorAllocate((0,0,0))
    image.colorTransparent(black)
    image.rectangle((0,0), (width-1,height-1), black, black)
    try:
        r, g, b = map(int, fg_colour_text.split(','))
    except ValueError:
        raise Exception('invalid colour specification: ' + fg_colour_text)
    return image, image.colorAllocate((r, g, b))

class TemporaryDirectory:
    """Class of temporary directory proxies.  Its instances each
    create a directory when created and delete it (and its contents)
    when they are deleted.  The directory's path is made available
    through the path attribute."""
    def __init__(self):
        import errno, os, shutil, tempfile
        self._rmtree = shutil.rmtree
        while 1:
            self.path = tempfile.mktemp()
            try:
                os.mkdir(self.path)
                break
            except OSError, e:
                if e.errno == errno.EEXIST:
                    continue
                raise
    def __del__(self):
        self._rmtree(self.path, ignore_errors=1)

def shell_escape(arg):
    '''Return arg suitably escaped for use in a Bourne shell command-line.'''
    return "'" + arg.replace("'", "'\\''") + "'"

def xml_attr_escape(arg):
    '''Return arg suitably escaped for use as an XML attribute value.'''
    return "'" + arg.replace('&', '&amp;').replace('<', '&lt;').replace("'", '&apos;') + "'"

def main(args):

    import getopt, os, os.path

    try:
        opt_list, arg_list = getopt.getopt(args,
                                           'w:h:m:b:o:n:i:s:f:',
                                           ['xscale=', 'yscale='])
        if len(arg_list) != 0:
            raise getopt.GetoptError('unexpected non-option arguments given')
        opts = {}
        for key, value in opt_list:
             opts[key] = value
        width = int(opts['-w'])
        height = int(opts['-h'])
        menu_spec = opts['-m']
        background_name = opts['-b']
        output_name = opts['-o']
        norm_colour_text = opts['-n']
        high_colour_text = opts['-i']
        sel_colour_text = opts['-s']
        font_name = opts['-f']
        xscale = float(opts.get('--xscale', 1.0))
        yscale = float(opts.get('--yscale', 1.0))
    except (getopt.GetoptError, KeyError, ValueError):
        usage()
        return 0

    norm_image, norm_colour = prepare_image(width, height, norm_colour_text)
    high_image, high_colour = prepare_image(width, height, high_colour_text)
    sel_image, sel_colour = prepare_image(width, height, sel_colour_text)

    temp_dir = TemporaryDirectory()
    sub_file_name = os.path.join(temp_dir.path, 'menu.sub')
    norm_image_file_name = os.path.join(temp_dir.path, 'normal.png')
    high_image_file_name = os.path.join(temp_dir.path, 'highlight.png')
    sel_image_file_name = os.path.join(temp_dir.path, 'select.png')

    sub_file = open(sub_file_name, 'w')
    sub_file.write("<subpictures>\n"
                   "<stream>\n"
                   "<spu force='yes' start='00:00:00.00'\n"
                   "     image=%s\n"
                   "     highlight=%s\n"
                   "     select=%s>\n"
                   % (xml_attr_escape(norm_image_file_name),
                      xml_attr_escape(high_image_file_name),
                      xml_attr_escape(sel_image_file_name)))

    # Initialise button dimensions to out-of-range values
    x0, y0, x1, y1 = width, height, -1, -1

    for line in open(menu_spec):

        # Ignore blank lines and comment lines
        if line[0] in '#\n':
            continue

        args = line.strip().split(' ', 4)
        tcode, x, y, size, text = \
               args[0], int(args[1]), int(args[2]), int(args[3]), args[4]
        if tcode not in 'BCT':
            raise Exception('invalid menu entry type code: ' + tcode)

        # Fudge for supporting different TV standards: scale the dimensions.
        # Really we should apply both xscale and yscale to the font, but GD
        # appears to assume square pixels.
        size = int(size * yscale)
        x = int(x * xscale)
        y = int(y * yscale)

        # Note negation of the colour index; this disables anti-aliasing
        # of the text, which unfortunately uses more colour indices than
        # we can spare.
        norm_image.string_ttf(font_name, size, 0, (x, y), text, -norm_colour)

        if tcode != 'T':
            # Render highlighted and selected version
            high_image.string_ttf(font_name, size, 0, (x, y), text,
                                  -high_colour)
            sel_image.string_ttf(font_name, size, 0, (x, y), text, -sel_colour)

            # Update and validate button dimensions
            _, _, new_x1, new_y1, _, _, new_x0, new_y0 = \
               norm_image.get_bounding_rect(font_name, size, 0, (x, y), text)
            if new_x0 < 0 or new_y0 < 0 or new_x1 >= width or new_y1 >= height:
                raise Exception(
                    'text "' + text + '" does not fit within the frame')
            x0, y0 = min(x0, new_x0), min(y0, new_y0)
            x1, y1 = max(x1, new_x1), max(y1, new_y1)

            if tcode != 'C':
                # Write button dimensions then reset them
                sub_file.write("<button x0='%d' y0='%d' x1='%d' y1='%d'/>"
                               % (x0, y0, x1, y1))
                x0, y0, x1, y1 = width, height, -1, -1

    sub_file.write("</spu>\n</stream>\n</subpictures>\n")
    sub_file.close()

    norm_image.writePng(norm_image_file_name)
    high_image.writePng(high_image_file_name)
    sel_image.writePng(sel_image_file_name)

    command = ("mplex -v0 -f 8 -o /dev/stdout %s"
               " | (spumux -v0 -m dvd %s 2>&1 >%s || rm -f %s)"
               " | egrep '(WARN|ERR)'"
               % (shell_escape(background_name),
                  shell_escape(sub_file_name),
                  shell_escape(output_name),
                  shell_escape(output_name)))
    print command
    os.system(command)

    # We don't get any useful result from os.system, so check whether
    # the output file was actually created.
    return os.path.isfile(output_name) 

if __name__ == '__main__':
    import sys
    sys.exit(1 - int(main(sys.argv[1:])))
