#!/usr/bin/perl

# pic2mpg: Convert picture files to MPEG still frames
#
# Converts either a single picture file or all files in a
# given directory (recursively) to MPEG still frames.
#
# See the README file for copyright information and how to reach the author.
#
# $Id: pic2mpg-sd 2.1 2011/07/23 14:23:59 kls Exp $

## TODO implement HDTV (1920 x 1080)

use File::Path;
use File::Spec;
use Getopt::Std;
use Image::ExifTool qw(:Public);
use Image::Size;

$Usage = qq{
Usage: $0 [options] picture-dir mpeg-dir
       $0 [options] picture-file mpeg-file

Options: -a             Aspect ratio 4:3 (default is 16:9)
         -f             Force conversion
         -h             print Help
         -i             Ignore unknown file types
         -n             NTSC (default is PAL)
         -v num         Verbose (0=none, 1=list files, 2=detailed)
         -x percent     X overscan in percent
         -y percent     Y overscan in percent
};

getopts("afhinv:x:y:") || die $Usage;

die $Usage if $opt_h;

$Aspect    = $opt_a;
$Force     = $opt_f;
$Ignore    = $opt_i;
$NTSC      = $opt_n;
$Verbose   = $opt_v;
$OverscanX = $opt_x;
$OverscanY = $opt_y;

$ListFiles = $Verbose >= 1;
$Detailed  = $Verbose >= 2;

# Screen size:

$SW = $NTSC ? 720 : 720;
$SH = $NTSC ? 480 : 576;

$ScreenRatio = $Aspect ? 4 / 3 : 16 / 9;

# Converter programs:

%PNMCONV = (
  bmp  => "bmptopnm",
  gif  => "giftopnm",
  jpeg => "jpegtopnm",
  jpg  => "jpegtopnm",
  png  => "pngtopnm",
  pnm  => "cat",
  tif  => "tifftopnm",
  tiff => "tifftopnm",
  );

# Command options:

die "$0: missing parameter\n" unless $ARGV[0] && $ARGV[1];
die "$0: file or directory not found: $ARGV[0]\n" unless -e $ARGV[0];
die "$0: source and destination must be different\n" if $ARGV[0] eq $ARGV[1];

$verbose1  = $Detailed ? "--verbose" : "";
$verbose2  = $Detailed ? "-v 2" : "-v 0";
$system1   = $NTSC ? "" : "--pal";
$system2   = $NTSC ? "n" : "p";
$framerate = $NTSC ? "30000:1001" : "25:1";
$aspect    = $Aspect ? "2" : "3";

# Convert a single file:

if (-f $ARGV[0]) {
   die "$0: mixed file and directory ('$ARGV[0]' <-> '$ARGV[1]')\n" unless !-e $ARGV[1] || -f $ARGV[1];
   ConvertFile($ARGV[0], $ARGV[1]);
   exit;
   }

die "$0: mixed directory and file ('$ARGV[0]' <-> '$ARGV[1]')\n" unless !-e $ARGV[1] || -d $ARGV[1];

$PICDIR = File::Spec->rel2abs($ARGV[0]);
$MPGDIR = File::Spec->rel2abs($ARGV[1]);

# Convert pictures to mpegs:

chdir($PICDIR) || die "$PICDIR: $!\n";

@Pictures = `find -type f`;
chomp(@Pictures);

for $pic (@Pictures) {
    my $mpg = "$MPGDIR/$pic.mpg";
    if ($Force || !-e $mpg || -M $mpg > -M $pic) {
       (my $dir = $mpg) =~ s/\/[^\/]*$//;
       mkpath($dir);
       ConvertFile($pic, $mpg);
       }
    }

# Remove mpegs without pictures:

chdir($MPGDIR) || die "$MPGDIR: $!\n";

@Mpegs = `find -type f`;
chomp(@Mpegs);

for $mpg (@Mpegs) {
    my $pic = "$PICDIR/$mpg";
    $pic =~ s/\.mpg$//;
    if (!-e $pic) {
       print "removing $mpg\n";
       unlink($mpg);
       }
    }

# Remove empty directories:

chdir($MPGDIR) || die "$MPGDIR: $!\n";

for ($i = 0; $i < 10; $i++) { # dirs might become empty when removing empty subdirs
    @Dirs = `find -type d -empty`;
    chomp(@Dirs);
    last unless @Dirs;

    for $dir (@Dirs) {
        $dir = EscapeMeta($dir);
        print "removing $dir\n";
        !system("rm -rf $dir") || die "$dir: $!\n";
        }
    }

# Actual file conversion:

sub ConvertFile
{
  my ($Pict, $Mpeg) = @_;
  (my $Type) = lc($Pict) =~ /\.([^\.]*)$/;
  if (!defined $PNMCONV{$Type}) {
     return if ($Ignore);
     die "unknown file type '$Type': '$Pict'\n";
     }
  my ($w, $h) = imgsize($Pict);
  print "image size is $w x $h\n" if ($Detailed);
  my $Exif = ImageInfo($Pict);
  my $Orientation = $$Exif{"Orientation"};
  my ($Degrees) = $Orientation =~ /Rotate ([0-9]+) /;
  my $Rotate = "-null";
  $Rotate = "-cw"   if $Degrees eq "90";
  $Rotate = "-ccw"  if $Degrees eq "270";
  $Rotate = "-r180" if $Degrees eq "180";
  print "orientation = '$Orientation' -> rotation = $Rotate\n" if ($Detailed);
  ($w, $h) = ($h, $w) if ($Degrees eq "90" || $Degrees eq "270");
  if ($w / $h <= $ScreenRatio) {
     $w = $h * $ScreenRatio;
     }
  else {
     $h = $w / $ScreenRatio;
     }
  my $ScaleW = $SW / $w * (100 - 2 * $OverscanX) / 100;
  my $ScaleH = $SH / $h * (100 - 2 * $OverscanY) / 100;
  $Pict = EscapeMeta($Pict);
  $Mpeg = EscapeMeta($Mpeg);
  print "$Pict -> $Mpeg $Rotate\n" if $ListFiles;
  my $Cmd = "$PNMCONV{$Type} $Pict 2> /dev/null |"
          . "pamflip $verbose1 $Rotate |"
          . "pnmscale $verbose1 --xscale=$ScaleW --yscale=$ScaleH |"
          . "pnmpad $verbose1 --black --width $SW --height $SH |"
          . "ppmntsc $verbose1 $system1 |"
          . "ppmtoy4m $verbose2 -F $framerate -I p -S 420mpeg2 |"
          . "mpeg2enc $verbose2 -f 3 -b 12500 -a $aspect -q 1 -n $system2 -o $Mpeg";
  !system($Cmd) || die "$Cmd: $!\n";
  $Cmd = "touch -r $Pict $Mpeg";
  !system($Cmd) || die "$Cmd: $!\n";
}

sub EscapeMeta
{
  my $META = ' !"#$%&\'()*;<>?[\\]`{|}~';
  my $s = shift;
  $s =~ s/([$META])/\\$1/g;
  return $s;
}