# Copyright 2003 Tom Rothamel <tom-potw@rothamel.us>
# 
# 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.


"""
All of the commands given in the documentation are either python
functions or python class constructors. (The distinction is not
important to the user, as the classes automatically add themselves to
the approprate data structures.)

If you're not familiar with python, the following is a simple guide to
python syntax that should get you familar enough to write a menu
file. If you are familar with python, realize that the menu file can
contain arbitrary python code, as long as it calls the approprate
functions. The order of function calls at runtime is what matters, not
when the calls appear in the python code.

A python string is given in quotes. Inside it, backslashes are used to
escape the following character, or to introduce special characters
such as newline (\n). For example, one can write:

<python>
   "/home/tom/dvd/chapter1.mpg"
   "How to \\"quote\\" quotes.mpg"
</python>

Numbers are simply given as a sequence of digits. Be sure to remove
leading zeros. Lists are given as a comma-separated list of items
given in betweed square brackets ('[' and ']'). A tuple is a
comma-separated list of items given in parenthesis ( '(' and ')'.
For example:

<python>
   [ 1, 2, 3, 4, 5 ]
   (1, 2, 3)
</python>

In this documentation, functions are given by specifying a fuction
name, followed by a list of parameters. A parameter may be followed by
an equal sign, which gives the default value for that parameter. If a
parameter begins with two stars, it is a bin for keyword arguments,
which are passed into another function. An example function is:

<tt><b>my_function</b></tt>(<i>a, b=100, c=42, **kwargs</i>)

Arguments without defaults must be given in every call to the
function. Arguments with defaults may be given in order, or they mey
be specified as a keyword = value. Unknown keywords are placed in
kwargs, which may then be passed to another function.

All of the following pairs of function calls are equivalent.

<python>
  my_function(1) == my_function(1, 100, 42)
  my_function(1, 2) == my_function(1, 2, 42)
  my_function(1, b=2) == my_function(1, 2, 42)
  my_function(1, c=3) == my_function(1, 100, 3)
  my_function(1, 2, 3) == my_function(1, 2, 3)
  my_function(1, d=100) == my_function(1, 100, 42, d=100)
</python>

There are two concepts that are important in understanding how menus
are created. The first is that of the drawing cursor, and the second
is the default action.

Menus are drawn on a 704 x 480 grid, with (0, 0) being the upper-left
corner of the picture. The drawing cursor consists of three
values. The first is a y coordinate, the second is an x coordinate,
and the third is the x coordinate after a carriage return (crx). The x
coordinate is used for the current thing being drawn, while crx is the
value that x is reset to after a carriage return occurs, when a
L{label} and L{text} is drawn. It's used to allow for lines of text
consisting of multiple buttons.

The default action is used by the L{label} command when it has no
action specified. It is set by the L{title} command, and is reset by
the L{label} or L{newmenu} commands, even if the commands have an
explicitly specified actions.

The default command allows L{title} and L{label} commands to be
specified in pairs, without requiring the explicit specification of
actions.
   
"""
    
# A library of items that can occur in DVD menus.

import Image
import ImageDraw
import ImageFont

# all items (not grouped into menus).
_allitems = []

# The number of the last titleset allocated.
_title = 0

class _Defaults(object):
    def __init__(self):
        self.font = None
        self.normal = ( 255, 255, 255 )
        self.highlight = ( 255, 255, 0 )
        self.selected = ( 255, 0, 0 )
        self.chapters = "0"
        self.skip = 0

_defaults = _Defaults()

# The base class for all things that can appear on a DVD.
class _DVDItem(object):

    def __init__(self, *args, **kwargs):

        self.merge_args(_defaults.__dict__)
        
        self.init(*args, **kwargs)
        _allitems.append(self)

    # This places all of the keyword arguments into the object's
    # dictionary.
    def merge_args(self, kwargs):
        for k, v in kwargs.iteritems():
            self.__dict__[k] = v

    # Returns True if this is a menu delimiter, false otherwise.
    def menu_delimiter(self):
        return False

    # Return the code that goes in the title portion of the xml
    # file.
    def title_xml(self, emit):
        pass

    # Return the xml code for this button.
    def button_xml(self, emit):
        pass

    # Draw this object on the menu.
    def draw(self, md):
        pass

    # Sets the default action, and returns a new default action.
    def set_action(self, act):
        return act


class newmenu(_DVDItem):
    def init(self):
        """
        Introduces a new menu. This command is used as a separator
        between menus, so one shouldn't place a newmenu command before
        the items in the first menu. The newmenu command clears out
        the background and contents of the menus, so a new background
        should be introduced if desired. (Otherwise, the default black
        background will be used.) It does not reset the count of
        titlesets, which are added to the DVD as a whole, not to any
        individual menu.              
        """
        
        pass

    def menu_delimiter(self):
        return True


def chapters(chapters):
    """
    Sets the default chapters list that is used by the L{title}
    command, when it is called with the chapters parameter set to
    None. Used when multiple titles have chapter breaks in the same
    place, as in a TV show with a title sequence of a fixed length.
    
    @param chapters: A list of chapters strings, or a single chapter
    string. 
    """
    
    _defaults.chapters = chapters

chapter = chapters

class title(_DVDItem):
    def init(self, file, pause=0, chapters=None,
             pre=None, post="call vmgm menu 1;"):
        """
        This introduces a new titleset to the dvd, and inserts into it
        one or more mpeg files. The titlesets on the DVD are numbered
        in the order in which title commands execute in the menu file,
        starting from 1. The movies are always title 1 in the
        titleset, but they can also be accesed as a numbered title
        from the VMGM domain (that is, the menus).

        This command also sets the default action to start playing the
        newly added title.
        
        @param file: A list of mpeg filenames, which comprise the
        contents of the title. If a single string is given instead, it
        is automatically turned into a list containing the string. The
        files are added in the order in which they appear in the list.

        @param pause: How long to pause after each movie file
        plays. This is a list of values, that must be of the same
        length as the list of files. If a single value is given, it is
        turned into a list of the appropriate length. This should be a
        lenght of time in seconds, or the string "inf" to pause
        forever. 

        @param chapters: A list of strings giving the times of chapter
        breaks in each movie file. Once again, it must be the same length
        as the list of movie files, and is automatically turned into a
        list of the appropriate length if it is not. Each string is a
        comma separated list of chapter breaks, which may be either an
        integer number of seconds or in MM:SS (minutes and seconds)
        format. If None, takes the default set by the L{chapters}
        command. 
        
        @param pre: A string containing a command in the dvdauthor
        command language specifiying something to do before playing
        the title. I've yet to use this.
        
        @param post: A string containing a command in the dvdauthor
        command language specifiying what to do after all movies have
        finished playing. The default command jumps to the first menu
        on the DVD.
        """

        if not (isinstance(file, list)
                or isinstance(file, tuple)):
            file = [ file ]

        if not (isinstance(pause, list)
                or isinstance(pause, tuple)):
            pause = [ pause ] * len(file)
        
        self.file = file
        self.pre = pre
        self.post = post
        self.pause = pause

        global _title
        _title += 1
        self.title = _title

        if chapters:
            self.chapters = chapters

        if not (isinstance(self.chapters, list)
                or isinstance(self.chapters, tuple)):
            self.chapters = [ self.chapters ] * len(self.file)

    def title_xml(self, emit):

        emit('<titleset>')
        emit('<titles>')

        emit('<pgc>')

        if self.pre:
            emit('<pre>%s</pre>', self.pre)

        for f, c, p in zip(self.file, self.chapters, self.pause):
            emit('<vob file="%s" chapters="%s" pause="%s" />', f, c, p)

        if self.post:
            emit('<post>%s</post>', self.post)

        emit ('</pgc>')
        emit('</titles>')
        emit('</titleset>')

    def set_action(self, act):
        return 'jump title %d;' % self.title
        

# Commands that set up the drawing of menus.

def font(file, size, skip=0):
    """
    Sets a new default font for text and labels in the
    menus. This must appear before any call to L{text} or
    L{label}. 

    @param file: A string containing a full path to a truetype
    font file. 

    @param size: The size, in pixels, of the font.

    @param skip: An additional amount of spacing between things
    written in this font.
    """

    _defaults.font = ImageFont.truetype(file, size)
    _defaults.skip = skip

def color(normal=None, highlight=None, selected=None):
    """
    Allows the user to change the color of text that appears in the
    menus. The color depends on the context that something is used
    in. Normal is the color of text, or a button on the menu that does
    not have the focus. Higlight is the color of a button that the
    user has given focus to, but not selected. Selected is the color
    of a button that has been selected (by pushing enter), but for
    which the action assigned to that button has not been completed. 

    All colors are given as tuples of (red, green, blue), where each
    of the three values may range between 0 and 255. For example,
    white would be (255, 255, 255), yellow would be (255, 255, 0), and
    red would be (255, 0, 0). (These are the defaults for normal,
    highlight and selected, respectively.)

    One should not use black as a color, as it is used as the
    chroma-key. 
    
    @param normal: The color of normal text. If None, keep the current
    value.
    @param highlight: The color of higlighted text. If None, keep the
    current value.
    @param selected: The color of selected text. If None, keep the
    current value.
    """

    if normal:
        _defaults.normal = normal
        
    if highlight:
        _defaults.highlight = highlight

    if selected:
        _defaults.selected = selected

colors = color

# These commands actually participate in the drawing of the menu.

class background(_DVDItem):
    def init(self, file):
        """
        This loads in an image file, scales it to 704 x 480, and uses
        it as a background for the menu.
        
        @param file: A string containing the filename of an image.
        """
        
        self.image = Image.open(file)

    def draw(self, md):
        md.background = self.image.resize((704, 480))
        md.bd = ImageDraw.Draw(md.background)


class moveto(_DVDItem):
    def init(self, x, y):
        """
        Moves the drawing cursor to the specified x and y coordinates.        
        """
        
        self.x = x
        self.y = y

    def draw(self, md):
        md.crx = self.x
        md.x = self.x
        md.y = self.y

class rmoveto(_DVDItem):
        
    def init(self, x, y):
        """
        Moves the drawing cursor to new x and y coordinates. The new
        location of the cursor is the current location with the
        arguments added to x and y, respectively.
        """
        
        self.x = x
        self.y = y

    def draw(self, md):
        md.crx = md.crx + self.x
        md.x = md.x + self.x
        md.y = md.y + self.y


class text(_DVDItem):
    def init(self, text, cr=True, halign="left", valign="top"):
        """
        Draws the text to the menu, using the current font and
        colors.

        The halign and valign arguments specify the position of
        the drawing cursor in the text. To center the text on the
        screen, one can use a command like:

        <python>
            moveto(704 / 2, 480 / 2)
            text("Centered!", halign="center", valign="center")
        </python>

        If cr is False, the cursor is moved to the right by the
        horizontal size of the drawn text. If cr is True, the x
        position of the cursor is set to the saved crx position, and
        the cursor is moved down by the height of the text.
        
        @param text: The text to draw.
        @param cr: Should a carriage return occur.
        @param halign: One of "left", "center", "right".
        @param valign: One of "top", "left", "bottom".
        """
        
        self.text = text
        self.cr = cr
        self.valign = valign
        self.halign = halign

    def draw(self, md):
        x = md.x
        y = md.y

        xs, ys = md.nd.textsize(self.text, font=self.font)

        if self.halign == "center":
            x = x - xs / 2
        elif self.halign == "right":
            x = x - xs
            

        if self.valign == "bottom":
            y = y - ys
        elif self.valign == "center":
            y = y - ys / 2

        md.nd.text( (x, y), self.text, fill=self.normal, font=self.font)
        md.hd.text( (x, y), self.text, fill=self.highlight, font=self.font)
        md.sd.text( (x, y), self.text, fill=self.selected, font=self.font)

        if self.cr:
            md.x = md.crx
            md.y = md.y + ys + self.skip
        else:
            md.x = md.x + xs
        
        return x, y, x+xs, y+ys

    
class label(text):    
    def init(self, text, action=None, **kwargs):
        """
        This defines a textual button in the dvd menu, which when
        selected invokes the specified action. The action must be
        given in dvdauthor's command language. Some examples of useful
        actions are:

        <ul>
        <li> <tt>jump title 1;</tt> - Jumps to the start of the first
        title. Since we're in the vmgm (menu) domain when running this
        command, we can jump to the titles by number, ignoring the
        existence of titlesets.
        <li> <tt>jump title 2 chapter 3;</tt> - Jumps to the third
        chapter of the second title. Like titles, chapters are
        numbered starting from 1. 
        <li> <tt>jump menu 2;</tt> - Jumps to the numbered menu. The
        first menu is numbered 1, additional menus added with the
        L{newmenu} command are numbered starting with 2.
        </ul>

        @param text: The text of the button, passed to L{text}.

        @param action: The action, or None to take the default action.

        @param kwargs: Additional keyword arguments are passed to
        L{text} when the text is drawn. This allows halign and valign
        to be used.
        """
        
        super(label, self).init(text, **kwargs)
        self.action = action

    def draw(self, md):

        x0, y0, x1, y1 = super(label, self).draw(md)

        self.button = md.button
        md.button += 1

        md.buttons.append((x0, y0, x1, y1, self.button))

    def button_xml(self, emit):
        emit('<button name="%d">%s</button>'
             % ( self.button, self.action ))
    
    def set_action(self, act):
        if not self.action:
            if act:
                self.action = act
            else:
                raise Exception, "Label '%s' has no action."

        return None


def _image_to_mpeg(image, frames, sound, prefix):
    """
    This is a utility program used to convert an image to a movie.
    
    @param image: The image file.
    @param frames: The number of frames in the created move.
    @param sound: The file name of an mp2, the background sound.
    @param prefix: The prefix of the generated movie.
    """
    
    import os

    # Get rid of pnmscale, eventually.
    os.system("anytopnm %s | pnmscale -xysize 704 480 |  ppmtoy4m -r -n %d | mpeg2enc -q 2 -a 2 -n n -f 8 -v 0 -o %s.m2v"
              % (image, frames, prefix))
    os.system("tcmplex -m d -o %s.mpg -i %s.m2v -p %s"
              % (prefix, prefix, sound))
