| … | |
… | |
| 16 | |
16 | |
| 17 | =cut |
17 | =cut |
| 18 | |
18 | |
| 19 | use strict; |
19 | use strict; |
| 20 | use warnings; |
20 | use warnings; |
|
|
21 | use WeBWorK::PG::EquationCache; |
| 21 | use WeBWorK::Utils qw(readDirectory makeTempDirectory removeTempDirectory); |
22 | use WeBWorK::Utils qw(readDirectory makeTempDirectory removeTempDirectory); |
| 22 | |
23 | |
| 23 | use constant PREAMBLE => <<'EOF'; |
24 | use constant PREAMBLE => <<'EOF'; |
| 24 | \documentclass[12pt]{article} |
25 | \documentclass[12pt]{article} |
| 25 | \nonstopmode |
26 | \nonstopmode |
| … | |
… | |
| 44 | |
45 | |
| 45 | =over |
46 | =over |
| 46 | |
47 | |
| 47 | =item new |
48 | =item new |
| 48 | |
49 | |
| 49 | Returns a new ImageGenerator object. <C%options> must contain the following |
50 | Returns a new ImageGenerator object. C<%options> must contain the following |
| 50 | entries: |
51 | entries: |
| 51 | |
52 | |
| 52 | tempDir => directory in which to create temporary processing directory |
53 | tempDir => directory in which to create temporary processing directory |
| 53 | dir => directory for resulting files |
54 | dir => directory for resulting files |
| 54 | url => url to directory for resulting files |
55 | url => url to directory for resulting files |
| 55 | basename => base name for image files |
56 | basename => base name for image files |
| 56 | latex => path to latex binary |
57 | latex => path to latex binary |
| 57 | dvipng => path to dvipng binary |
58 | dvipng => path to dvipng binary |
| 58 | |
59 | |
|
|
60 | C<%options> may also contain the following entries: |
|
|
61 | |
|
|
62 | useCache => boolean, whether to use global image cache |
|
|
63 | cacheDir => directory for resulting files |
|
|
64 | cacheURL => url to imageCacheDir |
|
|
65 | cacheDB => path to cache database file |
|
|
66 | |
|
|
67 | If C<useCache> is true, then C<basename> is ignored, and C<cacheDir> |
|
|
68 | and C<cacheURL> override C<dir> and C<url>, respectively. |
|
|
69 | |
| 59 | =cut |
70 | =cut |
| 60 | |
71 | |
| 61 | sub new { |
72 | sub new { |
| 62 | my ($invocant, %options) = @_; |
73 | my ($invocant, %options) = @_; |
| 63 | my $class = ref $invocant || $invocant; |
74 | my $class = ref $invocant || $invocant; |
| 64 | my $self = { |
75 | my $self = { |
|
|
76 | names => [], |
| 65 | strings => [], |
77 | strings => [], |
| 66 | %options, |
78 | %options, |
| 67 | }; |
79 | }; |
| 68 | |
80 | |
|
|
81 | if ($self->{useCache}) { |
|
|
82 | $self->{dir} = $self->{cacheDir}; |
|
|
83 | $self->{url} = $self->{cacheURL}; |
|
|
84 | $self->{basename} = ""; |
|
|
85 | $self->{equationCache} = WeBWorK::PG::EquationCache->new(cacheDB => $self->{cacheDB}); |
|
|
86 | } |
|
|
87 | |
| 69 | bless $self, $class; |
88 | bless $self, $class; |
| 70 | } |
89 | } |
| 71 | |
90 | |
| 72 | =item add($string, $mode) |
91 | =item add($string, $mode) |
| 73 | |
92 | |
| … | |
… | |
| 78 | =cut |
97 | =cut |
| 79 | |
98 | |
| 80 | sub add { |
99 | sub add { |
| 81 | my ($self, $string, $mode) = @_; |
100 | my ($self, $string, $mode) = @_; |
| 82 | |
101 | |
|
|
102 | my $names = $self->{names}; |
| 83 | my $strings = $self->{strings}; |
103 | my $strings = $self->{strings}; |
| 84 | my $dir = $self->{dir}; |
104 | my $dir = $self->{dir}; |
| 85 | my $url = $self->{url}; |
105 | my $url = $self->{url}; |
| 86 | my $basename = $self->{basename}; |
106 | my $basename = $self->{basename}; |
|
|
107 | my $useCache = $self->{useCache}; |
| 87 | |
108 | |
| 88 | # if the string came in with delimiters, chop them off and set the mode |
109 | # if the string came in with delimiters, chop them off and set the mode |
| 89 | # based on whether they were \[ .. \] or \( ... \). this means that if |
110 | # based on whether they were \[ .. \] or \( ... \). this means that if |
| 90 | # the string has delimiters, the mode *argument* is ignored. |
111 | # the string has delimiters, the mode *argument* is ignored. |
| 91 | if ($string =~ s/^\\\[(.*)\\\]$/$1/s) { |
112 | if ($string =~ s/^\\\[(.*)\\\]$/$1/s) { |
| … | |
… | |
| 96 | # otherwise, leave the string and the mode alone. |
117 | # otherwise, leave the string and the mode alone. |
| 97 | |
118 | |
| 98 | # assume that a bare string with no mode specified is inline |
119 | # assume that a bare string with no mode specified is inline |
| 99 | $mode ||= "inline"; |
120 | $mode ||= "inline"; |
| 100 | |
121 | |
| 101 | my $imageNum = @$strings + 1; |
122 | # now that we know what mode we're dealing with, we can generate a "real" |
|
|
123 | # string to pass to latex |
|
|
124 | my $realString = ($mode eq "display") |
|
|
125 | ? '\(\displaystyle{' . $string . '}\)' |
|
|
126 | : '\(' . $string . '\)'; |
|
|
127 | |
|
|
128 | # determine what the image's "number" is |
|
|
129 | my $imageNum = ($useCache) |
|
|
130 | ? $self->{equationCache}->lookup($realString) |
|
|
131 | : @$strings + 1; |
|
|
132 | |
|
|
133 | # get the full file name of the image |
|
|
134 | my $imageName = ($basename) |
|
|
135 | ? "$basename.$imageNum.png" |
|
|
136 | : "$imageNum.png"; |
|
|
137 | |
|
|
138 | # store the full file name of the image, and the "real" tex string to the object |
|
|
139 | push @$names, $imageName; |
|
|
140 | push @$strings, $realString; |
|
|
141 | #warn "ImageGenerator: added string $realString with name $imageName\n"; |
|
|
142 | |
|
|
143 | # ... and the full URL. |
| 102 | my $imageURL = "$url/$basename.$imageNum.png"; |
144 | my $imageURL = "$url/$imageName"; |
|
|
145 | |
|
|
146 | my $imageTag = ($mode eq "display") |
|
|
147 | ? " <div align=\"center\"><img src=\"$imageURL\" align=\"middle\" alt=\"$string\"></div> " |
| 103 | my $imageTag = "<img src=\"$imageURL\" align=\"middle\" alt=\"$string\">"; |
148 | : " <img src=\"$imageURL\" align=\"middle\" alt=\"$string\"> "; |
| 104 | |
149 | |
| 105 | if ($mode eq "display") { |
|
|
| 106 | push @$strings, '\(\displaystyle{' . $string . '}\)'; |
|
|
| 107 | return " <div align=\"center\">$imageTag</div> "; |
|
|
| 108 | } else { |
|
|
| 109 | push @$strings, '\(' . $string . '\)'; |
|
|
| 110 | return " $imageTag "; |
150 | return $imageTag; |
| 111 | } |
|
|
| 112 | } |
151 | } |
| 113 | |
152 | |
| 114 | =item render(%options) |
153 | =item render(%options) |
| 115 | |
154 | |
| 116 | Uses LaTeX and dvipng to render the equations stored in the object. If the key |
155 | Uses LaTeX and dvipng to render the equations stored in the object. If the key |
| … | |
… | |
| 129 | my $tempDir = $self->{tempDir}; |
168 | my $tempDir = $self->{tempDir}; |
| 130 | my $dir = $self->{dir}; |
169 | my $dir = $self->{dir}; |
| 131 | my $basename = $self->{basename}; |
170 | my $basename = $self->{basename}; |
| 132 | my $latex = $self->{latex}; |
171 | my $latex = $self->{latex}; |
| 133 | my $dvipng = $self->{dvipng}; |
172 | my $dvipng = $self->{dvipng}; |
|
|
173 | my $names = $self->{names}; |
| 134 | my $strings = $self->{strings}; |
174 | my $strings = $self->{strings}; |
| 135 | |
175 | |
| 136 | my $mtime = $options{mtime}; |
176 | my $mtime = $options{mtime}; |
| 137 | my $refresh = $options{refresh} || ! defined $mtime; |
177 | my $refresh = $options{refresh} || ! defined $mtime; |
| 138 | # must refresh if no mtime is given |
178 | # must refresh if no mtime is given |
| 139 | |
179 | |
| 140 | return unless @$strings; # Don't run latex if there are no images to generate |
|
|
| 141 | |
|
|
| 142 | unless ($refresh) { |
180 | #unless ($refresh) { |
| 143 | my $firstImage = "$dir/$basename.1.png"; |
181 | # #my $firstImage = "$dir/$basename.1.png"; |
|
|
182 | # my $firstImage = "$dir/" . $names->[0]; |
| 144 | if (-e $firstImage) { |
183 | # if (-e $firstImage) { |
| 145 | # return if first image newer than $mtime |
184 | # # return if first image newer than $mtime |
| 146 | return if (stat $firstImage)[9] >= $mtime; |
185 | # return if (stat $firstImage)[9] >= $mtime; |
|
|
186 | # } |
|
|
187 | #} |
|
|
188 | |
|
|
189 | # determine which images need to be generated |
|
|
190 | my (@newStrings, @newNames); |
|
|
191 | for (my $i = 0; $i < @$strings; $i++) { |
|
|
192 | my $string = $strings->[$i]; |
|
|
193 | my $name = $names->[$i]; |
|
|
194 | if (-e "$dir/$name") { |
|
|
195 | #warn "ImageGenerator: found a file named $name, skipping string $string\n"; |
|
|
196 | } else { |
|
|
197 | #warn "ImageGenerator: didn't find a file named $name, including string $string\n"; |
|
|
198 | push @newStrings, $string; |
|
|
199 | push @newNames, $name; |
| 147 | } |
200 | } |
| 148 | } |
201 | } |
|
|
202 | |
|
|
203 | return unless @newStrings; # Don't run latex if there are no images to generate |
| 149 | |
204 | |
| 150 | # create temporary directory in which to do TeX processing |
205 | # create temporary directory in which to do TeX processing |
| 151 | my $wd = makeTempDirectory($tempDir, "ImageGenerator"); |
206 | my $wd = makeTempDirectory($tempDir, "ImageGenerator"); |
| 152 | |
207 | |
| 153 | # store equations in a tex file |
208 | # store equations in a tex file |
| 154 | my $texFile = "$wd/equation.tex"; |
209 | my $texFile = "$wd/equation.tex"; |
| 155 | open my $tex, ">", $texFile |
210 | open my $tex, ">", $texFile |
| 156 | or die "failed to open file $texFile for writing: $!"; |
211 | or die "failed to open file $texFile for writing: $!"; |
| 157 | print $tex PREAMBLE; |
212 | print $tex PREAMBLE; |
| 158 | print $tex "$_\n" foreach @$strings; |
213 | print $tex "$_\n" foreach @newStrings; |
| 159 | print $tex POSTAMBLE; |
214 | print $tex POSTAMBLE; |
| 160 | close $tex; |
215 | close $tex; |
| 161 | |
216 | |
| 162 | # call LaTeX |
217 | # call LaTeX |
| 163 | my $latexCommand = "cd $wd && $latex equation > latex.out 2> latex.err"; |
218 | my $latexCommand = "cd $wd && $latex equation > latex.out 2> latex.err"; |
| … | |
… | |
| 179 | next unless $image =~ m/^equation(\d+)\.png$/; |
234 | next unless $image =~ m/^equation(\d+)\.png$/; |
| 180 | |
235 | |
| 181 | # get image number from above match |
236 | # get image number from above match |
| 182 | my $imageNum = $1; |
237 | my $imageNum = $1; |
| 183 | |
238 | |
|
|
239 | #warn "ImageGenerator: found generated image $imageNum with name $newNames[$imageNum-1]\n"; |
|
|
240 | |
| 184 | # move/rename image |
241 | # move/rename image |
| 185 | my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/$basename.$imageNum.png"; |
242 | #my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/$basename.$imageNum.png"; |
|
|
243 | my $mvCommand = "cd $wd && /bin/mv $wd/$image $dir/" . $newNames[$imageNum-1]; |
| 186 | my $mvStatus = system $mvCommand; |
244 | my $mvStatus = system $mvCommand; |
| 187 | warn "$mvCommand returned non-zero status $mvStatus: $!" |
245 | warn "$mvCommand returned non-zero status $mvStatus: $!" |
| 188 | if $mvStatus; |
246 | if $mvStatus; |
| 189 | } |
247 | } |
| 190 | |
248 | |
| 191 | # remove temporary directory (and its contents) |
249 | # remove temporary directory (and its contents) |
| 192 | removeTempDirectory($wd); |
250 | removeTempDirectory($wd); |
| 193 | } |
251 | } |
| 194 | |
252 | |
| 195 | 1; |
253 | 1; |
| 196 | |
|
|
| 197 | __END__ |
|
|
| 198 | |
|
|
| 199 | ################################################################################ |
|
|
| 200 | # OLD VERSIONS OF SUBROUTINES |
|
|
| 201 | ################################################################################ |
|
|
| 202 | |
|
|
| 203 | |
|
|
| 204 | sub getCount { |
|
|
| 205 | my $self = shift; |
|
|
| 206 | return $self->{count}; |
|
|
| 207 | } |
|
|
| 208 | |
|
|
| 209 | sub tmpurl { |
|
|
| 210 | my $self = shift; |
|
|
| 211 | return "$self->{tmpURLstart}/$self->{filenamestart}"; |
|
|
| 212 | } |
|
|
| 213 | |
|
|
| 214 | sub initialize { |
|
|
| 215 | my ($self, $envir) = @_; |
|
|
| 216 | |
|
|
| 217 | my $problemnum = $envir->{'probNum'}; |
|
|
| 218 | my $studname = $envir->{'studentLogin'}; |
|
|
| 219 | my $psvn = $envir->{'psvn'}; |
|
|
| 220 | my $setname = $envir->{'setNumber'}; |
|
|
| 221 | my $tmpURLstart = $envir->{'tempURL'}; |
|
|
| 222 | |
|
|
| 223 | my $path=main::surePathToTmpFile(main::convertPath("png/$setname/$psvn/foo")); |
|
|
| 224 | $path =~ s/foo$//; # remove final foo |
|
|
| 225 | |
|
|
| 226 | $self->{sourceFile} = $envir->{templateDirectory} . "/" . $envir->{fileName}; |
|
|
| 227 | $self->{tmpURLstart} = $tmpURLstart."png/$setname/$psvn"; |
|
|
| 228 | $self->{tmppath}=$path; |
|
|
| 229 | $self->{filenamestart}="$studname-prob${problemnum}image"; |
|
|
| 230 | } |
|
|
| 231 | |
|
|
| 232 | # Add another string to list to be LaTeX'ed |
|
|
| 233 | # return the tag |
|
|
| 234 | sub add { |
|
|
| 235 | my $self = shift; |
|
|
| 236 | my $newstr = shift; |
|
|
| 237 | my $tag = $newstr; |
|
|
| 238 | $self->{count}++; |
|
|
| 239 | my $tempURL= $self->tmpurl()."$self->{count}.png"; |
|
|
| 240 | |
|
|
| 241 | if ($tag =~ /^\\\(/) { |
|
|
| 242 | $tag =~ s|^\\\( *||; |
|
|
| 243 | $tag =~ s|\\\)$||; |
|
|
| 244 | $tag = qq!<img src="$tempURL" align="middle" alt="$tag">!; |
|
|
| 245 | } else { |
|
|
| 246 | # Displayed math comes in with \[ stuff \]. To get a good |
|
|
| 247 | # bounding box through preview, we change that to \( \displaystyle{ |
|
|
| 248 | # stuff } \), and then center the resulting image |
|
|
| 249 | $tag =~ s|^\\\[ *||; |
|
|
| 250 | $tag =~ s|\\\]$||; |
|
|
| 251 | $newstr = '\(\displaystyle{'.$tag.'}\)'; |
|
|
| 252 | $tag = qq!<div align="center"><img src="$tempURL" align="middle" alt="$tag"></div>!; |
|
|
| 253 | } |
|
|
| 254 | |
|
|
| 255 | push @{$self->{latexlines}}, $newstr; |
|
|
| 256 | return $tag; |
|
|
| 257 | } |
|
|
| 258 | |
|
|
| 259 | sub render { |
|
|
| 260 | my $self = shift; |
|
|
| 261 | my %opts = @_; |
|
|
| 262 | |
|
|
| 263 | # Don't run latex if there are no images |
|
|
| 264 | if($self->{count}==0) { |
|
|
| 265 | return; |
|
|
| 266 | } |
|
|
| 267 | |
|
|
| 268 | my $refreshMe = 0; |
|
|
| 269 | if (defined($opts{refresh}) and (($opts{refresh} eq "yes") or ($opts{refresh} == 1))) { |
|
|
| 270 | $refreshMe = 1; |
|
|
| 271 | } |
|
|
| 272 | |
|
|
| 273 | #$refreshMe = 1; # Uncomment for testing |
|
|
| 274 | my $latexfilenamebase = $self->{tmppath} . $self->{filenamestart}; |
|
|
| 275 | |
|
|
| 276 | my $sourcePath = $self->{sourceFile}; |
|
|
| 277 | my $tempFile = "${latexfilenamebase}" . $self->{count} . ".png"; # last image |
|
|
| 278 | |
|
|
| 279 | if ($refreshMe or not -e $tempFile or (stat $sourcePath)[9] > (stat $tempFile)[9]) { |
|
|
| 280 | # image file doesn't exist, or source file is newer then image file |
|
|
| 281 | # or we just want new images produced |
|
|
| 282 | |
|
|
| 283 | #my $old_cdir = `pwd`; # cd for running latex |
|
|
| 284 | #chomp($old_cdir); |
|
|
| 285 | chdir($self->{tmppath}) |
|
|
| 286 | || warn "Could not move into temporary directory $self->{tmppath}"; |
|
|
| 287 | |
|
|
| 288 | if (-e "$latexfilenamebase.tex") { |
|
|
| 289 | unlink("$latexfilenamebase.tex") || |
|
|
| 290 | warn "Could not delete old LaTeX file"; |
|
|
| 291 | } |
|
|
| 292 | |
|
|
| 293 | local *LATEXME; |
|
|
| 294 | open(LATEXME,">$latexfilenamebase.tex") || warn "Cannot create temporary tex file"; |
|
|
| 295 | print LATEXME <<'EOT'; |
|
|
| 296 | \documentclass[12pt]{article} |
|
|
| 297 | \nonstopmode |
|
|
| 298 | \usepackage{amsmath,amsfonts,amssymb} |
|
|
| 299 | \def\gt{>} |
|
|
| 300 | \def\lt{<} |
|
|
| 301 | |
|
|
| 302 | \usepackage[active,textmath,displaymath]{preview} |
|
|
| 303 | \begin{document} |
|
|
| 304 | EOT |
|
|
| 305 | |
|
|
| 306 | my $j; |
|
|
| 307 | for $j (@{$self->{latexlines}}) { |
|
|
| 308 | print LATEXME "\n$j\n"; |
|
|
| 309 | } |
|
|
| 310 | |
|
|
| 311 | print LATEXME '\end{document}'."\n"; |
|
|
| 312 | close(LATEXME); |
|
|
| 313 | |
|
|
| 314 | chmod(0666, "$latexfilenamebase.tex") || warn "Could not change permissions on $latexfilenamebase.tex"; |
|
|
| 315 | |
|
|
| 316 | my $error_log = '/dev/null'; ## by default do not log error messages |
|
|
| 317 | $error_log = &Global::getErrorLog if $Global::imageDebugMode; |
|
|
| 318 | |
|
|
| 319 | $ENV{PATH} .= "$Global::extendedPath"; |
|
|
| 320 | my $dvipng_res = int($Global::dvipngScaling * 1000+0.5); |
|
|
| 321 | my $cmdout=""; |
|
|
| 322 | |
|
|
| 323 | # remove any old files using this name |
|
|
| 324 | unlink("$self->{filenamestart}.dvi","$self->{filenamestart}.log", |
|
|
| 325 | "$self->{filenamestart}.aux","missfont.log"); |
|
|
| 326 | |
|
|
| 327 | $cmdout=system("$Global::externalLatexPath $self->{filenamestart}.tex >>$error_log 2>>$error_log"); |
|
|
| 328 | warn "$Global::externalLatexPath $self->{filenamestart}.tex >>$error_log 2>>$error_log -- FAILED in ImageGenerator returned $cmdout" if $cmdout; |
|
|
| 329 | |
|
|
| 330 | $cmdout=system("$Global::externalDvipngPath -x$dvipng_res -bgTransparent -Q$Global::dvipngShrinkFactor -mode $Global::dvipngMode -D$Global::dvipngDPI $self->{filenamestart}.dvi >>$error_log 2>>$error_log"); |
|
|
| 331 | warn "$Global::externalDvipngPath -x$dvipng_res -bgTransparent -Q$Global::dvipngShrinkFactor -mode $Global::dvipngMode -D$Global::dvipngDPI $self->{filenamestart}.dvi >>$error_log 2>>$error_log -- FAILED in ImageGenerator.pm returned $cmdout" if $cmdout !=256; |
|
|
| 332 | |
|
|
| 333 | unless ($Global::imageDebugMode) { |
|
|
| 334 | unlink("$self->{filenamestart}.dvi","$self->{filenamestart}.log", |
|
|
| 335 | "$self->{filenamestart}.tex", |
|
|
| 336 | "$self->{filenamestart}.aux" |
|
|
| 337 | ); |
|
|
| 338 | } |
|
|
| 339 | #chdir($old_cdir); |
|
|
| 340 | } |
|
|
| 341 | } |
|
|
| 342 | |
|
|
| 343 | 1; |
|
|