This is your main code. It configures the script.
Save it as, for example,
make-menu.php.
<?php
## Required programs:
## ffmpeg (libavcodec mother project)
## mplex (from mjpegtools)
## spumux (comes from dvdauthor)
## Required PHP extensions:
## GD2
## Freetype
## Required minimal PHP version:
## 4.0.3 (for escapeshellarg)
## 5 (for file_put_contents)
/* Where do you want the root menu mpeg to be stored */
$g_output_menu = 'root_menu.mpg';
/* A static backgournd image and a music for the main menu. You provide these. */
$g_background_image = 'backgroundim.jpg';
$g_background_music = 'kippur.ac3';
$g_background_music_length = 1191; // In seconds
/* Rendering settings */
$g_width = 720; // Display resolution for the main menu
$g_height = 576; // Use 720x576 for PAL, 720x480 for NTSC.
$g_aspect = "4:3"; // Intended display aspect for the main menu
$g_margin_top = 30;
$g_margin_bottom = 40;
$g_margin_left = 40;
$g_margin_right = 30;
$g_margin_title = 50; // Space between the title and menu items
$g_max_title_fontsize = 24;
$g_max_prog_fontsize = 19.0;
/* A ttf file containing the font for the menu.
* Up to you to ensure it exists in your system.
*/
$g_textfont = "arial.ttf";
$g_promptcolor = 0xFFF490;
$g_titlecolor = 0xFFFF20;
$g_menucolor = 0xFFFFFF;
/* Main title for the DVD */
$g_title =
"Information package: Bible\n".
"prophecies about end times,\n".
"Islam, terrorism, and Antichrist";
/* List of main menu items */
$g_labels = Array
(
// Program number => Program label
4 => "Walid Shoebat in IPC 2007, abridged, ~1h25m",
1 => "Walid Shoebat in SCPC 2008, part 1, ~1h0m",
2 => "Walid Shoebat in SCPC 2008, part 2, ~1h7m",
3 => "Walid Shoebat in SCPC 2008, part 3, ~1h17m\n(South California Prophecy Conference)",
5 => "Avi Lipkin in PitN on GLC in 2010, ~0h30m",
7 => "Avi Lipkin in LotSW on GLC in 2010-02-24, ~2h",
8 => "Avi Lipkin in LotSW on GLC in 2009-03-11, ~2h",
9 => "Walid Shoebat in LotSW on GLC in 2010-02-07, ~2h",
6 => "Bonus: Jim Staley at God's Learning Channel (GLC)\n".
"in 2010-02-07, speaking about pagan origins\n".
"of Christmas and Easter, ~2h"
);
/* List of programs constituting the title */
/* Completely optional, but you can specify *
* these if you like that the produced dvdauthor
* script will be complete and ready to be used.
*/
$g_programs = Array
(
// Program number => Program settings
1 => Array('aspect' => '16:9',
'files' => Array('parts/parta1.mpg', // List of mpeg2 files that
'parts/parta2.mpg', // you provide, which contain
'parts/parta3.mpg', // the audiovisual content of
'parts/parta4.mpg', // the particular program.
'parts/parta5.mpg', // The will be concatenated
'parts/parta6.mpg', // to form te program (each file
'parts/parta7.mpg')),// begins a new chapter though).
2 => Array('aspect' => '16:9',
'files' => Array('parts/partb1.mpg',
'parts/partb2.mpg',
'parts/partb3.mpg',
'parts/partb4.mpg',
'parts/partb5.mpg',
'parts/partb6.mpg',
'parts/partb7.mpg')),
3 => Array('aspect' => '16:9',
'files' => Array('parts/partc1.mpg',
'parts/partc2.mpg',
'parts/partc3.mpg',
'parts/partc4.mpg',
'parts/partc5.mpg',
'parts/partc6.mpg',
'parts/partc7.mpg',
'parts/partc8.mpg')),
4 => Array('aspect' => '4:3',
'files' => Array('parts/partd01.mpg',
'parts/partd02.mpg',
'parts/partd03.mpg',
'parts/partd04.mpg',
'parts/partd05.mpg',
'parts/partd06.mpg',
'parts/partd07.mpg',
'parts/partd08.mpg',
'parts/partd09.mpg',
'parts/partd10.mpg')),
5 => Array('aspect' => '16:9',
'chapters' => '0,15:0',
'files' => Array('parts/parte.mpg')),
6 => Array('aspect' => '4:3',
'chapters' => '0,10:0,20:0,30:0,40:0,50:0,60:0,70:0,80:0,90:0,100:0,110:0,120:0',
'files' => Array('parts/partst.mpg')),
7 => Array('aspect' => '4:3',
'chapters' => '0,10:0,20:0,30:0,40:0,50:0,60:0,70:0,80:0,90:0,100:0,110:0,120:0',
'files' => Array('parts/partl1.mpg')),
8 => Array('aspect' => '4:3',
'chapters' => '0,10:0,20:0,30:0,40:0,50:0,60:0,70:0,80:0,90:0,100:0,110:0,120:0',
'files' => Array('parts/partl2.mpg')),
9 => Array('aspect' => '4:3',
'chapters' => '0,10:0,20:0,30:0,40:0,50:0,60:0,70:0,80:0,90:0,100:0,110:0,120:0',
'files' => Array('parts/partw1.mpg'))
);
/*************************************************/
// Temporary files used by the script. You can ignore this section.
$g_tmp_bgimage = '/tmp/t_background.jpg';
$g_tmp_menumask = '/tmp/t_menu.png';
$g_tmp_focusmask = '/tmp/t_highlight.png';
$g_tmp_selectmask = '/tmp/t_select.png';
$g_tmp_spuxml = '/tmp/t_submux.xml';
/*************************************************/
require 'make-menu-common.php';
To run it, issue the following command:
php make-menu.php > dvdauthor.xml
To produce the DVD image, run DVDAuthor as such:
mkdir DVD
rm DVD/VIDEO_TS/*
dvdauthor -o DVD -x dvdauthor.xml -m
mkisofs -dvd-video -o DVD.iso DVD
Note that you also need the common code file, below.
On the right, you can see the result of this example menu
(sans the cursor, which is a filled yellow circle).
Save this code as
make-menu-common.php
<?php
/* Create DVDAuthor script */
$format = 'pal';
if($g_height == 480 || $g_height == 240) $format = 'ntsc';
print "<dvdauthor>\n".
" <vmgm>\n".
" <menus>\n".
" <audio lang=\"en\" />\n".
" <video format=\"$format\" aspect=\"$g_aspect\" />\n".
" <pgc entry=\"title\">\n";
foreach($g_labels as $labelno => $label)
print " <button> jump title $labelno; </button>\n";
print " <vob file=\"".htmlspecialchars($g_output_menu)."\" pause=\"inf\" />\n".
" </pgc>\n".
" </menus>\n".
" </vmgm>\n";
$prev_titleset_aspect = '';
foreach($g_programs as $progno => $progsets)
{
if($prev_titleset_aspect != ''
&& $prev_titleset_aspect != $progsets['aspect'])
{
print " </titles>\n".
" </titleset>\n";
$prev_titleset_aspect = '';
}
if($prev_titleset_aspect == '')
{
print " <titleset>\n".
" <menus>\n".
" <pgc entry=\"root\">\n".
" <pre> jump vmgm menu entry title; </pre>\n".
" </pgc>\n".
" </menus>\n".
" <titles>\n".
" <audio lang=\"en\" />\n".
" <video aspect=\"{$progsets['aspect']}\" />\n";
$prev_titleset_aspect = $progsets['aspect'];
}
$chap = '';
if(isset($progsets['chapters']))
$chap = " chapters=\"{$progsets['chapters']}\"";
print " <pgc>\n";
foreach($progsets['files'] as $fn) $lastfn=$fn;
foreach($progsets['files'] as $fn)
{
$p = ($fn==$lastfn) ? ' pause="2"' : '';
print " <vob file=\"".htmlspecialchars($fn)."\"$chap $p/>\n";
}
print " <post> call vmgm menu 1;</post>\n".
" </pgc>\n";
}
if($prev_titleset_aspect != '')
{
print " </titles>\n".
" </titleset>\n";
}
print "</dvdauthor>\n";
#exit; -- enable this line if you want the xml only and not any mpegs
/* Create menu mpeg */
$im_sub = Array(); // menu, highlight, select
$im_colors = Array();
for($c=0; $c<3; ++$c)
{
$im_sub[$c] = ImageCreate($g_width,$g_height);
$im_colors[$c][0] = ImageColorAllocate($im_sub[$c], 255,255,255);
$im_colors[$c][1] = ImageColorAllocate($im_sub[$c], 150,150,150);
$im_colors[$c][2] = ImageColorAllocate($im_sub[$c], 255,140,30);
$im_colors[$c][3] = ImageColorAllocate($im_sub[$c], 0,0,0);
ImageColorTransparent($im_sub[$c], $im_colors[$c][3]);
ImageFilledRectangle($im_sub[$c], 0,0, $g_width-1,$g_height-1, $im_colors[$c][3]);
}
/* Create the text layer for the background image */
$im_text = ImageCreateTrueColor($g_width,$g_height);
imageAlphaBlending($im_text, false);
ImageFilledRectangle($im_text, 0,0, $g_width-1, $g_height-1,
ImageColorAllocateAlpha($im_text, 0,0,0, 127) );
$title_height = WrapText($g_title,
$g_margin_left, $g_margin_top,
$g_width - $g_margin_left - $g_margin_right,
$g_height,
8, $g_max_title_fontsize, max(8, $g_max_title_fontsize*0.9),
$g_titlecolor,
0);
$labels_begin_y = $g_margin_top + $title_height + $g_margin_title;
$labels_end_y = $g_height - $g_margin_bottom;
WrapText('Please select program:',
$g_margin_left, $g_margin_top+$title_height+16,
$g_width-$g_margin_left-$g_margin_right, 30,
1,15,15, $g_promptcolor, 0);
$remaining_label_lines = 0;
foreach($g_labels as $label)
$remaining_label_lines += 1 + (count(explode("\n", $label))-1)*1.0;
$labels = Array();
$label_width = $g_width - ($g_margin_left + 30) - $g_margin_right;
$remaining_room = $labels_end_y - $labels_begin_y;
$mandatory_air = (count($g_labels)-1)*3;
foreach($g_labels as $label)
{
$nlines = 1 + (count(explode("\n", $label))-1)*1.0;
$cell_height = ($remaining_room - $mandatory_air) * $nlines / $remaining_label_lines;
$ptsize = DetermineLargestFontSize(
$label,
$label_width, $cell_height,
9, $g_max_prog_fontsize, 12.0);
$box = calculateTextBox($ptsize, 0, $g_textfont, $label);
$label_height = $box['height'];
$labels[] = Array
(
'text' => $label,
'ptsize' => $ptsize,
'height' => $label_height
);
$remaining_room -= $label_height;
$remaining_label_lines -= $nlines;
}
for($iterations = 1; $iterations <= 3; ++$iterations)
{
foreach($labels as $index => &$label)
{
$remaining_room += $label['height'];
$cell_height = $remaining_room - $mandatory_air;
if($cell_height <= $label['height']) continue;
$cell_height = $cell_height*0.7 + $label['height']*0.3;
$ptsize = DetermineLargestFontSize(
$label['text'],
$label_width, $cell_height,
$label['ptsize'], 26, $label['ptsize']);
$box = calculateTextBox($ptsize, 0, $g_textfont, $label['text']);
$label_height = $box['height'];
$label['ptsize'] = $ptsize;
$label['height'] = $label_height;
$remaining_room -= $label_height;
}
}
unset($label);
$total_text_height = 0;
foreach($labels as $label)
$total_text_height += $label['height'];
$average_air = $remaining_room / (count($g_labels)-1);
$y = $labels_begin_y;
foreach($labels as $label)
{
ImageLine($im_text, $g_margin_left,
$y-$average_air/2,
$g_width-$g_margin_right,
$y-$average_air/2,
ImageColorAllocateAlpha($im_text,
255,255,255, 96) );
$label_height = WrapText(
$label['text'],
$g_margin_left + 30, $y,
$label_width,
$label['height'],
$label['ptsize'],
$label['ptsize'],
$label['ptsize'],
$g_menucolor);
$dot_center_x = $g_margin_left+30;
$dot_center_y = $y + $label_height / 2 + 10;
DrawEllipse($im_text, $dot_center_x, $dot_center_y, 35, min(35,$label_height*1.0),
0x000020, 0x000050); // in background
DrawEllipse($im_sub[0], $dot_center_x, $dot_center_y, 30, min(30,$label_height*0.9),
$im_colors[0][1], $im_colors[0][1]); // in menu
DrawEllipse($im_sub[1], $dot_center_x, $dot_center_y, 30, min(30,$label_height*0.9),
$im_colors[1][1], $im_colors[1][2]); // in highlight
DrawEllipse($im_sub[2], $dot_center_x, $dot_center_y, 30, min(30,$label_height*0.9),
$im_colors[2][1], $im_colors[2][1]); // in select
$y += $label['height'];
$y += $average_air;
}
/* Create the background image and blend the text into it */
$im_bg = ImageCreateTrueColor($g_width,$g_height);
$im = ImageCreateFromJpeg($g_background_image);
ImageCopyResampled($im_bg, $im, 0,0, 0,0, $g_width,$g_height, ImageSx($im),ImageSy($im));
ImageDestroy($im);
imageAlphaBlending($im_bg, true);
ImageCopy($im_bg, $im_text, 0,0, 0,0, $g_width,$g_height);
ImageJpeg($im_bg, $g_tmp_bgimage, 100); ImageDestroy($im_bg);
ImageDestroy($im_text);
/* Save the mask images for spumux */
ImagePng($im_sub[0], $g_tmp_menumask); ImageDestroy($im_sub[0]);
ImagePng($im_sub[1], $g_tmp_focusmask); ImageDestroy($im_sub[1]);
ImagePng($im_sub[2], $g_tmp_selectmask); ImageDestroy($im_sub[2]);
file_put_contents($g_tmp_spuxml,
"<subpictures><stream><spu start=\"00:00:1.0\" ".
"image=\"".htmlspecialchars($g_tmp_menumask)."\" ".
"highlight=\"".htmlspecialchars($g_tmp_focusmask)."\" ".
"select=\"".htmlspecialchars($g_tmp_selectmask)."\" ".
"autooutline=\"infer\" force=\"yes\" autoorder=\"rows\">".
"</spu></stream></subpictures>"
);
$command =
"ffmpeg -t ".escapeshellarg($g_background_music_length).
" -loop_input -i ".escapeshellarg($g_tmp_bgimage).
" -vcodec mpeg2video -qscale 1 -g 600 -threads 4".
" -f mp2 -".
" | ".
"mplex -V -f 8 -o /dev/stdout /dev/stdin ".escapeshellarg($g_background_music).
" | ".
"spumux ".escapeshellarg($g_tmp_spuxml)." > ".escapeshellarg($g_output_menu);
exec($command);
/* All done. The rest of this PHP file are functions. */
/*************************************************/
/* Render text into the given region with given settings. */
function WrapText($text, $x,$y, $maxwidth,$maxheight,
$minptsize,$maxptsize, $guess_ptsize,
$color,
$align = -1)
{
global $g_textfont, $im_text;
$ptsize = DetermineLargestFontSize(
$text, $maxwidth,$maxheight,
$minptsize,$maxptsize, $guess_ptsize);
$box = calculateTextBox($ptsize, 0, $g_textfont, $text);
imageAlphaBlending($im_text, true);
/*
imageFilledRectangle($im_text, $x-5,$y-5, $x+$maxwidth+5,$y+$box['height']+5,
ImageColorAllocateAlpha($im_text, 0,0,0, 100) );
*/
$bd = Array(ImageColorAllocateAlpha($im_text, 0,0,0, 0),
ImageColorAllocateAlpha($im_text, 0xFF,0xFF,0, 80),
ImageColorAllocateAlpha($im_text, 0x55,0x40,0, 110),
ImageColorAllocateAlpha($im_text, 0,0,0, 90));
switch($align)
{
case -1: // left-align: default
for($xo=-3; $xo<=3; ++$xo)
for($yo=$xo; $yo<=3; ++$yo)
imageTTFtext($im_text, $ptsize,0, $x+$box['left']+$xo, $y+$box['top']+$yo,
$bd[max(abs($xo),abs($yo))], $g_textfont, $text);
imageTTFtext($im_text, $ptsize,0, $x+$box['left'], $y+$box['top'], $color, $g_textfont, $text);
break;
case 0: // center
foreach(explode("\n", $text) as $line)
{
$box2 = calculateTextBox($ptsize,0, $g_textfont, $line);
$xp = $box2['left'] + $x + ($maxwidth - $box2['width']) / 2;
for($xo=-3; $xo<=3; ++$xo)
for($yo=$xo; $yo<=3; ++$yo)
imageTTFtext($im_text, $ptsize,0, $xp+$xo, $y+$box2['top']+$yo,
$bd[max(abs($xo),abs($yo))], $g_textfont, $line);
imageTTFtext($im_text, $ptsize,0, $xp, $y+$box2['top'], $color, $g_textfont, $line);
$y += $box2['height'] + 3;
}
break;
case 1: // right-align
foreach(explode("\n", $text) as $line)
{
$box2 = calculateTextBox($ptsize,0, $g_textfont, $line);
$xp = $box2['left'] + $x + ($maxwidth - $box2['width']);
imageTTFtext($im_text, $ptsize,0, $xp, $y+$box2['top'], $color, $g_textfont, $line);
$y += $box2['height'] + 3;
}
break;
}
return $box['height'];
}
/* Determine the largest font size that can be used to
* render the given text, with the given font, into a
* region with the given dimensions. The font size should
* be between the given ranges, and the given initial
* guess is to be used. Given given given.
*/
function DetermineLargestFontSize($text, $width,$height, $minptsize,$maxptsize, $guess_ptsize)
{
global $g_textfont;
$stderr = fopen('php://stderr', 'w');
fprintf($stderr, "Fitting text in {$width}x{$height} ({$minptsize}..{$maxptsize}): ".str_replace("\n","\\n",$text)."\n");
while($minptsize+0.01 < $maxptsize)
{
$middle = ($maxptsize + $minptsize) / 2.0;
if(isset($guess_ptsize))
{
$middle = $guess_ptsize;
$guess_ptsize = null;
}
fprintf($stderr, "Trying $middle ...");
$box = calculateTextBox($middle, 0, $g_textfont, $text);
fprintf($stderr, " Produces {$box['width']}x{$box['height']}\n");
if($box['width'] > $width
|| $box['height'] > $height) // Too large?
{
$maxptsize = $middle;
continue;
}
if($box['width'] < $width
&& $box['height'] < $height) // Could be larger?
{
$minptsize = $middle;
continue;
}
$maxptsize = $middle;
/*
break;*/
}
fprintf($stderr, "Selected $minptsize\n");
fclose($stderr);
return $minptsize;
}
/* Determine dimensions required to render the given text. */
function calculateTextBox($font_size, $font_angle, $font_file, $text) {
// This function created by blackbart at simail dot it
$box = imagettfbbox($font_size, $font_angle, $font_file, $text);
if( !$box )
return false;
$min_x = min( array($box[0], $box[2], $box[4], $box[6]) );
$max_x = max( array($box[0], $box[2], $box[4], $box[6]) );
$min_y = min( array($box[1], $box[3], $box[5], $box[7]) );
$max_y = max( array($box[1], $box[3], $box[5], $box[7]) );
$width = ( $max_x - $min_x );
$height = ( $max_y - $min_y );
$left = abs( $min_x ) + $width;
$top = abs( $min_y ) + $height;
// to calculate the exact bounding box i write the text in a large image
$img = @imagecreatetruecolor( $width << 2, $height << 2 );
$white = imagecolorallocate( $img, 255, 255, 255 );
$black = imagecolorallocate( $img, 0, 0, 0 );
imagefilledrectangle($img, 0, 0, imagesx($img), imagesy($img), $black);
// for sure the text is completely in the image!
imagettftext( $img, $font_size,
$font_angle, $left, $top,
$white, $font_file, $text);
// start scanning (0=> black => empty)
$rleft = $w4 = $width<<2;
$rright = 0;
$rbottom = 0;
$rtop = $h4 = $height<<2;
for( $x = 0; $x < $w4; $x++ )
for( $y = 0; $y < $h4; $y++ )
if( imagecolorat( $img, $x, $y ) ){
$rleft = min( $rleft, $x );
$rright = max( $rright, $x );
$rtop = min( $rtop, $y );
$rbottom = max( $rbottom, $y );
}
// destroy img and serve the result
imagedestroy( $img );
return array( "left" => $left - $rleft,
"top" => $top - $rtop,
"width" => $rright - $rleft + 1,
"height" => $rbottom - $rtop + 1 );
}
function DrawEllipse(&$im, $centerx,$centery, $width,$height, $fillcolor,$edgecolor)
{
ImageFilledEllipse($im, $centerx-$width/2, $centery-$height/2, $width,$height, $fillcolor);
ImageEllipse($im, $centerx-$width/2, $centery-$height/2, $width,$height, $edgecolor);
}