#!/usr/local/bin/perl

# Eqe, LaTeX equation editor.
#     Ronan Le Hy, 2005-2006.

#     This program is free software; you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation; version 2 of the License only.

#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.

#     You should have received a copy of the GNU General Public License
#     along with this program; if not, write to the Free Software
#     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use strict;
use warnings;

our $VERSION = '1.2.0';

use File::Temp qw/tempfile/;
use File::Spec;
use File::Copy;
use Getopt::Long;
use Cwd;
use Template;
use File::Slurp;
use MIME::Base64;

use Data::Dumper;

sub usage
{
    "eqedit: command line LaTeX image generator, version $VERSION
Ronan Le Hy, 2004 - 2006
usage: $0 [--help] <options>
  where options are:

  help:
    --help
    --version

  essential:
    --output=<file> [eq.png]

  formatting:
    --magnification=<float> [3.0]
    --text-color=<black|white|red|green|blue|cyan|magenta|yellow|transparent>
      [LaTeX default]
    --page-color=<...> [LaTeX default]
    --font=<8r|avant|bookman|chancery|charter|courier|helvet|mathpazo|mathpple|
            mathptm|mathptmx|newcent|palatino|pifont|times>
      [LaTeX default]
    --displaymath=<0|1> [0]

  advanced and experimental:
    --latex-template=<file> [template.tt.tex]
      (search path: ~/.eqe/, /usr/local/share/eqe/, /usr/share/eqe/,
       relative and absolute paths are allowed)
    --edit-template runs an editor on the user LaTeX template
    --width=<int>  [as defined after magnification]
    --height=<int> [as defined after magnification]
    --use-dvipng [off, ie use dvips]
    --lumitransparency (experimental)

  logging and debugging:
    --verbose [no]
    --log=<file> [/dev/null]
    --keep-temp [no]

  All options can be abbreviated.
";
}

###############################
# hash as object
# synopsis:
#  my $h = SafeHash::new;
#  $h->{key} = $value;
#  print $h->key;
#  my $g = $h->with({'other_key' => 'other_value'});
package SafeHash;
use Carp;
sub new { bless(shift || {}, __PACKAGE__) }
sub with
{
    my ($h, $add) = @_;
    return new { %$h, %$add };
}

AUTOLOAD
{
    our $AUTOLOAD;
    my ($name) = $AUTOLOAD =~ /([^:]+)$/;
    croak "$AUTOLOAD: no such function" 
	unless defined $_[0] and defined $_[0]->{$name};
    return $_[0]->{$name};
};

DESTROY
{
};

1;

package main;

###############################

my $template_search_path =
    ["$ENV{HOME}/.eqe", '/usr/local/share/eqe', '/usr/share/eqe'];

my $default_tt_latex =
    '
%% This is a template for eqedit (and eqe).
%% You may want to adjust the included packages at the beginning.
%% eqedit will search for templates in ~/.eqe and /usr/share/eqe

\documentclass{article}
\pagestyle{empty}

\usepackage[T1]{fontenc}
\usepackage[latin1]{inputenc}
\usepackage[french]{babel}
\usepackage[dvips]{graphicx}
\usepackage{color}

[% IF font %]
\usepackage{[% font %]}
[% END %]

\begin{document}

[% IF text_color %]
\color{[% text_color %]}
[% END %]

[% IF page_color %]
\pagecolor{[% page_color %]}
[% END %]

[% IF displaymath -%]
\begin{displaymath}
[%- END -%]
[% latex_input %]
[%- IF displaymath -%]
\end{displaymath}
[% END %]

\end{document}
';

my $options = SafeHash::new
{
 magnification => 3,
 output => 'eq.png',
 verbose => 0,
 version => 0,
 help => 0,
 keep_temp => 0,
 width => '',
 height => '',
 text_color => '',
 page_color => '',
 font => '',
 log => '/dev/null',
 displaymath => 0,
 lumitransparence => 0,
 use_dvipng => 0,
 latex_template => 'template.tt.tex',
 edit_template => 0
};

sub mywarn
{
    write_file($options->log, {append=>1}, join(' ', "!!! warning:", @_, "\n"));
    warn @_;
}

sub mydie
{
    write_file($options->log, {append=>1}, join(' ', "!!! error:", @_, "\n"));
    die @_;
}

GetOptions('magnification=i' => \$options->{magnification},
	   'output=s' => \$options->{output},
	   'verbose' => \$options->{verbose},
	   'version' => \$options->{version},
	   'help' => \$options->{help},
	   'keep-temp' => \$options->{keep_temp},
	   'width=i' => \$options->{width},
	   'height=i' => \$options->{height},
	   'text-color=s' => \$options->{text_color},
	   'page-color=s' => \$options->{page_color},
	   'font=s' => \$options->{font},
	   'log=s' => \$options->{log},
	   'displaymath=i' => \$options->{displaymath},
	   'latex-template=s' => \$options->{latex_template},
	   'edit-template' => \$options->{edit_template},
	   'lumitransparency' => \$options->{lumitransparence},
	   'use-dvipng' => \$options->{use_dvipng}
	  )
    or mydie usage;

$options->help and do { print usage; exit 0 };
$options->version and do { print $VERSION; exit 0 };

$options->edit_template and do { edit_template($options); exit 0 };

###############################
# temporary file handling
# OOOOOOh, bad Ronan, this is a copy and paste from eqe...

my @temporary_files;

# returns the name of a fresh temporary file
sub make_temp
{
    my ($id, $suffix) = @_;

    my ($fh, $filename) = 
	$suffix ?
	    tempfile("eqe_temp_${id}_XXXXXX",
		     DIR => File::Spec->tmpdir(),
		     SUFFIX => $suffix) :
			 tempfile("eqe_temp_${id}_XXXXXX",
				  DIR => File::Spec->tmpdir());

    push @temporary_files, $filename;
    #close $fh or warn "Cannot close file '$filename': $!\n";

    return $filename;
}

sub cleanup
{
    for my $temp (@temporary_files)
    {
	if ($options->keep_temp)
	{
	    $options->verbose and mywarn "keeping temporary file '$temp'\n";
	    next;
	}

	$options->verbose and mywarn "deleting temporary file '$temp'\n";
	unlink $temp
	    or mywarn "Cannot delete temporary file '$temp': $!\n";
    }
}

$SIG{QUIT} = $SIG{KILL} = $SIG{TERM} = \&cleanup;

END
{
    cleanup;
}

# runs the passed function, masking all its output on stdout and stderr
sub no_output
{
    my $fun = shift;

    open(OLDOUT, ">&STDOUT") or warn "Cannot duplicate STDOUT: $!";
    open(OLDERR, ">&STDERR") or warn "Cannot duplicate STDERR: $!";

    close STDERR or warn "Cannot close STDERR: $!";
    close STDOUT or warn "Cannot close STDOUT: $!";

    $fun->();

    # restore stdout and stderr
    open(STDERR, ">&OLDERR") or warn "Can't restore stderr: $!";
    open(STDOUT, ">&OLDOUT") or warn "Can't restore stdout: $!";

    # avoid leaks by closing the independent copies
    close(OLDOUT) or warn "Can't close OLDOUT: $!";
    close(OLDERR) or warn "Can't close OLDERR: $!";
}

# runs the passed function, masking all its output on stdout and stderr
sub redirect_to_log
{
    my ($log, $fun) = @_;

    open(OLDOUT, ">&STDOUT") or warn "Cannot duplicate STDOUT: $!";
    open(OLDERR, ">&STDERR") or warn "Cannot duplicate STDERR: $!";

    close STDERR or warn "Cannot close STDERR: $!";
    close STDOUT or warn "Cannot close STDOUT: $!";

    # redirect STDOUT and STDERR to the log
    open(STDOUT, '>>', $log) or warn "Cannot redirect STDOUT to file '$log': $!";
    open(STDERR, ">&STDOUT") or warn "Cannot redirect STDERR to STDOUT: $!";

    my $ret = $fun->();

    close STDERR or warn "Cannot close STDERR: $!";
    close STDOUT or warn "Cannot close STDOUT: $!";

    # restore stdout and stderr
    open(STDERR, ">&OLDERR") or warn "Can't restore stderr: $!";
    open(STDOUT, ">&OLDOUT") or warn "Can't restore stdout: $!";

    # avoid leaks by closing the independent copies
    close(OLDOUT) or warn "Can't close OLDOUT: $!";
    close(OLDERR) or warn "Can't close OLDERR: $!";

    return $ret;
}

#################################################3

my @transparent;
if ($options->text_color eq 'transparent')
{
    $options->{text_color} =
	$options->page_color eq 'black' ? 'white' : 'black';
    push @transparent, '-transparent', $options->{text_color};
}
if ($options->page_color eq 'transparent')
{
    $options->{page_color} =
	$options->text_color eq 'white' ? 'black' : 'white';
    push @transparent, '-transparent', $options->{page_color};
}

if ($options->lumitransparence)
{
    @transparent = ();
}

sub make_latex_wrapper;
sub latex;
sub dvips;
sub dvipng;
sub convert;

my $file = make_latex_wrapper;

my $dvi = latex $file;
my $interm;
if ($options->use_dvipng)
{
    $interm = dvipng $dvi, $options->output;
}
else
{
    $interm = dvips $dvi;	# generates an eps
}

my $final = convert $interm, $options->output, $options->width, $options->height;

if ($options->lumitransparence)
{
    $final = lumitrans($final);
}
$options->verbose and mywarn "created $final\n";

exit 0;

sub command
{
    my @com = (@_, " >> $options->{log} 2>\&1");
    my $com = join ' ', @com;
    $options->verbose and print "executing $com\n";
    system $com and mydie "command '$com' failed with error code $?";
}

sub find_template
{
    my ($options) = @_;
    for (@$template_search_path)
    {
	if (-r "$_/$options->{latex_template}")
	{
	    $options->verbose and
		mywarn "Found a template in the search path: '$_/$options->{latex_template}'\n";
	    return $options->latex_template;
	}
    }
    return undef;
}

sub find_editable_template
{
    my ($options) = @_;
    my $temp = find_template($options);
    if (defined $temp and -w $temp)
    {
	return $temp;
    }

    return "$ENV{HOME}/.eqe/$options->{latex_template}";
}

sub make_latex_wrapper
{
    my ($fh, $filename) = tempfile('temp_eqedit_XXXXX',
				   SUFFIX => '.tex',
				   DIR => File::Spec->tmpdir());

    my $tt = Template->new({INCLUDE_PATH => $template_search_path,
			    RELATIVE => 1,
			    ABSOLUTE => 1,
			   });

    $options->{latex_input} = read_file \*STDIN;

    my $template = find_template($options);
	
    if (not defined $template)
    {
	mywarn "Did not find template '$options->{latex_template}' in the search path, using builtin default template.";
	$template = \$default_tt_latex;
    }

    $tt->process($template,
		 $options,
		 $fh)
 	or mydie "template error: ", $tt->error;

    close $fh;

    push @temporary_files, $filename;

    my ($aux, $log);
    $aux = $log = $filename;
    $aux =~ s/\.\w+$/.aux/;
    $log =~ s/\.\w+$/.log/;
    push @temporary_files, $aux, $log;

    $options->verbose and print "Made LaTeX wrapper: $filename\n";
    return $filename;
}

sub latex
{
    my $tex = shift;

    # latex is stupid, it can only create files in the current working dir
    my $cwd = getcwd;
    my $td = File::Spec->tmpdir();
    chdir $td or mydie "Cannot chdir to $td\n";
    command 'latex', $tex;
    chdir $cwd or mydie "Cannot chdir back to $td\n";

    $tex =~ s/\.tex$/.dvi/;
    push @temporary_files, $tex;
    return $tex;
}

sub dvips
{
    my $dvi = shift;
    my $eps = $dvi; $eps =~ s/\.dvi$/.eps/;

    my $mag = $options->magnification * 1000;

    command 'dvips', '-E', '-Ta3', '-Ppdf', '-x', $mag, $dvi, '-o', $eps;
    push @temporary_files, $eps;
    return $eps;
}

sub dvipng
{
    my ($dvi, $png) = @_;
    unless (defined $png)
    {
	$png = $dvi; $png =~ s/\.dvi$/.png/;
    }

    my $mag = $options->magnification * 1000;

    command 'dvipng', '-T', 'tight', '-x', $mag, $dvi, '-o', $png;
    return $png;
}

sub ext
{
    my $file = shift;
    my ($ext) = $file =~ /\.([^.]+)$/;
    return $ext;
}

sub same_ext
{
    my ($e1, $e2) = map {ext $_} @_;

    return 
	defined($e1) &&
	    defined($e2) &&
		($e1 eq $e2);
}

sub convert
{
    my ($in, $out, $width, $height) = @_;
    my @res = $width || $height ? ('-resize', "${width}x$height") : ();
    warn Dumper $options->latex_input;
    # XXX TODO: use a proper PNG custom keyword instead of the Comment field
    my $comment = MIME::Base64::encode("eqedit:" . $options->latex_input);

    if (@transparent or @res or not same_ext($in, $out))
    {
	command 'convert', @res, @transparent, $in, $out;
    }
    elsif ($in ne $out)
    {
	copy($in, $out) or mydie "Cannot copy '$in' to '$out': $!\n";
    }
    else
    {
	# nothing to do!
    }

    # push @temporary_files, $out;
    return $out;
}

sub lumitrans
{
    my $file = shift;

    eval { require GD; };
    if ($@)
    {
	mywarn "Cannot require GD, lumitransparency cannot be used: $@\n";
	return $file;
    }

    my $image = GD::Image->newFromPng($file);

    $image->saveAlpha(1);
    $image->alphaBlending(0);

    my %colors;

    my ($w, $h) = $image->getBounds();
    for my $x (0..$w-1)
    {
	for my $y (0..$h-1)
	{
	    my ($r, $g, $b) = $image->rgb($image->getPixel($x, $y));
	
	    my $lumi = int(($r * 0.30 + $g * 0.59 + $b * 0.11) * 127. / 255.);

	    my $c =
		$colors{"$r,$g,$b,$lumi"} ||
		    ($colors{"$r,$g,$b,$lumi"} = $image->colorAllocateAlpha($r, $g, $b, $lumi));
	
	    $image->setPixel($x, $y, $c);
	}
    }

    open FIN, '>', $final or mydie "Cannot open '$final' for writing.\n";
    print FIN $image->png;
    close FIN;
    return $final;
}

# runs an external editor on the LaTeX template
sub edit_template
{
    my ($options) = @_;
    my $template = find_editable_template($options);
    unless (-e $template)
    {
	write_file($template, {no_clobber => 1}, $default_tt_latex) or
	    die "Cannot write default template to file '$template'.\n";
    }

    my @candidates =
	($ENV{VISUAL}, '/usr/bin/kate', '/usr/bin/gedit', '/usr/bin/xemacs', '/usr/bin/emacs', '/usr/bin/gvim');
    for my $c (@candidates)
    {
	next if $c =~ /^\s*$/;
	my $ret;
	if ($options->verbose)
	{
	    warn "Trying editor: $c $template\n";
	    $ret = system($c, $template);
	    warn "Editor returns code: $ret.\n";
	}
	else
	{
	    # no_output seems to be a problem for some editors
	    #no_output(sub { $ret = system($c, $template) });
	    $ret = system($c, $template);
	}

	# -1 is 'failed to execute'
	return if $ret != -1;
    }
    warn "Did not find an editor to edit '$template'. Specify one using the \$VISUAL environment variable.\n";
}
