#!/usr/bin/python

# Copyright 2005 Ben Hutchings
# 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 '#'.
'''

# Create an image with black (but transparent) background and a
# colour index allocated to the specified foreground colour.
# Return the image and foreground colour index.
def prepare_image(width, height, fg_colour_text):
    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))

# Reduce the palette of an image to the specified number of colours.
def squeeze_palette(image, n):
    image.toPalette(n, 0)
    for i in range(n, image.colorsTotal()):
        image.colorDeallocate(i)

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)

    # FIXME: Should create these as secure temporary files
    sub_file_name = 'make-menu-vob.sub'
    norm_image_file_name = 'make-menu-vob-norm.png'
    high_image_file_name = 'make-menu-vob-high.png'
    sel_image_file_name = 'make-menu-vob-sel.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"
                   % (norm_image_file_name,
                      high_image_file_name,
                      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)

        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()

    # Remove anti-aliasing of text, since subtitles must be limited
    # to 4 colours in total (here, transparent, normal, highlighted
    # and selected colour).
    squeeze_palette(norm_image, 2)
    norm_image.writePng(norm_image_file_name)
    squeeze_palette(high_image, 2)
    high_image.writePng(high_image_file_name)
    squeeze_palette(sel_image, 2)
    sel_image.writePng(sel_image_file_name)

    # FIXME: Should escape any single quotes in the output 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)'"
               % (background_name,
                  sub_file_name, output_name, output_name))
    print command
    os.system(command)

    os.remove(sub_file_name)
    os.remove(norm_image_file_name)
    os.remove(high_image_file_name)
    os.remove(sel_image_file_name)

    # 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:])))
