#!/usr/bin/perl -w
use strict;
use Imager;
use Getopt::Long;
use File::Spec;
use File::Basename qw(basename);
use vars qw($VERSION);

$VERSION="0.01_00";

my $help;
my $verbose;
my $backup; # backup extension name
my $directory; # output directory
my $output;
my $type;
my %write_opts; # options supplied to write()
my $understand;

my @collection; # actions/options in order to allow us to set values as needed

# each entry consists of:
#  - ref to action/option handler function
#  - help text
#  - optional ref to value parser function

my %funcs = im_functions();
my %options = im_options();
my %all = ( %funcs, %options );

my $action_func =
  sub { 
    my ($option, $value) = @_;
    if ($all{$option}[1] && ref $all{$option}[1]) {
      $value = $all{$option}[1]->($option, $value);
    }
    push @collection, [ $option, $value ] 
  };

my $did_help;
my $help_func =
  sub {
    my ($option) = @_;

    $option =~ s/help-//;
    help_on($option);
    ++$did_help;
  };

my @getoptions;
for my $option_name (keys %all) {
  my $option = $all{$option_name};
  my @names = ( $option_name );
  my @other_names = split /\|/, $option->[2] if $option->[2];
  push @names, @other_names;
  my $code = '';
  if ($option->[1]) {
    $code = ref $option->[1] ? "=s" : "=".$option->[1];
  }
  push @getoptions, join("|", @names) . $code => $action_func;
  # this would be evil $all{$_} = $option for @other_names;
  push @getoptions, join("|", map "help-$_", @names) => $help_func;
}

GetOptions('help' => sub { $help_func->("synopsis") },
	   'verbose|v+' => \$verbose,
	   'backup|i=s' => \$backup,
	   'directory|d=s' => \$directory,
	   'type|t=s' => \$type, # output file type
	   'write-option|wo=s' => \%write_opts,
	   'output|o=s' => \$output,
           'understand' => \$understand,

	   @getoptions,

	   'help-color-spec' => sub { $help_func->("color specifications") },
           'help-actions' => sub { $help_func->("actions") },
           'help-options' => sub { $help_func->("processing options") },
           'help-general' => sub { $help_func->("general options") },
	  )
  or usage();

$did_help and exit;

unless ($understand) {
  die <<EOS;
This tool is under-tested and will probably destroy your data.

If you understand and agree with this use the --understand option to
avoid this message.

In fact, only the --info and --tags actions have been used at all.
EOS
}

exists $write_opts{file}
  and die "Illegal write option 'file'\n";
exists $write_opts{type}
  and die "Use the --type option to set the output format\n";

delete $write_opts{qw/file fd fh data callback/};

my @actions = grep $funcs{$_->[0]}, @collection;

if ($help) {
  if (@actions) {
    print $funcs{$_}[1] for map $_->[0], @actions;
    exit;
  }
  else {
    usage();
  }
}

if (!@actions && !@ARGV) {
  usage();
}

unless (@ARGV) {
  die "No files to process\n";
}

unless (@actions) {
  die "Nothing to do, supply at least one action, see $0 --help\n";
}

my @type;
push @type, type => $type if $type;

for my $name (@ARGV) {
  my $im = Imager->new;
  if ($im->read(file=>$name)) {
    my %state = ( filename => $name );

    for my $action (@collection) {
      $im = $all{$action->[0]}[0]->($im, $action->[1], \%state);
      last unless $im;
    }

    if ($im) {
      my $outname = $name;
      if ($directory) {
	my $file;
	(undef, undef, $file) = File::Spec->split_path($outname);
	$outname = File::Spec->catfile($directory, $file);
      }
      if ($backup) {
	my $backfile = $name . $backup;
	rename $name, $backfile
	  or die "Couldn't rename source '$name' to backup '$backfile': $!\n";
      }

      unless ($im->write(file=>$outname, @type)) {
	die "Could not write result from '$name' to '$outname': ", $im->errstr,"\n";
      }
    }
  }
  else {
    print STDERR "Failed reading $name: ",$im->errstr,"\n";
  }
}

sub _replace_codes {
  my ($im, $state, $format) = @_;

  my %replace =
    (
     f => [ 's', $state->{filename} ],
     b => [ 's', basename($state->{filename}) ],
     w => [ 'd', $im->getwidth ],
     h => [ 'd', $im->getheight ],
     c => [ 'd', $im->getchannels ],
     t => [ 's', $im->type ],
     n => [ 'c', ord("\n") ], # a bit of a hack
     '%' => [ '%' ],
    );
  my @values;
  $format =~ s{%(-?(?:\d+(?:\.\d*)?|\.\d+)?)([fwhctbn%])}
    {
      my $which = $replace{$2};
      push @values, @$which[1..$#$which];
      "%$1$which->[0]"
    }eg;

  return sprintf $format, @values;
}

sub req_info {
  my ($im, $ignored, $state) = @_;

  my $format = $state->{info_format} || <<EOS;
Image: %f
Dimensions: %ww x %hh
Channels: %c
Type: %t
EOS

  print _replace_codes($im, $state, $format);

  return;
}

sub req_info_format {
  my ($im, $value, $state) = @_;

  $state->{info_format} = $value;

  $im;
}

sub req_tags {
  my ($im, $ignored, $state) = @_;

  print $state->{filename},"\n";
  my @tags = $im->tags;
  for my $tag (sort { $a->[0] cmp $b->[0] } @tags) {
    my $name = shift @$tag;
    print "  $name: @$tag\n";
  }

  return;
}

sub req_palette {
  my ($im, $ignored, $state) = @_;

  print $state->{filename},"\n";
  if ($im->type eq 'direct') {
    print "  No palette - this is a direct color image\n";
  }
  else {
    my @colors = $im->getcolors;
    for my $index (0..$#colors) {
      printf "%3d: (%3d, %3d, %3d)\n", $index, ($colors[$index]->rgba)[0..2];
    }
  }

  return;
}

sub val_scale {
  my ($option, $value) = @_;

  my %options;
  if ($option =~ /^(\d+)\s*x\s*(\d+)$/i) {
    return { xpixels=>$1, ypixels=>$2 };
  }
  elsif ($option =~ /^(\d+(?:\.\d*)?|\.\d+)$/) {
    return { scalefactor => $option };
  }
  elsif ($option =~ /^(\d+)\s*x\s*(\d+)\s*min$/i) {
    return { xpixels=>$1, ypixels=>$2, type=>'min' };
  }
  elsif ($option =~ /^(\d+)\s*(?:w|wide)$/i) {
    return { xpixels => $1 };
  }
  elsif ($option =~ /^(\d+)\s*(?:h|high)$/i) {
    return { ypixels => $1 };
  }
  else {
    die "Invalid parameter to --scale, try $0 --help-scale\n";
  }
}

sub req_scale {
  my ($im, $args) = @_;

  return $im->scale(%$args);
}

sub val_rotate {
  my ($option, $value) = @_;

  if ($value =~ /^[-+]?(?:\d+(?:\.\d*)|\.\d+)$/) {
    return { degrees => $value };
  }
  elsif ($value =~ /^([-+]?(?:\d+(?:\.\d*)|\.\d+))\s*(?:r|radians)$/i) {
    return { radians => $1 };
  }
  else {
    die "Invalid parameter to --rotate, try $0 --help-rotate\n";
  }
}

sub req_rotate {
  my ($im, $args, $state) = @_;

  my @moreargs;
  if ($state->{background}) {
    push @moreargs, back => $state->{background};
  }
  return $im->rotate(%$args, @moreargs);
}

sub req_bg {
  my ($im, $value, $state) = @_;

  $state->{background} = $value;
  
  $im;
}

sub req_fg {
  my ($im, $value, $state) = @_;

  $state->{foreground} = $value;

  $im;
}

sub req_font {
  my ($im, $value, $state) = @_;

  $state->{font} = Imager::Font->new(file=>$value)
    or die "Could not create font from $value: ", Imager->errstr,"\n";

  $im;
}

sub val_font_size {
  my ($option, $value) = @_;

  unless ($value =~ /^\d+$/ && $value > 0) {
    die "$option must be a positive integer\n";
  }

  $value;
}

sub req_font_size {
  my ($im, $value, $state) = @_;

  $state->{font_size} = $value;

  $im;
}

sub req_caption {
  my ($im, $format, $state) = @_;

  my $text = _replace_codes($im, $state, $format);

  my $font = $state->{font}
    or die "You must supply a --font option before the --caption command\n";

  my $size = $state->{font_size} || 16;

  my $box = $font->bounding_box(size=>$size);
  $box->total_width <= $im->getwidth
    or die "Caption text '$text' is wider (", $box->total_width, 
      ") than the image (",$im->getwidth,")\n";

  die "not implemented yet";
}

sub usage {
  help_on("SYNOPSIS");
  exit 1;
}

sub im_functions {
  return
    (
     info => [ \&req_info ],
     tags => [ \&req_tags ],
     palette => [ \&req_palette ],
     scale => [ \&req_scale, \&val_scale ],
     rotate => [ \&req_rotate, \&val_rotate ],
     # caption => [ \&req_caption ], # not done yet
    );
}

sub val_color {
  my ($option, $value) = @_;

  if ($value =~ /^rgba\((\d+),(\d+),(\d+),(\d+)\)$/i) {
    return Imager::Color->new($1,$2,$3,$4);
  }
  elsif ($value =~ /^rgb\((\d+),(\d+),(\d+)\)$/i) {
    return Imager::Color->new($1,$2,$3);
  }
  elsif ($value =~ /^\#[\da-f]{3}([\da-f]{3})?$/) {
    return Imager::Color->new(web=>$value);
  }
  elsif ($value =~ /^hsv\((\d+(?:\.\d*)),(\d+\.\d*|\.\d+),(\d+\.\d*|\.\d+)\)$/) {
    return Imager::Color->new(hsv => [ $1, $2, $3 ]);
  }
  elsif ($value =~ /^hsva\((\d+(?:\.\d*)),(\d+\.\d*|\.\d+),(\d+\.\d*|\.\d+),(\d+)\)$/) {
    return Imager::Color->new(hsv => [ $1, $2, $3 ], alpha=>$4);
  }
  else {
    my $color = Imager::Color->new(name=>$value);
    return $color if $color;

    die "Unrecognized color specification $value supplied to --$option\n";
  }
}

sub im_options {
  return
    (
     background => [ \&req_bg, \&val_color, 'bg' ],
     foreground => [ \&req_fg, \&val_color, 'fg' ],
     'info-format' => [ \&req_info_format, 's'],
     font => [ \&req_font, \&val_font ],
     'font-size' => [ \&req_font_size, \&val_font_size, 'fs' ],
    );
}

sub help_on {
  my ($topic) = @_;

  open SOURCE, "< $0" or die "Cannot read source for help text: $!\n";
  my @lines;
  while (<SOURCE>) {
    # don't chomp it
    if (/^=item --$topic\s/) {
      push @lines, $_;
      # read any more =items then read text until we see =item or =back
      while (<SOURCE>) {
        last unless /^\s*$/ or /^=item /;
        push @lines, $_;
      }
      push @lines, $_;
      # and any other until another option or =back
      while (<SOURCE>) {
        last if /^=(item|cut|back)/;
        push @lines, $_;
      }
      print @lines;
      return;
    }
    elsif (/^=head(\d) $topic\s*$/i) {
      my $level = $1;
      push @lines, $_;
      while (<SOURCE>) {
        last if /=head[1-$level]/;
        push @lines, $_;
      }

      print @lines;
      return;
    }
  }
  close SOURCE;

  die "No help topic $topic found\n";
}


sub help_color_spec {
  print <<EOS;
EOS
}

=head1 NAME

imager - Imager command-line image manipulation tool

=head1 SYNOPSIS

 imager --help
 imager  [--font-size <size>] [--fs <size>] [--background <color>] 
    [--bg <color>] [--foreground <color>] [--fg <color] 
    [--info-format <format>] [--rotate <angle>] [--scale <scale-spec>] 
    [--caption <text>] [--info] [--tags] [--font fontfile] files ...
 imager --help-I<option>
 imager --help-I<operation>
 imager --help-options
 imager --help-actions
 imager --help-general
 imager --help-colorspec

=head1 DESCRIPTION

=head1 ACTIONS

=over

=item --info

Displays the width, height, channels, type for each image, and any tags
Imager picks up.  No options.

Note: Imager still converts many files into direct images when the source
is a paletted image, so the displayed image type may not match the
source image type.

No output image file is produced.

=item --tags

Displays all the tags the Imager reader for that format sets for each
file.

See L<Imager::Files> for file format specific tags and
L<Imager::ImageTypes> for common tags.

=item --palette

Dumps the palette of the given file, if it is an indexed image.

=item --scale <scalefactor>

=item --scale <width>x<height>

=item --scale <width>x<height>min

=item --scale <width>w

=item --scale <height>h

Scale either by the given scaling factor, given as a floating point number,
or to a given dimension.

The scaling is always proportional, if a dimension is given then the
scalefactor that results in the larger image that matches either the 
specified width or height is chosen, unless the word "min" is present".

  --scale 0.5         # half size image
  --scale 100x100     # aim for 100 pixel x 100 pixel image
  --scale 100x100min  # image that fits in 100 x 100 pixel box
  --scale 100w        # 100 pixel wide image
  --scale 100h        # 100 pixel high image

=item --rotate <degrees>

=item --rotate <radians>r

Rotate the image by the given number of degrees or radians.

=item --help

Displays the usage message if no extra parameter is found, otherwise displays
more detailed help for the given function, if any.

=item --caption text

Not implemented yet.

Expands the image to create a caption area and draws the given text in the
current font.

You must set a font with --font before this.

  imager --font arial.ttf --caption "my silly picture"

The text has the same replacements done as the --info command.

  imager --font arial.ttf --caption '%b - %w x %h'

If the caption text is too wide for the image an error is produced.

Any newlines that aren't at the beginning or end of the caption cause
multiple lines of text to be produced.

The --foreground and --background options can be used to set colors
for this.  By default black text on a white background is produced.

=back

=head1 GENERAL OPTIONS

=over

=item --help

Display the SYNOPSIS from this POD

=item --verbose

=item -v

Increase the verbosity level.

=item --backup <extension>

=item -i <extension>

Input files are renamed to I<filename><extension> before the output
file is written.

=item --directory <directory>

=item -d <directory>

If this is supplied the output files are written to this directory
instead of the 

=item --type <fileformat>

Specifies an output file format

=item --write-option name=value

=item --wo name=value

Sets the value of an option supplied to the Imager write() function.
The options available depend on the file format, see
L<Imager::Files/TYPE SPECIFIC INFORMATION> for file format specific
options.

You can also supply the L<Imager::ImageTypes/Common Tags>.

If you're writing to a gif file you can also supply the options
documented as tags under L<Imager::ImageTypes/Quantization options>.

=back

=head1 PROCESSING OPTIONS

These supply extra parameters to the actions

=over

=item --background <color-spec>

=item --bg <color-spec>

Sets the background color for the --rotate and --caption actions, and
possibly other actions in the future.

See $0 --help-color-spec for possible color specifications.

  --bg red
  --bg rgba(0,0,0,0)

=item --foreground <color-spec>

=item --fg <color-spec>

Sets the foreground color for the --caption action, and possibly other
actions in the future.

See $0 --help-color-spec for possible color specifications.

  --fg red
  --fg 'rgba(0,0,0,0)'

=item --font-size size

=item --fs size

Set the font size used by the --caption command, in pixels.

  --fs 16  # 16 pixels from baseline to top
  --font-size 40 # a bit bigger

=item --info-format format

Sets the format for the output of the --info command.

The format can contain printf style replacement codes, each value is %
followed by a sprintf() field width/precision, followed by the value
code.

The following values can be output:
  w - image width in pixels
  h - image height in pixels
  f - full image filename
  b - base image filename
  c - number of channels
  t - image type (direct vs paletted)
  n - inserts a newline
  % - inserts a '%' symbol

The default format is:

 Image: %f%nDimensions: %ww x %hh%nChannels: %c%nType: %t%n

You can use field widths to produce a more table like appearance:

  im --info-format '%4w %4h %4c %-8t %b%n' --info *.jpg

=item --font filename

Gives the name of a font file.  Required by actions that render text.

  --font ImUgly.ttf
  --font arial.ttf

=back

=head1 COLOR SPECIFICATIONS

Possible color specifications:
  color-name - the name of a built-in color
  rgb(red,green,blue) - as an RGB triplet
  #RRGGBB - as a HTML RGB hex triple
  #RGB - as a HTML CSS RGB hex triple
  rgba(red,green,blue,alpha) - as an RGBA quad
  hsv(hue,sat,value) - as an HSV triplet
  hsva(hue,sat,value,alpha) as an HSVA quad

For example:

  red
  rgb(255,0,0)
  #FF0000
  hsv(180,1,1)

If you use either of the HTML color specifications, or a specification
using parentheses from a Unix shell you will need to quote it, for
example:

  --fg '#FF0000'
  --bg 'rgb(255,0,255)'

=head1 AUTHOR

Tony Cook <tony@develop-help.com>

=cut