#!/usr/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] where options are: help: --help --version essential: --output= [eq.png] formatting: --magnification= [3.0] --text-color= [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= [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= [as defined after magnification] --height= [as defined after magnification] --use-dvipng [off, ie use dvips] --lumitransparency (experimental) logging and debugging: --verbose [no] --log= [/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"; }