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