#!/usr/bin/perl -w
#
# chaplin-genmenu
#
# written by Christian Vogelgsang <chris@lallafa.de>
# under the GNU Public License V2
#
# create still mpeg menus out of a menu generation info list created with
# the chaplin -g option
#
# Required tools: transcode, image magick, mjpegtools

use Getopt::Std;
use POSIX qw(ceil floor);

sub Usage {
  print STDERR <<'EOF';
chaplin-genmenu [options] <menu.txt>
  written by Christian Vogelgsang <chris@lallafa.de>
  under the GNU Public License V2
  $Revision: 1.2 $ $Date: 2004/03/21 17:47:43 $
						    defaults:
  -d <dev>     DVD input device/dir                (from <menu.txt>)
  -t <num>     DVD title                           (from <menu.txt>)
  -a <x>:<y>   DVD input video aspect ratio        (from <menu.txt>)
  -n <val>     video norm (PAL,NTSC)               (from <menu.txt>)

  -o <offset>  add frame offset for chapter images (16)  
  -m <x>,<y>   force menu layout (cells & rows)    (auto)
  -v	       enable VCD mode                     (SVCD)
  
  -b <file>    specify background file or xc:color (xc:black)
  -l <color>   label color			   (white)
  -f <font>    font name			   (helvetica-bold)
  -s <pntsize> font size                           (24)

  -c	       always create files		   (use existing)
  -u	       clean up created files              (keep files)
  -x	       enable debug output
  -h	       this help
EOF
  exit 0;
}

# parse args
our($opt_d,$opt_t,$opt_a,$opt_n);
our($opt_m,$opt_o,$opt_v);
our($opt_b,$opt_l,$opt_f,$opt_s);
our($opt_c,$opt_u,$opt_x,$opt_h);
getopts('d:t:a:n:m:o:vb:l:f:s:cuxh');
&Usage if(defined($opt_h));
$file = $ARGV[0];
&Usage if(!defined($file));

# ----- read menu info header -----
# open menu info file and read number of layout entries per page
open(FH,"<$file") || die;
$_ = <FH>;
if(!m/^chaplin-menu (\d+)$/) {
  print "FATAL: not a valid chaplin-menu file '$file'\n";
  exit 1;
}
$menu_entries = $1;
print "menu: $menu_entries entries per page\n";

# read in parameters from menu info file
$_ = <FH>;
if(!m/dvd \"(\S+)\" title (\d+) (\S+) (\S+)/) {
  print "FATAL: not a valid chaplin-menu file '$file'\n";
  exit 1;
}
$dvd_dev = $1;
$dvd_title = $2;
$tvnorm = $3;
$asr = $4;

# ----- eval command line options -----
# overwrite setup with command line options
$dvd_dev   = $opt_d if(defined($opt_d));
$dvd_title = $opt_t if(defined($opt_t));
$tvnorm    = $opt_n if(defined($opt_n));
$asr       = $opt_a if(defined($opt_a));
print "  (from dvd '$dvd_dev' title $dvd_title tvnorm=$tvnorm asr=$asr)\n";

# output options
$force_layout = $opt_m;
$offset = 16;
$offset = $opt_o if(defined($opt_o));
$svcd = 1;
$svcd = 0 if(defined($opt_v));

# prog options
$create  = defined($opt_c);
$cleanup = defined($opt_u);
$debug   = defined($opt_x);

# layout options
$background = "xc:black";
$background = $opt_b if(defined($opt_b));
$label_col = "white";
$label_col = $opt_l if(defined($opt_l));
$font_name = "helvetica-bold";
$font_name = $opt_f if(defined($opt_f));
$font_size = 24;
$font_size = $opt_s if(defined($opt_s));
print "  background: '$background'\n";
print "  labels: color='$label_col' font='$font_name' size='$font_size'\n";

# flags for reprocessing a stage
$redo_samples = defined($opt_d)||defined($opt_t)||defined($opt_n)||defined($opt_o);
$redo_menu = defined($opt_b)||defined($opt_l)||defined($opt_f)||defined($opt_s)
  ||defined($opt_m)||defined($opt_a);

# simplify options
$pal = ($tvnorm eq 'PAL');
my(@xy) = split(/:/,$asr);
$aspect = $xy[0] / $xy[1];

# ----- SETUP rendering -----
# screen for rendering
$screen_width  = 768;
$screen_height = 576;
$border_x = 10;
$border_y = 5;
$scan_border_x = 60;
$scan_border_y = 60;
$title_height = 60;
$title_size = 48;

# ----- determine menu layout -----
if(!defined($force_layout)) {
  # find automatically
  $cells = POSIX::ceil(sqrt($menu_entries));
  $rows  = POSIX::ceil($menu_entries / $cells);
} else {
  # user specified
  ($cells,$rows) = split(/,/,$force_layout);
  if(!defined($cells) || !defined($rows)) {
    print "FATAL: wrong menu layout specified!!\n";
    exit 1;
  }
  if(($cells * $rows) < $menu_entries) {
    print "FATAL: menu layout $cells x $rows is too small for $menu_entries entries!\n";
    exit 1;
  }
}  
print "  layout: $cells x $rows (for $menu_entries entries)\n";

# the render area we draw on
$render_w = $screen_width  - 2 * $scan_border_x;
$render_h = $screen_height - 2 * $scan_border_y;
$thumb_w  = $render_w;
$thumb_h  = $render_h - $title_height;
$wt = POSIX::floor($thumb_w / $cells) - 2 * $border_x;
$ht = POSIX::floor($thumb_h / $rows)  - 2 * $border_y - $font_size;

# try to adjust aspect of thumbs
$wt1 = $wt;
$ht1 = POSIX::floor($wt / $aspect);
$valid1 = ($ht1 <= $ht);
$wt2 = POSIX::floor($ht * $aspect);
$ht2 = $ht;
$valid2 = ($wt2 <= $wt);

# pick perfect thumb size
if($valid1 && $valid2) {
  # if both are valid - use larger one
  if(($wt1 * $ht1) > ($wt2 * $ht2)) {
    $wt = $wt1;
    $ht = $ht1;
  } else {
    $wt = $wt2;
    $ht = $ht2;
  }
} elsif($valid1) {
  $wt = $wt1;
  $ht = $ht1;
} elsif($valid2) {
  $wt = $wt2;
  $ht = $ht2;
}
$rat = POSIX::floor(($wt * 100 / $ht)+0.5) / 100;
print "  thumbs: $wt x $ht (ratio $rat:1) on area $thumb_w x $thumb_h\n";

# placement info
$title_y = POSIX::floor(-($screen_height/2) + $scan_border_y + $title_height / 2);
$chap_x  = $scan_border_x;
$chap_y  = $scan_border_y + $title_height;

# ----- main menu loop --------------------------------------------------------
while(<FH>) {

  # parse menu line: <file> <entries> <description>
  chomp;
  die if(!m/^(\S+) (\d+) (.+)$/);
  $menu = $1;
  $num  = $2;
  $heading = $3;
  
  @names = ();
  @titles = ();
  @args = ();
  $sample_gen = 0;
  
  # extract required chapters
  print "creating menu '$menu' with $num entries\n";
  for($i=0;$i<$num;$i++) {
  
    # parse entry line: <chapter> <sample-frame> <description>
    $_ = <FH>;
    chomp;
    die if(!m/^(\d+) (\d+) (.+)$/);
    $chap  = $1;
    $begin = $2;
    $title = $3;
    $frame = $begin + $offset;
    
    # ----- generate chapter image -----
    $name = sprintf("chapter%02d.ppm",$chap);
    if((! -e $name)|| $redo_samples || $create) {
      print "  generating sample image for chapter $chap at frame $frame: $name\n";
      $cmd  = sprintf("transcode -z -k -y ppm -o sample -c %d-%d "
	. "-i $dvd_dev -T $dvd_title,%02d -x dvd -q0",$frame,$frame+1,$chap);
      print "\t$cmd\n" if($debug);
      system("$cmd >/dev/null 2>&1");
      rename("sample000000.ppm",$name);
      
      if(! -e $name) {
	print "FATAL: can't create '$name' !!\n";
	exit 1;
      }

      $sample_gen = 1;
    } else {
      print "  found sample image for chapter $chap: $name\n";
    }
    push @names,$name;
    push @titles,$title;
    push @args,sprintf("-label \"%d: $title\" $name",$i+1);
  }
  
  # fill with empty images
  if($num < $menu_entries) {
    $mis = $menu_entries - $num;
    foreach(1 .. $mis) {
      push @args,"NULL:";
    }
  }
  
  # ----- montage menu image -----
  $name = $menu;
  $name =~ s/\.mpg//;
  $png = "$name.png";
  if((! -e $png) || $redo_menu) {
  
    # first create the sample chapters
    $png2 = "$name-chap.png";
    print "  creating montage of sample images: $png2\n";
    $cmd = "montage -geometry ${wt}x${ht}+${border_x}+${border_y} "
         . "-tile ${cells}x${rows} "
	 . "-background none -fill $label_col "
	 . "-font \"$font_name\" -pointsize $font_size "
         . join(' ',@args) 
	 . " $png2";
    print "\t$cmd\n" if($debug);
    system($cmd);
    
    if(! -e $png2) {
      print "FATAL: can't create '$png2'\n";
      exit 1;
    }
    
    # now compose chapter images with title
    print "  compositing menu image: $png\n";
    $cmd = "convert -size ${screen_width}x${screen_height} $background "
      . "-gravity center -font \"$font_name\" -pointsize $title_size "
      . "-draw \"fill $label_col text 0,$title_y '$heading' "
      . " image Over $chap_x,$chap_y $thumb_w,$thumb_h $png2\" "
      . "$png";
    print "\t$cmd\n" if($debug);
    system($cmd);
    
    unlink($png2) if($cleanup);
    
    if(! -e $png) {
      print "FATAL: can't create '$png'\n";
      exit 1;
    }
    
    $menu_gen = 1;
  } else {
    print "  found menu image: $png\n";
    $menu_gen = 0;
  }
  
  # cleanup
  if($cleanup && $sample_gen) {
    foreach(@names) {
      unlink $_;
    }
  }
  
  # ----- MPEG encoding -----
  # now render image: SVCD
  $fps = $pal ? 25.0 : 29.97;
  $norm = $pal ? 'p' : 'n';
  
  $readpng = "png2yuv -v0 -f $fps -j $png -I p -n 1";
  $mpegenc = "mpeg2enc -v0 -a 2 -n $norm";
  $scaler  = "yuvscaler -v0 -n $norm 2>/dev/null";
  # --- SVCD ---
  if($svcd) {
    print "  creating mpg: $name.m2v\n";
    $cmd = "$readpng | $scaler -O HIRESSTILL | $mpegenc -f7 -T200 -o $name.m2v";
    system($cmd);
    
    unlink($png) if($cleanup && $menu_gen);
    
    print "  multiplexing mpg: $menu\n";
    $cmd = "mplex -v0 -f7 -o $menu $name.m2v";
    system($cmd);
    
    unlink("$name.m2v") if($cleanup);
  }
  # --- VCD ---
  else {
    print "  creating mpeg: ${name}-hi.m1v\n";
    $cmd = "$readpng | $scaler -O HIRESSTILL | $mpegenc -f6 -T200 -o ${name}-hi.m1v";
    system($cmd);
    print "  creating mpeg: ${name}-lo.m1v\n";    
    $cmd = "$readpng | $scaler -O LOVCDSTILL | $mpegenc -f6 -T40 -o ${name}-lo.m1v";
    system($cmd);
    
    unlink($png) if($cleanup && $menu_gen);
    
    print "  multiplexing mpg: $menu\n";
    $cmd = "mplex -v0 -f6 -o $menu ${name}-lo.m1v ${name}-hi.m1v";
    system($cmd);
    
    unlink("${name}-hi.m1v","${name}-lo.m1v") if($cleanup);
  }
  
  if(! -e $menu) {
    print "FATAL: can't create '$menu'\n";
    exit 1
  }
}
close(FH);