#!/opt/bin/perl

eval 'exec /opt/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell
# pcg@goof.com
# a simpleminded uncompressed avi load/save plug-in

use Gimp 1.14;
use Gimp::Fu;
use Gimp::UI;
use Fcntl;

# Gimp::set_trace(TRACE_ALL);

# start a hunk
sub push_hunk($) {
   print FILE $_[0], "\xff\xff\xff\xff";
   push @hunks, tell FILE;
}

# fixup latest hunk
sub pop_hunk {
   my $end = tell FILE;
   my $len = pop @hunks;
   seek FILE,$len-4,0;
   print FILE pack "V", $end-$len;
   seek FILE,$end,0;
}

register "file_avi_save",
         "save image as uncompressed avi",
         "Saves images in the 24 bit uncompressed AVI format used by windows software",
         "Marc Lehmann",
         "Marc Lehmann <pcg\@goof.com>",
         "1999-11-08",
         "<Save>/AVI",
         "RGB",
         [
          [PF_RADIO,	"depth",	"format (currently always 0)", 24, ["24bpp" => 24, "15bpp" => 15]],
          [PF_RADIO,	"compression",	"compression (currently always 0)", 0, [none => 0]],
          [PF_BOOL,	"index",	"write an index hunk (required by some software)", 1],
         ],
         sub {
   my($img,$drawable,$filename,$raw_filename,$depth,$compression,$index) = @_;
   my($new_img,$new_drawable);
   my $export = Gimp::UI::export_image($new_img=$img, $new_drawable=$drawable, "AVI",
                                       EXPORT_CAN_HANDLE_RGB|EXPORT_CAN_HANDLE_LAYERS_AS_ANIMATION|EXPORT_CAN_HANDLE_ALPHA );
   die "export failed" if $export == EXPORT_CANCEL;
   sysopen FILE,$filename,O_CREAT|O_TRUNC|O_WRONLY or die "Unable to open '$filename' for writing: $!\n";
   my $us_frame = eval { $img->parasite_find("gimp-interframe-delay")->data } || 100000;
   #Gimp->tile_cache_ntiles($img->width / Gimp->tile_width + 3); coredumps!

   my ($width, $height) = ($img->width, $img->height);
   my @layers = $new_img->get_layers;
   for (@layers) {
      die "all layers must have the same size as the image\n" if $width != $_->width or $height != $_->height;
   }

   $depth = 16 if $depth == 15;

   $new_img->selection_all;
   my $framesize = ($width*$height*$depth) >> 3;

   my $idx1;

   init Progress "Saving '$filename' as AVI...";

   push_hunk "RIFF"; print FILE "AVI ";
      push_hunk "LIST"; print FILE "hdrl";
         push_hunk "avih";
            print FILE pack "V*",
                            $us_frame,
                            $framesize*1_000_000/$us_frame,
                            0,
                            0x00000810,  # only a god may know why...
                            scalar@layers,
                            0,
                            1,
                            $framesize,
                            $width,
                            $height,
                            0,
                            0,
                            0,
                            0;
         pop_hunk;
         push_hunk "LIST"; print FILE "strl";
            push_hunk "strh";
               print FILE pack "A4 V11 V2",
                               "vids",
                               0,
                               0,
                               0,
                               0,
                               $us_frame,
                               1_000_000,
                               0,
                               scalar@layers,
                               $framesize,
                               0,
                               0,
                                
                               0,
                               0;
            pop_hunk;
            push_hunk "strf";
               print FILE pack "V3 v2 V6",
                               40, # ??
                               $width,
                               $height,
                               1,
                               $depth,
                               0,
                               $framesize,
                               0,
                               0,
                               0,
                               0;
            pop_hunk;
         pop_hunk;
      pop_hunk;
      push_hunk "LIST"; print FILE "movi";
         for (0..$#layers) {
            my $r = new PixelRgn $layers[-1-$_],0,0,$width,$height,0,0;
            my $d = $r->get_rect2(0,0,$width,$height);
            Gimp::RAW::convert_32_24_inplace $d if $r->bpp == 4;
            Gimp::RAW::reverse_v_inplace $d, $width*3;
            Gimp::RAW::convert_bgr_rgb_inplace $d if $depth == 24;
            Gimp::RAW::convert_24_15_inplace $d if $depth == 16;

            $idx1 .= "00db" . pack "V*", 16, tell FILE, $framesize if $index;

            print FILE "00db",
                       (pack "V", $framesize),
                       $d;

            update Progress $_ / @layers;
         }
      pop_hunk;
      if ($index) {
         push_hunk "idx1";
            print FILE $idx1;
         pop_hunk;
      }
   pop_hunk;
   close FILE;
   $new_img->delete if $export == EXPORT_EXPORT;
   ();
};

# a generic iff/riff parser. LIST's are simply flattened out,
# JUNK is just skipped. 
sub parse_iff {
   my $size = shift;
   my $default = pop;
   my %action = @_;
   my($hunk,$len);
   while ($size > 0) {
      read FILE,$hunk,4; $size -= 4;
      $size >= 4 or die "AVI hunk $hunk ends unexpectedly\n";
      read FILE,$len,4; $size -= 4;
      $len = unpack "V", $len;
      $size >= $len or Gimp->message("WARNING: broken avi, hunk '$hunk' too long ($size < $len)");
      $size -= $len;
      if ($hunk eq "LIST") {
         read FILE,$hunk,4;
         parse_iff ($len-4, %action, $default);
      } elsif ($hunk eq "JUNK") {
         seek FILE,$len,1;
      } elsif ($action{$hunk}) {
         $action{$hunk}->($len);
      } else {
         $default->($hunk,$len);
      }
   }
}

sub skip_hunk {
   seek FILE,$_[0],1;
}

register "file_avi_load",
         "load uncompressed avi movie",
         "Loads images that were saved in 15/24 bit uncompressed RGB AVI format used mainly by windows",
         "Marc Lehmann",
         "Marc Lehmann <pcg\@goof.com>",
         "1999-11-08",
         "<Load>/AVI",
         undef,
         [],
         sub {
   my($filename) = @_;
   sysopen FILE,$filename,O_RDONLY or die "Unable to open '$filename' for reading: $!\n";
   my $image;
   my $comment;

   seek FILE, 0, 2; my $filesize = tell FILE; seek FILE, 0, 0;
   init Progress "Loading AVI image from '$filename'...";

   $filesize > 12 or die "File too small to be an AVI\n";
   read FILE,$comment,4; $filesize -= 4;
   die "File is not a RIFF file\n" unless $comment eq "RIFF";
   read FILE,$comment,4; $filesize -= 4;
   $comment = unpack "V", $comment;
   die "RIFF hunk too short\n" unless $comment <= $filesize;
   $filesize = $comment;
   read FILE,$comment,4;
   die "RIFF file is not an AVI\n" unless $comment eq "AVI ";

   my $frame = 0;
   my ($us_frame,$frames,$width,$height);
   my $type;
   my ($size,$planes,$depth,$compression,$image_size);

   parse_iff ($filesize-4,
      "avih" => sub {
         read FILE,$comment,$_[0];
         die "avih header too short\n" unless $_[0] >= 14*4;
         ($us_frame,undef,undef,undef,$frames,undef,undef,undef,$width,$height)
            = unpack "V10", $comment;
      },
      "strh" => sub {
         read FILE,$comment,$_[0];
         die "strh header too short\n" unless $_[0] >= 4;
         ($type)
            = unpack "A4", $comment;
      },
      "strf" => sub {
         read FILE,$comment,$_[0];
         if ($type eq "vids") {
            die "strh(vids)/strf header too short\n" unless $_[0] >= 7*4;
            ($size,$width,$height,$planes,$depth,$compression,$image_size)
               = unpack "V3 v2 V3", $comment;
            $depth == 24 or $depth == 16 or die "unsupported bit depth $depth (only 15/24 bit supported)\n";
            $compression == 0 or die "compressed streams not supported\n";
            $planes == 1 or die "incompatible frameformat ($planes)\n";
            ($width * $height * $depth) >> 3 == $image_size or die "strh(vids)/strf header format error\n";
            
            $image = new Image($width,$height,RGB);
            $image->undo_disable;
            $image->set_filename($filename);
            $image->parasite_attach(new GimpParasite "gimp-interframe-delay", PARASITE_PERSISTENT, $us_frame);
            $image->parasite_attach(new GimpParasite "gimp-avi-depth", PARASITE_PERSISTENT, $depth == 16 ? 15 : $depth);
            $image->parasite_attach(new GimpParasite "gimp-avi-compression", PARASITE_PERSISTENT, $compression);
         }
      },
      "00db" => sub {
         $_[0] == ($width * $height * $depth) >> 3 or die "frame has incorrect size\n";
         read FILE,$comment,$_[0];
         my $layer = $image->layer_new($width,$height,RGB_IMAGE,
                                       sprintf("(%.2fs)",$us_frame*$frame/1_000_000),
                                       100,NORMAL_MODE);

         Gimp::RAW::convert_15_24_inplace $comment if $depth == 16;
         Gimp::RAW::convert_bgr_rgb_inplace $comment if $depth == 24;
         Gimp::RAW::reverse_v_inplace $comment,$width*3;
         (new PixelRgn $layer,0,0,$width,$height,1,0)->set_rect2($comment,0,0);
         $layer->add_layer(0);
         $frame++;
         update Progress $frame/$frames;
      },
      "00dc" => sub { die "compressed data not handled\n" },
      "01wb" => \&skip_hunk,  # audio data
      "idx1" => \&skip_hunk,  # hunk index
      "ISFT" => \&skip_hunk,  # i? software?
      "ICOP" => \&skip_hunk,  # i? copyright?
      "IDIT" => \&skip_hunk,  # i? time stamp??
      sub {
         warn "skipping hunk (@_), please report!\n";
         skip_hunk $_[1];
      }
   );

   $image->undo_enable;
   return $image;
};

Gimp::on_query {
   Gimp->register_magic_load_handler("file_avi_load", "avi", "", "0,string,RIFF,&8,string,AVI ");
   Gimp->register_save_handler("file_avi_save", "avi", "");
};

exit main;

=head1 LICENSE

Copyright Marc Lehman.

Distributed under the same terms as Gimp-Perl.

