1#!/usr/local/bin/perl -w
2# L2P - Convert LaTeX expressions to PNG
3
4$version = '1.1.1';
5
6=head1 NAME
7
8l2p - create PNG images from LaTeX expressions
9
10=head1 SYNOPSIS
11
12B<l2p> [options...] -i '$I<latex_expression>$'
13
14or
15
16B<l2p> [options...] [I<expression_file>]
17
18I<expression_file> contains an expression or expressions in (La)TeX
19format - one per line.  If neither I<expression_file> nor an
20B<-i> option is given, the expression is read from standard input.
21
22=head1 DESCRIPTION
23
24Convert expressions in LaTeX format into PNGs
25
26=head1 EXAMPLES
27
28=over
29
30=item
31
32l2p -i '$4x^2-7=\cos{2 \pi x}$' -o 'eqn4.png'
33
34Produce a PNG image, named 'eqn4.png', of the equation described by the
35LaTeX expression '$4x^2 - 7 = \cos{2 \pi x}$'.
36
37=item
38
39l2p -o big_equation.png big_hairy_equation
40
41Produce a PNG image, called big_equation.png, from the LaTeX expression
42contained in the file big_hairy_equation (specifically, it contains
43'$x=2$'.) Note that this file is NOT a full LaTeX document - use the
44B<-F> option for that.
45
46=item
47
48l2p -d 250 -i '$\nabla \cdot \mathbf{D} = \rho$'
49
50Produce a PNG image from the LaTeX code given with the B<-i> argument
51(which happens to be one of Maxwell's equations), at 250 dots per inch.
52Since we did not specify an output file name with the B<-o> option, the
53image will be 'eqn.png' (the default).
54
55=item
56
57l2p -p amssymb -i '$\mho$' -o mho.png
58
59Produce a PNG image of the Mho symbol (an upside-down capital omega),
60saving the image in the file 'mho.png'.  We include the amssymb package,
61which defines that symbol.
62
63=item
64
65l2p -B 20x30 -i '$\sum_{n=0}^{\infty}\frac{(-\phi^2)^n}{(2n)!}$' -o cosine.png
66
67Produce an image of the indicated infinite summation, padded with a
68border that is 20 pixels on each side horizontally, and 30 pixels each
69side vertically.  The color of this border region will be the same as
70the rest of the image background.
71
72=back
73
74=head1 OPTIONS
75
76Many options have arguments that may contain characters, like '#' or
77spaces, that the shell considers special.  Be sure to surround all
78such arguments with single or double quotes, so that the shell
79understands what is meant.  (If unsure, it's always safe to use the
80quotes.)
81
82=over
83
84=item
85B<-i "$latex$">
86
87Argument is an equation/expression in (La)TeX format.  In most cases,
88you will want to enclose the argument in quotes to protect it from shell
89expansion.
90
91=item
92B<-b "rrggbb">
93
94Background color.  There are several ways to specify the color.  See the
95section L</COLORS>, below, for details.
96
97=item
98B<-d dpi>
99
100Pixel density at which the equation is rendered, in dots per inch
101(default 300).
102
103An image with a DPI of 600 will have twice as many pixels in each of the
104x and y directions than an image with a DPI of 300.  The effect is
105different in the normal context of printing, where a higher DPI will
106leave the text with the same physical size, but with a finer resolution.
107This is because the physical size of a pixel is not really variable; so
108to have double the resolution, a symbol in an image must be double the
109size.
110
111=item
112B<-f "rrggbb">
113
114Foreground color.  There are several ways to specify the color.  See the
115section L</COLORS>, below, for details.
116
117=item
118B<-h>
119
120Show a help summary.
121
122=item
123B<-o output.png>
124
125Name of output file.  Default is 'eqn.png'.
126
127=item
128B<-p packagename[,packagename2[,...]]]>
129
130Use additional LaTeX/TeX packages.  You can specify several, separated
131by commas.
132
133=item
134B<-B "WIDTHxHEIGHT [color]">
135
136or: B<-B "SIZE [color]">
137
138Pad the resulting image with a border of the indicated size, in pixels.
139
140You can optionally specify a color for the border region.  By default,
141the border will be the same color as the rest of the background. (See
142L</COLORS> below for the format.)
143
144=item
145B<-C>
146
147Suppress automatic removal (cleanup) of temporary files.  This will be
148useful if something goes wrong, or if you want to use the intermediate
149DVI or Postscript renditions.  B<l2p> will tell you which directory
150contains these files.
151
152=item
153B<-F>
154
155Supplied expression is a full LaTeX document, rather than just an
156expression fragment. Negates the B<-f>, B<-b>, B<-p>, B<-B> and B<-T>
157options.
158
159B<Note>:  B<l2p> currently only converts full LaTeX documents
160that are relatively simple: only one page in length, and with no external
161dependencies (such as included graphics).  If you need to convert a more
162complex document, you can generate a DVI file with latex like normal,
163then convert the DVI into a series of PNG images using B<convert> from
164the ImageMagick distribution.  See L<convert(1)>, or
165L<http://imagemagick.org/script/convert.php> for more information.
166
167=item
168B<-T>
169
170Create an image with a transparent background.
171
172=item
173B<-V>
174
175Show version information.
176
177=back
178
179=head1 COLORS
180
181Some options, such as B<-b> and B<-f>, take an argument specifying a
182color in RGB format.  B<l2p> will decipher most representations, such
183as:
184
185=over
186
187=item
188
189A hexidecimal triplet.  For example, '-f "FF0000" -b "#ffffff"' gives a
190red foreground on a white background.  Case is not important, and the
191"#" is optional.
192
193=item
194
195Three decimal whole numbers, in the range of 0 to 255.  These must be
196separated by spaces or punctuation (comma, semicolon or colon).  For
197example, '-b "0 127 255" -f "0,0,0"' is black on a nice bluish
198background.
199
200=item
201
202Three fractions between 0 and 1, inclusive.  At least one of the three
203numbers must contain a decimal point (to distinguish this format from
204the others), and they are separated by space or punctuation.  For
205example, "0.87 .78 .41" is the same as the hex triplet "DEC769", and "0,
2061.0, 0" is the color green.  (Remember that decimal point.  "0, 1, 0"
207will give you a nearly black color.)
208
209=back
210
211Note that you may need to put single or double quotes around the color
212string, to ensure the shell interprets it correctly.
213
214=head1 BUGS
215
216Error handling is imperfect.  Among other things, If a needed
217LaTeX package is not included, B<l2p> will silently produce a
218broken image.
219
220On certain platforms, images produced with the B<-T> option (transparent
221background) may leave pixels at the edges of symbols a mixture of the
222text color and some background color.  This may not look good if the
223resulting image is put on a differently colored background.  A
224workaround is to give a background color hint with the B<-b> option;
225the edge pixels will then be a mixture of specified foreground and
226background colors.
227
228=head1 ACKS
229
230Thanks to Jesse Merriman (L<http://www.jessemerriman.com/>) for
231providing a patch that improved transparent background support.
232Integrated in version 1.1.
233
234=head1 COPYRIGHT
235
236This software is in the public domain.
237
238=head1 AUTHOR
239
240Aaron Maxwell (amax@redsymbol.net).  Comments, feature requests, and
241patches are welcome.
242
243=cut
244
245use File::Temp qw/tempfile tempdir/;
246use Getopt::Std;
247
248# Takes a string and extracts an RGB value from it.
249# Returns ($r,$g,$b), all values between 0 and 1 if parsing is successful,
250# otherwise returns undef.
251# Usage: ($r,$g,$b) = parsergb($string);
252sub parsergb {
253    my $string=shift;
254    $string =~s/^\s+//;
255    $string =~s/\s+$//;
256    $string =~s/^#//;
257    my(@args,@vals,$arg,$val);
258
259    # hex triplet format?
260    if($string=~/^[0-9a-fA-F]{6}$/) {
261	@args=(substr($string,0,2), substr($string,2,2), substr($string,4,2));
262	foreach $arg (@args) {
263	    $val=hex($arg)/255;
264	    push @vals, $val;
265	}
266	return @vals;
267    }
268    $string =~s/[,:;]/ /g;
269    @args = split /\s+/, $string;
270    if (@args != 3) {
271	return undef;
272    }
273
274    # 0-255 decimal format?
275    if ($string =~ /^[\d\s]+$/) {
276	foreach $arg (@args) {
277	    if ($arg>=0 && $arg<=255) {
278		$val=$arg/255;
279	    } else { return undef; }
280	    push @vals, $val;
281	}
282
283	# 0.0-1.0 range format?
284    } elsif ($string =~ /^[\d\.\s]+$/) {
285	foreach $arg (@args) {
286	    if($arg>=0 && $arg<=1) {
287		$val = 0+$arg;
288	    } else { return undef; }
289	    push @vals, $val;
290	}
291
292    } else {
293	return undef;  # unrecognized format!
294    }
295
296    return @vals;
297}
298
299# norm2hex - convert an RGB color in the form 'r,g,b', 0<=[rgb]<=1,
300# to a hex triplet. Returns undef if invoked incorrectly.
301# usage: $hexrgb = norm2hex($normrgb);
302sub norm2hex {
303    $_=shift;
304    my @vals=split(/,/,$_);
305    scalar(@vals)==3 or return undef;
306    my($val,$hex);
307    foreach $val (@vals) {
308	unless($val>=0 and $val<=1) { return undef; }
309	$hex .= sprintf('%02x',$val*255);
310    }
311    return $hex;
312}
313
314my($pre,$post,$dpi,$eqn,$outfile,$fg,$bg);
315our($opt_o, # output file name
316    $opt_d, # dpi
317    $opt_i, # in-command-line latex expression
318    $opt_f, # foreground RGB triplet
319    $opt_b, # background RGB triplet
320    $opt_F, # set if input is a full LaTeX document
321    $opt_T, # transparent background
322    $opt_C, # suppress autocleaning of temp files
323    $opt_h, # display help message
324    $opt_p, # additional package(s)
325    $opt_V, # print version info
326    $opt_B, # border
327    $opt_Z, # reserved for hacks
328    );
329
330# check to see if needed software is available
331my($latex,$dvips,$convert);
332$latex = `which latex`; chomp $latex;
333if($latex eq '' or not -X $latex) {
334	print STDERR "Cannot find latex executable.  Aborting.\n";
335	exit(2);
336}
337$dvips = `which dvips`; chomp $dvips;
338if($dvips eq '' or not -X $dvips) {
339	print STDERR "Cannot find dvips executable.  Aborting.\n";
340	exit(2);
341}
342$convert = `which convert`; chomp $convert;
343if($convert eq '' or not -X $convert) {
344	print STDERR "Cannot find convert executable.  Aborting.\n";
345	exit(2);
346}
347
348# process command line opts
349getopt('odifbpB');
350
351if ($opt_V) {
352	print $version, "\n";
353	exit(0);
354}
355
356if ($opt_h) {
357print <<'EOT';
358Generate PNG images from LaTeX expressions
359usage:
360    l2p [options] [file_containing_latex_expressions]
361or
362    l2p [options] -i '$LaTeX-expression$'
363
364Note: Many options will require quotes around their arguments to
365ensure correct interpretation by the shell.
366
367Options:
368    -o output.png     Name of output file.  Default is 'eqn.png'.
369    -i '$latex$'      equation/expression in (La)TeX format
370    -f 'rrggbb'       foreground color
371    -b 'rrggbb'       background color
372    -d dpi            Conversion resolution (default 300)
373    -T                Transparent background
374    -p pkg[,pkg2...]  use TeX/LaTeX package(s)
375    -C                Suppress removal (cleanup) of temporary files
376    -F                Input is full LaTeX document, not just fragment
377    -V                Show version
378    -B 'geom [color]' Pad image with a border
379    -h                Show this help and exit
380Also see the full documentation (try typing 'perldoc l2p').
381EOT
382	exit(0);
383}
384
385$outfile = $opt_o || 'eqn.png';
386$dpi =  $opt_d || 300;
387
388# determine foreground color
389$fg='0,0,0';
390if($opt_f) {
391	my($r,$g,$b) = parsergb($opt_f);
392	if (not defined $r) {
393		print STDERR "Foreground color not in recognizeable format.  Reverting to default.\n";
394		($r,$g,$b) = (0,0,0);
395	}
396	$fg=join(',',$r,$g,$b);
397}
398
399$bg='1,1,1';
400# determine background color
401if($opt_b) {
402    my($r,$g,$b) = parsergb($opt_b);
403    if (not defined $r) {
404	print STDERR "Background color not in recognizeable format.  Reverting to default.\n";
405	($r,$g,$b) = (1,1,1);
406    }
407    $bg=join(',',$r,$g,$b);
408}
409# deal with transparent background
410$fuzz = 20;
411if ($opt_T) {
412    if($opt_b) {
413	# Workaround: with a BG hint, a nonzero fuzz can result in erased symbols
414	$fuzz = 0;
415    }
416    # $bg and $fg must be different for transparency to work
417    my($bR,$bG,$bB) = split(/,/, $bg);
418    my($fR,$fG,$fB) = split(/,/, $fg);
419    my($dR, $dG, $dB) = map { abs($_) } ($fR-$bR, $fG-$bG, $fB-$bB);
420    if($dR<0.1 && $dG<0.1 && $dB<0.1) {
421	$bg = (sqrt($fR**2+$fG**2+$fB**2)>0.5) ? '0,0,0' : '1,1,1';
422    }
423}
424
425my @packages = ('color');
426if ($opt_p) {
427	@packages = (@packages, split(/,/, $opt_p));
428}
429
430# get expression to render
431$pre = join "\n", (
432'\documentclass{article}',
433(map { '\usepackage{' . $_ . '}' } @packages),
434'\definecolor{bg}{rgb}{', $bg, '}',
435'\definecolor{fg}{rgb}{', $fg, '}',
436'\pagestyle{empty}',
437'\pagecolor{bg}',
438'\begin{document}',
439'\color{fg}',
440'\begin{center}',
441"");
442
443$post = <<'EOT';
444\end{center}
445\end{document}
446EOT
447
448# discover the LaTeX expression to render
449$eqn='';
450if (defined $opt_i) {
451    # expression from command line
452    $eqn =  $opt_i . "\n";
453} elsif (not $opt_F) {
454    # file/stdin contains LaTeX expression(s)
455    # TODO: rewrite using an expression iterator subroutine
456    while(<>) {
457        next if /^\s*#/ or /^\s*$/;
458            chomp;
459        $eqn .= $_ . "\n";
460        # If this is line contains a single inline expression, add
461        # an extra newline, so that it renders correctly
462        if (/^\s*\$.*\$\s*$/) {
463            $eqn .= "\n";
464        }
465}
466}
467if (not $opt_F and $eqn =~ /^\s*$/) {
468    print STDERR <<'EOT';
469Did not find a LaTeX expression to render.  Perhaps the supplied
470expression or file is empty, or does not exist.
471EOT
472    exit(3);
473}
474
475# create a temporary latex file to use
476my $tempdir = tempdir(CLEANUP=> $opt_C ? 0 : 1)
477    or die "Cannot not make temp dir.  Unable to proceed - aborting.";
478print "Temporary files stored in $tempdir\n" if $opt_C;
479my $latexfn = $tempdir . "/foo.latex";
480if($opt_F) {
481    $source = shift @ARGV;
482    if(not -e $source) {
483        die "LaTeX source ($source) does not exist!";
484    } elsif (not -r $source) {
485        die "Cannot read file $source.";
486    }
487    if(substr($source,0,1) ne '/') {
488        my $cwd = `pwd`; chomp $cwd;
489        $source = $cwd . '/' . $source;
490    }
491    system("ln -s $source $latexfn");
492    -e "$latexfn" or die "Unable to create link to source file ($source)";
493} else {
494    my $latexfh;
495    open($latexfh,">",$latexfn) or die "could not write to latex temp file";
496    print $latexfh $pre, $eqn, $post;
497    close($latexfh);
498}
499
500# produce dvi output
501system("cd $tempdir; $latex -interaction=batchmode $latexfn >/dev/null");
502unless (-e "${tempdir}/foo.dvi") {
503	print STDERR <<'EOT';
504latex run failed.  Perhaps the input is invalid, or a specified
505package was not found.
506EOT
507	exit(1);
508}
509
510# convert dvi to ps
511# the -E option prevents convert from freaking out later
512my $dvipscmd = "$dvips " . ($opt_F ? "": "-E") . " foo -o 2>/dev/null";
513system("cd $tempdir; $dvipscmd");
514unless (-e "${tempdir}/foo.ps") {
515	print STDERR <<'EOT';
516Conversion of DVI to PS, a needed intermediate step, has failed.
517This probably should not happen.  Please send a bug report to
518amax@redsymbol.net.
519EOT
520	exit(2);
521}
522
523# convert ps to png
524my @cargs;
525if($opt_F) { # make image of full latex document
526    @cargs = ();
527} else {
528    @cargs = (
529	'-units',
530	'PixelsPerInch',
531	'-density',
532	"$dpi",
533        );
534}
535if ($opt_T) { # transparent background
536    @cargs = (
537	'-matte',
538	'-fuzz',
539	$fuzz . '%',
540	'-transparent',
541	'#' . norm2hex($bg),
542	'-units',
543	'PixelsPerInch',
544	'-density',
545	"$dpi",
546	);
547}
548# Border
549if($opt_B) {
550    my($geom, $color);
551    if($opt_B =~ /\s/) {
552	# user has defined a border color
553	$opt_B =~ m|^(\S+)\s+(.+)$|;
554	($geom, $color) = ($1, $2);
555	$color = join(',', parsergb($color));
556    } else {
557	# no border color defined, so use regular background color
558	($geom, $color) = ($opt_B, $bg);
559    }
560    $color = '#' . norm2hex($color);
561    unshift @cargs, ('-border', $geom, '-bordercolor', $color);
562}
563unshift @cargs, ("$convert");
564push @cargs, (
565    "${tempdir}/foo.ps",
566    "${tempdir}/foo.png",
567    );
568# The following system() call seems to be completely successful.
569# However, using the ``system(...) or die "died: $!"'' idiom results
570# in death with the error message 'Inappropriate ioctl for device'.  I
571# never could discern why, so I've left it as is.  If you know why,
572# please let me know (amax@redsymbol.net)
573system(@cargs);
574unless (-e "$tempdir/foo.png") {
575    print STDERR <<'EOT';
576Sorry, something went wrong.  Final conversion to PNG format has failed.
577EOT
578    exit(2);
579}
580
581# rename final png
582system("cp $tempdir/foo.png $outfile");
583