[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 6851 - (download) (as text) (annotate)
Sat Jun 11 17:17:39 2011 UTC (8 years, 6 months ago) by gage
File size: 16086 byte(s)
?	added some warning messages to Course Admin if permissions on DATA, log and html directories are not set correctly.
?	added refreshEquations(1) to PG to force all equation images to be recalculated.

?added AddToTexPreamble($str ) to PG  to allow short macro definitions such as    \newcommand{\myVec}[#1]{\vec{#1}}
	?	this works in images mode and in hardcopy mode.  It does not work in jsMath mode (but fails gracefully).  MathJax also fails, not quite so gracefully.


Added some additional POD documentation


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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9