[system] / trunk / pg / lib / WeBWorK / PG / ImageGenerator.pm Repository:
ViewVC logotype

View of /trunk/pg/lib/WeBWorK/PG/ImageGenerator.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2461 - (download) (as text) (annotate)
Wed Jul 7 21:26:52 2004 UTC (15 years, 5 months ago) by apizer
File size: 14080 byte(s)
Change permission of tmp files so that they can be removed.

Arnie

    1 ################################################################################
    2 # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project
    3 # $Id$
    4 ################################################################################
    5 
    6 package WeBWorK::PG::ImageGenerator;
    7 
    8 =head1 NAME
    9 
   10 WeBWorK::PG::ImageGenerator - create an object for holding bits of math for
   11 LaTeX, and then to process them all at once.
   12 
   13 =head1 SYNPOSIS
   14 
   15 FIXME: add this
   16 
   17 =cut
   18 
   19 # Note, this now has the ability to communicate with mysql for storing depths of
   20 # images for alignments.  If you want to provide another way of storing the depths,
   21 # make up another "magic" alignment name, look for explicit mentions of mysql here
   22 # and add statements for the new special alignment name.  Most of the action is
   23 # in the function update_depth_cache near the end of this file.  Also look for the
   24 # place where PG creates a new ImageGenerator object, and possibly adjust there as
   25 # well.
   26 
   27 use strict;
   28 use warnings;
   29 use WeBWorK::EquationCache;
   30 use DBI;
   31 
   32 #use WeBWorK::Utils qw(readDirectory makeTempDirectory removeTempDirectory);
   33 # can't use WeBWorK::Utils from here :(
   34 # so we define the needed functions here instead
   35 sub readDirectory($) {
   36   my $dirName = shift;
   37   opendir my $dh, $dirName
   38     or die "Failed to read directory $dirName: $!";
   39   my @result = readdir $dh;
   40   close $dh;
   41   return @result;
   42 }
   43 use constant MKDIR_ATTEMPTS => 10;
   44 sub makeTempDirectory($$) {
   45   my ($parent, $basename) = @_;
   46   # Loop until we're able to create a directory, or it fails for some
   47   # reason other than there already being something there.
   48   my $triesRemaining = MKDIR_ATTEMPTS;
   49   my ($fullPath, $success);
   50   do {
   51     my $suffix = join "", map { ('A'..'Z','a'..'z','0'..'9')[int rand 62] } 1 .. 8;
   52     $fullPath = "$parent/$basename.$suffix";
   53     $success = mkdir $fullPath;
   54   } until ($success or not $!{EEXIST});
   55   unless ($success) {
   56     my $msg = '';
   57     $msg    .=  "Server does not have write access to the directory $parent" unless -w $parent;
   58     die "$msg\r\nFailed to create directory $fullPath:\r\n $!"
   59   }
   60 
   61   return $fullPath;
   62 }
   63 
   64 use File::Path qw(rmtree);
   65 # for deleting the temp directory
   66 sub removeTempDirectory($) {
   67   my ($dir) = @_;
   68   rmtree($dir, 0, 0);
   69 }
   70 
   71 sub readFile($) {
   72   my $filename = shift;
   73   my $contents = '';
   74   local(*FILEH);
   75   open FILEH,  "<$filename" or die "Unable to read $filename";
   76   local($/) = undef;
   77   $contents = <FILEH>;
   78   close(FILEH);
   79   return($contents);
   80 }
   81 ################################################################################
   82 
   83 =head1 CONFIGURATION VARIABLES
   84 
   85 =over
   86 
   87 =item $DvipngArgs
   88 
   89 Arguments to pass to dvipng.
   90 
   91 =cut
   92 
   93 our $DvipngArgs = "" unless defined $DvipngArgs;
   94 
   95 =item $PreserveTempFiles
   96 
   97 If true, don't delete temporary files.
   98 
   99 =cut
  100 
  101 our $PreserveTempFiles = 0 unless defined $PreserveTempFiles;
  102 
  103 =item $TexPreamble
  104 
  105 TeX to prepend to equations to be processed.
  106 
  107 =cut
  108 
  109 our $TexPreamble = "" unless defined $TexPreamble;
  110 
  111 =item $TexPostamble
  112 
  113 TeX to append to equations to be processed.
  114 
  115 =cut
  116 
  117 our $TexPostamble = "" unless defined $TexPostamble;
  118 
  119 =back
  120 
  121 =cut
  122 
  123 ################################################################################
  124 
  125 =head1 METHODS
  126 
  127 =over
  128 
  129 =item new
  130 
  131 Returns a new ImageGenerator object. C<%options> must contain the following
  132 entries:
  133 
  134  tempDir  => directory in which to create temporary processing directory
  135  latex    => path to latex binary
  136  dvipng   => path to dvipng binary
  137  useCache => boolean, whether to use global image cache
  138 
  139 If C<useCache> is false, C<%options> must also contain the following entries:
  140 
  141  dir    => directory for resulting files
  142  url    => url to directory for resulting files
  143  basename => base name for image files (i.e. "eqn-$psvn-$probNum")
  144 
  145 If C<useCache> is true, C<%options> must also contain the following entries:
  146 
  147  cacheDir => directory for resulting files
  148  cacheURL => url to cacheDir
  149  cacheDB  => path to cache database file
  150 
  151 Options may also contain:
  152 
  153  dvipng_align    => vertical alignment option (a string to use like baseline, or 'mysql')
  154  dvipng_depth_db => database connection information for a "depths database"
  155  useMarkers      => if you want to have the dvipng images vertically aligned, this involves adding markers
  156 
  157 =cut
  158 
  159 sub new {
  160   my ($invocant, %options) = @_;
  161   my $class = ref $invocant || $invocant;
  162   my $self = {
  163     names   => [],
  164     strings => [],
  165     depths => {},
  166     %options,
  167   };
  168 
  169   # set some values
  170   $self->{dvipng_align} = 'absmiddle' unless defined($self->{dvipng_align});
  171   $self->{store_depths} = 1 if ($self->{dvipng_align} eq 'mysql');
  172   $self->{useMarkers} = $self->{useMarkers} || 0;
  173 
  174   if ($self->{useCache}) {
  175     $self->{dir} = $self->{cacheDir};
  176     $self->{url} = $self->{cacheURL};
  177     $self->{basename} = "";
  178     $self->{equationCache} = WeBWorK::EquationCache->new(cacheDB => $self->{cacheDB});
  179   }
  180 
  181   bless $self, $class;
  182 }
  183 
  184 =item add($string, $mode)
  185 
  186 Adds the equation in C<$string> to the object. C<$mode> can be "display" or
  187 "inline". If not specified, "inline" is assumed. Returns the proper HTML tag
  188 for displaying the image.
  189 
  190 =cut
  191 
  192 sub add {
  193   my ($self, $string, $mode) = @_;
  194 
  195   my $names    = $self->{names};
  196   my $strings  = $self->{strings};
  197   my $dir      = $self->{dir};
  198   my $url      = $self->{url};
  199   my $basename = $self->{basename};
  200   my $useCache = $self->{useCache};
  201   my $depths  = $self->{depths};
  202 
  203   # if the string came in with delimiters, chop them off and set the mode
  204   # based on whether they were \[ .. \] or \( ... \). this means that if
  205   # the string has delimiters, the mode *argument* is ignored.
  206   if ($string =~ s/^\\\[(.*)\\\]$/$1/s) {
  207     $mode = "display";
  208   } elsif ($string =~ s/^\\\((.*)\\\)$/$1/s) {
  209     $mode = "inline";
  210   }
  211   # otherwise, leave the string and the mode alone.
  212 
  213   # assume that a bare string with no mode specified is inline
  214   $mode ||= "inline";
  215 
  216   # now that we know what mode we're dealing with, we can generate a "real"
  217   # string to pass to latex
  218   my $realString = ($mode eq "display")
  219     ? '\(\displaystyle{' . $string . '}\)'
  220     : '\(' . $string . '\)';
  221 
  222   # alignment tag could be a fixed default
  223   my ($imageNum, $aligntag) = (0, qq|align="$self->{dvipng_align}"|);
  224   # if the default is for variable heights, the default should be meaningful
  225   # in an answer preview, $self->{dvipng_align} might be 'mysql', but we still
  226         # use a static alignment
  227   $aligntag = 'align="baseline"' if ($self->{dvipng_align} eq 'mysql');
  228 
  229   # determine what the image's "number" is
  230   if($useCache) {
  231     $imageNum = $self->{equationCache}->lookup($realString);
  232     $aligntag = 'MaRkEr'.$imageNum if $self->{useMarkers};
  233     $depths->{"$imageNum"} = 'none' if ($self->{dvipng_align} eq 'mysql');
  234     # insert a slash after 2 characters
  235     # this effectively divides the images into 16^2 = 256 subdirectories
  236     substr($imageNum,2,0) = '/';
  237   } else {
  238     $imageNum = @$strings + 1;
  239   }
  240 
  241   # We are banking on the fact that if useCache is true, then basename is empty.
  242   # Maybe we should simplify and drop support for useCache =0 and having a basename.
  243 
  244   # get the full file name of the image
  245   my $imageName = ($basename)
  246     ? "$basename.$imageNum.png"
  247     : "$imageNum.png";
  248 
  249   # store the full file name of the image, and the "real" tex string to the object
  250   push @$names, $imageName;
  251   push @$strings, $realString;
  252   #warn "ImageGenerator: added string $realString with name $imageName\n";
  253 
  254   # ... and the full URL.
  255   my $imageURL = "$url/$imageName";
  256 
  257   my $imageTag  = ($mode eq "display")
  258     ? "<div align=\"center\"><img src=\"$imageURL\" $aligntag alt=\"$string\"></div>"
  259     : "<img src=\"$imageURL\" $aligntag alt=\"$string\">";
  260 
  261   return $imageTag;
  262 }
  263 
  264 =item render(%options)
  265 
  266 Uses LaTeX and dvipng to render the equations stored in the object.
  267 
  268 The option C<body_text> is a reference to the text of the problem's text.  After
  269 rendering the images and figuring out their depths, we go through and fix the tags
  270 of the images to get the vertical alignment right.  If it is left out, then we skip
  271 that step.
  272 
  273 =for comment
  274 
  275 If the key "mtime" in C<%options> is given, its value will be interpreted as a
  276 unix date and compared with the modification date on any existing copy of the
  277 first image to be generated. It is recommended that the modification time of the
  278 source file from which the equations originate be used for this value. If the
  279 key "refresh" in C<%options> is true, images will be regenerated regardless of
  280 when they were last modified. If neither option is supplied, "refresh" is
  281 assumed.
  282 
  283 =cut
  284 
  285 sub render {
  286   my ($self, %options) = @_;
  287 
  288   my $tempDir  = $self->{tempDir};
  289   my $dir      = $self->{dir};
  290   my $basename = $self->{basename};
  291   my $latex    = $self->{latex};
  292   my $dvipng   = $self->{dvipng};
  293   my $names    = $self->{names};
  294   my $strings  = $self->{strings};
  295   my $depths   = $self->{depths};
  296   $self->{body_text} = $options{body_text};
  297 
  298   # determine which images need to be generated
  299   my (@newStrings, @newNames);
  300   for (my $i = 0; $i < @$strings; $i++) {
  301     my $string = $strings->[$i];
  302     my $name = $names->[$i];
  303     if (-e "$dir/$name") {
  304       #warn "ImageGenerator: found a file named $name, skipping string $string\n";
  305     } else {
  306       #warn "ImageGenerator: didn't find a file named $name, including string $string\n";
  307       push @newStrings, $string;
  308       push @newNames, $name;
  309     }
  310   }
  311 
  312     # Outdented so that we don't change every line of this section
  313     # while making lots of changes along the way
  314     if(@newStrings) { # Don't run latex if there are no images to generate
  315 
  316   # create temporary directory in which to do TeX processing
  317   my $wd = makeTempDirectory($tempDir, "ImageGenerator");
  318 
  319   # store equations in a tex file
  320   my $texFile = "$wd/equation.tex";
  321   open my $tex, ">", $texFile
  322     or die "failed to open file $texFile for writing: $!";
  323   print $tex $TexPreamble;
  324   print $tex "$_\n" foreach @newStrings;
  325   print $tex $TexPostamble;
  326   close $tex;
  327   warn "tex file $texFile was not written" unless -e $texFile;
  328 
  329   # call LaTeX
  330   my $latexCommand  = "cd $wd && $latex equation > latex.out 2> latex.err";
  331   my $latexStatus = system $latexCommand;
  332 
  333   if ($latexStatus and $latexStatus !=256) {
  334     warn "$latexCommand returned non-zero status $latexStatus: $!";
  335     warn "cd $wd failed" if system "cd $wd";
  336     warn "Unable to write to directory $wd. " unless -w $wd;
  337     warn "Unable to execute $latex " unless -e $latex ;
  338 
  339     warn `ls -l $wd`;
  340     my $errorMessage = '';
  341     if (-r "$wd/equation.log") {
  342       $errorMessage = readFile("$wd/equation.log");
  343       warn "<pre> Logfile contents:\n$errorMessage\n</pre>";
  344     } else {
  345        warn "Unable to read logfile $wd/equation.log ";
  346     }
  347   }
  348 
  349   warn "$latexCommand failed to generate a DVI file"
  350     unless -e "$wd/equation.dvi";
  351 
  352   # call dvipng
  353   my $dvipngCommand = "cd $wd && $dvipng " . $DvipngArgs . " equation > dvipng.out 2> dvipng.err";
  354   my $dvipngStatus = system $dvipngCommand;
  355   warn "$dvipngCommand returned non-zero status $dvipngStatus: $!"
  356     if $dvipngStatus;
  357   # get depths
  358   my $dvipngout = '';
  359   $dvipngout = readFile("$wd/dvipng.out") if(-r "$wd/dvipng.out");
  360   my @dvipngdepths = ($dvipngout =~ /depth=(\d+)/g);
  361   # kill them all if something goes wrnog
  362   @dvipngdepths = () if(scalar(@dvipngdepths) != scalar(@newNames));
  363 
  364   # move/rename images
  365   foreach my $image (readDirectory($wd)) {
  366     # only work on equation#.png files
  367     next unless $image =~ m/^equation(\d+)\.png$/;
  368 
  369     # get image number from above match
  370     my $imageNum = $1;
  371     # record the dvipng offset
  372     my $hashkey = $newNames[$imageNum-1];
  373     $hashkey =~ s|/||;
  374     $hashkey =~ s|\.png$||;
  375     $depths->{"$hashkey"} = $dvipngdepths[$imageNum-1] if(defined($dvipngdepths[$imageNum-1]));
  376 
  377     #warn "ImageGenerator: found generated image $imageNum with name $newNames[$imageNum-1]\n";
  378 
  379     # move/rename image
  380     #my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/$basename.$imageNum.png";
  381     # check to see if this requires a directory we haven't made yet
  382     my $newdir = $newNames[$imageNum-1];
  383     $newdir =~ s|/.*$||;
  384     if($newdir and not -d "$dir/$newdir") {
  385       my $success = mkdir "$dir/$newdir";
  386       warn "Could not make directory $dir/$newdir" unless $success;
  387     }
  388     my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/" . $newNames[$imageNum-1];
  389     my $mvStatus = system $mvCommand;
  390     if ( $mvStatus) {
  391       warn "$mvCommand returned non-zero status $mvStatus: $!";
  392       warn "Can't write to tmp/equations directory $dir" unless -w $dir;
  393     }
  394 
  395   }
  396 
  397   # remove temporary directory (and its contents)
  398   if ($PreserveTempFiles) {
  399     warn "ImageGenerator: preserved temp files in working directory '$wd'.\n";
  400     chmod (0775,$wd);
  401     chmod (0664,<$wd/*>);
  402   } else {
  403     removeTempDirectory($wd);
  404   }
  405     }
  406     $self->update_depth_cache() if $self->{store_depths};
  407     $self->fix_markers() if ($self->{useMarkers} and defined $self->{body_text});
  408 }
  409 
  410 # internal utility function for updating both our internal record of dvipng depths,
  411 # but also the database.  This is the main function to change (provide an alternate
  412 # method for) if you want to use another method for storing dvipng depths
  413 
  414 sub update_depth_cache {
  415   my $self = shift;
  416   return() unless ($self->{dvipng_align} eq 'mysql');
  417   my $dbh = DBI->connect_cached($self->{dvipng_depth_db}->{dbsource},
  418      $self->{dvipng_depth_db}->{user}, $self->{dvipng_depth_db}->{passwd});
  419   my $sth = $dbh->prepare("INSERT IGNORE INTO depths(md5, depth) VALUES (?,?)");
  420   my $depthhash = $self->{depths};
  421   for my $md5 (keys %{$depthhash}) {
  422     if($depthhash->{$md5} eq 'none') {
  423       my $got_values = $dbh->selectall_arrayref('select depth from depths where md5 = ?', undef, "$md5");
  424       $depthhash->{"$md5"} = $got_values->[0]->[0] if(scalar(@{$got_values}));
  425       #warn "Get depth from mysql for $md5" . $depthhash->{"$md5"};
  426     } else {
  427       #warn "Put depth $depthhash->{$md5} for $md5 into mysql";
  428       $sth->execute($md5, $depthhash->{$md5});
  429     }
  430   }
  431   return();
  432 }
  433 
  434 sub fix_markers {
  435   my $self = shift;
  436   my %depths = %{$self->{depths}};
  437   for my $depthkey (keys %depths) {
  438     if($depths{$depthkey} eq 'none') { # we never found its depth :(
  439       ${ $self->{body_text} } =~ s/MaRkEr$depthkey/align="ABSMIDDLE"/g;
  440     } else {
  441       my $ndepth = 0 - $depths{$depthkey};
  442       ${ $self->{body_text} } =~ s/MaRkEr$depthkey/style="vertical-align:${ndepth}px"/g;
  443     }
  444   }
  445   return();
  446 }
  447 
  448 
  449 =back
  450 
  451 =cut
  452 
  453 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9