Skip to content

Commit

Permalink
Implement TikZ images for problems.
Browse files Browse the repository at this point in the history
This builds on the work of Michael Gage and Peter Peluso (and others?)
in PG openwebwork#292.
Care is taken to ensure that the command line is not exposed.
I have updated the examples in pg/t/tikz_test for its usage as I have
implemented it.
  • Loading branch information
drgrice1 committed Sep 26, 2019
1 parent 9652784 commit fa7ecab
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 57 deletions.
53 changes: 39 additions & 14 deletions lib/PGcore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ sub DESTROY {
# returns a path to the file containing the graph image.
$filePath = insertGraph($graphObject);
insertGraph writes a GIF or PNG image file to the gif subdirectory of the
insertGraph writes a GIF or PNG image file to the images subdirectory of the
current course's HTML temp directory. The file name is obtained from the graph
object. Warnings are issued if errors occur while writing to the file.
Expand All @@ -694,27 +694,52 @@ sub insertGraph {
# Convert the image to GIF and print it on standard output
my $self = shift;
my $graph = shift;
my $extension = ($WWPlot::use_png) ? '.png' : '.gif';
my $fileName = $graph->imageName . $extension;
my $filePath = $self->convertPath("gif/$fileName");
my $fileName = $graph->imageName . "." . $graph->ext;
my $filePath = $self->convertPath("images/$fileName");
my $templateDirectory = $self->{envir}{templateDirectory};
$filePath = $self->surePathToTmpFile( $filePath );
my $refreshCachedImages = $self->PG_restricted_eval(q!$refreshCachedImages!);
# Check to see if we already have this graph, or if we have to make it
if( not -e $filePath # does it exist?
or ((stat "$templateDirectory".$self->{envir}{probFileName})[9] > (stat $filePath)[9]) # source has changed
or $self->{envir}{setNumber} =~ /Undefined_Set/ # problems from SetMaker and its ilk should always be redone
or $refreshCachedImages
if (not -e $filePath # does it exist?
or ((stat "$templateDirectory".$self->{envir}{probFileName})[9] > (stat $filePath)[9]) # source has changed
or $self->{envir}{setNumber} =~ /Undefined_Set/ # problems from SetMaker and its ilk should always be redone
or $refreshCachedImages
) {
local(*OUTPUT); # create local file handle so it won't overwrite other open files.
open(OUTPUT, ">$filePath")||warn ("$0","Can't open $filePath<BR>","");
chmod( 0777, $filePath);
print OUTPUT $graph->draw|| warn("$0","Can't print graph to $filePath<BR>","");
close(OUTPUT)||warn("$0","Can't close $filePath<BR>","");
open(my $fh, ">", $filePath) || warn ("$0", "Can't open $filePath<BR>","");
chmod(0777, $filePath);
print $fh $graph->draw || warn("$0","Can't print graph to $filePath<BR>","");
close($fh) || warn("$0","Can't close $filePath<BR>","");
}
$filePath;
}

=head2 getUniqueName
# Returns a unique file name for use in the problem
$name = getUniqueName('png');
getUniqueName generates a unique file name for use in a problem. Its single
argument is the file type. This is used internally by PGgraphmacros.pl and
PGtikz.pl.
=cut

# Keep track of the names created during this session.
our %names_created;

# Generate a unique file name in a problem based on the user, seed, set
# number, and problem number.
sub getUniqueName {
my $self = shift;
my $ext = shift;
my $prob_name = "$self->{envir}{studentLogin}-$self->{envir}{problemSeed}" .
"-set$self->{envir}{setNumber}prob$self->{envir}{probNum}";
my $num = ++$names_created{$prob_name};
my $resource = $self->{PG_alias}->make_resource_object($prob_name, $ext);
$resource->path("__");
return $resource->create_unique_id;
}

=head1 Macros from IO.pm
includePGtext
Expand Down Expand Up @@ -765,7 +790,7 @@ sub AskSage {
my $self = shift;
my $python = shift;
my $options = shift;
$options->{curlCommand} = $self->{envir}->{externalCurlCommand};
$options->{curlCommand} = WeBWorK::PG::IO::curlCommand();
WeBWorK::PG::IO::AskSage($python, $options);
}

Expand Down
163 changes: 163 additions & 0 deletions lib/TikZImage.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/bin/perl
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright &copy; 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/
# $CVSHeader: pg/macros/parserMultiAnswer.pl,v 1.11 2009/06/25 23:28:44 gage Exp $
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of either: (a) the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version, or (b) the "Artistic License" which comes with this package.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
# Artistic License for more details.
################################################################################

# This is a Perl module which simplifies and automates the process of generating
# simple images using TikZ, and converting them into a web-useable format. Its
# typical usage is via the macro PGtikz.pl and is documented there.

use strict;
use warnings;
use Carp;
use WeBWorK::PG::IO;
use WeBWorK::PG::ImageGenerator;

package TikZImage;

# The constructor (it takes no parameters)
sub new {
my $class = shift;
my $data = {
tex => '',
tikzOptions => '',
tikzLibraries => '',
ext => 'png',
imageName => ''
};
my $self = sub {
my $field = shift;
if (@_) {
# The ext field is protected to ensure that unsafe commands can not
# be passed to the command line in the system call it is used in.
if ($field eq 'ext') {
my $ext = shift;
$data->{ext} = $ext
if ($ext eq 'png' || $ext eq 'gif' || $ext eq 'svg' || $ext eq 'pdf');
}
else {
$data->{$field} = shift;
}
}
return $data->{$field};
};
return bless $self, $class;
}

# Accessors

# Set TikZ image code, not including begin and end tags, as a single
# string parameter. Works best single quoted.
sub tex {
my $self = shift;
return &$self('tex', @_);
}

# Set TikZ picture options as a single string parameter.
sub tikzOptions {
my $self = shift;
return &$self('tikzOptions', @_);
}

# Set additional TikZ libraries to load as a single string parameter.
sub tikzLibraries {
my $self = shift;
return &$self('tikzLibraries', @_);
}

# Set the image type. The valid types are 'png', 'gif', 'svg', and 'pdf'.
# The 'pdf' option should be set for print.
sub ext {
my $self = shift;
return &$self('ext', @_);
}

# Set the file name.
sub imageName {
my $self = shift;
return &$self('imageName', @_);
}

sub header {
my $self = shift;
my @output = ();
push(@output, "\\documentclass{standalone}\n");
push(@output, "\\usepackage{tikz}\n");
push(@output, "\\usetikzlibrary{" . $self->tikzLibraries . "}") if ($self->tikzLibraries ne "");
push(@output, "\\begin{document}\n");
push(@output, "\\begin{tikzpicture}");
push(@output, "[" . $self->tikzOptions . "]") if ($self->tikzOptions ne "");
@output;
}

sub footer {
my $self = shift;
my @output = ();
push(@output, "\\end{tikzpicture}\n");
push(@output, "\\end{document}\n");
@output;
}

# Generate the image file and return the stored location of the image.
sub draw {
my $self = shift;

my $working_dir = WeBWorK::PG::ImageGenerator::makeTempDirectory(WeBWorK::PG::IO::ww_tmp_dir(), "tikz");
my $data;

my $fh;
open($fh, ">", "$working_dir/image.tex")
or warn "Can't open $working_dir/image.tex for writing.";
chmod(0777, "$working_dir/image.tex");
print $fh $self->header;
print $fh $self->tex . "\n";
print $fh $self->footer;
close $fh;

my $pdflatex_command = "cd " . $working_dir . " && " .
WeBWorK::PG::IO::pdflatexCommand() . " > pdflatex.stdout 2> pdflatex.stderr image.tex";

# Generate the pdf file.
system "$pdflatex_command";
if (-r "$working_dir/image.pdf" ) {
my $ext = $self->ext;

# Convert the file to the appropriate type of image file
system WeBWorK::PG::IO::convertCommand() . " $working_dir/image.pdf $working_dir/image.$ext"
if $ext ne 'pdf';

if (-r "$working_dir/image.$ext") {
# Read the generated image file into memory
open(my $in_fh, "<", "$working_dir/image.$ext")
or warn "Failed to open $working_dir/image.$ext for reading.", return;
local $/;
$data = <$in_fh>;
close($in_fh);
} else {
warn "Convert operation failed.";
}
} else {
warn "File $working_dir/image.pdf was not created.";
}

# Delete the files used to generate the image.
if (-e $working_dir) {
system "rm -rf $working_dir";
}

return $data;
}

1;
4 changes: 4 additions & 0 deletions lib/WWPlot.pm
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,10 @@ sub imageName {
}
}

sub ext {
return $WWPlot::use_png ? 'png' : 'ext';
}

sub position {
my $self = shift;
my $type = ref($self) || die "$self is not an object";
Expand Down
48 changes: 47 additions & 1 deletion lib/WeBWorK/PG/IO.pm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use utf8;
my $CE = new WeBWorK::CourseEnvironment({
webwork_dir => $ENV{WEBWORK_ROOT},
});

=head1 NAME
WeBWorK::PG::IO - Private functions used by WeBWorK::PG::Translator for file IO.
Expand Down Expand Up @@ -268,10 +269,55 @@ Checks to see if the given path is a sub directory of the courses directory
=cut

sub path_is_course_subdir {

return path_is_subdir(shift,$CE->{webwork_courses_dir},1);
}

sub ww_tmp_dir {
return $CE->{webworkDirs}{tmp};
}


=item curlCommand
curl -- path to curl defined in site.conf
=cut

sub curlCommand {
return $CE->{externalPrograms}{curl};
}

=item convertCommand
convert -- path to convert defined in site.conf
=cut

sub convertCommand {
return $CE->{externalPrograms}{convert};
}

=item pdflatexCommand
pdflatex -- path to pdflatex defined in site.conf
=cut

sub pdflatexCommand {
return $CE->{externalPrograms}{pdflatex};
}

=item copyCommand
copyCommand -- path to cp defined in site.conf
=cut

sub copyCommand {
return $CE->{externalPrograms}{cp};
}


#
# isolate the call to the sage server in case we have to jazz it up
#
Expand Down
41 changes: 4 additions & 37 deletions macros/PGgraphmacros.pl
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ =head2 Other constructs
# of MathObjects since that can mess up
# problems that don't use MathObjects but use Matrices.

our %images_created = (); # this keeps track of the base names of the images created during this session.
# We tack on
# $imageNum = ++$images_created{$imageName} to keep from overwriting files
# when we don't want to.




=head2 init_graph
Expand Down Expand Up @@ -115,26 +109,7 @@ sub init_graph {

my $graphRef = new WWPlot(@size);
# select a name for this graph based on the user, the psvn and the problem
my $setName = $main::setNumber;
# replace dots, commmas and @ signs in set and user names to keep latex and html happy
# this should be abstracted and placed in PGalias.pm or PGcore.pm or perhaps PG.pm
#FIXME
$setName =~ s/Q/QQ/g;
$setName =~ s/\./-Q-/g;
my $studentLogin = $main::studentLogin;
$studentLogin =~ s/Q/QQ/g;
$studentLogin =~ s/\./-Q-/g;
$studentLogin =~ s/\,/-Q-/g;
$studentLogin =~ s/\@/-Q-/g;
my $imageName = "$main::studentLogin-$main::problemSeed-set${main::setNumber}prob${main::probNum}";
# $imageNum counts the number of graphs with this name which have been created since PGgraphmacros.pl was initiated.
my $imageNum = ++$images_created{$imageName};
# this provides a unique name for the graph -- it does not include an extension.
# PG_alias->make_resource_object(fileName, type) --> returns a UUID
my $resource = $main::PG->{PG_alias}->make_resource_object("image$imageNum","png");
$resource->path("__"); # some kind of path is required in order for create_unique_id to work
# the only role of the resource object is to create the UUID -- the object is then discarded.
$graphRef->imageName($resource->create_unique_id);
$graphRef->imageName($main::PG->getUniqueName($graphRef->ext));

# Set the initial/default bounds for the graph.
$graphRef->xmin($xmin) if defined($xmin);
Expand Down Expand Up @@ -241,15 +216,7 @@ sub init_graph_no_labels {
}
my $graphRef = new WWPlot(@size);
# select a name for this graph based on the user, the psvn and the problem
my $imageName = "$main::studentLogin-$main::problemSeed-set${main::setNumber}prob${main::probNum}";
# $imageNum counts the number of graphs with this name which have been created since PGgraphmacros.pl was initiated.
my $imageNum = ++$images_created{$imageName};
# this provides a unique name for the graph -- it does not include an extension.
# PG_alias->make_resource_object(fileName, type) --> returns a UUID
my $resource = $main::PG->{PG_alias}->make_resource_object("image$imageNum","png");
$resource->path("__"); # some kind of path is required in order for create_unique_id to work
# the only role of the resource object is to create the UUID -- the object is then discarded.
$graphRef->imageName($resource->create_unique_id);
$graphRef->imageName($main::PG->getUniqueName($graphRef->ext));

$graphRef->xmin($xmin) if defined($xmin);
$graphRef->xmax($xmax) if defined($xmax);
Expand Down Expand Up @@ -416,7 +383,7 @@ =head2 insertGraph
B<Note:> insertGraph is defined in PGcore.pl, because it involves writing to the disk.
insertGraph(graphObject) writes a image file to the C<html/tmp/gif> directory of the current course.
insertGraph(graphObject) writes a image file to the C<html/tmp/images> directory of the current course.
The file name is obtained from the graphObject. Warnings are issued if errors occur while writing to
the file.
Expand All @@ -427,7 +394,7 @@ =head2 insertGraph
InsertGraph draws the object $graph, stores it in "${tempDirectory}gif/$imageName.gif (or .png)" where
InsertGraph draws the object $graph, stores it in "${tempDirectory}images/$imageName.png (or .gif)" where
the $imageName is obtained from the graph object. ConvertPath and surePathToTmpFile are used to insure
that the correct directory separators are used for the platform and that the necessary directories
are created if they are not already present. The directory address to the file is the result.
Expand Down
Loading

0 comments on commit fa7ecab

Please sign in to comment.