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